Clean up callhome-provide warnings
[netconf.git] / apps / callhome-provider / src / main / java / org / opendaylight / netconf / callhome / mount / CallHomeMountStatusReporter.java
1 /*
2  * Copyright (c) 2023 PANTHEON.tech s.r.o. 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.collect.ImmutableList;
11 import com.google.common.util.concurrent.FutureCallback;
12 import com.google.common.util.concurrent.MoreExecutors;
13 import java.io.IOException;
14 import java.net.SocketAddress;
15 import java.security.PublicKey;
16 import java.util.List;
17 import java.util.concurrent.ExecutionException;
18 import javax.annotation.PreDestroy;
19 import javax.inject.Inject;
20 import javax.inject.Singleton;
21 import org.eclipse.jdt.annotation.Nullable;
22 import org.opendaylight.mdsal.binding.api.DataBroker;
23 import org.opendaylight.mdsal.binding.api.DataTreeIdentifier;
24 import org.opendaylight.mdsal.binding.api.DataTreeModification;
25 import org.opendaylight.mdsal.common.api.CommitInfo;
26 import org.opendaylight.mdsal.common.api.LogicalDatastoreType;
27 import org.opendaylight.netconf.callhome.server.CallHomeStatusRecorder;
28 import org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.netconf.callhome.server.rev230428.NetconfCallhomeServer;
29 import org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.netconf.callhome.server.rev230428.netconf.callhome.server.AllowedDevices;
30 import org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.netconf.callhome.server.rev230428.netconf.callhome.server.allowed.devices.Device;
31 import org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.netconf.callhome.server.rev230428.netconf.callhome.server.allowed.devices.Device.DeviceStatus;
32 import org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.netconf.callhome.server.rev230428.netconf.callhome.server.allowed.devices.DeviceBuilder;
33 import org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.netconf.callhome.server.rev230428.netconf.callhome.server.allowed.devices.DeviceKey;
34 import org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.netconf.callhome.server.rev230428.netconf.callhome.server.allowed.devices.device.transport.SshBuilder;
35 import org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.netconf.callhome.server.rev230428.netconf.callhome.server.allowed.devices.device.transport.ssh.SshClientParamsBuilder;
36 import org.opendaylight.yangtools.concepts.Registration;
37 import org.opendaylight.yangtools.yang.binding.InstanceIdentifier;
38 import org.osgi.service.component.annotations.Activate;
39 import org.osgi.service.component.annotations.Component;
40 import org.osgi.service.component.annotations.Deactivate;
41 import org.osgi.service.component.annotations.Reference;
42 import org.slf4j.Logger;
43 import org.slf4j.LoggerFactory;
44
45 /**
46  * Service responsible for status update for call-home devices.
47  */
48 @Component(service = {CallHomeMountStatusReporter.class, CallHomeStatusRecorder.class}, immediate = true)
49 @Singleton
50 public final class CallHomeMountStatusReporter implements CallHomeStatusRecorder, AutoCloseable {
51     private static final Logger LOG = LoggerFactory.getLogger(CallHomeMountStatusReporter.class);
52     private static final InstanceIdentifier<AllowedDevices> ALL_DEVICES_II =
53         InstanceIdentifier.create(NetconfCallhomeServer.class).child(AllowedDevices.class);
54
55     private final DataBroker dataBroker;
56     private final Registration syncReg;
57
58     @Activate
59     @Inject
60     public CallHomeMountStatusReporter(final @Reference DataBroker broker) {
61         dataBroker = broker;
62         syncReg = dataBroker.registerDataTreeChangeListener(
63             DataTreeIdentifier.of(LogicalDatastoreType.CONFIGURATION, ALL_DEVICES_II.child(Device.class)),
64             this::onConfigurationDataTreeChanged);
65     }
66
67     @Deactivate
68     @PreDestroy
69     @Override
70     public void close() {
71         syncReg.close();
72     }
73
74     @Override
75     public void reportSuccess(final String id) {
76         updateCallHomeDeviceStatus(id, DeviceStatus.CONNECTED);
77     }
78
79     @Override
80     public void reportDisconnected(final String id) {
81         updateCallHomeDeviceStatus(id, DeviceStatus.DISCONNECTED);
82     }
83
84     @Override
85     public void reportFailedAuth(final String id) {
86         updateCallHomeDeviceStatus(id, DeviceStatus.FAILEDAUTHFAILURE);
87     }
88
89     @Override
90     public void reportNetconfFailure(final String id) {
91         updateCallHomeDeviceStatus(id, DeviceStatus.FAILED);
92     }
93
94     @Override
95     public void reportUnknown(final SocketAddress address, final PublicKey publicKey) {
96         // ignored --> the case is handled by ssh auth provider which are conditionally invoking
97         // reportNewSshDevice() directly
98     }
99
100     /**
101      * Update device status within operational datastore.
102      *
103      * @param id device id
104      * @param status new status
105      */
106     public void updateCallHomeDeviceStatus(final String id, final DeviceStatus status) {
107         LOG.debug("Setting status '{}' for call-home device {}.", status, id);
108         final var instanceIdentifier = buildInstanceIdentifier(id);
109         final var device = readDevice(instanceIdentifier);
110         if (device == null) {
111             LOG.warn("No call-home device '{}' found in operational datastore. Status update to '{}' is omitted.",
112                 id, status);
113             return;
114         }
115         if (status == device.getDeviceStatus()) {
116             LOG.debug("Call-home device '{}' already having status '{}'. Update omitted", id, status);
117             return;
118         }
119         writeDevice(instanceIdentifier, new DeviceBuilder(device).setDeviceStatus(status).build());
120     }
121
122     /**
123      * Persists new call-home device within operational datastore.
124      *
125      * @param id device id
126      * @param serverKey public key used for device identification over ssh
127      * @param status initial device status
128      */
129     public void reportNewSshDevice(final String id, final PublicKey serverKey, final DeviceStatus status) {
130         writeDevice(buildInstanceIdentifier(id), newSshDevice(id, serverKey, status));
131     }
132
133     private static Device newSshDevice(final String id, final PublicKey serverKey, final DeviceStatus status) {
134         // used only for netconf devices that are connected via SSH transport and global credentials
135         String sshEncodedKey = serverKey.toString();
136         try {
137             sshEncodedKey = AuthorizedKeysDecoder.encodePublicKey(serverKey);
138         } catch (IOException e) {
139             LOG.warn("Unable to encode public key to ssh format.", e);
140         }
141         return new DeviceBuilder()
142             .setUniqueId(id)
143             .withKey(new DeviceKey(id))
144             .setTransport(new SshBuilder().setSshClientParams(
145                 new SshClientParamsBuilder().setHostKey(sshEncodedKey).build()).build())
146             .setDeviceStatus(status)
147             .build();
148     }
149
150     private @Nullable Device readDevice(final InstanceIdentifier<Device> instanceIdentifier) {
151         try (var readTx = dataBroker.newReadOnlyTransaction()) {
152             return readTx.read(LogicalDatastoreType.OPERATIONAL, instanceIdentifier).get().orElse(null);
153         } catch (InterruptedException | ExecutionException e) {
154             return null;
155         }
156     }
157
158     private void writeDevice(final InstanceIdentifier<Device> instanceIdentifier, final Device device) {
159         final var tx = dataBroker.newWriteOnlyTransaction();
160         tx.merge(LogicalDatastoreType.OPERATIONAL, instanceIdentifier, device);
161         tx.commit().addCallback(new FutureCallback<CommitInfo>() {
162             @Override
163             public void onSuccess(final CommitInfo result) {
164                 LOG.debug("Device {} committed", device);
165             }
166
167             @Override
168             public void onFailure(final Throwable cause) {
169                 LOG.warn("Failed to commit device {}", device, cause);
170             }
171         }, MoreExecutors.directExecutor());
172     }
173
174     private static InstanceIdentifier<Device> buildInstanceIdentifier(final String id) {
175         return ALL_DEVICES_II.child(Device.class, new DeviceKey(id));
176     }
177
178     // DataTreeChangeListener dedicated to call-home device data synchronization
179     // from CONFIGURATION to OPERATIONAL datastore (excluding device status)
180     private void onConfigurationDataTreeChanged(final List<DataTreeModification<Device>> changes) {
181         final var deleted = ImmutableList.<InstanceIdentifier<Device>>builder();
182         final var modified = ImmutableList.<Device>builder();
183         for (var change : changes) {
184             var changeRootNode = change.getRootNode();
185             switch (changeRootNode.modificationType()) {
186                 case SUBTREE_MODIFIED:
187                 case WRITE:
188                     modified.add(changeRootNode.dataAfter());
189                     break;
190                 case DELETE:
191                     deleted.add(change.getRootPath().path());
192                     break;
193                 default:
194                     break;
195             }
196         }
197         syncModifiedDevices(modified.build());
198         syncDeletedDevices(deleted.build());
199     }
200
201     private void syncModifiedDevices(final List<Device> updatedDevices) {
202         if (updatedDevices.isEmpty()) {
203             return;
204         }
205         for (var configDevice : updatedDevices) {
206             final var instanceIdentifier = buildInstanceIdentifier(configDevice.getUniqueId());
207             final var operDevice = readDevice(instanceIdentifier);
208             final var currentStatus = operDevice == null || operDevice.getDeviceStatus() == null
209                 ? DeviceStatus.DISCONNECTED : operDevice.getDeviceStatus();
210             writeDevice(instanceIdentifier, new DeviceBuilder(configDevice).setDeviceStatus(currentStatus).build());
211         }
212     }
213
214     private void syncDeletedDevices(final List<InstanceIdentifier<Device>> deletedDeviceIdentifiers) {
215         if (deletedDeviceIdentifiers.isEmpty()) {
216             return;
217         }
218         final var writeTx = dataBroker.newWriteOnlyTransaction();
219         deletedDeviceIdentifiers.forEach(instancedIdentifier ->
220             writeTx.delete(LogicalDatastoreType.OPERATIONAL, instancedIdentifier));
221
222         writeTx.commit().addCallback(new FutureCallback<CommitInfo>() {
223             @Override
224             public void onSuccess(final CommitInfo result) {
225                 LOG.debug("Device deletions committed");
226             }
227
228             @Override
229             public void onFailure(final Throwable cause) {
230                 LOG.warn("Failed to commit device deletions", cause);
231             }
232         }, MoreExecutors.directExecutor());
233     }
234 }