Close read-only transactions
[netconf.git] / netconf / callhome-provider / src / main / java / org / opendaylight / netconf / callhome / mount / IetfZeroTouchCallHomeServerProvider.java
1 /*
2  * Copyright (c) 2016 Brocade Communication Systems 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.netconf.callhome.mount;
9
10 import com.google.common.annotations.VisibleForTesting;
11 import com.google.common.util.concurrent.FutureCallback;
12 import com.google.common.util.concurrent.ListenableFuture;
13 import com.google.common.util.concurrent.MoreExecutors;
14 import io.netty.channel.nio.NioEventLoopGroup;
15 import java.io.IOException;
16 import java.net.InetSocketAddress;
17 import java.util.Collection;
18 import java.util.Collections;
19 import java.util.HashSet;
20 import java.util.List;
21 import java.util.Optional;
22 import java.util.Set;
23 import java.util.concurrent.ExecutionException;
24 import org.opendaylight.mdsal.binding.api.DataBroker;
25 import org.opendaylight.mdsal.binding.api.DataObjectModification;
26 import org.opendaylight.mdsal.binding.api.DataTreeChangeListener;
27 import org.opendaylight.mdsal.binding.api.DataTreeIdentifier;
28 import org.opendaylight.mdsal.binding.api.DataTreeModification;
29 import org.opendaylight.mdsal.binding.api.ReadTransaction;
30 import org.opendaylight.mdsal.binding.api.ReadWriteTransaction;
31 import org.opendaylight.mdsal.binding.api.WriteTransaction;
32 import org.opendaylight.mdsal.common.api.CommitInfo;
33 import org.opendaylight.mdsal.common.api.LogicalDatastoreType;
34 import org.opendaylight.netconf.callhome.protocol.CallHomeAuthorizationProvider;
35 import org.opendaylight.netconf.callhome.protocol.NetconfCallHomeServer;
36 import org.opendaylight.netconf.callhome.protocol.NetconfCallHomeServerBuilder;
37 import org.opendaylight.yang.gen.v1.urn.opendaylight.callhome.device.status.rev170112.Device1;
38 import org.opendaylight.yang.gen.v1.urn.opendaylight.callhome.device.status.rev170112.Device1Builder;
39 import org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.netconf.callhome.server.rev161109.NetconfCallhomeServer;
40 import org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.netconf.callhome.server.rev161109.netconf.callhome.server.AllowedDevices;
41 import org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.netconf.callhome.server.rev161109.netconf.callhome.server.allowed.devices.Device;
42 import org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.netconf.callhome.server.rev161109.netconf.callhome.server.allowed.devices.DeviceBuilder;
43 import org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.netconf.callhome.server.rev161109.netconf.callhome.server.allowed.devices.DeviceKey;
44 import org.opendaylight.yangtools.concepts.ListenerRegistration;
45 import org.opendaylight.yangtools.yang.binding.InstanceIdentifier;
46 import org.slf4j.Logger;
47 import org.slf4j.LoggerFactory;
48
49 public class IetfZeroTouchCallHomeServerProvider implements AutoCloseable, DataTreeChangeListener<AllowedDevices> {
50     private static final String APPNAME = "CallHomeServer";
51     static final InstanceIdentifier<AllowedDevices> ALL_DEVICES = InstanceIdentifier.create(NetconfCallhomeServer.class)
52             .child(AllowedDevices.class);
53
54     private static final Logger LOG = LoggerFactory.getLogger(IetfZeroTouchCallHomeServerProvider.class);
55
56     private final DataBroker dataBroker;
57     private final CallHomeMountDispatcher mountDispacher;
58     private final CallHomeAuthProviderImpl authProvider;
59
60     protected NetconfCallHomeServer server;
61
62     private ListenerRegistration<IetfZeroTouchCallHomeServerProvider> listenerReg = null;
63
64     private static final String CALL_HOME_PORT_KEY = "DefaultCallHomePort";
65     private int port = 0; // 0 = use default in NetconfCallHomeBuilder
66     private final CallhomeStatusReporter statusReporter;
67
68     public IetfZeroTouchCallHomeServerProvider(final DataBroker dataBroker,
69             final CallHomeMountDispatcher mountDispacher) {
70         this.dataBroker = dataBroker;
71         this.mountDispacher = mountDispacher;
72         this.authProvider = new CallHomeAuthProviderImpl(dataBroker);
73         this.statusReporter = new CallhomeStatusReporter(dataBroker);
74     }
75
76     public void init() {
77         // Register itself as a listener to changes in Devices subtree
78         try {
79             LOG.info("Initializing provider for {}", APPNAME);
80             initializeServer();
81             listenerReg = dataBroker.registerDataTreeChangeListener(
82                 DataTreeIdentifier.create(LogicalDatastoreType.CONFIGURATION, ALL_DEVICES), this);
83             LOG.info("Initialization complete for {}", APPNAME);
84         } catch (IOException | Configuration.ConfigurationException e) {
85             LOG.error("Unable to successfully initialize", e);
86         }
87     }
88
89     public void setPort(final String portStr) {
90         try {
91             Configuration configuration = new Configuration();
92             configuration.set(CALL_HOME_PORT_KEY, portStr);
93             port = configuration.getAsPort(CALL_HOME_PORT_KEY);
94             LOG.info("Setting port for call home server to {}", portStr);
95         } catch (Configuration.ConfigurationException e) {
96             LOG.error("Problem trying to set port for call home server {}", portStr, e);
97         }
98     }
99
100     private CallHomeAuthorizationProvider getCallHomeAuthorization() {
101         return new CallHomeAuthProviderImpl(dataBroker);
102     }
103
104     private void initializeServer() throws IOException {
105         LOG.info("Initializing Call Home server instance");
106         CallHomeAuthorizationProvider provider = this.getCallHomeAuthorization();
107         NetconfCallHomeServerBuilder builder = new NetconfCallHomeServerBuilder(provider, mountDispacher,
108                                                                                 statusReporter);
109         if (port > 0) {
110             builder.setBindAddress(new InetSocketAddress(port));
111         }
112         builder.setNettyGroup(new NioEventLoopGroup());
113         server = builder.build();
114         server.bind();
115         mountDispacher.createTopology();
116         LOG.info("Initialization complete for Call Home server instance");
117     }
118
119     @VisibleForTesting
120     void assertValid(final Object obj, final String description) {
121         if (obj == null) {
122             throw new RuntimeException(
123                     String.format("Failed to find %s in IetfZeroTouchCallHomeProvider.initialize()", description));
124         }
125     }
126
127     @Override
128     public void close() {
129         authProvider.close();
130         statusReporter.close();
131
132         // FIXME unbind the server
133         if (this.listenerReg != null) {
134             listenerReg.close();
135         }
136         if (server != null) {
137             server.close();
138         }
139
140         LOG.info("Successfully closed provider for {}", APPNAME);
141     }
142
143     @Override
144     public void onDataTreeChanged(final Collection<DataTreeModification<AllowedDevices>> changes) {
145         // In case of any changes to the devices datatree, register the changed values with callhome server
146         // As of now, no way to add a new callhome client key to the CallHomeAuthorization instance since
147         // its created under CallHomeAuthorizationProvider.
148         // Will have to redesign a bit here.
149         // CallHomeAuthorization.
150         final ListenableFuture<Optional<AllowedDevices>> devicesFuture;
151         try (ReadTransaction roConfigTx = dataBroker.newReadOnlyTransaction()) {
152             devicesFuture = roConfigTx.read(LogicalDatastoreType.CONFIGURATION,
153                 IetfZeroTouchCallHomeServerProvider.ALL_DEVICES);
154         }
155
156         Set<InstanceIdentifier<?>> deletedDevices = new HashSet<>();
157         for (DataTreeModification<AllowedDevices> change : changes) {
158             DataObjectModification<AllowedDevices> rootNode = change.getRootNode();
159             switch (rootNode.getModificationType()) {
160                 case DELETE:
161                     deletedDevices.add(change.getRootPath().getRootIdentifier());
162                     break;
163                 default:
164                     break;
165             }
166         }
167
168         handleDeletedDevices(deletedDevices);
169
170         try {
171             for (Device confDevice : getReadDevices(devicesFuture)) {
172                 readAndUpdateStatus(confDevice);
173             }
174         } catch (ExecutionException | InterruptedException e) {
175             LOG.error("Error trying to read the whitelist devices", e);
176         }
177     }
178
179     private void handleDeletedDevices(final Set<InstanceIdentifier<?>> deletedDevices) {
180         if (deletedDevices.isEmpty()) {
181             return;
182         }
183
184         WriteTransaction opTx = dataBroker.newWriteOnlyTransaction();
185
186         for (InstanceIdentifier<?> removedIID : deletedDevices) {
187             LOG.info("Deleting the entry for callhome device {}", removedIID);
188             opTx.delete(LogicalDatastoreType.OPERATIONAL, removedIID);
189         }
190
191         opTx.commit().addCallback(new FutureCallback<CommitInfo>() {
192             @Override
193             public void onSuccess(final CommitInfo result) {
194                 LOG.debug("Device deletions committed");
195             }
196
197             @Override
198             public void onFailure(final Throwable cause) {
199                 LOG.warn("Failed to commit device deletions", cause);
200             }
201         }, MoreExecutors.directExecutor());
202     }
203
204     private static List<Device> getReadDevices(final ListenableFuture<Optional<AllowedDevices>> devicesFuture)
205             throws InterruptedException, ExecutionException {
206         return devicesFuture.get().map(AllowedDevices::getDevice).orElse(Collections.emptyList());
207     }
208
209     private void readAndUpdateStatus(final Device cfgDevice) throws InterruptedException, ExecutionException {
210         InstanceIdentifier<Device> deviceIID = InstanceIdentifier.create(NetconfCallhomeServer.class)
211                 .child(AllowedDevices.class).child(Device.class, new DeviceKey(cfgDevice.getUniqueId()));
212
213         ReadWriteTransaction tx = dataBroker.newReadWriteTransaction();
214         ListenableFuture<Optional<Device>> deviceFuture = tx.read(LogicalDatastoreType.OPERATIONAL, deviceIID);
215
216         final Device1 devStatus;
217         Optional<Device> opDevGet = deviceFuture.get();
218         if (opDevGet.isPresent()) {
219             devStatus = opDevGet.get().augmentation(Device1.class);
220         } else {
221             devStatus = new Device1Builder().setDeviceStatus(Device1.DeviceStatus.DISCONNECTED).build();
222         }
223
224         tx.merge(LogicalDatastoreType.OPERATIONAL, deviceIID, new DeviceBuilder()
225             .addAugmentation(Device1.class, devStatus).setSshHostKey(cfgDevice.getSshHostKey())
226             .setUniqueId(cfgDevice.getUniqueId()).build());
227         tx.commit().addCallback(new FutureCallback<CommitInfo>() {
228             @Override
229             public void onSuccess(final CommitInfo result) {
230                 LOG.debug("Device {} status update committed", cfgDevice.key());
231             }
232
233             @Override
234             public void onFailure(final Throwable cause) {
235                 LOG.warn("Failed to commit device {} status update", cfgDevice.key(), cause);
236             }
237         }, MoreExecutors.directExecutor());
238     }
239 }