Configurable update-strategy for clusteredAppConfig
[controller.git] / opendaylight / blueprint / src / main / java / org / opendaylight / controller / blueprint / ext / DataStoreAppConfigMetadata.java
1 /*
2  * Copyright (c) 2016 Brocade Communications Systems, Inc. and others.  All rights reserved.
3  *
4  * This program and the accompanying materials are made available under the
5  * terms of the Eclipse Public License v1.0 which accompanies this distribution,
6  * and is available at http://www.eclipse.org/legal/epl-v10.html
7  */
8 package org.opendaylight.controller.blueprint.ext;
9
10 import com.google.common.base.Optional;
11 import com.google.common.base.Preconditions;
12 import com.google.common.base.Strings;
13 import com.google.common.util.concurrent.CheckedFuture;
14 import com.google.common.util.concurrent.FutureCallback;
15 import com.google.common.util.concurrent.Futures;
16 import java.io.File;
17 import java.io.FileInputStream;
18 import java.util.Collection;
19 import java.util.Collections;
20 import java.util.List;
21 import java.util.Objects;
22 import java.util.concurrent.atomic.AtomicBoolean;
23 import javax.annotation.Nonnull;
24 import javax.annotation.Nullable;
25 import javax.xml.parsers.DocumentBuilderFactory;
26 import org.apache.aries.blueprint.services.ExtendedBlueprintContainer;
27 import org.opendaylight.controller.md.sal.binding.api.ClusteredDataTreeChangeListener;
28 import org.opendaylight.controller.md.sal.binding.api.DataBroker;
29 import org.opendaylight.controller.md.sal.binding.api.DataObjectModification;
30 import org.opendaylight.controller.md.sal.binding.api.DataObjectModification.ModificationType;
31 import org.opendaylight.controller.md.sal.binding.api.DataTreeIdentifier;
32 import org.opendaylight.controller.md.sal.binding.api.DataTreeModification;
33 import org.opendaylight.controller.md.sal.binding.api.ReadOnlyTransaction;
34 import org.opendaylight.controller.md.sal.common.api.data.LogicalDatastoreType;
35 import org.opendaylight.controller.md.sal.common.api.data.ReadFailedException;
36 import org.opendaylight.controller.sal.core.api.model.SchemaService;
37 import org.opendaylight.yangtools.binding.data.codec.api.BindingNormalizedNodeSerializer;
38 import org.opendaylight.yangtools.concepts.ListenerRegistration;
39 import org.opendaylight.yangtools.yang.binding.DataObject;
40 import org.opendaylight.yangtools.yang.binding.Identifiable;
41 import org.opendaylight.yangtools.yang.binding.Identifier;
42 import org.opendaylight.yangtools.yang.binding.InstanceIdentifier;
43 import org.opendaylight.yangtools.yang.binding.util.BindingReflections;
44 import org.opendaylight.yangtools.yang.common.QName;
45 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
46 import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNode;
47 import org.opendaylight.yangtools.yang.data.impl.codec.xml.XmlUtils;
48 import org.opendaylight.yangtools.yang.data.impl.schema.ImmutableNodes;
49 import org.opendaylight.yangtools.yang.data.impl.schema.transform.dom.parser.DomToNormalizedNodeParserFactory;
50 import org.opendaylight.yangtools.yang.model.api.ContainerSchemaNode;
51 import org.opendaylight.yangtools.yang.model.api.DataSchemaNode;
52 import org.opendaylight.yangtools.yang.model.api.ListSchemaNode;
53 import org.opendaylight.yangtools.yang.model.api.Module;
54 import org.opendaylight.yangtools.yang.model.api.SchemaContext;
55 import org.osgi.service.blueprint.container.ComponentDefinitionException;
56 import org.slf4j.Logger;
57 import org.slf4j.LoggerFactory;
58 import org.w3c.dom.Document;
59 import org.w3c.dom.Element;
60
61 /**
62  * Factory metadata corresponding to the "clustered-app-config" element that obtains an application's
63  * config data from the data store and provides the binding DataObject instance to the Blueprint container
64  * as a bean. In addition registers a DataTreeChangeListener to restart the Blueprint container when the
65  * config data is changed.
66  *
67  * @author Thomas Pantelis
68  */
69 public class DataStoreAppConfigMetadata extends AbstractDependentComponentFactoryMetadata {
70     private static final Logger LOG = LoggerFactory.getLogger(DataStoreAppConfigMetadata.class);
71
72     static final String BINDING_CLASS = "binding-class";
73     static final String DEFAULT_CONFIG = "default-config";
74     static final String DEFAULT_CONFIG_FILE_NAME = "default-config-file-name";
75     static final String LIST_KEY_VALUE = "list-key-value";
76
77     private static final String DEFAULT_APP_CONFIG_FILE_PATH = "etc" + File.separator +
78             "opendaylight" + File.separator + "datastore" + File.separator + "initial" +
79             File.separator + "config";
80
81     private static final DocumentBuilderFactory DOC_BUILDER_FACTORY;
82
83     static {
84         DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
85         factory.setNamespaceAware(true);
86         factory.setCoalescing(true);
87         factory.setIgnoringElementContentWhitespace(true);
88         factory.setIgnoringComments(true);
89         DOC_BUILDER_FACTORY = factory;
90     }
91
92     private final Element defaultAppConfigElement;
93     private final String defaultAppConfigFileName;
94     private final String appConfigBindingClassName;
95     private final String appConfigListKeyValue;
96     private final UpdateStrategy appConfigUpdateStrategy;
97     private final AtomicBoolean readingInitialAppConfig = new AtomicBoolean(true);
98
99     private volatile BindingContext bindingContext;
100     private volatile ListenerRegistration<?> appConfigChangeListenerReg;
101     private volatile DataObject currentAppConfig;
102
103     // Note: the BindingNormalizedNodeSerializer interface is annotated as deprecated because there's an
104     // equivalent interface in the mdsal project but the corresponding binding classes in the controller
105     // project are still used - conversion to the mdsal binding classes hasn't occurred yet.
106     private volatile BindingNormalizedNodeSerializer bindingSerializer;
107
108     public DataStoreAppConfigMetadata(@Nonnull String id, @Nonnull String appConfigBindingClassName,
109             @Nullable String appConfigListKeyValue, @Nullable String defaultAppConfigFileName,
110             @Nonnull UpdateStrategy updateStrategyValue, @Nullable Element defaultAppConfigElement) {
111         super(id);
112         this.defaultAppConfigElement = defaultAppConfigElement;
113         this.defaultAppConfigFileName = defaultAppConfigFileName;
114         this.appConfigBindingClassName = appConfigBindingClassName;
115         this.appConfigListKeyValue = appConfigListKeyValue;
116         this.appConfigUpdateStrategy = updateStrategyValue;
117     }
118
119     @Override
120     @SuppressWarnings("unchecked")
121     public void init(ExtendedBlueprintContainer container) {
122         super.init(container);
123
124         Class<DataObject> appConfigBindingClass;
125         try {
126             Class<?> bindingClass = container.getBundleContext().getBundle().loadClass(appConfigBindingClassName);
127             if(!DataObject.class.isAssignableFrom(bindingClass)) {
128                 throw new ComponentDefinitionException(String.format(
129                         "%s: Specified app config binding class %s does not extend %s",
130                         logName(), appConfigBindingClassName, DataObject.class.getName()));
131             }
132
133             appConfigBindingClass = (Class<DataObject>) bindingClass;
134         } catch(ClassNotFoundException e) {
135             throw new ComponentDefinitionException(String.format("%s: Error loading app config binding class %s",
136                     logName(), appConfigBindingClassName), e);
137         }
138
139         if(Identifiable.class.isAssignableFrom(appConfigBindingClass)) {
140             // The binding class corresponds to a yang list.
141             if(Strings.isNullOrEmpty(appConfigListKeyValue)) {
142                 throw new ComponentDefinitionException(String.format(
143                         "%s: App config binding class %s represents a yang list therefore \"%s\" must be specified",
144                         logName(), appConfigBindingClassName, LIST_KEY_VALUE));
145             }
146
147             try {
148                 bindingContext = ListBindingContext.newInstance(appConfigBindingClass, appConfigListKeyValue);
149             } catch(Exception e) {
150                 throw new ComponentDefinitionException(String.format(
151                         "%s: Error initializing for app config list binding class %s",
152                         logName(), appConfigBindingClassName), e);
153             }
154
155         } else {
156             bindingContext = new ContainerBindingContext(appConfigBindingClass);
157         }
158     }
159
160     @Override
161     public Object create() throws ComponentDefinitionException {
162         LOG.debug("{}: In create - currentAppConfig: {}", logName(), currentAppConfig);
163
164         super.onCreate();
165
166         return currentAppConfig;
167     }
168
169     @Override
170     protected void startTracking() {
171         // First get the BindingNormalizedNodeSerializer OSGi service. This will be used to create a default
172         // instance of the app config binding class, if necessary.
173
174         retrieveService("binding-codec", BindingNormalizedNodeSerializer.class, service -> {
175             bindingSerializer = (BindingNormalizedNodeSerializer)service;
176             retrieveDataBrokerService();
177         });
178     }
179
180     private void retrieveDataBrokerService() {
181         LOG.debug("{}: In retrieveDataBrokerService", logName());
182
183         // Get the binding DataBroker OSGi service.
184
185         retrieveService("data-broker", DataBroker.class, service -> retrieveInitialAppConfig((DataBroker)service));
186     }
187
188     private void retrieveInitialAppConfig(DataBroker dataBroker) {
189         LOG.debug("{}: Got DataBroker instance - reading app config {}", logName(), bindingContext.appConfigPath);
190
191         setDependendencyDesc("Initial app config " + bindingContext.appConfigBindingClass.getSimpleName());
192
193         // We register a DTCL to get updates and also read the app config data from the data store. If
194         // the app config data is present then both the read and initial DTCN update will return it. If the
195         // the data isn't present, we won't get an initial DTCN update so the read will indicate the data
196         // isn't present.
197
198         DataTreeIdentifier<DataObject> dataTreeId = new DataTreeIdentifier<>(LogicalDatastoreType.CONFIGURATION,
199                 bindingContext.appConfigPath);
200         appConfigChangeListenerReg = dataBroker.registerDataTreeChangeListener(dataTreeId,
201                 new ClusteredDataTreeChangeListener<DataObject>() {
202                     @Override
203                     public void onDataTreeChanged(Collection<DataTreeModification<DataObject>> changes) {
204                         onAppConfigChanged(changes);
205                     }
206                 });
207
208         readInitialAppConfig(dataBroker);
209     }
210
211     private void readInitialAppConfig(final DataBroker dataBroker) {
212
213         final ReadOnlyTransaction readOnlyTx = dataBroker.newReadOnlyTransaction();
214         CheckedFuture<Optional<DataObject>, ReadFailedException> future = readOnlyTx.read(
215                 LogicalDatastoreType.CONFIGURATION, bindingContext.appConfigPath);
216         Futures.addCallback(future, new FutureCallback<Optional<DataObject>>() {
217             @Override
218             public void onSuccess(Optional<DataObject> possibleAppConfig) {
219                 LOG.debug("{}: Read of app config {} succeeded: {}", logName(), bindingContext.appConfigBindingClass.getName(),
220                         possibleAppConfig);
221
222                 readOnlyTx.close();
223                 setInitialAppConfig(possibleAppConfig);
224             }
225
226             @Override
227             public void onFailure(Throwable t) {
228                 readOnlyTx.close();
229
230                 // We may have gotten the app config via the data tree change listener so only retry if not.
231                 if(readingInitialAppConfig.get()) {
232                     LOG.warn("{}: Read of app config {} failed - retrying", logName(),
233                             bindingContext.appConfigBindingClass.getName(), t);
234
235                     readInitialAppConfig(dataBroker);
236                 }
237             }
238         });
239     }
240
241     private void onAppConfigChanged(Collection<DataTreeModification<DataObject>> changes) {
242         for(DataTreeModification<DataObject> change: changes) {
243             DataObjectModification<DataObject> changeRoot = change.getRootNode();
244             ModificationType type = changeRoot.getModificationType();
245
246             LOG.debug("{}: onAppConfigChanged: {}, {}", logName(), type, change.getRootPath());
247
248             if(type == ModificationType.SUBTREE_MODIFIED || type == ModificationType.WRITE) {
249                 DataObject newAppConfig = changeRoot.getDataAfter();
250
251                 LOG.debug("New app config instance: {}, previous: {}", newAppConfig, currentAppConfig);
252
253                 if(!setInitialAppConfig(Optional.of(newAppConfig)) &&
254                         !Objects.equals(currentAppConfig, newAppConfig)) {
255                     LOG.debug("App config was updated");
256
257                     if(appConfigUpdateStrategy == UpdateStrategy.RELOAD) {
258                         restartContainer();
259                     }
260                 }
261             } else if(type == ModificationType.DELETE) {
262                 LOG.debug("App config was deleted");
263
264                 if(appConfigUpdateStrategy == UpdateStrategy.RELOAD) {
265                     restartContainer();
266                 }
267             }
268         }
269     }
270
271     private boolean setInitialAppConfig(Optional<DataObject> possibleAppConfig) {
272         boolean result = readingInitialAppConfig.compareAndSet(true, false);
273         if(result) {
274             DataObject localAppConfig;
275             if(possibleAppConfig.isPresent()) {
276                 localAppConfig = possibleAppConfig.get();
277             } else {
278                 // No app config data is present so create an empty instance via the bindingSerializer service.
279                 // This will also return default values for leafs that haven't been explicitly set.
280                 localAppConfig = createDefaultInstance();
281             }
282
283             LOG.debug("{}: Setting currentAppConfig instance: {}", logName(), localAppConfig);
284
285             // Now publish the app config instance to the volatile field and notify the callback to let the
286             // container know our dependency is now satisfied.
287             currentAppConfig = localAppConfig;
288             setSatisfied();
289         }
290
291         return result;
292     }
293
294     private DataObject createDefaultInstance() {
295         YangInstanceIdentifier yangPath = bindingSerializer.toYangInstanceIdentifier(bindingContext.appConfigPath);
296
297         LOG.debug("{}: Creating app config instance from path {}, Qname: {}", logName(), yangPath, bindingContext.bindingQName);
298
299         SchemaService schemaService = getOSGiService(SchemaService.class);
300         if(schemaService == null) {
301             setFailureMessage(String.format("%s: Could not obtain the SchemaService OSGi service", logName()));
302             return null;
303         }
304
305         SchemaContext schemaContext = schemaService.getGlobalContext();
306
307         Module module = schemaContext.findModuleByNamespaceAndRevision(bindingContext.bindingQName.getNamespace(),
308                 bindingContext.bindingQName.getRevision());
309         if(module == null) {
310             setFailureMessage(String.format("%s: Could not obtain the module schema for namespace %s, revision %s",
311                     logName(), bindingContext.bindingQName.getNamespace(), bindingContext.bindingQName.getRevision()));
312             return null;
313         }
314
315         DataSchemaNode dataSchema = module.getDataChildByName(bindingContext.bindingQName);
316         if(dataSchema == null) {
317             setFailureMessage(String.format("%s: Could not obtain the schema for %s", logName(), bindingContext.bindingQName));
318             return null;
319         }
320
321         if(!bindingContext.schemaType.isAssignableFrom(dataSchema.getClass())) {
322             setFailureMessage(String.format("%s: Expected schema type %s for %s but actual type is %s", logName(),
323                     bindingContext.schemaType, bindingContext.bindingQName, dataSchema.getClass()));
324             return null;
325         }
326
327         NormalizedNode<?, ?> dataNode = parsePossibleDefaultAppConfigXMLFile(schemaContext, dataSchema);
328         if(dataNode == null) {
329             dataNode = parsePossibleDefaultAppConfigElement(schemaContext, dataSchema);
330         }
331
332         if(dataNode == null) {
333             dataNode = bindingContext.newDefaultNode(dataSchema);
334         }
335
336         DataObject appConfig = bindingSerializer.fromNormalizedNode(yangPath, dataNode).getValue();
337
338         if(appConfig == null) {
339             // This shouldn't happen but need to handle it in case...
340             setFailureMessage(String.format("%s: Could not create instance for app config binding %s",
341                     logName(), bindingContext.appConfigBindingClass));
342         }
343
344         return appConfig;
345     }
346
347     private NormalizedNode<?, ?> parsePossibleDefaultAppConfigXMLFile(SchemaContext schemaContext,
348             DataSchemaNode dataSchema) {
349
350         String appConfigFileName = defaultAppConfigFileName;
351         if(Strings.isNullOrEmpty(appConfigFileName)) {
352             String moduleName = findYangModuleName(bindingContext.bindingQName, schemaContext);
353             if(moduleName == null) {
354                 return null;
355             }
356
357             appConfigFileName = moduleName + "_" + bindingContext.bindingQName.getLocalName() + ".xml";
358         }
359
360         File appConfigFile = new File(DEFAULT_APP_CONFIG_FILE_PATH, appConfigFileName);
361
362         LOG.debug("{}: parsePossibleDefaultAppConfigXMLFile looking for file {}", logName(),
363                 appConfigFile.getAbsolutePath());
364
365         if(!appConfigFile.exists()) {
366             return null;
367         }
368
369         LOG.debug("{}: Found file {}", logName(), appConfigFile.getAbsolutePath());
370
371         DomToNormalizedNodeParserFactory parserFactory = DomToNormalizedNodeParserFactory.getInstance(
372                 XmlUtils.DEFAULT_XML_CODEC_PROVIDER, schemaContext);
373
374         try(FileInputStream fis = new FileInputStream(appConfigFile)) {
375             Document root = DOC_BUILDER_FACTORY.newDocumentBuilder().parse(fis);
376             NormalizedNode<?, ?> dataNode = bindingContext.parseDataElement(root.getDocumentElement(), dataSchema,
377                     parserFactory);
378
379             LOG.debug("{}: Parsed data node: {}", logName(), dataNode);
380
381             return dataNode;
382         } catch (Exception e) {
383             setFailureMessage(String.format("%s: Could not read/parse app config file %s", logName(), appConfigFile));
384         }
385
386         return null;
387     }
388
389     private String findYangModuleName(QName qname, SchemaContext schemaContext) {
390         for(Module m: schemaContext.getModules()) {
391             if(qname.getModule().equals(m.getQNameModule())) {
392                 return m.getName();
393             }
394         }
395
396         setFailureMessage(String.format("%s: Could not find yang module for QName %s", logName(), qname));
397         return null;
398     }
399
400     @Nullable
401     private NormalizedNode<?, ?> parsePossibleDefaultAppConfigElement(SchemaContext schemaContext,
402             DataSchemaNode dataSchema) {
403         if(defaultAppConfigElement == null) {
404             return null;
405         }
406
407         LOG.debug("{}: parsePossibleDefaultAppConfigElement for {}", logName(), bindingContext.bindingQName);
408
409         DomToNormalizedNodeParserFactory parserFactory = DomToNormalizedNodeParserFactory.getInstance(
410                 XmlUtils.DEFAULT_XML_CODEC_PROVIDER, schemaContext);
411
412
413         LOG.debug("{}: Got app config schema: {}", logName(), dataSchema);
414
415         NormalizedNode<?, ?> dataNode = bindingContext.parseDataElement(defaultAppConfigElement, dataSchema,
416                 parserFactory);
417
418         LOG.debug("{}: Parsed data node: {}", logName(), dataNode);
419
420         return dataNode;
421     }
422
423
424     @Override
425     public void destroy(Object instance) {
426         super.destroy(instance);
427
428         if(appConfigChangeListenerReg != null) {
429             appConfigChangeListenerReg.close();
430             appConfigChangeListenerReg = null;
431         }
432     }
433
434     /**
435      * Internal base class to abstract binding type-specific behavior.
436      */
437     private static abstract class BindingContext {
438         final InstanceIdentifier<DataObject> appConfigPath;
439         final Class<DataObject> appConfigBindingClass;
440         final Class<? extends DataSchemaNode> schemaType;
441         final QName bindingQName;
442
443         protected BindingContext(Class<DataObject> appConfigBindingClass, InstanceIdentifier<DataObject> appConfigPath,
444                 Class<? extends DataSchemaNode> schemaType) {
445             this.appConfigBindingClass = appConfigBindingClass;
446             this.appConfigPath = appConfigPath;
447             this.schemaType = schemaType;
448
449             bindingQName = BindingReflections.findQName(appConfigBindingClass);
450         }
451
452         abstract NormalizedNode<?, ?> parseDataElement(Element element, DataSchemaNode dataSchema,
453                 DomToNormalizedNodeParserFactory parserFactory);
454
455         abstract NormalizedNode<?, ?> newDefaultNode(DataSchemaNode dataSchema);
456     }
457
458     /**
459      * BindingContext implementation for a container binding.
460      */
461     private static class ContainerBindingContext extends BindingContext {
462         ContainerBindingContext(Class<DataObject> appConfigBindingClass) {
463             super(appConfigBindingClass, InstanceIdentifier.create(appConfigBindingClass), ContainerSchemaNode.class);
464         }
465
466         @Override
467         NormalizedNode<?, ?> newDefaultNode(DataSchemaNode dataSchema) {
468             return ImmutableNodes.containerNode(bindingQName);
469         }
470
471         @Override
472         NormalizedNode<?, ?> parseDataElement(Element element, DataSchemaNode dataSchema,
473                 DomToNormalizedNodeParserFactory parserFactory) {
474             return parserFactory.getContainerNodeParser().parse(Collections.singletonList(element),
475                     (ContainerSchemaNode)dataSchema);
476         }
477     }
478
479     /**
480      * BindingContext implementation for a list binding.
481      */
482     private static class ListBindingContext extends BindingContext {
483         final String appConfigListKeyValue;
484
485         ListBindingContext(Class<DataObject> appConfigBindingClass, InstanceIdentifier<DataObject> appConfigPath,
486                 String appConfigListKeyValue) {
487             super(appConfigBindingClass, appConfigPath, ListSchemaNode.class);
488             this.appConfigListKeyValue = appConfigListKeyValue;
489         }
490
491         @SuppressWarnings({ "rawtypes", "unchecked" })
492         private static ListBindingContext newInstance(Class<DataObject> bindingClass, String listKeyValue)
493                 throws Exception {
494             // We assume the yang list key type is string.
495             Identifier keyInstance = (Identifier) bindingClass.getMethod("getKey").getReturnType().
496                     getConstructor(String.class).newInstance(listKeyValue);
497             InstanceIdentifier appConfigPath = InstanceIdentifier.builder((Class)bindingClass, keyInstance).build();
498             return new ListBindingContext(bindingClass, appConfigPath, listKeyValue);
499         }
500
501         @Override
502         NormalizedNode<?, ?> newDefaultNode(DataSchemaNode dataSchema) {
503             // We assume there's only one key for the list.
504             List<QName> keys = ((ListSchemaNode)dataSchema).getKeyDefinition();
505             Preconditions.checkArgument(keys.size() == 1, "Expected only 1 key for list %s", appConfigBindingClass);
506             QName listKeyQName = keys.iterator().next();
507             return ImmutableNodes.mapEntryBuilder(bindingQName, listKeyQName, appConfigListKeyValue).build();
508         }
509
510         @Override
511         NormalizedNode<?, ?> parseDataElement(Element element, DataSchemaNode dataSchema,
512                 DomToNormalizedNodeParserFactory parserFactory) {
513             return parserFactory.getMapEntryNodeParser().parse(Collections.singletonList(element),
514                     (ListSchemaNode)dataSchema);
515         }
516     }
517 }