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