2 * Copyright (c) 2023 PANTHEON.tech s.r.o. and others. All rights reserved.
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
8 package org.opendaylight.netconf.topology.callhome;
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.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.netconf.callhome.server.rev240129.NetconfCallhomeServer;
28 import org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.netconf.callhome.server.rev240129.SshPublicKey;
29 import org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.netconf.callhome.server.rev240129.netconf.callhome.server.AllowedDevices;
30 import org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.netconf.callhome.server.rev240129.netconf.callhome.server.allowed.devices.Device;
31 import org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.netconf.callhome.server.rev240129.netconf.callhome.server.allowed.devices.Device.DeviceStatus;
32 import org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.netconf.callhome.server.rev240129.netconf.callhome.server.allowed.devices.DeviceBuilder;
33 import org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.netconf.callhome.server.rev240129.netconf.callhome.server.allowed.devices.DeviceKey;
34 import org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.netconf.callhome.server.rev240129.netconf.callhome.server.allowed.devices.device.transport.SshBuilder;
35 import org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.netconf.callhome.server.rev240129.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;
46 * Service responsible for status update for call-home devices.
48 @Component(service = {CallHomeMountStatusReporter.class, CallHomeStatusRecorder.class}, immediate = true)
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);
55 private final DataBroker dataBroker;
56 private final Registration syncReg;
60 public CallHomeMountStatusReporter(final @Reference DataBroker broker) {
62 syncReg = dataBroker.registerLegacyTreeChangeListener(
63 DataTreeIdentifier.of(LogicalDatastoreType.CONFIGURATION, ALL_DEVICES_II.child(Device.class)),
64 this::onConfigurationDataTreeChanged);
75 public void reportSuccess(final String id) {
76 updateCallHomeDeviceStatus(id, DeviceStatus.CONNECTED);
80 public void reportDisconnected(final String id) {
81 updateCallHomeDeviceStatus(id, DeviceStatus.DISCONNECTED);
85 public void reportFailedAuth(final String id) {
86 updateCallHomeDeviceStatus(id, DeviceStatus.FAILEDAUTHFAILURE);
90 public void reportNetconfFailure(final String id) {
91 updateCallHomeDeviceStatus(id, DeviceStatus.FAILED);
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
101 * Update device status within operational datastore.
103 * @param id device id
104 * @param status new status
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.",
115 if (status == device.getDeviceStatus()) {
116 LOG.debug("Call-home device '{}' already having status '{}'. Update omitted", id, status);
119 writeDevice(instanceIdentifier, new DeviceBuilder(device).setDeviceStatus(status).build());
123 * Persists new call-home device within operational datastore.
125 * @param id device id
126 * @param serverKey public key used for device identification over ssh
127 * @param status initial device status
129 public void reportNewSshDevice(final String id, final PublicKey serverKey, final DeviceStatus status) {
130 final var device = newSshDevice(id, serverKey, status);
131 if (device != null) {
132 writeDevice(buildInstanceIdentifier(id), device);
136 private static Device newSshDevice(final String id, final PublicKey serverKey, final DeviceStatus status) {
137 // used only for netconf devices that are connected via SSH transport and global credentials
138 final SshPublicKey sshEncodedKey;
140 sshEncodedKey = AuthorizedKeysDecoder.encodePublicKey(serverKey);
141 } catch (IOException e) {
142 LOG.warn("Unable to encode public key to ssh format, skipping update", e);
146 return new DeviceBuilder()
148 .withKey(new DeviceKey(id))
149 .setTransport(new SshBuilder().setSshClientParams(
150 new SshClientParamsBuilder().setHostKey(sshEncodedKey).build()).build())
151 .setDeviceStatus(status)
155 private @Nullable Device readDevice(final InstanceIdentifier<Device> instanceIdentifier) {
156 try (var readTx = dataBroker.newReadOnlyTransaction()) {
157 return readTx.read(LogicalDatastoreType.OPERATIONAL, instanceIdentifier).get().orElse(null);
158 } catch (InterruptedException | ExecutionException e) {
163 private void writeDevice(final InstanceIdentifier<Device> instanceIdentifier, final Device device) {
164 final var tx = dataBroker.newWriteOnlyTransaction();
165 tx.merge(LogicalDatastoreType.OPERATIONAL, instanceIdentifier, device);
166 tx.commit().addCallback(new FutureCallback<CommitInfo>() {
168 public void onSuccess(final CommitInfo result) {
169 LOG.debug("Device {} committed", device);
173 public void onFailure(final Throwable cause) {
174 LOG.warn("Failed to commit device {}", device, cause);
176 }, MoreExecutors.directExecutor());
179 private static InstanceIdentifier<Device> buildInstanceIdentifier(final String id) {
180 return ALL_DEVICES_II.child(Device.class, new DeviceKey(id));
183 // DataTreeChangeListener dedicated to call-home device data synchronization
184 // from CONFIGURATION to OPERATIONAL datastore (excluding device status)
185 private void onConfigurationDataTreeChanged(final List<DataTreeModification<Device>> changes) {
186 final var deleted = ImmutableList.<InstanceIdentifier<Device>>builder();
187 final var modified = ImmutableList.<Device>builder();
188 for (var change : changes) {
189 var changeRootNode = change.getRootNode();
190 switch (changeRootNode.modificationType()) {
191 case SUBTREE_MODIFIED:
193 modified.add(changeRootNode.dataAfter());
196 deleted.add(change.getRootPath().path());
202 syncModifiedDevices(modified.build());
203 syncDeletedDevices(deleted.build());
206 private void syncModifiedDevices(final List<Device> updatedDevices) {
207 if (updatedDevices.isEmpty()) {
210 for (var configDevice : updatedDevices) {
211 final var instanceIdentifier = buildInstanceIdentifier(configDevice.getUniqueId());
212 final var operDevice = readDevice(instanceIdentifier);
213 final var currentStatus = operDevice == null || operDevice.getDeviceStatus() == null
214 ? DeviceStatus.DISCONNECTED : operDevice.getDeviceStatus();
215 writeDevice(instanceIdentifier, new DeviceBuilder(configDevice).setDeviceStatus(currentStatus).build());
219 private void syncDeletedDevices(final List<InstanceIdentifier<Device>> deletedDeviceIdentifiers) {
220 if (deletedDeviceIdentifiers.isEmpty()) {
223 final var writeTx = dataBroker.newWriteOnlyTransaction();
224 deletedDeviceIdentifiers.forEach(instancedIdentifier ->
225 writeTx.delete(LogicalDatastoreType.OPERATIONAL, instancedIdentifier));
227 writeTx.commit().addCallback(new FutureCallback<CommitInfo>() {
229 public void onSuccess(final CommitInfo result) {
230 LOG.debug("Device deletions committed");
234 public void onFailure(final Throwable cause) {
235 LOG.warn("Failed to commit device deletions", cause);
237 }, MoreExecutors.directExecutor());