RD: Squashed changes from 2023-10-06 to 2023-10-30
Change-Id: I8acdbaf7414e40a928de79b48ca8c996b154d62d
@ -1,50 +0,0 @@
|
||||
package eu.nebulous.resource.discovery;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.ToString;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
@Slf4j
|
||||
@Data
|
||||
@Configuration
|
||||
@ConfigurationProperties(prefix = "discovery")
|
||||
public class ResourceDiscoveryProperties {
|
||||
private long subscriptionStartupDelay = 10;
|
||||
private long subscriptionRetry = 60;
|
||||
private boolean enablePeriodicProcessing = true;
|
||||
private long processingStartupDelay = 10;
|
||||
private long processingPeriod = 60;
|
||||
|
||||
private boolean createSampleDataAtStartup;
|
||||
private boolean createSampleDataPeriodically;
|
||||
private int createSampleDataStartupDelay = 30;
|
||||
private int createSampleDataPeriod = 60;
|
||||
private String createSampleDataOwner = "admin";
|
||||
|
||||
private String dataCollectionRequestTopic = "ems.client.installation.requests";
|
||||
private String dataCollectionResponseTopic = "ems.client.installation.reports";
|
||||
private List<String> allowedDeviceInfoKeys = new ArrayList<>(List.of("*"));
|
||||
|
||||
private boolean automaticArchivingEnabled;
|
||||
private long archivingThreshold; // in minutes
|
||||
|
||||
private String brokerUsername;
|
||||
@ToString.Exclude
|
||||
private String brokerPassword;
|
||||
private String brokerURL;
|
||||
|
||||
private List<UserData> users;
|
||||
|
||||
@Data
|
||||
public static class UserData {
|
||||
private final String username;
|
||||
@ToString.Exclude
|
||||
private final String password;
|
||||
private final List<String> roles;
|
||||
}
|
||||
}
|
@ -1,14 +0,0 @@
|
||||
|
||||
package eu.nebulous.resource.discovery.monitor;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
//@EnableAsync
|
||||
//@EnableScheduling
|
||||
@RequiredArgsConstructor
|
||||
public class DeviceProcessor {
|
||||
}
|
@ -1,27 +0,0 @@
|
||||
|
||||
spring.web.resources.static-locations: file:resource-discovery/management/src/main/resources/static/freebees_webdesign_6
|
||||
|
||||
#security.ignored: /**
|
||||
#security.basic.enable: false
|
||||
#spring.autoconfigure.exclude: org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration
|
||||
|
||||
#spring.security.user.name: user
|
||||
#spring.security.user.password: user
|
||||
#spring.security.user.roles: USER
|
||||
|
||||
spring.data.mongodb.uri: mongodb://root:example@localhost:27017/admin
|
||||
spring.data.mongodb.database: registration_request
|
||||
|
||||
discovery:
|
||||
brokerUsername: "aaa"
|
||||
brokerPassword: "111"
|
||||
brokerURL: "tcp://localhost:61616?daemon=true&trace=false&useInactivityMonitor=false&connectionTimeout=0&keepAlive=true"
|
||||
allowedDeviceInfoKeys:
|
||||
- '*'
|
||||
users:
|
||||
- username: admin
|
||||
password: admin1
|
||||
roles: [ ADMIN ]
|
||||
- username: user
|
||||
password: user1
|
||||
roles: [ USER ]
|
@ -1,207 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="author" content="Firmbee.com - Free Project Management Platform for remote teams">
|
||||
<title>NebulOuS Resource Discovery - Management page</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Nunito+Sans:wght@400;600;700&display=swap" rel="stylesheet">
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.0/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-KyZXEAg3QhqLMpG8r+8fhAXLRk2vvoC2f3B09zVXn8CA5QIVfZOJ3BCsw2P0p/We" crossorigin="anonymous">
|
||||
<link rel="stylesheet" href="css/style.css">
|
||||
|
||||
<script src="https://kit.fontawesome.com/0e035b9984.js" crossorigin="anonymous"></script>
|
||||
<script src="https://code.jquery.com/jquery-3.7.1.min.js" integrity="sha256-/JqT3SQfawRcv/BIHPThkBvs0OEvtFFmqPF/lYI/Cxo=" crossorigin="anonymous"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.0/dist/js/bootstrap.bundle.min.js" integrity="sha384-U1DAWAznBHeqEIlVSCgzq+c9gqGAJn5c/t99JyeKa9xxaYpSvHU5awsuZVVFIhvj" crossorigin="anonymous"></script>
|
||||
<script src="js/addshadow.js"></script>
|
||||
|
||||
<script>
|
||||
$(function() {
|
||||
updateRequestsList(false);
|
||||
setInterval(() => updateRequestsList(), 5000);
|
||||
});
|
||||
|
||||
var isAdmin = false;
|
||||
var lastUpdateAsAdmin;
|
||||
|
||||
function updateRequestsList(asAdmin) {
|
||||
if (asAdmin === undefined) asAdmin = lastUpdateAsAdmin;
|
||||
else lastUpdateAsAdmin = asAdmin;
|
||||
|
||||
$.ajax({
|
||||
url: '/discovery/request/archived' + (asAdmin ? '/all' : ''),
|
||||
dataType: 'json'
|
||||
})
|
||||
.done(function(data, status) {
|
||||
//console.log('updateRequestsList: OK: ', data);
|
||||
var tbody = $('#requestsTable-tbody');
|
||||
tbody.empty();
|
||||
var ii = 0;
|
||||
data.forEach(item => {
|
||||
var reqId = item.id;
|
||||
var requester = item.requester;
|
||||
var devName = item.device.deviceName;
|
||||
var ipAddress = item.device.ipAddress;
|
||||
var date = new Date( Date.parse( item.requestDate ) );
|
||||
var dateStr = date.toLocaleDateString('en-GB') + ' ' + date.toLocaleTimeString('en-GB')
|
||||
+ '<br/>' + Intl.DateTimeFormat().resolvedOptions().timeZone;
|
||||
var status = item.status;
|
||||
var color = getStatusColor(status);
|
||||
var adminActions = (isAdmin) ? `
|
||||
<button class="btn btn-outline-primary btn-sm" onClick="unarchiveRequest('${reqId}')">
|
||||
<i class="fas fa-box-open"></i>
|
||||
</button>
|
||||
`: '';
|
||||
ii++;
|
||||
tbody.append( $(`
|
||||
<tr class="${color}">
|
||||
<th scope="row">${ii}</th>
|
||||
<td>${requester}</td>
|
||||
<td class="text-start">
|
||||
<a href="/request-edit.html?id=${reqId}">${devName} @ ${ipAddress}</a>
|
||||
</td>
|
||||
<td>${ipAddress}</td>
|
||||
<td>${dateStr}</td>
|
||||
<td>${status}</td>
|
||||
<td>
|
||||
<button type="button" class="btn btn-success btn-sm" onClick="document.location='/archived-view.html?id=${reqId}'; ">
|
||||
<i class="fas fa-eye"></i>
|
||||
</button>
|
||||
${adminActions}
|
||||
</td>
|
||||
</tr> `
|
||||
) );
|
||||
});
|
||||
})
|
||||
.fail(function(xhr, status, error) {
|
||||
console.error('updateRequestsList: ERROR: ', status, error);
|
||||
})
|
||||
;
|
||||
}
|
||||
|
||||
function getStatusColor(status) {
|
||||
if (status.indexOf('ERROR')>0) return 'table-danger';
|
||||
if (status.indexOf('REJECT')>0) return 'bg-danger';
|
||||
if (status.indexOf('PENDING')>=0) return 'table-warning';
|
||||
if (status=='NEW_REQUEST') return '';
|
||||
if (status=='SUCCESS') return 'table-success';
|
||||
return 'table-info';
|
||||
}
|
||||
|
||||
function unarchiveRequest(reqId) {
|
||||
if (! confirm('Restore request?')) return;
|
||||
|
||||
$.ajax({ url: `/discovery/request/${reqId}/unarchive` })
|
||||
.done(function(data, status) {
|
||||
console.log('unarchiveRequest: OK: ', data);
|
||||
updateRequestsList(true);
|
||||
})
|
||||
.fail(function(xhr, status, error) {
|
||||
console.error('unarchiveRequest: ERROR: ', status, error);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<div style="position: absolute; left: 10px; top: 10px;">
|
||||
<a href="index.html"><img src="img/nebulous-logo-basic.png" width="155px" height="155px" alt=""></a>
|
||||
</div>
|
||||
<div class="text-end text-secondary nowrap">
|
||||
<img src="img/user-icon.png" width="24" height="auto">
|
||||
<span id="whoami"><i class="fas fa-spinner fa-spin"></i></span>
|
||||
|
||||
<span onClick="document.location = '/logout';">
|
||||
<i class="fas fa-sign-out-alt"></i>
|
||||
</span>
|
||||
|
||||
<script>
|
||||
$(function() {
|
||||
$.ajax({ url: '/discovery/whoami', dataType: 'json' })
|
||||
.done(function(data) {
|
||||
isAdmin = data.admin;
|
||||
data.admin ? $('#whoami').html( $(`<span class="text-primary fw-bold">${data.user}</span>`) ) : $('#whoami').html( data.user );
|
||||
if (isAdmin) $('.adminOnly').toggleClass('d-none');
|
||||
})
|
||||
.fail(function(xhr, status, error) { $('#whoami').html( $(`Error: ${status} ${JSON.stringify(error)}`) ); });
|
||||
});
|
||||
</script>
|
||||
</div>
|
||||
<section class="light-section">
|
||||
<div class="container">
|
||||
<div class="text-center">
|
||||
<h2>Archived Registration Requests</h2>
|
||||
<!--<p class="sub-header">Device registration requests</p>-->
|
||||
|
||||
<button type="button" class="btn btn-primary" onClick="document.location = 'index.html';">
|
||||
<i class="fa fa-home"></i>
|
||||
</button>
|
||||
|
||||
<span class="adminOnly d-none"> </span>
|
||||
<button type="button" class="adminOnly btn btn-danger d-none" onClick="updateRequestsList(true)">
|
||||
<i class="fa fa-refresh"></i>
|
||||
</button>
|
||||
<span class="adminOnly d-none"> </span>
|
||||
|
||||
<button type="button" class="btn btn-primary" onClick="updateRequestsList(false)">
|
||||
<i class="fa fa-refresh"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Query server for stored requests -->
|
||||
<div class="table-responsive text-nowrap">
|
||||
<table class="table table-hover table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">#</th>
|
||||
<th scope="col">Requester</th>
|
||||
<th scope="col" class="w-50">Device name</th>
|
||||
<th scope="col">IP Address</th>
|
||||
<th scope="col">Reg. Date</th>
|
||||
<th scope="col">Status</th>
|
||||
<th scope="col">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="requestsTable-tbody">
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
<footer class="py-5">
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="footer-item col-md-8">
|
||||
<p class="footer-item-title">Links</p>
|
||||
<a href="">About Us</a>
|
||||
<a href="">Portfolio</a>
|
||||
<a href="">Blog</a>
|
||||
<a href="">Sing In</a>
|
||||
</div>
|
||||
<div class="footer-item col-md-4">
|
||||
<p class="footer-item-title">Get In Touch</p>
|
||||
<form>
|
||||
<div class="mb-3 pb-3">
|
||||
<label for="exampleInputEmail1" class="form-label pb-3">Enter your email and we'll send you more information.</label>
|
||||
<input type="email" placeholder="Your Email" class="form-control" id="exampleInputEmail1" aria-describedby="emailHelp">
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">Subscribe</button>
|
||||
</form>
|
||||
</div>
|
||||
<div class="copyright pt-4 text-center text-muted">
|
||||
<p>© 2022 YOUR-DOMAIN | Created by <a href="https://firmbee.com/solutions/to-do-list/" title="Firmbee - Free To-do list App" target="_blank">Firmbee.com</a></p>
|
||||
<!--
|
||||
This template is licenced under Attribution 3.0 (CC BY 3.0 PL),
|
||||
You are free to: Share and Adapt. You must give appropriate credit, you may do so in any reasonable manner, but not in any way that suggests the licensor endorses you or your use.
|
||||
-->
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</main>
|
||||
<div class="fb2022-copy">Fbee 2022 copyright</div>
|
||||
</body>
|
||||
</html>
|
@ -10,14 +10,15 @@
|
||||
<relativePath/> <!-- lookup parent from repository -->
|
||||
</parent>
|
||||
|
||||
<groupId>eu.nebulous.resource-discovery</groupId>
|
||||
<artifactId>management</artifactId>
|
||||
<groupId>eu.nebulous.resource-management</groupId>
|
||||
<artifactId>resource-discovery</artifactId>
|
||||
<version>1.0.0-SNAPSHOT</version>
|
||||
<name>Resource discovery management service</name>
|
||||
<description>Nebulous resource discovery management service</description>
|
||||
<name>Resource discovery service</name>
|
||||
<description>Nebulous resource discovery service</description>
|
||||
|
||||
<properties>
|
||||
<java.version>17</java.version>
|
||||
<imageName>${project.artifactId}:${project.version}</imageName>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
@ -88,6 +89,21 @@
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
<executions>
|
||||
<execution>
|
||||
<goals>
|
||||
<goal>build-image</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
<configuration>
|
||||
<image>
|
||||
<name>${imageName}</name>
|
||||
<env>
|
||||
<BPE_LANG>C.UTF-8</BPE_LANG>
|
||||
</env>
|
||||
</image>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
@ -0,0 +1,95 @@
|
||||
package eu.nebulous.resource.discovery;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.ToString;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
@Slf4j
|
||||
@Data
|
||||
@Configuration
|
||||
@ConfigurationProperties(prefix = "discovery")
|
||||
public class ResourceDiscoveryProperties {
|
||||
// Broker configuration
|
||||
private String brokerURL;
|
||||
private String brokerUsername;
|
||||
@ToString.Exclude
|
||||
private String brokerPassword;
|
||||
|
||||
private String keyStoreFile;
|
||||
private String keyStorePassword;
|
||||
private String keyStoreType = "PKCS12";
|
||||
private String trustStoreFile;
|
||||
private String trustStorePassword;
|
||||
private String trustStoreType = "PKCS12";
|
||||
|
||||
private int connectionHealthCheckPeriod = 60; // in seconds
|
||||
private String healthCheckTopic = "_HEALTH_CHECK";
|
||||
|
||||
// Subscription to Broker settings
|
||||
private long subscriptionStartupDelay = 10;
|
||||
private long subscriptionRetryDelay = 60;
|
||||
|
||||
// Sample data creation settings
|
||||
private boolean createSampleDataAtStartup;
|
||||
private boolean createSampleDataPeriodically;
|
||||
private int createSampleDataStartupDelay = 30;
|
||||
private int createSampleDataPeriod = 60;
|
||||
private String createSampleDataOwner = "admin";
|
||||
|
||||
// Device and Registration request processing settings (DeviceProcessor, RegistrationRequestProcessor)
|
||||
private boolean enablePeriodicProcessing = true;
|
||||
private long processingStartupDelay = 10;
|
||||
private long processingPeriod = 60;
|
||||
|
||||
// Data collection settings
|
||||
private String dataCollectionRequestTopic = "ems.client.installation.requests";
|
||||
private String dataCollectionResponseTopic = "ems.client.installation.reports";
|
||||
private List<String> allowedDeviceInfoKeys = new ArrayList<>(List.of("*"));
|
||||
|
||||
// Device monitoring settings
|
||||
private String deviceStatusMonitorTopic = "_ui_instance_info"; //XXX:TODO: Change Topic name. Also update EMS config.
|
||||
private String deviceMetricsMonitorTopic = "_client_metrics"; //XXX:TODO: Change Topic name. Also update EMS config.
|
||||
|
||||
private String deviceLifeCycleRequestsTopic = "ems.client.installation.requests";
|
||||
private String deviceLifeCycleResponsesTopic = "ems.client.installation.reports";
|
||||
|
||||
// Failed devices detection
|
||||
private boolean automaticFailedDetection = true;
|
||||
private long suspectDeviceThreshold = 5; // in minutes
|
||||
private long failedDeviceThreshold = 10; // in minutes
|
||||
|
||||
// Device detailed data settings
|
||||
private String deviceInfoRequestsTopic = "ems.client.info.requests";
|
||||
private String deviceInfoResponsesTopic = "ems.client.info.reports";
|
||||
|
||||
// Archiving settings
|
||||
private boolean automaticArchivingEnabled;
|
||||
private long archivingThreshold; // in minutes
|
||||
private boolean immediatelyArchiveSuccessRequests = true;
|
||||
private boolean immediatelyArchiveOffboardedDevices = true;
|
||||
|
||||
// Encryption settings
|
||||
private boolean enableEncryption; // Set to 'true' to enable message encryption
|
||||
private boolean usePasswordGeneratedKey = true;
|
||||
private String generatedKeyFile; // NOTE: If blank, the key will be logged
|
||||
private String keyPasswordFile; // If provided, it will override the next settings
|
||||
private char[] symmetricKeyPassword;
|
||||
private byte[] salt;
|
||||
|
||||
// Users
|
||||
private List<UserData> users;
|
||||
|
||||
@Data
|
||||
public static class UserData {
|
||||
private final String username;
|
||||
@ToString.Exclude
|
||||
private final String password;
|
||||
private final List<String> roles;
|
||||
}
|
||||
}
|
@ -4,18 +4,18 @@ import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
|
||||
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
|
||||
import org.springframework.security.core.userdetails.User;
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
|
||||
import org.springframework.security.web.SecurityFilterChain;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.security.SecureRandom;
|
||||
|
||||
import static org.springframework.security.config.Customizer.withDefaults;
|
||||
|
||||
@ -39,21 +39,31 @@ public class SecurityConfig {
|
||||
return httpSecurity.build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public InMemoryUserDetailsManager userDetailsService() {
|
||||
List<UserDetails> users = new ArrayList<>();
|
||||
properties.getUsers().forEach(userData -> {
|
||||
UserDetails user = User.withUsername(userData.getUsername())
|
||||
.password(encoder().encode(userData.getPassword()))
|
||||
.roles(userData.getRoles().toArray(new String[0]))
|
||||
.build();
|
||||
users.add(user);
|
||||
});
|
||||
return new InMemoryUserDetailsManager(users.toArray(new UserDetails[0]));
|
||||
public void configAuthentication(AuthenticationManagerBuilder auth) throws Exception {
|
||||
auth.inMemoryAuthentication().passwordEncoder(passwordEncoder());
|
||||
}
|
||||
|
||||
@Bean
|
||||
public InMemoryUserDetailsManager inMemoryUserDetailsManager() {
|
||||
return new InMemoryUserDetailsManager(
|
||||
properties.getUsers().stream()
|
||||
.map(userData -> User.builder()
|
||||
.username(userData.getUsername())
|
||||
.password(userData.getPassword())
|
||||
.roles(userData.getRoles().toArray(new String[0]))
|
||||
.build())
|
||||
.toList());
|
||||
}
|
||||
|
||||
@Bean
|
||||
public PasswordEncoder passwordEncoder() {
|
||||
int strength = 10; // iterations
|
||||
return new BCryptPasswordEncoder(strength, new SecureRandom());
|
||||
}
|
||||
|
||||
/*@Bean
|
||||
public PasswordEncoder encoder() {
|
||||
// Clear-text password encoder
|
||||
return new PasswordEncoder() {
|
||||
@Override
|
||||
public String encode(CharSequence rawPassword) {
|
||||
@ -65,5 +75,5 @@ public class SecurityConfig {
|
||||
return rawPassword.toString().equals(encodedPassword);
|
||||
}
|
||||
};
|
||||
}
|
||||
}*/
|
||||
}
|
@ -0,0 +1,14 @@
|
||||
package eu.nebulous.resource.discovery;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
@Slf4j
|
||||
@RestController
|
||||
public class StatusController {
|
||||
@GetMapping(value = "/status")
|
||||
public String status() {
|
||||
return "OK";
|
||||
}
|
||||
}
|
@ -0,0 +1,231 @@
|
||||
|
||||
package eu.nebulous.resource.discovery.common;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import eu.nebulous.resource.discovery.ResourceDiscoveryProperties;
|
||||
import jakarta.jms.*;
|
||||
import lombok.NonNull;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.activemq.ActiveMQConnection;
|
||||
import org.apache.activemq.ActiveMQSslConnectionFactory;
|
||||
import org.apache.activemq.command.ActiveMQTextMessage;
|
||||
import org.apache.activemq.command.ActiveMQTopic;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.beans.factory.InitializingBean;
|
||||
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
|
||||
import org.springframework.context.annotation.Scope;
|
||||
import org.springframework.scheduling.TaskScheduler;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.*;
|
||||
|
||||
@Slf4j
|
||||
@Component
|
||||
@Scope(value = ConfigurableBeanFactory.SCOPE_SINGLETON)
|
||||
@RequiredArgsConstructor
|
||||
public class BrokerUtil implements InitializingBean, MessageListener {
|
||||
private final ResourceDiscoveryProperties properties;
|
||||
private final EncryptionUtil encryptionUtil;
|
||||
private final TaskScheduler taskScheduler;
|
||||
private final ObjectMapper objectMapper;
|
||||
private final Map<String,MessageProducer> producers = new HashMap<>();
|
||||
private final Map<String,MessageConsumer> consumers = new HashMap<>();
|
||||
private final Map<String, Set<Listener>> listeners = new HashMap<>();
|
||||
private ActiveMQConnection connection;
|
||||
private Session session;
|
||||
|
||||
@Override
|
||||
public void afterPropertiesSet() throws Exception {
|
||||
// Initialize broker connection
|
||||
taskScheduler.schedule(this::initializeBrokerConnection,
|
||||
Instant.now().plusSeconds(properties.getSubscriptionStartupDelay()));
|
||||
|
||||
// Initialize connection health check
|
||||
int healthCheckPeriod = properties.getConnectionHealthCheckPeriod();
|
||||
if (healthCheckPeriod>0) {
|
||||
taskScheduler.scheduleAtFixedRate(this::connectionHealthCheck,
|
||||
Instant.now().plusSeconds(properties.getSubscriptionStartupDelay()),
|
||||
Duration.ofSeconds(healthCheckPeriod));
|
||||
log.info("BrokerUtil: Enabled connection health check: period={}s", healthCheckPeriod);
|
||||
}
|
||||
}
|
||||
|
||||
private void initializeBrokerConnection() {
|
||||
try {
|
||||
openBrokerConnection();
|
||||
} catch (Exception e) {
|
||||
log.error("BrokerUtil: ERROR while opening connection to Message broker: ", e);
|
||||
taskScheduler.schedule(this::initializeBrokerConnection,
|
||||
Instant.now().plusSeconds(properties.getSubscriptionRetryDelay()));
|
||||
}
|
||||
}
|
||||
|
||||
private void openBrokerConnection() throws Exception {
|
||||
ActiveMQSslConnectionFactory cf = new ActiveMQSslConnectionFactory(properties.getBrokerURL());
|
||||
cf.setUserName(properties.getBrokerUsername());
|
||||
cf.setPassword(properties.getBrokerPassword());
|
||||
|
||||
log.debug("BrokerUtil: Keystore and Truststore settings: keystore-file={}, keystore-type={}, truststore-file={}, truststore-type={}",
|
||||
properties.getKeyStoreFile(), properties.getKeyStoreType(), properties.getTrustStoreFile(), properties.getTrustStoreType());
|
||||
if (StringUtils.isNotBlank(properties.getKeyStoreFile())) {
|
||||
cf.setKeyStore(properties.getKeyStoreFile());
|
||||
cf.setKeyStorePassword(properties.getKeyStorePassword());
|
||||
cf.setKeyStoreType(properties.getKeyStoreType());
|
||||
}
|
||||
if (StringUtils.isNotBlank(properties.getTrustStoreFile())) {
|
||||
cf.setTrustStore(properties.getTrustStoreFile());
|
||||
cf.setTrustStorePassword(properties.getTrustStorePassword());
|
||||
cf.setTrustStoreType(properties.getKeyStoreType());
|
||||
}
|
||||
cf.setWatchTopicAdvisories(true);
|
||||
|
||||
ActiveMQConnection conn = (ActiveMQConnection) cf.createConnection();
|
||||
Session ses = conn.createSession();
|
||||
conn.start();
|
||||
this.connection = conn;
|
||||
this.session = ses;
|
||||
log.info("BrokerUtil: Opened connection to Message broker: {}", properties.getBrokerURL());
|
||||
}
|
||||
|
||||
private void closeBrokerConnection() throws JMSException {
|
||||
producers.clear();
|
||||
consumers.clear();
|
||||
listeners.clear();
|
||||
if (session!=null) this.session.close();
|
||||
if (connection!=null && ! connection.isClosed() && ! connection.isClosing())
|
||||
this.connection.close();
|
||||
this.session = null;
|
||||
this.connection = null;
|
||||
log.info("BrokerUtil: Closed connection to Message broker: {}", properties.getBrokerURL());
|
||||
}
|
||||
|
||||
public void connectionHealthCheck() {
|
||||
log.debug("BrokerUtil: Checking connection health: {}", properties.getBrokerURL());
|
||||
boolean error = false;
|
||||
try {
|
||||
sendMessage(properties.getHealthCheckTopic(), Map.of("ping", "pong"));
|
||||
} catch (JMSException | JsonProcessingException e) {
|
||||
log.warn("BrokerUtil: EXCEPTION during connection health: ", e);
|
||||
error = true;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
// Close connection
|
||||
try {
|
||||
closeBrokerConnection();
|
||||
} catch (JMSException e) {
|
||||
log.error("BrokerUtil: ERROR while closing connection to Message broker: ", e);
|
||||
this.session = null;
|
||||
this.connection = null;
|
||||
}
|
||||
|
||||
// Try to re-connect
|
||||
taskScheduler.schedule(this::initializeBrokerConnection,
|
||||
Instant.now().plusSeconds(1));
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
|
||||
public void sendMessage(@NonNull String topic, @NonNull Map<String,? extends Object> message) throws JMSException, JsonProcessingException {
|
||||
sendMessage(topic, message, false);
|
||||
}
|
||||
|
||||
public void sendMessage(@NonNull String topic, @NonNull Map<String,? extends Object> message, boolean encrypt) throws JMSException, JsonProcessingException {
|
||||
String jsonMessage = objectMapper.writer().writeValueAsString(message);
|
||||
sendMessage(topic, jsonMessage, encrypt);
|
||||
}
|
||||
|
||||
public void sendMessage(@NonNull String topic, @NonNull String message) throws JMSException, JsonProcessingException {
|
||||
sendMessage(topic, message, false);
|
||||
}
|
||||
|
||||
public void sendMessage(@NonNull String topic, @NonNull String message, boolean encrypt) throws JMSException, JsonProcessingException {
|
||||
ActiveMQTextMessage textMessage = new ActiveMQTextMessage();
|
||||
if (encrypt) {
|
||||
sendMessage(topic, Map.of("encrypted-message", encryptionUtil.encryptText(message)), false);
|
||||
} else {
|
||||
textMessage.setText(message);
|
||||
getOrCreateProducer(topic).send(textMessage);
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
|
||||
public MessageProducer getOrCreateProducer(@NonNull String topic) throws JMSException {
|
||||
MessageProducer producer = producers.get(topic);
|
||||
if (producer==null) { producer = createProducer(topic); producers.put(topic, producer); }
|
||||
return producer;
|
||||
}
|
||||
|
||||
public MessageConsumer getOrCreateConsumer(@NonNull String topic) throws JMSException {
|
||||
MessageConsumer consumer = consumers.get(topic);
|
||||
if (consumer==null) { consumer = createConsumer(topic); consumers.put(topic, consumer); }
|
||||
return consumer;
|
||||
}
|
||||
|
||||
public MessageProducer createProducer(@NonNull String topic) throws JMSException {
|
||||
if (session==null) initializeBrokerConnection();
|
||||
return session.createProducer(new ActiveMQTopic(topic));
|
||||
}
|
||||
|
||||
public MessageConsumer createConsumer(@NonNull String topic) throws JMSException {
|
||||
if (session==null) initializeBrokerConnection();
|
||||
return session.createConsumer(new ActiveMQTopic(topic));
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
|
||||
public void subscribe(@NonNull String topic, @NonNull Listener listener) throws JMSException {
|
||||
Set<Listener> set = listeners.computeIfAbsent(topic, t -> new HashSet<>());
|
||||
if (set.contains(listener)) return;
|
||||
set.add(listener);
|
||||
getOrCreateConsumer(topic).setMessageListener(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMessage(Message message) {
|
||||
try {
|
||||
log.debug("BrokerUtil: Received a message from broker: {}", message);
|
||||
if (message instanceof ActiveMQTextMessage textMessage) {
|
||||
String payload = textMessage.getText();
|
||||
log.trace("BrokerUtil: Message payload: {}", payload);
|
||||
|
||||
TypeReference<Map<String,Object>> typeRef = new TypeReference<>() { };
|
||||
Object obj = objectMapper.readerFor(typeRef).readValue(payload);
|
||||
|
||||
if (obj instanceof Map<?,?> dataMap) {
|
||||
handlePayload(((ActiveMQTextMessage) message).getDestination().getPhysicalName(), dataMap);
|
||||
} else {
|
||||
log.debug("BrokerUtil: Message payload is not recognized. Expected Map but got: type={}, object={}", obj.getClass().getName(), obj);
|
||||
}
|
||||
} else {
|
||||
log.debug("BrokerUtil: Message type is not supported: {}", message);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("BrokerUtil: ERROR while processing message: {}\nException: ", message, e);
|
||||
}
|
||||
}
|
||||
|
||||
private void handlePayload(@NonNull String topic, @NonNull Map<?, ?> dataMap) {
|
||||
// Decrypt message (if encrypted)
|
||||
Object encryptedMessage = dataMap.get("encrypted-message");
|
||||
if (encryptedMessage!=null)
|
||||
dataMap = encryptionUtil.decryptMap(encryptedMessage.toString());
|
||||
|
||||
// Dispatch message to listeners
|
||||
Set<Listener> set = listeners.get(topic);
|
||||
if (set==null) return;
|
||||
final Map<?, ?> immutableMap = Collections.unmodifiableMap(dataMap);
|
||||
set.forEach(l -> l.onMessage(immutableMap));
|
||||
}
|
||||
|
||||
public interface Listener {
|
||||
void onMessage(Map map);
|
||||
}
|
||||
}
|
@ -0,0 +1,25 @@
|
||||
package eu.nebulous.resource.discovery.common;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.experimental.SuperBuilder;
|
||||
|
||||
@Data
|
||||
@SuperBuilder
|
||||
@NoArgsConstructor
|
||||
public class DeviceLocation {
|
||||
private String id;
|
||||
private String name;
|
||||
private String continent;
|
||||
private String continentCode;
|
||||
private String country;
|
||||
private String countryCode;
|
||||
private String state;
|
||||
private String stateCode;
|
||||
private String city;
|
||||
private String zipcode;
|
||||
private String address;
|
||||
private String extra;
|
||||
private double latitude;
|
||||
private double longitude;
|
||||
}
|
@ -0,0 +1,231 @@
|
||||
|
||||
package eu.nebulous.resource.discovery.common;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import eu.nebulous.resource.discovery.ResourceDiscoveryProperties;
|
||||
import lombok.NonNull;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.tomcat.util.codec.binary.Base64;
|
||||
import org.springframework.beans.factory.InitializingBean;
|
||||
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
|
||||
import org.springframework.context.annotation.Scope;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import javax.crypto.*;
|
||||
import javax.crypto.spec.PBEKeySpec;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
import javax.security.auth.DestroyFailedException;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Paths;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.spec.InvalidKeySpecException;
|
||||
import java.security.spec.KeySpec;
|
||||
import java.util.Arrays;
|
||||
import java.util.Map;
|
||||
|
||||
/*
|
||||
* SEE:
|
||||
* https://www.baeldung.com/java-aes-encryption-decryption
|
||||
* https://stackoverflow.com/questions/6538485/java-using-aes-256-and-128-symmetric-key-encryption
|
||||
*/
|
||||
|
||||
@Slf4j
|
||||
@Component
|
||||
@Scope(value = ConfigurableBeanFactory.SCOPE_SINGLETON)
|
||||
@RequiredArgsConstructor
|
||||
public class EncryptionUtil implements InitializingBean {
|
||||
public final static String KEY_FACTORY_ALGORITHM = "PBKDF2WithHmacSHA256";
|
||||
public final static String KEY_GEN_ALGORITHM = "AES";
|
||||
public final static String CIPHER_ALGORITHM = "AES";
|
||||
//public final static String CIPHER_ALGORITHM = "AES/CTR/PKCS5Padding";
|
||||
public final static int SIZE = 256;
|
||||
public final static int ITERATION_COUNT = 65536;
|
||||
|
||||
public final static byte SEPARATOR = 32;
|
||||
public final static byte NULL = 0;
|
||||
public final static char NULL_CHAR = '\0';
|
||||
|
||||
private final ResourceDiscoveryProperties properties;
|
||||
private final ObjectMapper objectMapper;
|
||||
private SecretKey key;
|
||||
|
||||
@Override
|
||||
public void afterPropertiesSet() throws Exception {
|
||||
if (properties.isEnableEncryption())
|
||||
initializeSymmetricKey();
|
||||
}
|
||||
|
||||
private void initializeSymmetricKey() throws NoSuchAlgorithmException, InvalidKeySpecException, IOException {
|
||||
if (properties.isUsePasswordGeneratedKey()) {
|
||||
char[] password;
|
||||
byte[] salt;
|
||||
if (StringUtils.isNotBlank(properties.getKeyPasswordFile())) {
|
||||
// Read key password from file
|
||||
File file = Paths.get(properties.getKeyPasswordFile()).toFile();
|
||||
try (FileInputStream in = new FileInputStream(file)) {
|
||||
byte[] bytes = in.readAllBytes();
|
||||
// find separator
|
||||
int ii = Arrays.binarySearch(bytes, SEPARATOR);
|
||||
password = new char[ii/2];
|
||||
for (int j=0, k=0; j<bytes.length; j+=2, k++)
|
||||
password[k] = (char)((bytes[j]<<8) | (bytes[j+1] & 255));
|
||||
salt = Arrays.copyOfRange(bytes, ii+1, bytes.length);
|
||||
// Clear bytes
|
||||
Arrays.fill(bytes, NULL);
|
||||
}
|
||||
} else {
|
||||
// Read key password from properties
|
||||
password = properties.getSymmetricKeyPassword();
|
||||
salt = properties.getSalt();
|
||||
}
|
||||
|
||||
// Generate key from password
|
||||
key = getKeyFromPassword(password, salt);
|
||||
|
||||
// Clear key password from properties and variables
|
||||
Arrays.fill(properties.getSymmetricKeyPassword(), NULL_CHAR);
|
||||
Arrays.fill(properties.getSalt(), NULL);
|
||||
Arrays.fill(password, NULL_CHAR);
|
||||
Arrays.fill(salt, NULL);
|
||||
} else {
|
||||
// Generate new key
|
||||
key = generateKey();
|
||||
if (StringUtils.isNotBlank(properties.getGeneratedKeyFile())) {
|
||||
// Write generated key to file
|
||||
File file = Paths.get(properties.getGeneratedKeyFile()).toFile();
|
||||
try (FileOutputStream out = new FileOutputStream(file)) {
|
||||
out.write(key.getAlgorithm().getBytes(StandardCharsets.UTF_8));
|
||||
out.write(SEPARATOR); // space
|
||||
out.write(key.getFormat().getBytes(StandardCharsets.UTF_8));
|
||||
out.write(SEPARATOR); // space
|
||||
out.write(key.getEncoded());
|
||||
}
|
||||
log.info("EncryptionUtil: Generated key stored in file: {}", file);
|
||||
} else {
|
||||
// Log generated key
|
||||
log.info("EncryptionUtil: Generated key: {} {} {}",
|
||||
key.getAlgorithm(), key.getFormat(), Base64.encodeBase64String(key.getEncoded()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public SecretKey generateKey() throws NoSuchAlgorithmException {
|
||||
//final SecretKeySpec secretKey = new SecretKeySpec(keyStr.getBytes(StandardCharsets.UTF_8), "AES");
|
||||
KeyGenerator generator = KeyGenerator.getInstance(KEY_GEN_ALGORITHM);
|
||||
generator.init(SIZE);
|
||||
return generator.generateKey();
|
||||
}
|
||||
|
||||
public SecretKey getKeyFromPassword(char[] password, byte[] salt)
|
||||
throws NoSuchAlgorithmException, InvalidKeySpecException
|
||||
{
|
||||
SecretKeyFactory factory = SecretKeyFactory.getInstance(KEY_FACTORY_ALGORITHM);
|
||||
KeySpec spec = new PBEKeySpec(password, salt, ITERATION_COUNT, SIZE);
|
||||
return new SecretKeySpec(factory
|
||||
.generateSecret(spec).getEncoded(), KEY_GEN_ALGORITHM);
|
||||
}
|
||||
|
||||
public void destroyKey() throws DestroyFailedException {
|
||||
if (key!=null) {
|
||||
key.destroy();
|
||||
key = null;
|
||||
}
|
||||
}
|
||||
|
||||
public String encryptText(@NonNull String message) {
|
||||
if (! properties.isEnableEncryption()) return message;
|
||||
try {
|
||||
return encrypt(message.getBytes(StandardCharsets.UTF_8), key);
|
||||
} catch (IllegalBlockSizeException | NoSuchPaddingException | BadPaddingException |
|
||||
NoSuchAlgorithmException | InvalidKeyException e)
|
||||
{
|
||||
log.warn("EncryptionUtil: ERROR while encrypting message: ", e);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public String decryptText(@NonNull String message) {
|
||||
if (! properties.isEnableEncryption()) return message;
|
||||
try {
|
||||
return new String(decrypt(message, key), StandardCharsets.UTF_8);
|
||||
} catch (NoSuchPaddingException | IllegalBlockSizeException | NoSuchAlgorithmException
|
||||
| BadPaddingException | InvalidKeyException e)
|
||||
{
|
||||
log.warn("EncryptionUtil: ERROR while decrypting message: ", e);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public String encryptMap(@NonNull Map<?,?> message) {
|
||||
try {
|
||||
if (! properties.isEnableEncryption()) return objectMapper.writeValueAsString(message);
|
||||
|
||||
byte[] bytes = objectMapper.writeValueAsBytes(message);
|
||||
|
||||
try {
|
||||
return encrypt(bytes, key);
|
||||
} catch (IllegalBlockSizeException | NoSuchPaddingException | BadPaddingException |
|
||||
NoSuchAlgorithmException | InvalidKeyException e)
|
||||
{
|
||||
log.warn("EncryptionUtil: ERROR while encrypting message: ", e);
|
||||
} finally {
|
||||
Arrays.fill(bytes, NULL);
|
||||
}
|
||||
} catch (JsonProcessingException e) {
|
||||
log.warn("EncryptionUtil: ERROR while converting message (Map) to bytes: ", e);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public Map<?, ?> decryptMap(@NonNull String message) {
|
||||
try {
|
||||
if (! properties.isEnableEncryption()) return objectMapper.readValue(message, Map.class);
|
||||
|
||||
byte[] bytes = decrypt(message, key);
|
||||
|
||||
try {
|
||||
TypeReference<Map<String, Object>> tr = new TypeReference<>() {};
|
||||
return objectMapper.readValue(bytes, tr);
|
||||
} catch (IOException e) {
|
||||
log.warn("EncryptionUtil: ERROR while converting decrypted bytes to Map: ", e);
|
||||
} finally {
|
||||
Arrays.fill(bytes, NULL);
|
||||
}
|
||||
} catch (NoSuchPaddingException | IllegalBlockSizeException | NoSuchAlgorithmException
|
||||
| BadPaddingException | InvalidKeyException | IOException e)
|
||||
{
|
||||
log.warn("EncryptionUtil: ERROR while decrypting message: ", e);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private String encrypt(byte[] bytesToEncrypt, SecretKey secretKey)
|
||||
throws IllegalBlockSizeException, BadPaddingException, NoSuchPaddingException,
|
||||
NoSuchAlgorithmException, InvalidKeyException
|
||||
{
|
||||
Cipher cipher = Cipher.getInstance(CIPHER_ALGORITHM);
|
||||
cipher.init(Cipher.ENCRYPT_MODE, secretKey);
|
||||
return Base64.encodeBase64String(
|
||||
cipher.doFinal(bytesToEncrypt));
|
||||
}
|
||||
|
||||
private byte[] decrypt(final String encryptedMessage, SecretKey secretKey)
|
||||
throws NoSuchPaddingException, NoSuchAlgorithmException, IllegalBlockSizeException,
|
||||
BadPaddingException, InvalidKeyException
|
||||
{
|
||||
//final SecretKeySpec secretKey = new SecretKeySpec(keyStr.getBytes(StandardCharsets.UTF_8), "AES");
|
||||
final Cipher c = Cipher.getInstance(CIPHER_ALGORITHM);
|
||||
c.init(Cipher.DECRYPT_MODE, secretKey);
|
||||
return c.doFinal(
|
||||
Base64.decodeBase64(encryptedMessage));
|
||||
}
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
package eu.nebulous.resource.discovery.common;
|
||||
|
||||
public enum REQUEST_TYPE {
|
||||
INSTALL, REINSTALL, UNINSTALL, NODE_DETAILS, INFO, DIAGNOSTICS, OTHER
|
||||
}
|
@ -0,0 +1,161 @@
|
||||
|
||||
package eu.nebulous.resource.discovery.monitor;
|
||||
|
||||
import eu.nebulous.resource.discovery.ResourceDiscoveryProperties;
|
||||
import eu.nebulous.resource.discovery.monitor.model.Device;
|
||||
import eu.nebulous.resource.discovery.monitor.model.DeviceStatus;
|
||||
import eu.nebulous.resource.discovery.monitor.service.DeviceManagementService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.InitializingBean;
|
||||
import org.springframework.scheduling.TaskScheduler;
|
||||
import org.springframework.scheduling.annotation.EnableAsync;
|
||||
import org.springframework.scheduling.annotation.EnableScheduling;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.time.ZoneId;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@EnableAsync
|
||||
@EnableScheduling
|
||||
@RequiredArgsConstructor
|
||||
public class DeviceProcessor implements InitializingBean {
|
||||
private final static List<DeviceStatus> STATUSES_TO_EXCLUDE_FROM_SUSPECT_CHECK = List.of(
|
||||
DeviceStatus.ON_HOLD, DeviceStatus.ONBOARDING, DeviceStatus.FAILED,
|
||||
DeviceStatus.OFFBOARDING, DeviceStatus.OFFBOARDED, DeviceStatus.OFFBOARD_ERROR
|
||||
);
|
||||
private final static List<DeviceStatus> STATUSES_TO_ARCHIVE = List.of(
|
||||
DeviceStatus.FAILED,
|
||||
DeviceStatus.OFFBOARDED,
|
||||
DeviceStatus.OFFBOARD_ERROR
|
||||
);
|
||||
|
||||
private final ResourceDiscoveryProperties processorProperties;
|
||||
private final DeviceManagementService deviceManagementService;
|
||||
private final TaskScheduler taskScheduler;
|
||||
private final AtomicBoolean isRunning = new AtomicBoolean(false);
|
||||
|
||||
@Override
|
||||
public void afterPropertiesSet() throws Exception {
|
||||
// Check configuration
|
||||
Instant suspectDeviceThreshold = Instant.now().minus(processorProperties.getSuspectDeviceThreshold(), ChronoUnit.MINUTES);
|
||||
Instant failedDeviceThreshold = Instant.now().minus(processorProperties.getFailedDeviceThreshold(), ChronoUnit.MINUTES);
|
||||
if (suspectDeviceThreshold.isBefore(failedDeviceThreshold))
|
||||
throw new IllegalArgumentException("DeviceProcessor: Configuration error: suspectDeviceThreshold is before failedDeviceThreshold: "
|
||||
+ processorProperties.getSuspectDeviceThreshold() + " < " + processorProperties.getFailedDeviceThreshold());
|
||||
|
||||
// Initialize periodic device processing
|
||||
if (processorProperties.isEnablePeriodicProcessing()) {
|
||||
Instant firstRun;
|
||||
taskScheduler.scheduleAtFixedRate(this::processDevices,
|
||||
firstRun = Instant.now().plusSeconds(processorProperties.getProcessingStartupDelay()),
|
||||
Duration.ofSeconds(processorProperties.getProcessingPeriod()));
|
||||
log.info("DeviceProcessor: Started periodic device processing: period={}s, first-run-at={}",
|
||||
processorProperties.getProcessingPeriod(), firstRun.atZone(ZoneId.systemDefault()));
|
||||
} else {
|
||||
log.info("DeviceProcessor: Periodic device processing is disabled. You can still invoke it through GUI");
|
||||
}
|
||||
}
|
||||
|
||||
public Future<String> processDevices() {
|
||||
try {
|
||||
// Check and set if already running
|
||||
if (!isRunning.compareAndSet(false, true)) {
|
||||
log.warn("processDevices: Already running");
|
||||
return CompletableFuture.completedFuture("ALREADY RUNNING");
|
||||
}
|
||||
log.debug("processDevices: Processing devices");
|
||||
|
||||
// Process requests
|
||||
try {
|
||||
if (processorProperties.isAutomaticFailedDetection())
|
||||
processFailedDevices();
|
||||
if (processorProperties.isAutomaticArchivingEnabled())
|
||||
archiveDevices();
|
||||
} catch (Throwable t) {
|
||||
log.error("processDevices: ERROR while processing devices: ", t);
|
||||
}
|
||||
|
||||
log.debug("processDevices: Processing completed");
|
||||
|
||||
return CompletableFuture.completedFuture("DONE");
|
||||
} catch (Throwable e) {
|
||||
log.error("processDevices: EXCEPTION: ", e);
|
||||
return CompletableFuture.completedFuture("ERROR: "+e.getMessage());
|
||||
} finally {
|
||||
// Clear running flag
|
||||
isRunning.set(false);
|
||||
}
|
||||
}
|
||||
|
||||
private void processFailedDevices() {
|
||||
Instant suspectDeviceThreshold = Instant.now().minus(processorProperties.getSuspectDeviceThreshold(), ChronoUnit.MINUTES);
|
||||
Instant failedDeviceThreshold = Instant.now().minus(processorProperties.getFailedDeviceThreshold(), ChronoUnit.MINUTES);
|
||||
log.trace("processFailedDevices: BEGIN: suspect-threshold={}, failed-threshold={}",
|
||||
suspectDeviceThreshold, failedDeviceThreshold);
|
||||
List<Device> suspectDevices = deviceManagementService.getAll().stream()
|
||||
.filter(r -> ! STATUSES_TO_EXCLUDE_FROM_SUSPECT_CHECK.contains(r.getStatus()))
|
||||
.filter(r -> r.getStatusUpdate()==null || r.getStatusUpdate().getStateLastUpdate().isBefore(suspectDeviceThreshold))
|
||||
.filter(r -> r.getMetrics()==null || r.getMetrics().getTimestamp().isBefore(suspectDeviceThreshold))
|
||||
.filter(r -> r.getCreationDate().isBefore(suspectDeviceThreshold))
|
||||
.toList();
|
||||
|
||||
if (log.isDebugEnabled())
|
||||
log.debug("processFailedDevices: Found {} suspect devices: {}",
|
||||
suspectDevices.size(), suspectDevices.stream().map(Device::getId).toList());
|
||||
|
||||
for (Device device : suspectDevices) {
|
||||
// Mark device as suspect
|
||||
log.debug("processFailedDevices: Marking as suspect device with Id: {}", device.getId());
|
||||
device.setStatus(DeviceStatus.SUSPECT);
|
||||
if (device.getSuspectTimestamp()==null) {
|
||||
device.setSuspectTimestamp(Instant.now());
|
||||
device.setRetries(0);
|
||||
log.info("processFailedDevices: Marked as suspect device with Id: {}", device.getId());
|
||||
} else {
|
||||
device.incrementRetries();
|
||||
}
|
||||
|
||||
// If fail threshold exceeded the mark device as PROBLEMATIC
|
||||
if ( (device.getStatusUpdate()==null || device.getStatusUpdate().getStateLastUpdate().isBefore(failedDeviceThreshold))
|
||||
&& (device.getMetrics()==null || device.getMetrics().getTimestamp().isBefore(failedDeviceThreshold))
|
||||
&& device.getCreationDate().isBefore(failedDeviceThreshold) )
|
||||
{
|
||||
device.setStatus(DeviceStatus.FAILED);
|
||||
log.warn("processFailedDevices: Marked as FAILED device with Id: {}", device.getId());
|
||||
}
|
||||
|
||||
deviceManagementService.update(device);
|
||||
}
|
||||
|
||||
log.trace("processProblematicDevices: END");
|
||||
}
|
||||
|
||||
private void archiveDevices() {
|
||||
Instant archiveThreshold = Instant.now().minus(processorProperties.getArchivingThreshold(), ChronoUnit.MINUTES);
|
||||
log.trace("archiveDevices: BEGIN: archive-threshold: {}", archiveThreshold);
|
||||
List<Device> devicesForArchiving = deviceManagementService.getAll().stream()
|
||||
.filter(r -> STATUSES_TO_ARCHIVE.contains(r.getStatus()))
|
||||
.filter(r -> r.getLastUpdateDate().isBefore(archiveThreshold))
|
||||
.toList();
|
||||
|
||||
log.debug("archiveDevices: Found {} devices for archiving: {}",
|
||||
devicesForArchiving.size(), devicesForArchiving.stream().map(Device::getId).toList());
|
||||
|
||||
for (Device device : devicesForArchiving) {
|
||||
log.debug("archiveDevices: Archiving device with Id: {}", device.getId());
|
||||
deviceManagementService.archiveDeviceBySystem(device.getId());
|
||||
log.info("archiveDevices: Archived device with Id: {}", device.getId());
|
||||
}
|
||||
|
||||
log.trace("archiveDevices: END");
|
||||
}
|
||||
}
|
@ -6,12 +6,12 @@ import eu.nebulous.resource.discovery.monitor.model.DeviceException;
|
||||
import eu.nebulous.resource.discovery.monitor.service.DeviceManagementService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Slf4j
|
||||
@RestController
|
||||
@ -42,9 +42,10 @@ public class ArchivedDeviceManagementController {
|
||||
return deviceService.getArchivedByIpAddress(ipAddress);
|
||||
}
|
||||
|
||||
@GetMapping(value = "/device/{id}/unarchive", produces = MediaType.APPLICATION_JSON_VALUE)
|
||||
public String unarchiveDevice(@PathVariable String id) {
|
||||
deviceService.unarchiveDevice(id);
|
||||
@PostMapping(value = "/device/{id}/unarchive",
|
||||
consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
|
||||
public String unarchiveDevice(@PathVariable String id, @RequestBody Map<String,String> credentials) {
|
||||
deviceService.unarchiveDevice(id, credentials);
|
||||
return "UNARCHIVED";
|
||||
}
|
||||
}
|
@ -1,11 +1,16 @@
|
||||
package eu.nebulous.resource.discovery.monitor.controller;
|
||||
|
||||
import eu.nebulous.resource.discovery.monitor.DeviceProcessor;
|
||||
import eu.nebulous.resource.discovery.monitor.model.ArchivedDevice;
|
||||
import eu.nebulous.resource.discovery.monitor.model.Device;
|
||||
import eu.nebulous.resource.discovery.monitor.model.DeviceException;
|
||||
import eu.nebulous.resource.discovery.monitor.service.DeviceLifeCycleRequestService;
|
||||
import eu.nebulous.resource.discovery.monitor.service.DeviceManagementService;
|
||||
import eu.nebulous.resource.discovery.registration.model.RegistrationRequestException;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.security.core.Authentication;
|
||||
@ -13,6 +18,9 @@ import org.springframework.security.core.GrantedAuthority;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.Future;
|
||||
|
||||
@Slf4j
|
||||
@RestController
|
||||
@ -20,7 +28,9 @@ import java.util.List;
|
||||
@RequestMapping("/monitor")
|
||||
@PreAuthorize("hasAuthority('ROLE_ADMIN')")
|
||||
public class DeviceManagementController {
|
||||
private final DeviceProcessor deviceProcessor;
|
||||
private final DeviceManagementService deviceService;
|
||||
private final DeviceLifeCycleRequestService deviceLifeCycleRequestService;
|
||||
|
||||
private boolean isAuthenticated(Authentication authentication) {
|
||||
return authentication!=null && StringUtils.isNotBlank(authentication.getName());
|
||||
@ -89,9 +99,76 @@ public class DeviceManagementController {
|
||||
deviceService.deleteById(id);
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
|
||||
@PreAuthorize("hasAuthority('ROLE_ADMIN') || hasAuthority('ROLE_USER')")
|
||||
@GetMapping(value = "/device/{id}/onboard")
|
||||
public void onboardDevice(@PathVariable String id) {
|
||||
deviceLifeCycleRequestService.reinstallRequest(id);
|
||||
}
|
||||
|
||||
@PreAuthorize("hasAuthority('ROLE_ADMIN') || hasAuthority('ROLE_USER')")
|
||||
@GetMapping(value = "/device/{id}/offboard")
|
||||
public void offboardDevice(@PathVariable String id) {
|
||||
deviceLifeCycleRequestService.uninstallRequest(id);
|
||||
}
|
||||
|
||||
@PreAuthorize("hasAuthority('ROLE_ADMIN') || hasAuthority('ROLE_USER')")
|
||||
@GetMapping(value = "/request-update")
|
||||
public String requestUpdate() {
|
||||
deviceLifeCycleRequestService.requestInfoUpdate();
|
||||
return "REQUESTED-UPDATE";
|
||||
}
|
||||
|
||||
@GetMapping(value = "/device/process")
|
||||
public Map<String, String> processDevices() throws ExecutionException, InterruptedException {
|
||||
Future<String> future = deviceProcessor.processDevices();
|
||||
return Map.of("result", future.isDone() ? future.get() : "STARTED");
|
||||
}
|
||||
|
||||
@GetMapping(value = "/device/{id}/archive", produces = MediaType.APPLICATION_JSON_VALUE)
|
||||
public String archiveDevice(@PathVariable String id) {
|
||||
deviceService.archiveDevice(id);
|
||||
return "ARCHIVED";
|
||||
}
|
||||
|
||||
@PostMapping(value = "/device/{id}/unarchive",
|
||||
consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
|
||||
public String unarchiveDevice(@PathVariable String id, @RequestBody Map<String,String> credentials) {
|
||||
deviceService.unarchiveDevice(id, credentials);
|
||||
return "UNARCHIVED";
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
|
||||
@PreAuthorize("hasAuthority('ROLE_ADMIN') || hasAuthority('ROLE_USER')")
|
||||
@GetMapping(value = "/device/archived", produces = MediaType.APPLICATION_JSON_VALUE)
|
||||
public List<ArchivedDevice> listArchivedRequests(Authentication authentication) {
|
||||
return deviceService.getArchivedByOwner(authentication);
|
||||
}
|
||||
|
||||
@GetMapping(value = "/device/archived/all", produces = MediaType.APPLICATION_JSON_VALUE)
|
||||
public List<ArchivedDevice> listArchivedRequestsAdmin() {
|
||||
return deviceService.getArchivedAll();
|
||||
}
|
||||
|
||||
@PreAuthorize("hasAuthority('ROLE_ADMIN') || hasAuthority('ROLE_USER')")
|
||||
@GetMapping(value = "/device/archived/{id}", produces = MediaType.APPLICATION_JSON_VALUE)
|
||||
public ArchivedDevice getArchivedRequest(@PathVariable String id, Authentication authentication) {
|
||||
return deviceService.getArchivedById(id, authentication)
|
||||
.orElseThrow(() -> new RegistrationRequestException("Not found archived registration request with id: "+id));
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
|
||||
@ResponseStatus(value = HttpStatus.BAD_REQUEST)
|
||||
@ExceptionHandler(DeviceException.class)
|
||||
public Map<String,Object> handleRegistrationRequestException(DeviceException exception) {
|
||||
return Map.of(
|
||||
"status", HttpStatus.BAD_REQUEST.value(),
|
||||
"timestamp", System.currentTimeMillis(),
|
||||
"exception", exception.getClass().getName(),
|
||||
"message", exception.getMessage()
|
||||
);
|
||||
}
|
||||
}
|
@ -1,10 +1,8 @@
|
||||
package eu.nebulous.resource.discovery.monitor.model;
|
||||
|
||||
import eu.nebulous.resource.discovery.registration.model.RegistrationRequest;
|
||||
import lombok.AccessLevel;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import eu.nebulous.resource.discovery.common.DeviceLocation;
|
||||
import lombok.*;
|
||||
import lombok.experimental.SuperBuilder;
|
||||
import org.springframework.data.mongodb.core.mapping.Document;
|
||||
|
||||
@ -23,12 +21,17 @@ public class Device {
|
||||
private String name;
|
||||
private String owner;
|
||||
private String ipAddress;
|
||||
private DeviceLocation location;
|
||||
private String username;
|
||||
@ToString.Exclude
|
||||
@JsonProperty(access = JsonProperty.Access.WRITE_ONLY)
|
||||
private char[] password;
|
||||
@ToString.Exclude
|
||||
@JsonProperty(access = JsonProperty.Access.WRITE_ONLY)
|
||||
private char[] publicKey;
|
||||
private Map<String, String> deviceInfo;
|
||||
|
||||
private RegistrationRequest request;
|
||||
//private RegistrationRequest request;
|
||||
private String requestId;
|
||||
private Instant creationDate;
|
||||
private Instant lastUpdateDate;
|
||||
@ -38,4 +41,14 @@ public class Device {
|
||||
private String nodeReference;
|
||||
@Setter(AccessLevel.NONE)
|
||||
private List<String> messages = new ArrayList<>();
|
||||
|
||||
private DeviceStatusUpdate statusUpdate;
|
||||
private DeviceMetrics metrics;
|
||||
|
||||
private Instant suspectTimestamp;
|
||||
private int retries;
|
||||
|
||||
public void incrementRetries() {
|
||||
retries++;
|
||||
}
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
package eu.nebulous.resource.discovery.monitor.model;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Data
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public class DeviceMetrics {
|
||||
private String ipAddress;
|
||||
private String clientId;
|
||||
private Instant timestamp;
|
||||
private Map<String, Object> metrics;
|
||||
private List<Object> latestEvents;
|
||||
}
|
@ -3,6 +3,7 @@ package eu.nebulous.resource.discovery.monitor.model;
|
||||
public enum DeviceStatus {
|
||||
NEW_DEVICE, ON_HOLD,
|
||||
ONBOARDING, ONBOARDED, ONBOARD_ERROR,
|
||||
HEALTHY, BUSY, IDLE,
|
||||
HEALTHY, SUSPECT, FAILED,
|
||||
BUSY, IDLE,
|
||||
OFFBOARDING, OFFBOARDED, OFFBOARD_ERROR
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
package eu.nebulous.resource.discovery.monitor.model;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public class DeviceStatusUpdate {
|
||||
private String ipAddress;
|
||||
private String clientId;
|
||||
private String state;
|
||||
private Instant stateLastUpdate;
|
||||
private String reference;
|
||||
private List<String> errors;
|
||||
}
|
@ -0,0 +1,72 @@
|
||||
|
||||
package eu.nebulous.resource.discovery.monitor.service;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import eu.nebulous.resource.discovery.ResourceDiscoveryProperties;
|
||||
import eu.nebulous.resource.discovery.common.BrokerUtil;
|
||||
import eu.nebulous.resource.discovery.monitor.model.Device;
|
||||
import eu.nebulous.resource.discovery.monitor.model.DeviceStatus;
|
||||
import lombok.NonNull;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.InitializingBean;
|
||||
import org.springframework.scheduling.TaskScheduler;
|
||||
import org.springframework.scheduling.annotation.EnableAsync;
|
||||
import org.springframework.scheduling.annotation.EnableScheduling;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@EnableAsync
|
||||
@EnableScheduling
|
||||
@RequiredArgsConstructor
|
||||
public abstract class AbstractMonitorService implements InitializingBean, BrokerUtil.Listener {
|
||||
@NonNull protected final String name;
|
||||
protected final ResourceDiscoveryProperties monitorProperties;
|
||||
protected final TaskScheduler taskScheduler;
|
||||
protected final ObjectMapper objectMapper;
|
||||
protected final BrokerUtil brokerUtil;
|
||||
|
||||
@Override
|
||||
public void afterPropertiesSet() throws Exception {
|
||||
// Initialize device status listener
|
||||
taskScheduler.schedule(this::initializeDeviceStatusListener,
|
||||
Instant.now().plusSeconds(monitorProperties.getSubscriptionStartupDelay()));
|
||||
}
|
||||
|
||||
private void initializeDeviceStatusListener() {
|
||||
getTopicsToMonitor().forEach(topic -> {
|
||||
try {
|
||||
brokerUtil.subscribe(topic, this);
|
||||
log.debug("{}: Subscribed to topic: {}", name, topic);
|
||||
} catch (Exception e) {
|
||||
log.error("{}: ERROR while subscribing to topic: {}\n", name, topic, e);
|
||||
taskScheduler.schedule(this::initializeDeviceStatusListener, Instant.now().plusSeconds(monitorProperties.getSubscriptionRetryDelay()));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected abstract @NonNull List<String> getTopicsToMonitor();
|
||||
|
||||
@Override
|
||||
public void onMessage(Map message) {
|
||||
try {
|
||||
log.debug("{}: Received a message: {}", name, message);
|
||||
processPayload(message);
|
||||
} catch (Exception e) {
|
||||
log.warn("{}: ERROR while processing message: {}\nException: ", name, message, e);
|
||||
}
|
||||
}
|
||||
|
||||
public void setHealthyStatus(Device device) {
|
||||
device.setStatus(DeviceStatus.HEALTHY);
|
||||
device.setSuspectTimestamp(null);
|
||||
device.setRetries(0);
|
||||
}
|
||||
|
||||
protected abstract void processPayload(Map<?, ?> dataMap);
|
||||
}
|
@ -0,0 +1,137 @@
|
||||
package eu.nebulous.resource.discovery.monitor.service;
|
||||
|
||||
import eu.nebulous.resource.discovery.ResourceDiscoveryProperties;
|
||||
import eu.nebulous.resource.discovery.common.BrokerUtil;
|
||||
import eu.nebulous.resource.discovery.common.REQUEST_TYPE;
|
||||
import eu.nebulous.resource.discovery.monitor.model.Device;
|
||||
import eu.nebulous.resource.discovery.monitor.model.DeviceException;
|
||||
import eu.nebulous.resource.discovery.monitor.model.DeviceStatus;
|
||||
import lombok.NonNull;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class DeviceLifeCycleRequestService {
|
||||
private final ResourceDiscoveryProperties properties;
|
||||
private final DeviceManagementService deviceManagementService;
|
||||
private final BrokerUtil brokerUtil;
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
|
||||
public void reinstallRequest(String id) {
|
||||
log.trace("reinstallRequest: BEGIN: device-id {}", id);
|
||||
Optional<Device> result = deviceManagementService.getById(id);
|
||||
if (result.isEmpty())
|
||||
throw new DeviceException(
|
||||
"Device with the Id does not exists in repository: " + id);
|
||||
Device device = result.get();
|
||||
|
||||
try {
|
||||
// Prepare request
|
||||
log.debug("reinstallRequest: Requesting device re-onboarding with Id: {}", device.getId());
|
||||
Map<String, String> onboardingRequest = prepareRequestPayload(REQUEST_TYPE.REINSTALL, device);
|
||||
|
||||
// Send request
|
||||
brokerUtil.sendMessage(properties.getDeviceLifeCycleRequestsTopic(), onboardingRequest);
|
||||
device.setStatus(DeviceStatus.ONBOARDING);
|
||||
|
||||
log.debug("reinstallRequest: Save updated device: id={}, device={}", device.getId(), device);
|
||||
deviceManagementService.update(device);
|
||||
log.debug("reinstallRequest: Onboarding request sent for device with Id: {}", device.getId());
|
||||
} catch (Exception e) {
|
||||
log.warn("reinstallRequest: EXCEPTION while sending onboarding request for device with Id: {}\n", device.getId(), e);
|
||||
device.setStatus(DeviceStatus.ONBOARD_ERROR);
|
||||
device.getMessages().add("EXCEPTION "+e.getMessage());
|
||||
deviceManagementService.update(device);
|
||||
}
|
||||
|
||||
log.trace("reinstallRequest: END");
|
||||
}
|
||||
|
||||
public void uninstallRequest(String id) {
|
||||
log.trace("uninstallRequest: BEGIN: device-id {}", id);
|
||||
Optional<Device> result = deviceManagementService.getById(id);
|
||||
if (result.isEmpty())
|
||||
throw new DeviceException(
|
||||
"Device with the Id does not exists in repository: " + id);
|
||||
Device device = result.get();
|
||||
|
||||
try {
|
||||
// Prepare request
|
||||
log.debug("uninstallRequest: Requesting device off-onboarding with Id: {}", device.getId());
|
||||
Map<String, String> offboardingRequest = prepareRequestPayload(REQUEST_TYPE.UNINSTALL, device);
|
||||
|
||||
// Send request
|
||||
brokerUtil.sendMessage(properties.getDeviceLifeCycleRequestsTopic(), offboardingRequest);
|
||||
device.setStatus(DeviceStatus.OFFBOARDING);
|
||||
|
||||
log.debug("uninstallRequest: Save updated device: id={}, device={}", device.getId(), device);
|
||||
deviceManagementService.update(device);
|
||||
log.debug("uninstallRequest: Off-boarding request sent for device with Id: {}", device.getId());
|
||||
} catch (Exception e) {
|
||||
log.warn("uninstallRequest: EXCEPTION while sending off-boarding request for device with Id: {}\n", device.getId(), e);
|
||||
device.setStatus(DeviceStatus.OFFBOARD_ERROR);
|
||||
device.getMessages().add("EXCEPTION "+e.getMessage());
|
||||
deviceManagementService.update(device);
|
||||
}
|
||||
|
||||
log.trace("uninstallRequest: END");
|
||||
}
|
||||
|
||||
public void requestInfoUpdate() {
|
||||
try {
|
||||
// Prepare request
|
||||
log.debug("requestInfoUpdate: Requesting device info and metrics update");
|
||||
Map<String, String> updateRequest = prepareRequestPayload(REQUEST_TYPE.INFO, null);
|
||||
|
||||
// Send request
|
||||
brokerUtil.sendMessage(properties.getDeviceLifeCycleRequestsTopic(), updateRequest);
|
||||
|
||||
log.debug("requestInfoUpdate: Update request sent");
|
||||
} catch (Exception e) {
|
||||
log.warn("requestInfoUpdate: EXCEPTION while sending update request:\n", e);
|
||||
}
|
||||
log.trace("requestInfoUpdate: END");
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
|
||||
private static Map<String, String> prepareRequestPayload(@NonNull REQUEST_TYPE requestType, Device device) {
|
||||
try {
|
||||
Map<String, String> payload;
|
||||
if (device==null) {
|
||||
payload = new LinkedHashMap<>(Map.of(
|
||||
"requestType", requestType.name()
|
||||
));
|
||||
} else {
|
||||
payload = new LinkedHashMap<>(Map.of(
|
||||
"requestId", device.getRequestId(),
|
||||
"requestType", requestType.name(),
|
||||
"deviceId", device.getId(),
|
||||
"deviceOs", device.getOs(),
|
||||
"deviceName", device.getName(),
|
||||
"deviceIpAddress", device.getIpAddress(),
|
||||
"deviceUsername", device.getUsername(),
|
||||
"devicePassword", new String(device.getPassword()),
|
||||
"devicePublicKey", new String(device.getPublicKey())
|
||||
));
|
||||
}
|
||||
payload.put("timestamp", Long.toString(Instant.now().toEpochMilli()));
|
||||
payload.put("priority", Double.toString(1.0));
|
||||
payload.put("retry", Integer.toString(1));
|
||||
return payload;
|
||||
} catch (Exception e) {
|
||||
log.error("prepareRequestPayload: EXCEPTION: request-type={}, device={}\nException: ",
|
||||
requestType, device, e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,137 @@
|
||||
|
||||
package eu.nebulous.resource.discovery.monitor.service;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import eu.nebulous.resource.discovery.common.BrokerUtil;
|
||||
import eu.nebulous.resource.discovery.common.REQUEST_TYPE;
|
||||
import eu.nebulous.resource.discovery.ResourceDiscoveryProperties;
|
||||
import eu.nebulous.resource.discovery.monitor.model.Device;
|
||||
import eu.nebulous.resource.discovery.monitor.model.DeviceStatus;
|
||||
import lombok.NonNull;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.scheduling.TaskScheduler;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
public class DeviceLifeCycleResponseService extends AbstractMonitorService {
|
||||
private final DeviceManagementService deviceManagementService;
|
||||
|
||||
public DeviceLifeCycleResponseService(ResourceDiscoveryProperties monitorProperties, TaskScheduler taskScheduler,
|
||||
ObjectMapper objectMapper, DeviceManagementService deviceManagementService,
|
||||
BrokerUtil brokerUtil)
|
||||
{
|
||||
super("DeviceLifeCycleResponseService", monitorProperties, taskScheduler, objectMapper, brokerUtil);
|
||||
this.deviceManagementService = deviceManagementService;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected @NonNull List<String> getTopicsToMonitor() {
|
||||
return List.of(monitorProperties.getDeviceLifeCycleResponsesTopic());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void processPayload(Map<?, ?> dataMap) {
|
||||
log.trace("DeviceLifeCycleResponseService: BEGIN: {}", dataMap);
|
||||
if (dataMap==null || dataMap.isEmpty()) {
|
||||
log.debug("DeviceLifeCycleResponseService: Device Life-Cycle map is empty: {}", dataMap);
|
||||
return;
|
||||
}
|
||||
|
||||
// Extract needed message data
|
||||
Map<String, Object> responseMap = dataMap.entrySet().stream()
|
||||
.filter(e -> e.getKey() != null)
|
||||
.collect(Collectors.toMap(e -> e.getKey().toString(), Map.Entry::getValue));
|
||||
|
||||
String requestTypeStr = responseMap.getOrDefault("requestType", "").toString();
|
||||
String requestId = responseMap.getOrDefault("requestId", "").toString();
|
||||
String deviceId = responseMap.getOrDefault("deviceId", "").toString();
|
||||
String ipAddress = responseMap.getOrDefault("deviceIpAddress", "").toString();
|
||||
String reference = responseMap.getOrDefault("reference", "").toString();
|
||||
String status = responseMap.getOrDefault("status", "").toString();
|
||||
log.debug("DeviceLifeCycleResponseService: Device Life-Cycle map data:: requestType={}, status={}, requestId={}, deviceId={}, ipAddress={}, reference={}",
|
||||
requestTypeStr, status, requestId, deviceId, ipAddress, reference);
|
||||
|
||||
// Check if we process the indicated requestType
|
||||
REQUEST_TYPE requestType = REQUEST_TYPE.valueOf(requestTypeStr);
|
||||
if (requestType!=REQUEST_TYPE.REINSTALL && requestType!=REQUEST_TYPE.UNINSTALL) {
|
||||
log.debug("DeviceLifeCycleResponseService: Ignoring message due to its requestType: requestType={}, status={}, requestId={}, deviceId={}, ipAddress={}, reference={}",
|
||||
requestType, status, requestId, deviceId, ipAddress, reference);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if we have all needed fields
|
||||
if (StringUtils.isBlank(ipAddress) || StringUtils.isBlank(reference) || StringUtils.isBlank(status)) {
|
||||
log.warn("DeviceLifeCycleResponseService: Ignoring message because ipAddress, reference, or status field is missing: requestType={}, status={}, requestId={}, deviceId={}, ipAddress={}, reference={}",
|
||||
requestType, status, requestId, deviceId, ipAddress, reference);
|
||||
return;
|
||||
}
|
||||
|
||||
// Find device record
|
||||
Optional<Device> opt = deviceManagementService.getByIpAddress(ipAddress);
|
||||
if (opt.isEmpty()) {
|
||||
log.warn("DeviceLifeCycleResponseService: Not found device with given ipAddress: requestType={}, status={}, requestId={}, deviceId={}, ipAddress={}, reference={}",
|
||||
requestType, status, requestId, deviceId, ipAddress, reference);
|
||||
return;
|
||||
}
|
||||
Device device = opt.get();
|
||||
|
||||
// Check if reference matches
|
||||
if (StringUtils.isBlank(device.getNodeReference()) || ! device.getNodeReference().equals(reference)) {
|
||||
log.warn("DeviceLifeCycleResponseService: Reference mismatch: requestType={}, status={}, requestId={}, deviceId={}, ipAddress={}, reference={}, device-reference={}",
|
||||
requestType, status, requestId, deviceId, ipAddress, reference, device.getNodeReference());
|
||||
return;
|
||||
}
|
||||
// Device identified
|
||||
|
||||
// Process by requestType
|
||||
if (requestType==REQUEST_TYPE.REINSTALL)
|
||||
processReinstallMessage(responseMap, device, requestType, requestId, deviceId, ipAddress, reference, status);
|
||||
else
|
||||
processUninstallMessage(responseMap, device, requestType, requestId, deviceId, ipAddress, reference, status);
|
||||
|
||||
log.trace("DeviceLifeCycleResponseService: END: requestType={}, status={}, requestId={}, deviceId={}, ipAddress={}, reference={}, device-reference={}",
|
||||
requestType, status, requestId, deviceId, ipAddress, reference, device.getNodeReference());
|
||||
}
|
||||
|
||||
private void processReinstallMessage(Map<String, Object> responseMap, Device device, REQUEST_TYPE requestType, String requestId, String deviceId, String ipAddress, String reference, String status) {
|
||||
// Update device state
|
||||
DeviceStatus newStatus;
|
||||
if ("SUCCESS".equalsIgnoreCase(status)) {
|
||||
device.setStatus(newStatus = DeviceStatus.ONBOARDED);
|
||||
} else {
|
||||
device.setStatus(newStatus = DeviceStatus.ONBOARD_ERROR);
|
||||
}
|
||||
deviceManagementService.update(device);
|
||||
|
||||
log.debug("DeviceLifeCycleResponseService: processReinstallMessage: Device status updated: newStatus={} --requestType={}, status={}, requestId={}, deviceId={}, ipAddress={}, reference={}, device-reference={}",
|
||||
newStatus, requestType, status, requestId, deviceId, ipAddress, reference, device.getNodeReference());
|
||||
}
|
||||
|
||||
private void processUninstallMessage(Map<String, Object> responseMap, Device device, REQUEST_TYPE requestType, String requestId, String deviceId, String ipAddress, String reference, String status) {
|
||||
// Update device state
|
||||
DeviceStatus newStatus;
|
||||
if ("SUCCESS".equalsIgnoreCase(status)) {
|
||||
device.setStatus(newStatus = DeviceStatus.OFFBOARDED);
|
||||
} else {
|
||||
device.setStatus(newStatus = DeviceStatus.OFFBOARD_ERROR);
|
||||
}
|
||||
deviceManagementService.update(device);
|
||||
|
||||
log.debug("DeviceLifeCycleResponseService: processUninstallMessage: Device status updated: newStatus={} --requestType={}, status={}, requestId={}, deviceId={}, ipAddress={}, reference={}, device-reference={}",
|
||||
newStatus, requestType, status, requestId, deviceId, ipAddress, reference, device.getNodeReference());
|
||||
|
||||
// Archive device, if successfully off-boarded
|
||||
if (newStatus==DeviceStatus.OFFBOARDED && monitorProperties.isImmediatelyArchiveOffboardedDevices()) {
|
||||
deviceManagementService.archiveDevice(device.getId());
|
||||
log.debug("DeviceLifeCycleResponseService: processUninstallMessage: Device ARCHIVED: id={}, ip-address={}, reference={}",
|
||||
device.getId(), device.getIpAddress(), device.getNodeReference());
|
||||
}
|
||||
}
|
||||
}
|
@ -1,15 +1,19 @@
|
||||
package eu.nebulous.resource.discovery.monitor.service;
|
||||
|
||||
import eu.nebulous.resource.discovery.ResourceDiscoveryProperties;
|
||||
import eu.nebulous.resource.discovery.monitor.model.ArchivedDevice;
|
||||
import eu.nebulous.resource.discovery.monitor.model.Device;
|
||||
import eu.nebulous.resource.discovery.monitor.model.DeviceException;
|
||||
import eu.nebulous.resource.discovery.monitor.model.DeviceStatus;
|
||||
import eu.nebulous.resource.discovery.monitor.repository.ArchivedDeviceRepository;
|
||||
import eu.nebulous.resource.discovery.monitor.repository.DeviceRepository;
|
||||
import eu.nebulous.resource.discovery.registration.model.RegistrationRequestException;
|
||||
import lombok.NonNull;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.GrantedAuthority;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.time.Instant;
|
||||
@ -19,6 +23,7 @@ import java.util.*;
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class DeviceManagementService {
|
||||
private final ResourceDiscoveryProperties properties;
|
||||
private final DeviceRepository deviceRepository;
|
||||
private final ArchivedDeviceRepository archivedDeviceRepository;
|
||||
private final DeviceConversionService deviceConversionService;
|
||||
@ -41,6 +46,10 @@ public class DeviceManagementService {
|
||||
return deviceRepository.findByIpAddress(ipAddress);
|
||||
}
|
||||
|
||||
public boolean isIpAddressInUse(@NonNull String ipAddress) {
|
||||
return deviceRepository.findByIpAddress(ipAddress).isPresent();
|
||||
}
|
||||
|
||||
public @NonNull Device save(@NonNull Device device) {
|
||||
DeviceStatus status = device.getStatus();
|
||||
checkDevice(device, true);
|
||||
@ -119,6 +128,22 @@ public class DeviceManagementService {
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
|
||||
private boolean canAccess(@NonNull Device device, Authentication authentication) {
|
||||
return canAccess(device, authentication, false);
|
||||
}
|
||||
|
||||
private boolean canAccess(@NonNull Device device, Authentication authentication, boolean sameUserOnly) {
|
||||
String owner = device.getOwner();
|
||||
if (owner == null && authentication.getName() == null) return true;
|
||||
return owner != null && (
|
||||
owner.equals(authentication.getName()) ||
|
||||
!sameUserOnly && authentication.getAuthorities().stream()
|
||||
.map(GrantedAuthority::getAuthority).toList().contains("ROLE_ADMIN")
|
||||
);
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
|
||||
public List<ArchivedDevice> getArchivedAll() {
|
||||
return Collections.unmodifiableList(archivedDeviceRepository.findAll());
|
||||
}
|
||||
@ -126,20 +151,40 @@ public class DeviceManagementService {
|
||||
public List<ArchivedDevice> getArchivedByOwner(@NonNull String owner) {
|
||||
return archivedDeviceRepository.findByOwner(owner);
|
||||
}
|
||||
public List<ArchivedDevice> getArchivedByOwner(Authentication authentication) {
|
||||
return getArchivedAll().stream()
|
||||
.filter(dev -> canAccess(dev, authentication, true))
|
||||
.toList();
|
||||
}
|
||||
|
||||
public Optional<ArchivedDevice> getArchivedById(@NonNull String id, Authentication authentication) {
|
||||
Optional<ArchivedDevice> result = getArchivedById(id);
|
||||
if (result.isEmpty())
|
||||
throw new DeviceException(
|
||||
"Device with the Id does not exists in repository: " + id);
|
||||
return canAccess(result.get(), authentication)
|
||||
? result : Optional.empty();
|
||||
}
|
||||
|
||||
public Optional<ArchivedDevice> getArchivedById(@NonNull String id) {
|
||||
return archivedDeviceRepository.findById(id);
|
||||
}
|
||||
|
||||
public List<ArchivedDevice> getArchivedByIpAddress(@NonNull String ipAddress, Authentication authentication) {
|
||||
return getArchivedByIpAddress(ipAddress).stream()
|
||||
.filter(dev -> canAccess(dev, authentication, true))
|
||||
.toList();
|
||||
}
|
||||
|
||||
public List<ArchivedDevice> getArchivedByIpAddress(@NonNull String ipAddress) {
|
||||
return archivedDeviceRepository.findByIpAddress(ipAddress);
|
||||
}
|
||||
|
||||
public void archiveDevice(String id) {
|
||||
archiveRequestBySystem(id);
|
||||
archiveDeviceBySystem(id);
|
||||
}
|
||||
|
||||
public void archiveRequestBySystem(String id) {
|
||||
public void archiveDeviceBySystem(String id) {
|
||||
Optional<Device> result = getById(id);
|
||||
if (result.isEmpty())
|
||||
throw new DeviceException(
|
||||
@ -149,14 +194,32 @@ public class DeviceManagementService {
|
||||
deviceRepository.delete(result.get());
|
||||
}
|
||||
|
||||
public void unarchiveDevice(String id) {
|
||||
public void unarchiveDevice(String id, Map<String,String> credentials) {
|
||||
Optional<ArchivedDevice> result = getArchivedById(id);
|
||||
if (result.isEmpty())
|
||||
throw new DeviceException(
|
||||
"Archived device with Id does not exists in repository: "+id);
|
||||
checkCredentials(result.get().getId(), credentials);
|
||||
|
||||
result.get().setArchiveDate(null);
|
||||
deviceRepository.save(deviceConversionService.toDevice(result.get()));
|
||||
Device restoredDevice = deviceConversionService.toDevice(result.get());
|
||||
restoredDevice.setUsername(credentials.get("username"));
|
||||
restoredDevice.setPassword(credentials.get("password").toCharArray());
|
||||
restoredDevice.setPublicKey(credentials.get("publicKey").toCharArray());
|
||||
deviceRepository.save(restoredDevice);
|
||||
archivedDeviceRepository.deleteById(result.get().getId());
|
||||
}
|
||||
|
||||
private void checkCredentials(String id, Map<String, String> credentials) {
|
||||
if (credentials==null || credentials.isEmpty())
|
||||
throw new RegistrationRequestException(
|
||||
"No credentials provided for un-archiving device with Id: "+id);
|
||||
if (StringUtils.isBlank(credentials.getOrDefault("username", "")))
|
||||
throw new RegistrationRequestException(
|
||||
"No username provided for un-archiving device with Id: "+id);
|
||||
if (StringUtils.isBlank(credentials.getOrDefault("password", "")) &&
|
||||
StringUtils.isBlank(credentials.getOrDefault("publicKey", "")))
|
||||
throw new RegistrationRequestException(
|
||||
"No password or SSH key provided for un-archiving device with Id: "+id);
|
||||
}
|
||||
}
|
@ -0,0 +1,127 @@
|
||||
|
||||
package eu.nebulous.resource.discovery.monitor.service;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import eu.nebulous.resource.discovery.ResourceDiscoveryProperties;
|
||||
import eu.nebulous.resource.discovery.common.BrokerUtil;
|
||||
import eu.nebulous.resource.discovery.monitor.model.Device;
|
||||
import eu.nebulous.resource.discovery.monitor.model.DeviceMetrics;
|
||||
import eu.nebulous.resource.discovery.monitor.model.DeviceStatus;
|
||||
import lombok.NonNull;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.scheduling.TaskScheduler;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.time.Instant;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
public class DeviceMetricsMonitorService extends AbstractMonitorService {
|
||||
private final DeviceManagementService deviceManagementService;
|
||||
|
||||
public DeviceMetricsMonitorService(ResourceDiscoveryProperties monitorProperties, TaskScheduler taskScheduler,
|
||||
ObjectMapper objectMapper, DeviceManagementService deviceManagementService,
|
||||
BrokerUtil brokerUtil)
|
||||
{
|
||||
super("DeviceMetricsMonitorService", monitorProperties, taskScheduler, objectMapper, brokerUtil);
|
||||
this.deviceManagementService = deviceManagementService;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected @NonNull List<String> getTopicsToMonitor() {
|
||||
return List.of(monitorProperties.getDeviceMetricsMonitorTopic());
|
||||
}
|
||||
|
||||
protected void processPayload(@NonNull Map<?,?> dataMap) {
|
||||
Object obj = dataMap.get("message");
|
||||
if (obj==null) {
|
||||
log.debug("DeviceMetricsMonitorService: Message does not contain device metrics (message field is null): {}", dataMap);
|
||||
return;
|
||||
}
|
||||
if (obj instanceof Map<?,?> infoMap) {
|
||||
if (infoMap.isEmpty())
|
||||
log.debug("DeviceMetricsMonitorService: Device metrics map (message field) is empty: {}", dataMap);
|
||||
else
|
||||
updateDeviceMetrics(infoMap);
|
||||
} else {
|
||||
log.debug("DeviceMetricsMonitorService: Message is not a device metrics (message field is not a map): {}", dataMap);
|
||||
}
|
||||
}
|
||||
|
||||
private void updateDeviceMetrics(@NonNull Map<?, ?> infoMap) {
|
||||
try {
|
||||
@NonNull Map<?,?> metricsMap = objectMapper.convertValue(infoMap, Map.class);
|
||||
|
||||
// Extract required data from metrics map
|
||||
String clientId = stringValue(metricsMap.get("clientId"));
|
||||
String ipAddress = stringValue(metricsMap.get("ipAddress"));
|
||||
String timestampStr = stringValue(metricsMap.get("receivedAtServer"));
|
||||
if (clientId.isEmpty() || ipAddress.isEmpty() || timestampStr.isEmpty()) {
|
||||
log.warn("DeviceMetricsMonitorService: Device metrics received do not contain clientId or ipAddress or receivedAtServer. Ignoring them: {}", metricsMap);
|
||||
return;
|
||||
}
|
||||
Instant timestamp = StringUtils.isNotBlank(timestampStr)
|
||||
? Instant.parse(timestampStr) : null;
|
||||
|
||||
// Get registered device using IP address
|
||||
Optional<Device> result = deviceManagementService.getByIpAddress(ipAddress);
|
||||
if (result.isEmpty()) {
|
||||
log.debug("DeviceMetricsMonitorService: Device metrics IP address does not match any registered device: {}", infoMap);
|
||||
return;
|
||||
}
|
||||
Device device = result.get();
|
||||
|
||||
// Check if the received device metrics are older than the cached
|
||||
if (device.getMetrics()!=null && device.getMetrics().getTimestamp()!=null && timestamp!=null) {
|
||||
if (device.getMetrics().getTimestamp().isAfter(timestamp)) {
|
||||
log.warn("DeviceMetricsMonitorService: Device metrics received are older than the cached. Ignoring them: id={}, update-timestamp={}, registered-timestamp={}",
|
||||
device.getId(), timestamp, device.getMetrics().getTimestamp());
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Prepare DeviceMetrics object
|
||||
metricsMap.remove("clientId");
|
||||
metricsMap.remove("ipAddress");
|
||||
metricsMap.remove("receivedAtServer");
|
||||
metricsMap.remove("_received_at_server_timestamp");
|
||||
Object latestEventsObj = metricsMap.remove("latest-events");
|
||||
List<Object> latestEvents = (latestEventsObj instanceof List list) ? list : Collections.emptyList();
|
||||
Map<String, Object> metricsMapClean = metricsMap.entrySet().stream()
|
||||
.filter(e -> e.getKey()!=null)
|
||||
.filter(e -> e.getValue()!=null)
|
||||
.collect(Collectors.toMap(
|
||||
e -> e.getKey().toString(),
|
||||
e -> (Serializable) e.getValue()
|
||||
));
|
||||
|
||||
DeviceMetrics metrics = new DeviceMetrics();
|
||||
metrics.setClientId(clientId);
|
||||
metrics.setIpAddress(ipAddress);
|
||||
metrics.setTimestamp(timestamp);
|
||||
metrics.setMetrics(metricsMapClean);
|
||||
metrics.setLatestEvents(latestEvents);
|
||||
|
||||
// Update device data
|
||||
device.setMetrics(metrics);
|
||||
setHealthyStatus(device);
|
||||
deviceManagementService.update(device);
|
||||
log.debug("DeviceMetricsMonitorService: Device metrics updated for device: id={}, ip-address={}, update={}",
|
||||
device.getId(), device.getIpAddress(), metrics);
|
||||
} catch (Exception e) {
|
||||
log.warn("DeviceMetricsMonitorService: EXCEPTION while processing device metrics map: {}\n", infoMap, e);
|
||||
}
|
||||
}
|
||||
|
||||
protected String stringValue(Object o) {
|
||||
if (o==null) return null;
|
||||
return o.toString().trim();
|
||||
}
|
||||
}
|
@ -0,0 +1,99 @@
|
||||
|
||||
package eu.nebulous.resource.discovery.monitor.service;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import eu.nebulous.resource.discovery.ResourceDiscoveryProperties;
|
||||
import eu.nebulous.resource.discovery.common.BrokerUtil;
|
||||
import eu.nebulous.resource.discovery.monitor.model.Device;
|
||||
import eu.nebulous.resource.discovery.monitor.model.DeviceStatusUpdate;
|
||||
import lombok.NonNull;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.scheduling.TaskScheduler;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
public class DeviceStatusMonitorService extends AbstractMonitorService {
|
||||
private final DeviceManagementService deviceManagementService;
|
||||
|
||||
public DeviceStatusMonitorService(ResourceDiscoveryProperties monitorProperties, TaskScheduler taskScheduler,
|
||||
ObjectMapper objectMapper, DeviceManagementService deviceManagementService,
|
||||
BrokerUtil brokerUtil)
|
||||
{
|
||||
super("DeviceStatusMonitorService", monitorProperties, taskScheduler, objectMapper, brokerUtil);
|
||||
this.deviceManagementService = deviceManagementService;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected @NonNull List<String> getTopicsToMonitor() {
|
||||
return List.of(monitorProperties.getDeviceStatusMonitorTopic());
|
||||
}
|
||||
|
||||
protected void processPayload(@NonNull Map<?,?> dataMap) {
|
||||
Object obj = dataMap.get("message");
|
||||
if (obj==null) {
|
||||
log.debug("DeviceStatusMonitorService: Message does not contain device status info (message field is null): {}", dataMap);
|
||||
return;
|
||||
}
|
||||
if (obj instanceof Map<?,?> infoMap) {
|
||||
if (infoMap.isEmpty())
|
||||
log.debug("DeviceStatusMonitorService: Device status map (message field) is empty: {}", dataMap);
|
||||
else
|
||||
updateDeviceInfo(infoMap);
|
||||
} else {
|
||||
log.debug("DeviceStatusMonitorService: Message is not a device status update (message field is not a map): {}", dataMap);
|
||||
}
|
||||
}
|
||||
|
||||
private void updateDeviceInfo(@NonNull Map<?, ?> infoMap) {
|
||||
try {
|
||||
@NonNull DeviceStatusUpdate deviceStatusUpdate = objectMapper.convertValue(infoMap, DeviceStatusUpdate.class);
|
||||
|
||||
// Get registered device using IP address
|
||||
String ipAddress = deviceStatusUpdate.getIpAddress();
|
||||
Optional<Device> result = deviceManagementService.getByIpAddress(ipAddress);
|
||||
if (result.isEmpty()) {
|
||||
log.debug("DeviceStatusMonitorService: Device status update IP address does not match any registered device: {}", infoMap);
|
||||
return;
|
||||
}
|
||||
Device device = result.get();
|
||||
|
||||
// Further check device reference
|
||||
if (! device.getNodeReference().equals(deviceStatusUpdate.getReference())) {
|
||||
log.debug("DeviceStatusMonitorService: Device status update node reference does NOT match to the registered device's one: update={}, registered-device={}", deviceStatusUpdate, device);
|
||||
log.warn("DeviceStatusMonitorService: Device status update node reference does NOT match to the registered device's one: id={}, update-ref={}, registered-device-ref={}",
|
||||
device.getId(), deviceStatusUpdate.getReference(), device.getNodeReference());
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if the received device status update has no state
|
||||
if (StringUtils.isBlank(deviceStatusUpdate.getState())) {
|
||||
log.warn("DeviceStatusMonitorService: Device status update has empty state field. Ignoring it: update={}", deviceStatusUpdate);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if the received device status update is older than the cached one
|
||||
if (device.getStatusUpdate()!=null && device.getStatusUpdate().getStateLastUpdate()!=null) {
|
||||
if (device.getStatusUpdate().getStateLastUpdate().isAfter(deviceStatusUpdate.getStateLastUpdate())) {
|
||||
log.warn("DeviceStatusMonitorService: Device status update received is older than the cached one. Ignoring it: id={}, update-timestamp={}, registered-timestamp={}",
|
||||
device.getId(), deviceStatusUpdate.getStateLastUpdate(), device.getStatusUpdate().getStateLastUpdate());
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Update device data
|
||||
device.setStatusUpdate(deviceStatusUpdate);
|
||||
setHealthyStatus(device);
|
||||
deviceManagementService.update(device);
|
||||
log.debug("DeviceStatusMonitorService: Device status updated for device: id={}, ip-address={}, update={}",
|
||||
device.getId(), device.getIpAddress(), deviceStatusUpdate);
|
||||
} catch (Exception e) {
|
||||
log.warn("DeviceStatusMonitorService: EXCEPTION while converting device status update info map to DeviceStatus object: {}\n", infoMap, e);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,253 @@
|
||||
|
||||
package eu.nebulous.resource.discovery.monitor.service;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import eu.nebulous.resource.discovery.ResourceDiscoveryProperties;
|
||||
import eu.nebulous.resource.discovery.common.BrokerUtil;
|
||||
import eu.nebulous.resource.discovery.common.REQUEST_TYPE;
|
||||
import eu.nebulous.resource.discovery.monitor.model.Device;
|
||||
import eu.nebulous.resource.discovery.monitor.model.DeviceStatus;
|
||||
import eu.nebulous.resource.discovery.registration.model.RegistrationRequest;
|
||||
import eu.nebulous.resource.discovery.registration.service.RegistrationRequestService;
|
||||
import lombok.NonNull;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.scheduling.TaskScheduler;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.*;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
public class UnknownDeviceRegistrationService extends AbstractMonitorService {
|
||||
private final static List<String> MONITORED_REQUEST_TYPES = List.of(
|
||||
REQUEST_TYPE.INFO.name(),
|
||||
REQUEST_TYPE.INSTALL.name(),
|
||||
REQUEST_TYPE.REINSTALL.name()
|
||||
);
|
||||
private final RegistrationRequestService registrationRequestService;
|
||||
private final DeviceManagementService deviceManagementService;
|
||||
private final Map<String, String> detectedDevices = Collections.synchronizedMap(new LinkedHashMap<>());
|
||||
private final List<Map> deviceDetailsQueue = Collections.synchronizedList(new LinkedList<>());
|
||||
|
||||
public UnknownDeviceRegistrationService(ResourceDiscoveryProperties monitorProperties, TaskScheduler taskScheduler,
|
||||
ObjectMapper objectMapper, DeviceManagementService deviceManagementService,
|
||||
RegistrationRequestService registrationRequestService, BrokerUtil brokerUtil)
|
||||
{
|
||||
super("UnknownDeviceRegistrationService", monitorProperties, taskScheduler, objectMapper, brokerUtil);
|
||||
this.registrationRequestService = registrationRequestService;
|
||||
this.deviceManagementService = deviceManagementService;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterPropertiesSet() throws Exception {
|
||||
super.afterPropertiesSet();
|
||||
|
||||
// Initialize unknown device processor
|
||||
taskScheduler.scheduleWithFixedDelay(this::processCachedData,
|
||||
Instant.now().plusSeconds(10), Duration.ofSeconds(10));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected @NonNull List<String> getTopicsToMonitor() {
|
||||
return List.of(
|
||||
monitorProperties.getDeviceInfoResponsesTopic(),
|
||||
monitorProperties.getDeviceLifeCycleResponsesTopic(),
|
||||
monitorProperties.getDeviceStatusMonitorTopic(),
|
||||
monitorProperties.getDeviceMetricsMonitorTopic());
|
||||
}
|
||||
|
||||
protected void processPayload(@NonNull Map<?, ?> dataMap) {
|
||||
log.trace("UnknownDeviceRegistrationService: BEGIN: {}", dataMap);
|
||||
|
||||
// Extract 'message' field if present
|
||||
boolean isMetricEvent = false;
|
||||
if (dataMap.get("message") instanceof Map map) {
|
||||
log.trace("UnknownDeviceRegistrationService: Extracted message field: {}", map);
|
||||
dataMap = map;
|
||||
isMetricEvent = true;
|
||||
}
|
||||
|
||||
// Get 'ipAddress' and 'reference' fields
|
||||
Object requestTypeObj = dataMap.get("requestType");
|
||||
Object ipAddressObj = dataMap.get("deviceIpAddress");
|
||||
if (ipAddressObj == null)
|
||||
ipAddressObj = dataMap.get("ipAddress");
|
||||
Object referenceObj = dataMap.get("reference");
|
||||
String requestType = requestTypeObj == null ? null : requestTypeObj.toString();
|
||||
String ipAddress = ipAddressObj == null ? null : ipAddressObj.toString();
|
||||
String reference = referenceObj == null ? null : referenceObj.toString();
|
||||
log.trace("UnknownDeviceRegistrationService: requestType={}, ipAddress={}, reference={}", requestType, ipAddress, reference);
|
||||
|
||||
if (StringUtils.isNotBlank(ipAddress) && StringUtils.isNotBlank(reference)) {
|
||||
// Process message based on its requestType
|
||||
if (REQUEST_TYPE.NODE_DETAILS.name().equalsIgnoreCase(requestType)) {
|
||||
// It is a Node-details response message
|
||||
deviceDetailsQueue.add(dataMap);
|
||||
log.trace("UnknownDeviceRegistrationService: END: Cached device details response for processing: ipAddress={}", ipAddress);
|
||||
} else if (isMetricEvent || MONITORED_REQUEST_TYPES.contains(requestType)) {
|
||||
// It is a Device status or Metrics message
|
||||
// cache ipAddress and message
|
||||
detectedDevices.put(ipAddress, reference);
|
||||
log.trace("UnknownDeviceRegistrationService: END: Cached device ipAddress and data for processing: ipAddress={}", ipAddress);
|
||||
} else
|
||||
log.trace("UnknownDeviceRegistrationService: END: Ignored message due to requestType: {}", dataMap);
|
||||
} else {
|
||||
// ipAddress or reference is missing. Ignoring message
|
||||
log.trace("UnknownDeviceRegistrationService: END: Missing ipAddress or reference field. Ignore message: {}", dataMap);
|
||||
}
|
||||
}
|
||||
|
||||
// Invoked by taskScheduler
|
||||
public void processCachedData() {
|
||||
processDetectedDevices();
|
||||
processDeviceDetailsResponses();
|
||||
}
|
||||
|
||||
private void processDetectedDevices() {
|
||||
// Copy and clear the unknown devices queue
|
||||
LinkedHashMap<String, String> map;
|
||||
synchronized (detectedDevices) {
|
||||
map = new LinkedHashMap<>(detectedDevices);
|
||||
detectedDevices.clear();
|
||||
}
|
||||
|
||||
// Process detected devices
|
||||
LinkedHashMap<String, String> unknownDevices = new LinkedHashMap<>();
|
||||
map.forEach((ipAddress, reference) -> {
|
||||
log.trace("UnknownDeviceRegistrationService: Processing device data: ipAddress={}, reference={}", ipAddress, reference);
|
||||
|
||||
// Check if there is a registration request for the device
|
||||
List<RegistrationRequest> requests = registrationRequestService.getByDeviceIpAddress(ipAddress.trim());
|
||||
if (requests.isEmpty()) {
|
||||
// No registration request found with this IP address
|
||||
|
||||
// Check if device is registered
|
||||
Optional<Device> device = deviceManagementService.getByIpAddress(ipAddress.trim());
|
||||
if (device.isEmpty() || ! reference.equalsIgnoreCase(device.get().getNodeReference())) {
|
||||
// Device is not registered
|
||||
log.trace("UnknownDeviceRegistrationService: Unknown device: ipAddress={}, reference={}, device={}", ipAddress, reference, device.orElse(null));
|
||||
unknownDevices.put(ipAddress, reference);
|
||||
} else {
|
||||
// Device is already registered
|
||||
log.trace("UnknownDeviceRegistrationService: Device is already registered: ipAddress={}, device={}",
|
||||
ipAddress, device.get());
|
||||
}
|
||||
} else {
|
||||
// There is a registration request for Device
|
||||
log.trace("UnknownDeviceRegistrationService: Device is already registered: ipAddress={}, request-id={}",
|
||||
ipAddress, requests.stream().map(RegistrationRequest::getId).toList());
|
||||
}
|
||||
});
|
||||
|
||||
log.trace("UnknownDeviceRegistrationService: Unknown devices: {}", unknownDevices);
|
||||
if (!unknownDevices.isEmpty())
|
||||
processUnknownDevices(unknownDevices);
|
||||
log.trace("UnknownDeviceRegistrationService: END: Unknown devices: {}", unknownDevices);
|
||||
}
|
||||
|
||||
private void processUnknownDevices(LinkedHashMap<String, String> unknownDevices) {
|
||||
log.info("UnknownDeviceRegistrationService: Unknown devices: {}", unknownDevices);
|
||||
unknownDevices.forEach((ipAddress, reference) -> {
|
||||
try {
|
||||
// Query EMS for device info
|
||||
log.debug("UnknownDeviceRegistrationService: Sending Node-Details-Request Message: ipAddress={}, reference={}",
|
||||
ipAddress, reference);
|
||||
Map<String, String> request = new LinkedHashMap<>(Map.of(
|
||||
"requestType", REQUEST_TYPE.NODE_DETAILS.name(),
|
||||
"deviceIpAddress", ipAddress,
|
||||
"reference", reference
|
||||
));
|
||||
|
||||
log.debug("UnknownDeviceRegistrationService: Sending Node-Details-Request Message: request={}", request);
|
||||
brokerUtil.sendMessage(monitorProperties.getDeviceInfoRequestsTopic(), request);
|
||||
log.debug("UnknownDeviceRegistrationService: Node-Details-Request Message sent: ipAddress={}", ipAddress);
|
||||
} catch (Exception e) {
|
||||
log.error("UnknownDeviceRegistrationService: ERROR while creating Node-Details-Request Message: ", e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void processDeviceDetailsResponses() {
|
||||
// Copy and clear the device-details responses queue
|
||||
LinkedList<Map> list;
|
||||
synchronized (deviceDetailsQueue) {
|
||||
list = new LinkedList<>(deviceDetailsQueue);
|
||||
deviceDetailsQueue.clear();
|
||||
}
|
||||
|
||||
// Process device details responses
|
||||
list.forEach((map) -> {
|
||||
try {
|
||||
log.debug("UnknownDeviceRegistrationService: Processing device details response: {}", map);
|
||||
|
||||
// Collect needed data from response
|
||||
String os = map.getOrDefault("os", null).toString();
|
||||
String name = map.getOrDefault("name", null).toString();
|
||||
String owner = "-EMS-";
|
||||
String ipAddress = map.getOrDefault("deviceIpAddress", null).toString();
|
||||
String username = map.getOrDefault("username", null).toString();
|
||||
char[] password = map.getOrDefault("password", "").toString().toCharArray();
|
||||
char[] publicKey = map.getOrDefault("key", "").toString().toCharArray();
|
||||
|
||||
String requestId = map.getOrDefault("requestId", "").toString();
|
||||
DeviceStatus status = DeviceStatus.NEW_DEVICE;
|
||||
String state = map.getOrDefault("state", "").toString();
|
||||
String nodeReference = map.getOrDefault("reference", "").toString();
|
||||
|
||||
Map<String, String> deviceInfo = new LinkedHashMap<>();
|
||||
if (map.get("nodeInfo") instanceof Map<?, ?> nodeInfoMap) {
|
||||
nodeInfoMap.forEach((k, v) -> {
|
||||
if (k != null && v != null) {
|
||||
String key = k.toString().trim();
|
||||
String val = v.toString();
|
||||
if (StringUtils.isNotBlank(key)) {
|
||||
deviceInfo.put(key, val);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
log.debug("""
|
||||
UnknownDeviceRegistrationService: Device data collected from device details response:
|
||||
- os={}
|
||||
- name={}
|
||||
- owner={}
|
||||
- requestId={}
|
||||
- ipAddress={}
|
||||
- reference={}
|
||||
- username={}
|
||||
- password={}
|
||||
- key={}
|
||||
- status={}
|
||||
- state={}
|
||||
- deviceInfo={}
|
||||
""",
|
||||
os, name, owner, requestId, ipAddress, nodeReference,
|
||||
username, password, publicKey, status, state, deviceInfo);
|
||||
|
||||
Device newDevice = Device.builder()
|
||||
.name(name)
|
||||
.owner("--EMS--")
|
||||
.requestId(requestId)
|
||||
.ipAddress(ipAddress)
|
||||
.nodeReference(nodeReference)
|
||||
.os(os)
|
||||
.username(username)
|
||||
.password(password)
|
||||
.publicKey(publicKey)
|
||||
.status(status)
|
||||
.deviceInfo(deviceInfo)
|
||||
.build();
|
||||
newDevice = deviceManagementService.save(newDevice);
|
||||
log.info("UnknownDeviceRegistrationService: Registered device: {}", newDevice);
|
||||
} catch (Exception e) {
|
||||
log.warn("UnknownDeviceRegistrationService: EXCEPTION while processing device details response: {}\nException: ", map, e);
|
||||
}
|
||||
});
|
||||
|
||||
log.trace("UnknownDeviceRegistrationService: END: Completed processing device-details responses");
|
||||
}
|
||||
}
|
@ -1,23 +1,17 @@
|
||||
package eu.nebulous.resource.discovery.registration;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import eu.nebulous.resource.discovery.ResourceDiscoveryProperties;
|
||||
import eu.nebulous.resource.discovery.common.BrokerUtil;
|
||||
import eu.nebulous.resource.discovery.common.REQUEST_TYPE;
|
||||
import eu.nebulous.resource.discovery.monitor.model.Device;
|
||||
import eu.nebulous.resource.discovery.monitor.service.DeviceManagementService;
|
||||
import eu.nebulous.resource.discovery.registration.model.RegistrationRequest;
|
||||
import eu.nebulous.resource.discovery.registration.model.RegistrationRequestStatus;
|
||||
import eu.nebulous.resource.discovery.registration.service.RegistrationRequestService;
|
||||
import jakarta.jms.*;
|
||||
import lombok.NonNull;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.activemq.ActiveMQConnection;
|
||||
import org.apache.activemq.ActiveMQConnectionFactory;
|
||||
import org.apache.activemq.command.ActiveMQMessage;
|
||||
import org.apache.activemq.command.ActiveMQTextMessage;
|
||||
import org.apache.activemq.command.ActiveMQTopic;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.beans.factory.InitializingBean;
|
||||
import org.springframework.scheduling.TaskScheduler;
|
||||
@ -42,10 +36,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
||||
@EnableAsync
|
||||
@EnableScheduling
|
||||
@RequiredArgsConstructor
|
||||
public class RegistrationRequestProcessor implements IRegistrationRequestProcessor, InitializingBean, MessageListener {
|
||||
private static final String REQUEST_TYPE_DATA_COLLECTION = "DIAGNOSTICS"; // EMS task type for collecting node info
|
||||
private static final String REQUEST_TYPE_ONBOARDING = "VM"; // EMS task type for installing EMS client
|
||||
|
||||
public class RegistrationRequestProcessor implements IRegistrationRequestProcessor, InitializingBean, BrokerUtil.Listener {
|
||||
private final static List<RegistrationRequestStatus> STATUSES_TO_ARCHIVE = List.of(
|
||||
RegistrationRequestStatus.PRE_AUTHORIZATION_REJECT,
|
||||
RegistrationRequestStatus.PRE_AUTHORIZATION_ERROR,
|
||||
@ -61,10 +52,11 @@ public class RegistrationRequestProcessor implements IRegistrationRequestProcess
|
||||
private final DeviceManagementService deviceManagementService;
|
||||
private final TaskScheduler taskScheduler;
|
||||
private final ObjectMapper objectMapper;
|
||||
private final BrokerUtil brokerUtil;
|
||||
private final AtomicBoolean isRunning = new AtomicBoolean(false);
|
||||
|
||||
@Override
|
||||
public void afterPropertiesSet() throws Exception {
|
||||
public void afterPropertiesSet() {
|
||||
// Initialize request processing results listener
|
||||
taskScheduler.schedule(this::initializeResultsListener, Instant.now().plusSeconds(processorProperties.getSubscriptionStartupDelay()));
|
||||
|
||||
@ -91,28 +83,16 @@ public class RegistrationRequestProcessor implements IRegistrationRequestProcess
|
||||
}
|
||||
log.debug("processRequests: Processing registration requests");
|
||||
|
||||
// Connect to Message broker
|
||||
ActiveMQConnectionFactory connectionFactory = new ActiveMQConnectionFactory(
|
||||
processorProperties.getBrokerUsername(), processorProperties.getBrokerPassword(),
|
||||
processorProperties.getBrokerURL());
|
||||
ActiveMQConnection conn = (ActiveMQConnection) connectionFactory.createConnection();
|
||||
Session session = conn.createSession();
|
||||
MessageProducer producer = session.createProducer(
|
||||
new ActiveMQTopic(processorProperties.getDataCollectionRequestTopic()));
|
||||
|
||||
// Process requests
|
||||
try {
|
||||
processNewRequests(producer);
|
||||
processOnboardingRequests(producer);
|
||||
processNewRequests();
|
||||
processOnboardingRequests();
|
||||
if (processorProperties.isAutomaticArchivingEnabled())
|
||||
archiveRequests();
|
||||
} catch (Throwable t) {
|
||||
log.error("processRequests: ERROR processing requests: ", t);
|
||||
}
|
||||
|
||||
// Close connection to Message broker
|
||||
conn.close();
|
||||
|
||||
log.debug("processRequests: Processing completed");
|
||||
|
||||
return CompletableFuture.completedFuture("DONE");
|
||||
@ -125,8 +105,8 @@ public class RegistrationRequestProcessor implements IRegistrationRequestProcess
|
||||
}
|
||||
}
|
||||
|
||||
private void processNewRequests(MessageProducer producer) throws JsonProcessingException, JMSException {
|
||||
log.trace("processNewRequests: BEGIN: {}", producer);
|
||||
private void processNewRequests() {
|
||||
log.trace("processNewRequests: BEGIN");
|
||||
List<RegistrationRequest> newRequests = registrationRequestService.getAll().stream()
|
||||
.filter(r -> r.getStatus() == RegistrationRequestStatus.NEW_REQUEST).toList();
|
||||
|
||||
@ -136,10 +116,11 @@ public class RegistrationRequestProcessor implements IRegistrationRequestProcess
|
||||
for (RegistrationRequest registrationRequest : newRequests) {
|
||||
try {
|
||||
log.debug("processNewRequests: Requesting collection of device data for request with Id: {}", registrationRequest.getId());
|
||||
Map<String, String> dataCollectionRequest = prepareRequestPayload(REQUEST_TYPE_DATA_COLLECTION, registrationRequest);
|
||||
String jsonMessage = objectMapper.writer().writeValueAsString(dataCollectionRequest);
|
||||
producer.send(createMessage(jsonMessage));
|
||||
Map<String, String> dataCollectionRequest = prepareRequestPayload(REQUEST_TYPE.DIAGNOSTICS, registrationRequest);
|
||||
brokerUtil.sendMessage(processorProperties.getDataCollectionRequestTopic(), dataCollectionRequest);
|
||||
registrationRequest.setStatus(RegistrationRequestStatus.DATA_COLLECTION_REQUESTED);
|
||||
|
||||
log.debug("processNewRequests: Save updated request: id={}, request={}", registrationRequest.getId(), registrationRequest);
|
||||
registrationRequestService.update(registrationRequest);
|
||||
log.debug("processNewRequests: Data collection request sent for request with Id: {}", registrationRequest.getId());
|
||||
} catch (Exception e) {
|
||||
@ -152,8 +133,8 @@ public class RegistrationRequestProcessor implements IRegistrationRequestProcess
|
||||
log.trace("processNewRequests: END");
|
||||
}
|
||||
|
||||
private void processOnboardingRequests(MessageProducer producer) throws JsonProcessingException, JMSException {
|
||||
log.trace("processOnboardingRequests: BEGIN: {}", producer);
|
||||
private void processOnboardingRequests() {
|
||||
log.trace("processOnboardingRequests: BEGIN");
|
||||
List<RegistrationRequest> onboardingRequests = registrationRequestService.getAll().stream()
|
||||
.filter(r -> r.getStatus() == RegistrationRequestStatus.PENDING_ONBOARDING).toList();
|
||||
|
||||
@ -162,33 +143,36 @@ public class RegistrationRequestProcessor implements IRegistrationRequestProcess
|
||||
|
||||
for (RegistrationRequest registrationRequest : onboardingRequests) {
|
||||
try {
|
||||
log.debug("processOnboardingRequests: Checking device before requesting onboarding for request with Id: {}", registrationRequest.getId());
|
||||
deviceManagementService.checkDevice(
|
||||
objectMapper.convertValue(registrationRequest.getDevice(), Device.class),
|
||||
true);
|
||||
log.debug("processOnboardingRequests: Checking device data before requesting onboarding, for request with Id: {}", registrationRequest.getId());
|
||||
Device deviceForMonitoring = objectMapper.convertValue(registrationRequest.getDevice(), Device.class);
|
||||
deviceForMonitoring.setPassword(registrationRequest.getDevice().getPassword()); // ignored by 'objectMapper', so we've to copy them
|
||||
deviceForMonitoring.setPublicKey(registrationRequest.getDevice().getPublicKey()); // ignored by 'objectMapper', so we've to copy them
|
||||
deviceManagementService.checkDevice(deviceForMonitoring, true);
|
||||
|
||||
log.debug("processOnboardingRequests: Requesting device onboarding for request with Id: {}", registrationRequest.getId());
|
||||
Map<String, String> dataCollectionRequest = prepareRequestPayload(REQUEST_TYPE_ONBOARDING, registrationRequest);
|
||||
String jsonMessage = objectMapper.writer().writeValueAsString(dataCollectionRequest);
|
||||
producer.send(createMessage(jsonMessage));
|
||||
Map<String, String> dataCollectionRequest = prepareRequestPayload(REQUEST_TYPE.INSTALL, registrationRequest);
|
||||
brokerUtil.sendMessage(processorProperties.getDataCollectionRequestTopic(), dataCollectionRequest);
|
||||
registrationRequest.setStatus(RegistrationRequestStatus.ONBOARDING_REQUESTED);
|
||||
registrationRequestService.update(registrationRequest);
|
||||
|
||||
log.debug("processOnboardingRequests: Save updated request: id={}, request={}", registrationRequest.getId(), registrationRequest);
|
||||
registrationRequestService.update(registrationRequest, false);
|
||||
log.debug("processOnboardingRequests: Onboarding request sent for request with Id: {}", registrationRequest.getId());
|
||||
} catch (Exception e) {
|
||||
log.warn("processOnboardingRequests: EXCEPTION while sending onboarding request for request with Id: {}\n", registrationRequest.getId(), e);
|
||||
registrationRequest.setStatus(RegistrationRequestStatus.ONBOARDING_ERROR);
|
||||
registrationRequestService.update(registrationRequest);
|
||||
registrationRequest.getMessages().add("EXCEPTION "+e.getMessage());
|
||||
registrationRequestService.update(registrationRequest, false);
|
||||
}
|
||||
}
|
||||
|
||||
log.trace("processOnboardingRequests: END");
|
||||
}
|
||||
|
||||
private static Map<String, String> prepareRequestPayload(@NonNull String requestType, RegistrationRequest registrationRequest) {
|
||||
private static Map<String, String> prepareRequestPayload(@NonNull REQUEST_TYPE requestType, RegistrationRequest registrationRequest) {
|
||||
try {
|
||||
Map<String, String> payload = new LinkedHashMap<>(Map.of(
|
||||
"requestId", registrationRequest.getId(),
|
||||
"requestType", requestType,
|
||||
"requestType", requestType.name(),
|
||||
"deviceId", registrationRequest.getDevice().getId(),
|
||||
"deviceOs", registrationRequest.getDevice().getOs(),
|
||||
"deviceName", registrationRequest.getDevice().getName(),
|
||||
@ -208,12 +192,6 @@ public class RegistrationRequestProcessor implements IRegistrationRequestProcess
|
||||
}
|
||||
}
|
||||
|
||||
protected ActiveMQMessage createMessage(String message) throws MessageNotWriteableException {
|
||||
ActiveMQTextMessage textMessage = new ActiveMQTextMessage();
|
||||
textMessage.setText(message);
|
||||
return textMessage;
|
||||
}
|
||||
|
||||
private void archiveRequests() {
|
||||
Instant archiveThreshold = Instant.now().minus(processorProperties.getArchivingThreshold(), ChronoUnit.MINUTES);
|
||||
log.trace("archiveRequests: BEGIN: archive-threshold: {}", archiveThreshold);
|
||||
@ -238,46 +216,22 @@ public class RegistrationRequestProcessor implements IRegistrationRequestProcess
|
||||
|
||||
protected void initializeResultsListener() {
|
||||
try {
|
||||
ActiveMQConnectionFactory connectionFactory = new ActiveMQConnectionFactory(
|
||||
processorProperties.getBrokerUsername(), processorProperties.getBrokerPassword(), processorProperties.getBrokerURL());
|
||||
ActiveMQConnection conn = (ActiveMQConnection) connectionFactory.createConnection();
|
||||
Session session = conn.createSession();
|
||||
MessageConsumer consumer = session.createConsumer(
|
||||
new ActiveMQTopic(processorProperties.getDataCollectionResponseTopic()));
|
||||
consumer.setMessageListener(this);
|
||||
conn.start();
|
||||
brokerUtil.subscribe(processorProperties.getDataCollectionResponseTopic(), this);
|
||||
} catch (Exception e) {
|
||||
log.error("RegistrationRequestProcessor: ERROR while subscribing to Message broker for Device info announcements: ", e);
|
||||
taskScheduler.schedule(this::initializeResultsListener, Instant.now().plusSeconds(processorProperties.getSubscriptionRetry()));
|
||||
taskScheduler.schedule(this::initializeResultsListener, Instant.now().plusSeconds(processorProperties.getSubscriptionRetryDelay()));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMessage(Message message) {
|
||||
try {
|
||||
log.debug("RegistrationRequestProcessor: Received a JMS message: {}", message);
|
||||
if (message instanceof ActiveMQTextMessage textMessage) {
|
||||
String payload = textMessage.getText();
|
||||
log.trace("RegistrationRequestProcessor: Message payload: {}", payload);
|
||||
TypeReference<Map<String,Object>> typeRef = new TypeReference<>() { };
|
||||
Object obj = objectMapper.readerFor(typeRef).readValue(payload);
|
||||
if (obj instanceof Map response) {
|
||||
processResponse(response);
|
||||
} else {
|
||||
log.debug("RegistrationRequestProcessor: Message payload is not recognized. Expected Map: type={}, object={}", obj.getClass().getName(), obj);
|
||||
}
|
||||
} else {
|
||||
log.debug("RegistrationRequestProcessor: Message type is not supported: {}", message);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("RegistrationRequestProcessor: ERROR while processing message: {}\nException: ", message, e);
|
||||
}
|
||||
public void onMessage(Map message) {
|
||||
processResponse(message);
|
||||
}
|
||||
|
||||
private void processResponse(@NonNull Map<String, Object> response) {
|
||||
String requestType = response.getOrDefault("requestType", "").toString().trim();
|
||||
String requestId = response.getOrDefault("requestId", "").toString().trim();
|
||||
String reference = response.getOrDefault("reference", "").toString().trim();
|
||||
String status = response.getOrDefault("status", "").toString().trim();
|
||||
String responseStatus = response.getOrDefault("status", "").toString().trim();
|
||||
String deviceIpAddress = response.getOrDefault("deviceIpAddress", "").toString().trim();
|
||||
long timestamp = Long.parseLong(response.getOrDefault("timestamp", "-1").toString().trim());
|
||||
|
||||
@ -291,6 +245,7 @@ public class RegistrationRequestProcessor implements IRegistrationRequestProcess
|
||||
case ONBOARDING_REQUESTED -> RegistrationRequestStatus.ONBOARDING_ERROR;
|
||||
default -> currStatus;
|
||||
};
|
||||
log.debug("processResponse: Temporary status change: {} --> {}", currStatus, newStatus);
|
||||
registrationRequest.setStatus(newStatus);
|
||||
|
||||
if (currStatus==RegistrationRequestStatus.SUCCESS) {
|
||||
@ -301,8 +256,8 @@ public class RegistrationRequestProcessor implements IRegistrationRequestProcess
|
||||
|
||||
String ipAddress = registrationRequest.getDevice().getIpAddress();
|
||||
boolean isError = false;
|
||||
if (StringUtils.equals(ipAddress, deviceIpAddress)) {
|
||||
String mesg = String.format("Device IP address do not match with that in request: id=%s, ip-address=%s != %s", requestId, ipAddress, deviceIpAddress);
|
||||
if (StringUtils.isNotBlank(deviceIpAddress) && ! StringUtils.equals(ipAddress, deviceIpAddress)) {
|
||||
String mesg = String.format("Device IP address in RESPONSE does not match with that in the request: id=%s, ip-address-response=%s != ip-address-in-request%s", requestId, deviceIpAddress, ipAddress);
|
||||
log.warn("processResponse: {}", mesg);
|
||||
registrationRequest.getMessages().add(mesg);
|
||||
isError = true;
|
||||
@ -313,17 +268,21 @@ public class RegistrationRequestProcessor implements IRegistrationRequestProcess
|
||||
registrationRequest.getMessages().add(mesg);
|
||||
isError = true;
|
||||
}
|
||||
if (! "SUCCESS".equals(status)) {
|
||||
String mesg = String.format("Request status is not SUCCESS: id=%s, timestamp=%d, status=%s", requestId, timestamp, status);
|
||||
if (! "SUCCESS".equals(responseStatus)) {
|
||||
String mesg = String.format("RESPONSE status is not SUCCESS: id=%s, timestamp=%d, status=%s", requestId, timestamp, responseStatus);
|
||||
log.warn("processResponse: {}", mesg);
|
||||
registrationRequest.getMessages().add(mesg);
|
||||
isError = true;
|
||||
}
|
||||
if (isError) {
|
||||
registrationRequestService.update(registrationRequest);
|
||||
if (log.isDebugEnabled())
|
||||
log.debug("processResponse: Save request with errors: id={}, errors={}, request={}", requestId, registrationRequest.getMessages(), registrationRequest);
|
||||
log.warn("processResponse: Save request with errors: id={}, errors={}", requestId, registrationRequest.getMessages());
|
||||
registrationRequestService.update(registrationRequest, false, true);
|
||||
return;
|
||||
}
|
||||
|
||||
boolean doArchive = false;
|
||||
Object obj = response.get("nodeInfo");
|
||||
if (obj instanceof Map devInfo) {
|
||||
// Update request info
|
||||
@ -358,8 +317,10 @@ public class RegistrationRequestProcessor implements IRegistrationRequestProcess
|
||||
// Set new status
|
||||
if (currStatus==RegistrationRequestStatus.DATA_COLLECTION_REQUESTED)
|
||||
registrationRequest.setStatus(RegistrationRequestStatus.PENDING_AUTHORIZATION);
|
||||
if (currStatus==RegistrationRequestStatus.ONBOARDING_REQUESTED)
|
||||
if (currStatus==RegistrationRequestStatus.ONBOARDING_REQUESTED) {
|
||||
registrationRequest.setStatus(RegistrationRequestStatus.SUCCESS);
|
||||
doArchive = processorProperties.isImmediatelyArchiveSuccessRequests();
|
||||
}
|
||||
|
||||
log.debug("processResponse: Done processing response for request: id={}, timestamp={}", requestId, timestamp);
|
||||
} else {
|
||||
@ -378,19 +339,32 @@ public class RegistrationRequestProcessor implements IRegistrationRequestProcess
|
||||
}
|
||||
|
||||
// Store changes
|
||||
registrationRequestService.update(registrationRequest, false);
|
||||
log.debug("processResponse: Save updated request: id={}, request={}", requestId, registrationRequest);
|
||||
registrationRequestService.update(registrationRequest, false, true);
|
||||
|
||||
// Archive success requests
|
||||
if (doArchive) {
|
||||
registrationRequestService.archiveRequestBySystem(registrationRequest.getId());
|
||||
}
|
||||
} else {
|
||||
log.warn("processResponse: Request not found: id={}", requestId);
|
||||
log.debug("processResponse: Request not found: id={}, requestType={}", requestId, requestType);
|
||||
}
|
||||
}
|
||||
|
||||
private void copyDeviceToMonitoring(RegistrationRequest registrationRequest) {
|
||||
Device device = objectMapper.convertValue(registrationRequest.getDevice(), Device.class);
|
||||
// override values
|
||||
device.setId(null);
|
||||
device.setStatus(null);
|
||||
device.setRequest(registrationRequest);
|
||||
device.setRequestId(registrationRequest.getId());
|
||||
device.getMessages().clear();
|
||||
|
||||
// copy credentials
|
||||
device.setPassword(registrationRequest.getDevice().getPassword()); // ignored by 'objectMapper', so we've to copy them
|
||||
device.setPublicKey(registrationRequest.getDevice().getPublicKey()); // ignored by 'objectMapper', so we've to copy them
|
||||
|
||||
// fields specific to Monitoring Device
|
||||
//device.setRequest(registrationRequest);
|
||||
device.setRequestId(registrationRequest.getId());
|
||||
device.setNodeReference(registrationRequest.getNodeReference());
|
||||
deviceManagementService.save(device);
|
||||
}
|
@ -1,14 +1,15 @@
|
||||
package eu.nebulous.resource.discovery.registration.controller;
|
||||
|
||||
import eu.nebulous.resource.discovery.registration.IRegistrationRequestProcessor;
|
||||
import eu.nebulous.resource.discovery.registration.service.RegistrationRequestService;
|
||||
import eu.nebulous.resource.discovery.registration.model.ArchivedRegistrationRequest;
|
||||
import eu.nebulous.resource.discovery.registration.model.RegistrationRequest;
|
||||
import eu.nebulous.resource.discovery.registration.model.RegistrationRequestException;
|
||||
import eu.nebulous.resource.discovery.registration.model.RegistrationRequestStatus;
|
||||
import eu.nebulous.resource.discovery.registration.service.RegistrationRequestService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.security.core.Authentication;
|
||||
@ -118,9 +119,13 @@ public class RegistrationRequestController {
|
||||
}
|
||||
|
||||
@PreAuthorize("hasAuthority('ROLE_ADMIN')")
|
||||
@GetMapping(value = "/request/{id}/unarchive", produces = MediaType.APPLICATION_JSON_VALUE)
|
||||
public RegistrationRequest unarchiveRequest(@PathVariable String id, Authentication authentication) {
|
||||
registrationRequestService.unarchiveRequest(id, authentication);
|
||||
@PostMapping(value = "/request/{id}/unarchive",
|
||||
consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
|
||||
public RegistrationRequest unarchiveRequest(@PathVariable String id,
|
||||
@RequestBody Map<String,String> credentials,
|
||||
Authentication authentication)
|
||||
{
|
||||
registrationRequestService.unarchiveRequest(id, credentials, authentication);
|
||||
return registrationRequestService.getById(id)
|
||||
.orElseThrow(() -> new RegistrationRequestException("Failed to unarchive registration request with id: " + id));
|
||||
}
|
||||
@ -141,4 +146,15 @@ public class RegistrationRequestController {
|
||||
return registrationRequestService.getArchivedByIdAsUser(id, authentication)
|
||||
.orElseThrow(() -> new RegistrationRequestException("Not found archived registration request with id: "+id));
|
||||
}
|
||||
|
||||
@ResponseStatus(value = HttpStatus.BAD_REQUEST)
|
||||
@ExceptionHandler(RegistrationRequestException.class)
|
||||
public Map<String,Object> handleRegistrationRequestException(RegistrationRequestException exception) {
|
||||
return Map.of(
|
||||
"status", HttpStatus.BAD_REQUEST.value(),
|
||||
"timestamp", System.currentTimeMillis(),
|
||||
"exception", exception.getClass().getName(),
|
||||
"message", exception.getMessage()
|
||||
);
|
||||
}
|
||||
}
|
@ -1,7 +1,10 @@
|
||||
package eu.nebulous.resource.discovery.registration.model;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import eu.nebulous.resource.discovery.common.DeviceLocation;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.ToString;
|
||||
import lombok.experimental.SuperBuilder;
|
||||
|
||||
import java.util.Map;
|
||||
@ -15,8 +18,13 @@ public class Device {
|
||||
private String name;
|
||||
private String owner;
|
||||
private String ipAddress;
|
||||
private DeviceLocation location;
|
||||
private String username;
|
||||
@ToString.Exclude
|
||||
@JsonProperty(access = JsonProperty.Access.WRITE_ONLY)
|
||||
private char[] password;
|
||||
@ToString.Exclude
|
||||
@JsonProperty(access = JsonProperty.Access.WRITE_ONLY)
|
||||
private char[] publicKey;
|
||||
private Map<String, String> deviceInfo;
|
||||
}
|
@ -25,8 +25,18 @@ public class RegistrationRequest {
|
||||
private Instant lastUpdateDate;
|
||||
private Instant archiveDate;
|
||||
private RegistrationRequestStatus status;
|
||||
private List<RegistrationRequestHistoryEntry> history;
|
||||
private List<RegistrationRequestHistoryEntry> history = new ArrayList<>();
|
||||
private String nodeReference;
|
||||
@Setter(AccessLevel.NONE)
|
||||
private List<String> messages = new ArrayList<>();
|
||||
|
||||
// Required in order BeanUtils.copyProperties() to also copy this
|
||||
public void setHistory(List<RegistrationRequestHistoryEntry> history) {
|
||||
this.history = new ArrayList<>(history);
|
||||
}
|
||||
|
||||
// Required in order BeanUtils.copyProperties() to also copy this
|
||||
public void setMessages(List<String> messages) {
|
||||
this.messages = new ArrayList<>(messages);
|
||||
}
|
||||
}
|
@ -7,4 +7,5 @@ import java.util.List;
|
||||
|
||||
public interface RegistrationRequestRepository extends MongoRepository<RegistrationRequest, String> {
|
||||
List<RegistrationRequest> findByRequester(String requester);
|
||||
List<RegistrationRequest> findByDeviceIpAddress(String ipAddress);
|
||||
}
|
@ -1,5 +1,6 @@
|
||||
package eu.nebulous.resource.discovery.registration.service;
|
||||
|
||||
import eu.nebulous.resource.discovery.monitor.service.DeviceManagementService;
|
||||
import eu.nebulous.resource.discovery.registration.model.*;
|
||||
import eu.nebulous.resource.discovery.registration.repository.ArchivedRegistrationRequestRepository;
|
||||
import eu.nebulous.resource.discovery.registration.repository.RegistrationRequestRepository;
|
||||
@ -7,6 +8,7 @@ import lombok.NonNull;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.beans.BeanUtils;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.GrantedAuthority;
|
||||
import org.springframework.stereotype.Service;
|
||||
@ -21,6 +23,7 @@ public class RegistrationRequestService {
|
||||
private final RegistrationRequestRepository registrationRequestRepository;
|
||||
private final ArchivedRegistrationRequestRepository archivedRegistrationRequestRepository;
|
||||
private final RegistrationRequestConversionService registrationRequestConversionService;
|
||||
private final DeviceManagementService deviceManagementService;
|
||||
|
||||
/*private final InMemoryRegistrationRequestRepository<RegistrationRequest>
|
||||
registrationRequestRepository = new InMemoryRegistrationRequestRepository<>();
|
||||
@ -38,10 +41,19 @@ public class RegistrationRequestService {
|
||||
return registrationRequestRepository.findById(id);
|
||||
}
|
||||
|
||||
public List<RegistrationRequest> getByDeviceIpAddress(@NonNull String ipAddress) {
|
||||
return registrationRequestRepository.findByDeviceIpAddress(ipAddress);
|
||||
}
|
||||
|
||||
public List<RegistrationRequest> getAll() {
|
||||
return Collections.unmodifiableList(registrationRequestRepository.findAll());
|
||||
}
|
||||
|
||||
public boolean isIpAddressInUse(@NonNull String ipAddress, String excludeId) {
|
||||
List<RegistrationRequest> result = registrationRequestRepository.findByDeviceIpAddress(ipAddress);
|
||||
return result.stream().anyMatch(r -> !r.getId().equals(excludeId));
|
||||
}
|
||||
|
||||
public @NonNull RegistrationRequest save(@NonNull RegistrationRequest registrationRequest) {
|
||||
RegistrationRequestStatus status = registrationRequest.getStatus();
|
||||
if (status == null) {
|
||||
@ -62,6 +74,9 @@ public class RegistrationRequestService {
|
||||
registrationRequest.setRequestDate(Instant.now());
|
||||
checkRegistrationRequest(registrationRequest);
|
||||
|
||||
// check IP address uniqueness
|
||||
checkIpAddressUniqueness(registrationRequest);
|
||||
|
||||
registrationRequestRepository.save(registrationRequest);
|
||||
return registrationRequest;
|
||||
}
|
||||
@ -71,6 +86,10 @@ public class RegistrationRequestService {
|
||||
}
|
||||
|
||||
public RegistrationRequest update(@NonNull RegistrationRequest registrationRequest, boolean checkEditDel) {
|
||||
return update(registrationRequest, checkEditDel, false);
|
||||
}
|
||||
|
||||
public RegistrationRequest update(@NonNull RegistrationRequest registrationRequest, boolean checkEditDel, boolean skipUniqueIpAddressCheck) {
|
||||
Optional<RegistrationRequest> result = getById(registrationRequest.getId());
|
||||
if (result.isEmpty())
|
||||
throw new RegistrationRequestException(
|
||||
@ -79,13 +98,45 @@ public class RegistrationRequestService {
|
||||
if (checkEditDel)
|
||||
canEditOrDelete(result.get());
|
||||
|
||||
registrationRequest.setLastUpdateDate(Instant.now());
|
||||
registrationRequestRepository.save(registrationRequest);
|
||||
// check IP address uniqueness
|
||||
if (!skipUniqueIpAddressCheck)
|
||||
checkIpAddressUniqueness(registrationRequest);
|
||||
|
||||
// Copy submitted registration request data onto the retrieved request
|
||||
BeanUtils.copyProperties(registrationRequest, result.get(),
|
||||
"id", "device", "requester", "requestDate");
|
||||
result.get().setLastUpdateDate(Instant.now());
|
||||
|
||||
// Check if device password/public key need update...
|
||||
List<String> ignoreList = new ArrayList<>();
|
||||
if (isCharArrayIsBlank(registrationRequest.getDevice().getPassword())
|
||||
&& isCharArrayIsBlank(registrationRequest.getDevice().getPublicKey()))
|
||||
{
|
||||
ignoreList.add("password");
|
||||
ignoreList.add("publicKey");
|
||||
}
|
||||
// ...then copy submitted request's device data onto the retrieved request's device
|
||||
BeanUtils.copyProperties(
|
||||
registrationRequest.getDevice(),
|
||||
result.get().getDevice(),
|
||||
ignoreList.toArray(new String[0]));
|
||||
|
||||
registrationRequestRepository.save(result.get());
|
||||
|
||||
return getById(registrationRequest.getId()).orElseThrow(() ->
|
||||
new RegistrationRequestException("Request update failed for Id: "+registrationRequest.getId()));
|
||||
}
|
||||
|
||||
private boolean isCharArrayIsBlank(char[] arr) {
|
||||
if (arr==null) return true;
|
||||
for (char c : arr)
|
||||
if (!isWhiteSpaceChar(c)) return false;
|
||||
return true;
|
||||
}
|
||||
private boolean isWhiteSpaceChar(char c) {
|
||||
return c==' ' || c=='\t' || c=='\r' || c=='\n';
|
||||
}
|
||||
|
||||
private void checkRegistrationRequest(@NonNull RegistrationRequest registrationRequest) {
|
||||
List<String> errors = new ArrayList<>();
|
||||
if (StringUtils.isBlank(registrationRequest.getId())) errors.add("Null or blank Id");
|
||||
@ -105,6 +156,17 @@ public class RegistrationRequestService {
|
||||
//XXX:TODO
|
||||
}
|
||||
|
||||
private void checkIpAddressUniqueness(RegistrationRequest registrationRequest) {
|
||||
boolean exists1 = this.isIpAddressInUse(
|
||||
registrationRequest.getDevice().getIpAddress(), registrationRequest.getId());
|
||||
boolean exists2 = deviceManagementService.isIpAddressInUse(
|
||||
registrationRequest.getDevice().getIpAddress());
|
||||
if (exists1 || exists2) {
|
||||
throw new RegistrationRequestException(
|
||||
"The IP address is already in use by: another-registration-request="+exists1+", registered-device="+exists2);
|
||||
}
|
||||
}
|
||||
|
||||
private void canEditOrDelete(RegistrationRequest registrationRequest) {
|
||||
RegistrationRequestStatus status = registrationRequest.getStatus();
|
||||
if (status==RegistrationRequestStatus.ONBOARDING_REQUESTED || status== RegistrationRequestStatus.SUCCESS)
|
||||
@ -248,16 +310,34 @@ public class RegistrationRequestService {
|
||||
registrationRequestRepository.delete(result.get());
|
||||
}
|
||||
|
||||
public void unarchiveRequest(String id, Authentication authentication) {
|
||||
public void unarchiveRequest(String id, Map<String,String> credentials, Authentication authentication) {
|
||||
Optional<ArchivedRegistrationRequest> result = getArchivedById(id);
|
||||
if (result.isEmpty())
|
||||
throw new RegistrationRequestException(
|
||||
"Archived registration request with Id does not exists in repository: "+id);
|
||||
checkAdmin(result.get().getId(), authentication);
|
||||
checkCredentials(result.get().getId(), credentials);
|
||||
|
||||
result.get().setArchiveDate(null);
|
||||
registrationRequestRepository.save(
|
||||
registrationRequestConversionService.toRegistrationRequest(result.get()));
|
||||
RegistrationRequest restoredRequest = registrationRequestConversionService.toRegistrationRequest(result.get());
|
||||
Device device = restoredRequest.getDevice();
|
||||
device.setUsername(credentials.get("username"));
|
||||
device.setPassword(credentials.get("password").toCharArray());
|
||||
device.setPublicKey(credentials.get("publicKey").toCharArray());
|
||||
registrationRequestRepository.save(restoredRequest);
|
||||
archivedRegistrationRequestRepository.deleteById(result.get().getId());
|
||||
}
|
||||
|
||||
private void checkCredentials(String id, Map<String, String> credentials) {
|
||||
if (credentials==null || credentials.isEmpty())
|
||||
throw new RegistrationRequestException(
|
||||
"No credentials provided for un-archiving request with Id: "+id);
|
||||
if (StringUtils.isBlank(credentials.getOrDefault("username", "")))
|
||||
throw new RegistrationRequestException(
|
||||
"No username provided for un-archiving request with Id: "+id);
|
||||
if (StringUtils.isBlank(credentials.getOrDefault("password", "")) &&
|
||||
StringUtils.isBlank(credentials.getOrDefault("publicKey", "")))
|
||||
throw new RegistrationRequestException(
|
||||
"No password or SSH key provided for un-archiving request with Id: "+id);
|
||||
}
|
||||
}
|
38
resource-discovery/src/main/resources/application.yml
Normal file
@ -0,0 +1,38 @@
|
||||
|
||||
application.version: @project.version@
|
||||
spring.output.ansi.enabled: ALWAYS
|
||||
spring.web.resources.static-locations: file:resource-discovery/management/src/main/resources/static/freebees_webdesign_6, classpath:/static/freebees_webdesign_6/
|
||||
|
||||
#security.ignored: /**
|
||||
#security.basic.enable: false
|
||||
#spring.autoconfigure.exclude: org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration
|
||||
|
||||
#spring.security.user.name: user
|
||||
#spring.security.user.password: user
|
||||
#spring.security.user.roles: USER
|
||||
|
||||
server.servlet.session.timeout: 120m
|
||||
|
||||
spring.data.mongodb.uri: mongodb://root:example@localhost:27017/admin
|
||||
spring.data.mongodb.database: resource_discovery
|
||||
|
||||
discovery:
|
||||
brokerURL: "ssl://localhost:61617?daemon=true&trace=false&useInactivityMonitor=false&connectionTimeout=0&keepAlive=true"
|
||||
brokerUsername: "aaa"
|
||||
brokerPassword: "111"
|
||||
trustStoreFile: tests/config/broker-truststore.p12
|
||||
trustStorePassword: melodic
|
||||
trustStoreType: PKCS12
|
||||
allowedDeviceInfoKeys:
|
||||
- '*'
|
||||
# NOTE:
|
||||
# To generate BCrypt encrypted passwords you can use: https://bcrypt-generator.com/
|
||||
users:
|
||||
- username: admin
|
||||
password: '$2a$10$5jzrhbVKq.W2J1PMGYeHyeydQtlw71PoVgryzDP0VZ.88FsPlq1ne' # admin1 (BCrypt; 10 iterations)
|
||||
roles: [ ADMIN ]
|
||||
- username: user
|
||||
password: '$2a$10$I6GSOKiY5n4/Ql0LA7Js0.4HT4UXVCNaNpGv5UdZt/brEdv/F.ttG' # user1 (BCrypt; 10 iterations)
|
||||
roles: [ USER ]
|
||||
|
||||
#logging.level.eu.nebulous.resource.discovery.registration.RegistrationRequestProcessor: TRACE
|
11
resource-discovery/src/main/resources/banner.txt
Normal file
@ -0,0 +1,11 @@
|
||||
|
||||
${AnsiColor.051} ██████╗ ███████╗███████╗ ██████╗ ██╗ ██╗██████╗ ██████╗███████╗ ██████╗ ██╗███████╗ ██████╗ ██████╗ ██╗ ██╗███████╗██████╗ ██╗ ██╗
|
||||
${AnsiColor.051} ██╔══██╗██╔════╝██╔════╝██╔═══██╗██║ ██║██╔══██╗██╔════╝██╔════╝ ██╔══██╗██║██╔════╝██╔════╝██╔═══██╗██║ ██║██╔════╝██╔══██╗╚██╗ ██╔╝
|
||||
${AnsiColor.051} ██████╔╝█████╗ ███████╗██║ ██║██║ ██║██████╔╝██║ █████╗ ██║ ██║██║███████╗██║ ██║ ██║██║ ██║█████╗ ██████╔╝ ╚████╔╝
|
||||
${AnsiColor.012} ██╔══██╗██╔══╝ ╚════██║██║ ██║██║ ██║██╔══██╗██║ ██╔══╝ ██║ ██║██║╚════██║██║ ██║ ██║╚██╗ ██╔╝██╔══╝ ██╔══██╗ ╚██╔╝
|
||||
${AnsiColor.012} ██║ ██║███████╗███████║╚██████╔╝╚██████╔╝██║ ██║╚██████╗███████╗ ██████╔╝██║███████║╚██████╗╚██████╔╝ ╚████╔╝ ███████╗██║ ██║ ██║
|
||||
${AnsiColor.012} ╚═╝ ╚═╝╚══════╝╚══════╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═╝ ╚═════╝╚══════╝ ╚═════╝ ╚═╝╚══════╝ ╚═════╝ ╚═════╝ ╚═══╝ ╚══════╝╚═╝ ╚═╝ ╚═╝
|
||||
|
||||
${AnsiColor.046} :: App version :: ${AnsiColor.87} (${application.version})
|
||||
${AnsiColor.046} :: Spring Boot :: ${AnsiColor.87} ${spring-boot.formatted-version}
|
||||
${AnsiColor.046} :: Java (TM) :: ${AnsiColor.87} (${java.version})
|
@ -0,0 +1,434 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="author" content="Firmbee.com - Free Project Management Platform for remote teams">
|
||||
<title>NebulOuS Resource Discovery - Management page</title>
|
||||
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Nunito+Sans:wght@400;600;700&display=swap" rel="stylesheet">
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.0/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-KyZXEAg3QhqLMpG8r+8fhAXLRk2vvoC2f3B09zVXn8CA5QIVfZOJ3BCsw2P0p/We" crossorigin="anonymous">
|
||||
<link rel="stylesheet" href="css/style.css">
|
||||
|
||||
<script src="https://kit.fontawesome.com/0e035b9984.js" crossorigin="anonymous"></script>
|
||||
<script src="https://code.jquery.com/jquery-3.7.1.min.js" integrity="sha256-/JqT3SQfawRcv/BIHPThkBvs0OEvtFFmqPF/lYI/Cxo=" crossorigin="anonymous"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.0/dist/js/bootstrap.bundle.min.js" integrity="sha384-U1DAWAznBHeqEIlVSCgzq+c9gqGAJn5c/t99JyeKa9xxaYpSvHU5awsuZVVFIhvj" crossorigin="anonymous"></script>
|
||||
<script src="js/addshadow.js"></script>
|
||||
|
||||
<script>
|
||||
$(function() {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const devId = urlParams.get('id') ?? '';
|
||||
|
||||
deviceId = devId;
|
||||
|
||||
if (devId!=='')
|
||||
refreshDeviceInfo(devId);
|
||||
checkSameUser();
|
||||
});
|
||||
|
||||
var isAdmin = false;
|
||||
var username = '';
|
||||
var deviceId;
|
||||
var owner = '';
|
||||
|
||||
function refreshDeviceInfo(id) {
|
||||
// show loading spinner
|
||||
$('#loading-spinner').toggleClass('d-none');
|
||||
$('#main-page').toggleClass('d-none');
|
||||
|
||||
// retrieve device info
|
||||
$.ajax({
|
||||
url: '/monitor/device/archived/'+id,
|
||||
dataType: 'json'
|
||||
})
|
||||
.done(function(data, status) {
|
||||
// console.log('refreshDeviceInfo: OK: ', data);
|
||||
var devId = data.id;
|
||||
if (devId!=='')
|
||||
$('#page_title').html( 'Device '+devId );
|
||||
else
|
||||
$('#page_title').html( $(`<div class="text-warning bg-danger">Error: ${status}: ${error}</div>`) );
|
||||
|
||||
deviceId = data.id;
|
||||
owner = data.owner;
|
||||
checkSameUser();
|
||||
|
||||
updateFormData(data);
|
||||
})
|
||||
.fail(function(xhr, status, error) {
|
||||
console.error('refreshDeviceInfo: ERROR: ', status, error);
|
||||
$('#page_title').html(
|
||||
$(`<div class="text-warning bg-danger">Error: ${status}: ${error}</div>`)
|
||||
);
|
||||
})
|
||||
.always(function(data, status) {
|
||||
// hide loading spinner
|
||||
$('#loading-spinner').toggleClass('d-none');
|
||||
$('#main-page').toggleClass('d-none');
|
||||
})
|
||||
;
|
||||
}
|
||||
|
||||
function checkSameUser() {
|
||||
if (username!=='' && owner!='' && username===owner) {
|
||||
$('.sameUser').addClass('d-none');
|
||||
} else {
|
||||
$('.sameUser').removeClass('d-none');
|
||||
}
|
||||
}
|
||||
|
||||
function updateFormData(data) {
|
||||
// Prepare data for processing
|
||||
var device = {
|
||||
device: data
|
||||
};
|
||||
//console.log('updateFormData: device: ', device);
|
||||
|
||||
// Flatten data map
|
||||
var keyValuesMap = flattenObject(device);
|
||||
//console.log('updateFormData: flattenObject: ', keyValuesMap);
|
||||
|
||||
// Update form fields
|
||||
Object.entries(keyValuesMap).forEach((entry) => {
|
||||
//console.log('updateFormData: Form Update: ', entry[0], entry[1]);
|
||||
$(`[id="${entry[0]}"]`).val( entry[1] );
|
||||
});
|
||||
|
||||
// Update device info field
|
||||
if (data.deviceInfo) {
|
||||
var valStr = JSON.stringify(data.deviceInfo, null, 2);
|
||||
var rows = valStr.split(/\r\n|\r|\n/).length;
|
||||
if (rows<2) rows = 1;
|
||||
if (rows>50) rows = 50;
|
||||
$(`[id="device#deviceInfo"]`).val( valStr ).attr( 'rows', rows );
|
||||
} else {
|
||||
$(`[id="device#deviceInfo"]`).val( '' ).attr( 'rows', 1 );
|
||||
}
|
||||
|
||||
// Update messages field
|
||||
if (data.messages) {
|
||||
var valStr = data.messages.join('\n').trim();
|
||||
var rows = data.messages.length;
|
||||
if (rows<2) rows = 1;
|
||||
if (rows>50) rows = 50;
|
||||
$(`[id="device#messages"]`).val( valStr ).attr( 'rows', rows );
|
||||
} else {
|
||||
$(`[id="device#messages"]`).val( '' ).attr( 'rows', 1 );
|
||||
}
|
||||
|
||||
// Update device metrics
|
||||
if (data.metrics) {
|
||||
// Clear contents of 'Device metrics' form part
|
||||
var target = $('#device-metrics');
|
||||
target.html('');
|
||||
|
||||
// Add timestamp in 'Device metrics' form part
|
||||
var timestamp = data.metrics.timestamp;
|
||||
//console.log(`TIMESTAMP: ${timestamp}`);
|
||||
target.append(`
|
||||
<div class="form-group row">
|
||||
<label for="metric#timestamp" class="col-sm-6 col-form-label"><b>Timestamp</b></label>
|
||||
<div class="col-sm-6">
|
||||
<input type="text" readonly class="form-control-plaintext" id="metric#timestamp" value="${timestamp}">
|
||||
</div>
|
||||
</div>
|
||||
`);
|
||||
|
||||
// Order metrics by key
|
||||
const ordered = Object.keys(data.metrics.metrics).sort().reduce(
|
||||
(obj, key) => {
|
||||
obj[key] = data.metrics.metrics[key];
|
||||
return obj;
|
||||
},
|
||||
{}
|
||||
);
|
||||
// Move 'counts' to the end
|
||||
const countsMap = Object.entries(ordered).filter(([k, v]) => k.startsWith('count-'));
|
||||
const othersMap = Object.entries(ordered).filter(([k, v]) => ! k.startsWith('count-'));
|
||||
const finalMap = Object.fromEntries([...othersMap, ...countsMap]);
|
||||
|
||||
// Append metrics in 'Device metrics' form part
|
||||
for (const [key, value] of Object.entries(finalMap)) {
|
||||
//console.log(`${key}: ${value}`);
|
||||
target.append(`
|
||||
<div class="form-group row border-top">
|
||||
<label for="metric#metric#${key}" class="col-sm-6 col-form-label"><b>${key}</b></label>
|
||||
<div class="col-sm-6">
|
||||
<input type="text" readonly class="form-control-plaintext" id="metric#tmetric#${key}" value="${value}">
|
||||
</div>
|
||||
</div>
|
||||
`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function flattenObject(ob) {
|
||||
var toReturn = {};
|
||||
for (var i in ob) {
|
||||
if (!ob.hasOwnProperty(i)) continue;
|
||||
if ((typeof ob[i]) == 'object' && ob[i] !== null) {
|
||||
var flatObject = flattenObject(ob[i]);
|
||||
for (var x in flatObject) {
|
||||
if (!flatObject.hasOwnProperty(x)) continue;
|
||||
toReturn[i + '#' + x] = flatObject[x];
|
||||
}
|
||||
} else {
|
||||
toReturn[i] = ob[i];
|
||||
}
|
||||
}
|
||||
return toReturn;
|
||||
}
|
||||
</script>
|
||||
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<div style="position: absolute; left: 10px; top: 10px;">
|
||||
<a href="index.html"><img src="img/nebulous-logo-basic.png" width="155px" height="155px" alt=""></a>
|
||||
</div>
|
||||
<div class="text-end text-secondary nowrap">
|
||||
<a href="requests.html"><img src="img/icon/Group 1802.svg" width="32px" height="32px" /></a>
|
||||
<a href="devices.html"><img src="img/icon/Group 1953.svg" width="32px" height="32px" /></a>
|
||||
<a href="archived.html"><img src="img/icon/Group 1954.svg" width="32px" height="32px" /></a>
|
||||
<a xxxhref="#" style="-webkit-filter: grayscale(100%); /* Safari 6.0 - 9.0 */ filter: grayscale(100%);"><img src="img/icon/Group 1955.svg" width="32px" height="32px" /></a>
|
||||
|
||||
<img src="img/user-icon.png" width="24" height="auto">
|
||||
<span id="whoami"><i class="fas fa-spinner fa-spin"></i></span>
|
||||
|
||||
<span onClick="document.location = '/logout';">
|
||||
<i class="fas fa-sign-out-alt"></i>
|
||||
</span>
|
||||
|
||||
<script>
|
||||
$(function() {
|
||||
$.ajax({ url: '/discovery/whoami', dataType: 'json' })
|
||||
.done(function(data) {
|
||||
isAdmin = data.admin;
|
||||
username = data.user;
|
||||
data.admin ? $('#whoami').html( $(`<span class="text-primary fw-bold">${data.user}</span>`) ) : $('#whoami').html( data.user );
|
||||
if (isAdmin) $('.adminOnly').toggleClass('d-none');
|
||||
|
||||
checkSameUser();
|
||||
})
|
||||
.fail(function(xhr, status, error) { $('#whoami').html( $(`Error: ${status} ${JSON.stringify(error)}`) ); });
|
||||
});
|
||||
</script>
|
||||
</div>
|
||||
<section class="light-section">
|
||||
<div class="container">
|
||||
|
||||
<div id="loading-spinner" class="fa-5x text-secondary text-center d-none">
|
||||
<i class="fas fa-sync fa-spin fa-5x"></i>
|
||||
</div>
|
||||
|
||||
<div id="main-page" class="text-center">
|
||||
<h1 class="adminOnly sameUser d-none text-danger fw-bold">* * * CAUTION: YOU'RE VIEWING A DEVICE YOU DON'T OWN * * *</h1>
|
||||
|
||||
<h2 id="page_title">Device ---</h2>
|
||||
<!--<p class="sub-header">Device details</p>-->
|
||||
|
||||
<button type="button" class="btn btn-primary" onClick="document.location = 'index.html';">
|
||||
<i class="fa fa-home"></i>
|
||||
</button>
|
||||
|
||||
<button type="button" class="btn btn-primary" onClick="document.location = 'archived.html';">
|
||||
<i class="fa fa-arrow-left"></i>
|
||||
</button>
|
||||
|
||||
<button type="button" class="btn btn-primary" onClick="refreshDeviceInfo(deviceId);">
|
||||
<i class="fa fa-refresh"></i>
|
||||
</button>
|
||||
<p> </p>
|
||||
|
||||
<div class="row">
|
||||
<div id="device-info-part" class="col-8 p-3">
|
||||
|
||||
<form>
|
||||
<div class="form-group row text-center bg-dark bg-opacity-25">
|
||||
<h5>Device details</h5>
|
||||
</div>
|
||||
|
||||
<!-- Device id -->
|
||||
<div class="form-group row">
|
||||
<label for="device#id" class="col-sm-2 col-form-label"><b>Device Id</b></label>
|
||||
<div class="col-sm-10">
|
||||
<input type="text" readonly class="form-control-plaintext" id="device#id" value="">
|
||||
</div>
|
||||
</div>
|
||||
<!-- Device owner -->
|
||||
<div class="form-group row">
|
||||
<label for="device#owner" class="col-sm-2 col-form-label"><b>Owner</b></label>
|
||||
<div class="col-sm-10">
|
||||
<input type="text" readonly class="form-control-plaintext" id="device#owner" value="">
|
||||
</div>
|
||||
</div>
|
||||
<!-- Device Name -->
|
||||
<div class="form-group row">
|
||||
<label for="device#name" class="col-sm-2 col-form-label"><b>Device Name</b></label>
|
||||
<div class="col-sm-10">
|
||||
<input type="text" readonly class="form-control-plaintext" id="device#name" value="" placeholder="Device name">
|
||||
</div>
|
||||
</div>
|
||||
<!-- Device OS -->
|
||||
<div class="form-group row">
|
||||
<label for="device#os" class="col-sm-2 col-form-label"><b>Device OS</b></label>
|
||||
<div class="col-sm-10">
|
||||
<input type="text" readonly class="form-control-plaintext" id="device#os" value="LINUX" placeholder="Device OS">
|
||||
</div>
|
||||
</div>
|
||||
<!-- Device IP Address -->
|
||||
<div class="form-group row">
|
||||
<label for="device#ipAddress" class="col-sm-2 col-form-label"><b>IP address</b></label>
|
||||
<div class="col-sm-10">
|
||||
<input type="text" readonly class="form-control-plaintext" id="device#ipAddress" value="" placeholder="Device IP address">
|
||||
</div>
|
||||
</div>
|
||||
<!-- Device Location -->
|
||||
<div class="form-group row">
|
||||
<label for="device#location#name" class="col-sm-2 col-form-label"><b>Location</b></label>
|
||||
<div class="col-sm-4">
|
||||
<input type="text" readonly class="form-control-plaintext" id="device#location#name" value="" placeholder="Device Location">
|
||||
</div>
|
||||
<label for="device#location#latitude" class="col-sm-1 col-form-label"><b>Lat.:</b></label>
|
||||
<div class="col-sm-2">
|
||||
<input type="text" readonly class="form-control-plaintext" id="device#location#latitude" value="" placeholder="Device Latitude">
|
||||
</div>
|
||||
<label for="device#location#longitude" class="col-sm-1 col-form-label"><b>Long.:</b></label>
|
||||
<div class="col-sm-2">
|
||||
<input type="text" readonly class="form-control-plaintext" id="device#location#longitude" value="" placeholder="Device Longitude">
|
||||
</div>
|
||||
</div>
|
||||
<!-- Device Username -->
|
||||
<div class="form-group row">
|
||||
<label for="device#username" class="col-sm-2 col-form-label"><b>SSH Username</b></label>
|
||||
<div class="col-sm-10">
|
||||
<input type="text" readonly class="form-control-plaintext" id="device#username" value="" placeholder="SSH username">
|
||||
</div>
|
||||
</div>
|
||||
<!-- Device Password -->
|
||||
<div class="form-group row">
|
||||
<label for="device#password" class="col-sm-2 col-form-label"><b>SSH Password</b></label>
|
||||
<div class="col-sm-10">
|
||||
<input type="password" readonly class="form-control-plaintext" id="device#password" value="" placeholder="*** SSH password - Not exposed ***">
|
||||
</div>
|
||||
</div>
|
||||
<!-- Device Public Key -->
|
||||
<div class="form-group row">
|
||||
<label for="device#publicKey" class="col-sm-2 col-form-label"><b>SSH Public Key</b></label>
|
||||
<div class="col-sm-10">
|
||||
<textarea readonly class="form-control-plaintext" id="device#publicKey" placeholder="*** SSH public key - Not exposed ***"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Node reference (available after onboarding) -->
|
||||
<div class="form-group row">
|
||||
<label for="device#nodeReference" class="col-sm-2 col-form-label"><b>Node Reference</b></label>
|
||||
<div class="col-sm-10">
|
||||
<input type="text" readonly class="form-control-plaintext" id="device#nodeReference" value="">
|
||||
</div>
|
||||
</div>
|
||||
<!-- Device status -->
|
||||
<div class="form-group row">
|
||||
<label for="device#status" class="col-sm-2 col-form-label"><b>Device Status</b></label>
|
||||
<div class="col-sm-10">
|
||||
<input type="text" readonly class="form-control-plaintext" id="device#status" value="na">
|
||||
</div>
|
||||
</div>
|
||||
<!-- EMS Device status -->
|
||||
<div class="form-group row">
|
||||
<label for="device#statusUpdate#state" class="col-sm-2 col-form-label"><b>EMS Device Status</b></label>
|
||||
<div class="col-sm-10">
|
||||
<input type="text" readonly class="form-control-plaintext" id="device#statusUpdate#state" value="na">
|
||||
</div>
|
||||
</div>
|
||||
<!-- Device creation data -->
|
||||
<div class="form-group row">
|
||||
<label for="device#creationDate" class="col-sm-2 col-form-label"><b>Creation Date</b></label>
|
||||
<div class="col-sm-10">
|
||||
<input type="text" readonly class="form-control-plaintext" id="device#creationDate" value="">
|
||||
</div>
|
||||
</div>
|
||||
<!-- Device last update date -->
|
||||
<div class="form-group row">
|
||||
<label for="device#lastUpdateDate" class="col-sm-2 col-form-label"><b>Last Updated</b></label>
|
||||
<div class="col-sm-10">
|
||||
<input type="text" readonly class="form-control-plaintext" id="device#lastUpdateDate" value="">
|
||||
</div>
|
||||
</div>
|
||||
<!-- Device messages -->
|
||||
<div class="form-group row">
|
||||
<label for="device#messages" class="col-sm-2 col-form-label"><b>Messages</b></label>
|
||||
<div class="col-sm-10">
|
||||
<textarea readonly class="form-control-plaintext" id="device#messages"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Device Additional Info -->
|
||||
<div class="form-group row">
|
||||
<label for="device#deviceInfo" class="col-sm-2 col-form-label"><b>Additional Info</b></label>
|
||||
<div class="col-sm-10">
|
||||
<textarea readonly class="form-control-plaintext" id="device#deviceInfo" placeholder="Additional device info"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!--<div class="text-center">
|
||||
<p> </p>
|
||||
<input class="btn btn-primary" type="submit" value="Submit">
|
||||
<input class="btn btn-primary" type="reset" value="Reset">
|
||||
</div>-->
|
||||
|
||||
</form>
|
||||
|
||||
</div>
|
||||
|
||||
<div id="device-metrics-part" class="text-center col-4 p-3">
|
||||
<div class="form-group row text-center bg-dark bg-opacity-25">
|
||||
<h5>Device metrics</h5>
|
||||
</div>
|
||||
|
||||
<div id="device-metrics"></div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
<footer class="py-5">
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="footer-item col-md-8">
|
||||
<p class="footer-item-title">Links</p>
|
||||
<a href="">About Us</a>
|
||||
<a href="">Portfolio</a>
|
||||
<a href="">Blog</a>
|
||||
<a href="">Sing In</a>
|
||||
</div>
|
||||
<div class="footer-item col-md-4">
|
||||
<p class="footer-item-title">Get In Touch</p>
|
||||
<form>
|
||||
<div class="mb-3 pb-3">
|
||||
<label for="exampleInputEmail1" class="form-label pb-3">Enter your email and we'll send you more information.</label>
|
||||
<input type="email" placeholder="Your Email" class="form-control" id="exampleInputEmail1" aria-describedby="emailHelp">
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">Subscribe</button>
|
||||
</form>
|
||||
</div>
|
||||
<div class="copyright pt-4 text-center text-muted">
|
||||
<p>© 2022 YOUR-DOMAIN | Created by <a href="https://firmbee.com/solutions/to-do-list/" title="Firmbee - Free To-do list App" target="_blank">Firmbee.com</a></p>
|
||||
<!--
|
||||
This template is licenced under Attribution 3.0 (CC BY 3.0 PL),
|
||||
You are free to: Share and Adapt. You must give appropriate credit, you may do so in any reasonable manner, but not in any way that suggests the licensor endorses you or your use.
|
||||
-->
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</main>
|
||||
<div class="fb2022-copy">Fbee 2022 copyright</div>
|
||||
</body>
|
||||
</html>
|
@ -146,6 +146,11 @@ function flattenObject(ob) {
|
||||
<a href="index.html"><img src="img/nebulous-logo-basic.png" width="155px" height="155px" alt=""></a>
|
||||
</div>
|
||||
<div class="text-end text-secondary nowrap">
|
||||
<a href="requests.html"><img src="img/icon/Group 1802.svg" width="32px" height="32px" /></a>
|
||||
<a href="devices.html"><img src="img/icon/Group 1953.svg" width="32px" height="32px" /></a>
|
||||
<a href="archived.html"><img src="img/icon/Group 1954.svg" width="32px" height="32px" /></a>
|
||||
<a xxxhref="#" style="-webkit-filter: grayscale(100%); /* Safari 6.0 - 9.0 */ filter: grayscale(100%);"><img src="img/icon/Group 1955.svg" width="32px" height="32px" /></a>
|
||||
|
||||
<img src="img/user-icon.png" width="24" height="auto">
|
||||
<span id="whoami"><i class="fas fa-spinner fa-spin"></i></span>
|
||||
|
||||
@ -295,6 +300,21 @@ function flattenObject(ob) {
|
||||
<input type="text" readonly class="form-control" id="request#device#ipAddress" value="" placeholder="Device IP address">
|
||||
</div>
|
||||
</div>
|
||||
<!-- Device Location -->
|
||||
<div class="form-group row">
|
||||
<label for="request#device#location#name" class="col-sm-2 col-form-label"><b>Location</b></label>
|
||||
<div class="col-sm-4">
|
||||
<input type="text" readonly class="form-control" id="request#device#location#name" value="" placeholder="Device Location">
|
||||
</div>
|
||||
<label for="request#device#location#latitude" class="col-sm-1 col-form-label">Latitude</label>
|
||||
<div class="col-sm-2">
|
||||
<input type="text" readonly class="form-control" id="request#device#location#latitude" value="" placeholder="Device Latitude">
|
||||
</div>
|
||||
<label for="request#device#location#longitude" class="col-sm-1 col-form-label">Longitude</label>
|
||||
<div class="col-sm-2">
|
||||
<input type="text" readonly class="form-control" id="request#device#location#longitude" value="" placeholder="Device Longitude">
|
||||
</div>
|
||||
</div>
|
||||
<!-- Device Username -->
|
||||
<div class="form-group row">
|
||||
<label for="request#device#username" class="col-sm-2 col-form-label"><b>SSH Username</b></label>
|
@ -0,0 +1,454 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="author" content="Firmbee.com - Free Project Management Platform for remote teams">
|
||||
<title>NebulOuS Resource Discovery - Management page</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Nunito+Sans:wght@400;600;700&display=swap" rel="stylesheet">
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.0/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-KyZXEAg3QhqLMpG8r+8fhAXLRk2vvoC2f3B09zVXn8CA5QIVfZOJ3BCsw2P0p/We" crossorigin="anonymous">
|
||||
<link rel="stylesheet" href="css/style.css">
|
||||
|
||||
<script src="https://kit.fontawesome.com/0e035b9984.js" crossorigin="anonymous"></script>
|
||||
<script src="https://code.jquery.com/jquery-3.7.1.min.js" integrity="sha256-/JqT3SQfawRcv/BIHPThkBvs0OEvtFFmqPF/lYI/Cxo=" crossorigin="anonymous"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.0/dist/js/bootstrap.bundle.min.js" integrity="sha384-U1DAWAznBHeqEIlVSCgzq+c9gqGAJn5c/t99JyeKa9xxaYpSvHU5awsuZVVFIhvj" crossorigin="anonymous"></script>
|
||||
<script src="js/addshadow.js"></script>
|
||||
|
||||
<script>
|
||||
$(function() {
|
||||
updateRequestsList(false);
|
||||
updateDevicesList(false);
|
||||
setInterval(() => {
|
||||
updateRequestsList();
|
||||
updateDevicesList();
|
||||
}, 5000);
|
||||
});
|
||||
|
||||
var isAdmin = false;
|
||||
var lastUpdateAsAdmin;
|
||||
|
||||
function updateRequestsList(asAdmin) {
|
||||
if (asAdmin === undefined) asAdmin = lastUpdateAsAdmin;
|
||||
else lastUpdateAsAdmin = asAdmin;
|
||||
|
||||
$.ajax({
|
||||
url: '/discovery/request/archived' + (asAdmin ? '/all' : ''),
|
||||
dataType: 'json'
|
||||
})
|
||||
.done(function(data, status) {
|
||||
//console.log('updateRequestsList: OK: ', data);
|
||||
var tbody = $('#requestsTable-tbody');
|
||||
tbody.empty();
|
||||
var ii = 0;
|
||||
data.forEach(item => {
|
||||
var reqId = item.id;
|
||||
var requester = item.requester;
|
||||
var devName = item.device.name;
|
||||
var ipAddress = item.device.ipAddress;
|
||||
var date = new Date( Date.parse( item.requestDate ) );
|
||||
var dateStr = date.toLocaleDateString('en-GB') + ' ' + date.toLocaleTimeString('en-GB')
|
||||
+ '<br/>' + Intl.DateTimeFormat().resolvedOptions().timeZone;
|
||||
var status = item.status;
|
||||
var color = getRequestStatusColor(status);
|
||||
var adminActions = (isAdmin) ? `
|
||||
<button class="btn btn-outline-primary btn-sm" onClick="unarchiveRequest('${reqId}', '${ipAddress}')">
|
||||
<i class="fas fa-box-open"></i>
|
||||
</button>
|
||||
`: '';
|
||||
ii++;
|
||||
tbody.append( $(`
|
||||
<tr class="${color}">
|
||||
<th scope="row">${ii}</th>
|
||||
<td>${requester}</td>
|
||||
<td class="text-start">
|
||||
<a href="/archived-request-view.html?id=${reqId}">${devName}</a>
|
||||
</td>
|
||||
<td>${ipAddress}</td>
|
||||
<td>${dateStr}</td>
|
||||
<td>${status}</td>
|
||||
<td>
|
||||
<button type="button" class="btn btn-success btn-sm" onClick="document.location='/archived-request-view.html?id=${reqId}'; ">
|
||||
<i class="fas fa-eye"></i>
|
||||
</button>
|
||||
${adminActions}
|
||||
</td>
|
||||
</tr> `
|
||||
) );
|
||||
});
|
||||
})
|
||||
.fail(function(xhr, status, error) {
|
||||
console.error('updateRequestsList: ERROR: ', status, error);
|
||||
})
|
||||
;
|
||||
}
|
||||
|
||||
function getRequestStatusColor(status) {
|
||||
if (status.indexOf('ERROR')>0) return 'table-danger';
|
||||
if (status.indexOf('REJECT')>0) return 'bg-danger';
|
||||
if (status.indexOf('PENDING')>=0) return 'table-warning';
|
||||
if (status=='NEW_REQUEST') return '';
|
||||
if (status=='SUCCESS') return 'table-success';
|
||||
return 'table-info';
|
||||
}
|
||||
|
||||
function unarchiveRequest(reqId, ipAddress) {
|
||||
if (! confirm('Restore request?')) return;
|
||||
showModal('Request: '+ipAddress+' ['+reqId+']', 'REQUEST', reqId);
|
||||
}
|
||||
|
||||
function _unarchiveRequest(reqId, credentials) {
|
||||
//if (! confirm('Restore request?')) return;
|
||||
//if (! confirm('You will need to manually provide Device credentials in Registration Requests page')) return;
|
||||
|
||||
$.ajax({
|
||||
type: 'post',
|
||||
url: `/discovery/request/${reqId}/unarchive`,
|
||||
contentType:"application/json; charset=utf-8",
|
||||
data: credentials
|
||||
})
|
||||
.done(function(data, status) {
|
||||
console.log('unarchiveRequest: OK: ', data);
|
||||
updateRequestsList(true);
|
||||
})
|
||||
.fail(function(xhr, status, error) {
|
||||
console.error('unarchiveRequest: ERROR: ', status, error);
|
||||
});
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
function showModal(info, type, id) {
|
||||
// Set credentials modal data and clear form
|
||||
$('#credentialsModal_info').html(info);
|
||||
$(`[id="device#type"]`).val(type);
|
||||
$(`[id="device#id"]`).val(id);
|
||||
|
||||
$(`[id="device#username"]`).val('');
|
||||
$(`[id="device#password"]`).val('');
|
||||
$(`[id="device#publicKey"]`).val('');
|
||||
|
||||
$('#btn-restore-req-or-dev').html(type==='REQUEST' ? 'Restore Request' : 'Restore Device');
|
||||
|
||||
// Show credentials modal
|
||||
var modal = new bootstrap.Modal(document.getElementById('credentialsModal'), {
|
||||
//keyboard: true
|
||||
});
|
||||
modal.show();
|
||||
}
|
||||
|
||||
function doRestore() {
|
||||
// Get credentialsModal form data
|
||||
var type = $(`[id="device#type"]`).val();
|
||||
var id = $(`[id="device#id"]`).val();
|
||||
|
||||
var username = $(`[id="device#username"]`).val();
|
||||
var password = $(`[id="device#password"]`).val();
|
||||
var publicKey = $(`[id="device#publicKey"]`).val();
|
||||
|
||||
// Close credentials modal
|
||||
var modal = bootstrap.Modal.getInstance(document.getElementById('credentialsModal'));
|
||||
modal.hide();
|
||||
|
||||
// Check form input
|
||||
if (username.trim()==='' || password.trim()==='' && publicKey.trim()==='') {
|
||||
alert('You must provide username, and either password or SSH key');
|
||||
return;
|
||||
}
|
||||
|
||||
var credentials = JSON.stringify({ username: username, password: password, publicKey: publicKey});
|
||||
if (type==='REQUEST')
|
||||
_unarchiveRequest(id, credentials);
|
||||
else
|
||||
_unarchiveDevice(id, credentials);
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
function updateDevicesList(asAdmin) {
|
||||
if (asAdmin === undefined) asAdmin = lastUpdateAsAdmin;
|
||||
else lastUpdateAsAdmin = asAdmin;
|
||||
|
||||
$.ajax({
|
||||
url: '/monitor/device/archived' + (asAdmin ? '/all' : ''),
|
||||
dataType: 'json'
|
||||
})
|
||||
.done(function(data, status) {
|
||||
//console.log('updateDevicesList: OK: ', data);
|
||||
var tbody = $('#devicesTable-tbody');
|
||||
tbody.empty();
|
||||
var ii = 0;
|
||||
data.forEach(item => {
|
||||
var devId = item.id;
|
||||
var owner = item.owner;
|
||||
var devName = item.name;
|
||||
var ipAddress = item.ipAddress;
|
||||
var date = new Date( Date.parse( item.creationDate ) );
|
||||
var dateStr = date.toLocaleDateString('en-GB') + ' ' + date.toLocaleTimeString('en-GB')
|
||||
+ '<br/>' + Intl.DateTimeFormat().resolvedOptions().timeZone;
|
||||
var status = item.status;
|
||||
var color = getDeviceStatusColor(status);
|
||||
var adminActions = (isAdmin) ? `
|
||||
<button class="btn btn-outline-primary btn-sm" onClick="unarchiveDevice('${devId}', '${ipAddress}')">
|
||||
<i class="fas fa-box-open"></i>
|
||||
</button>
|
||||
`: '';
|
||||
ii++;
|
||||
tbody.append( $(`
|
||||
<tr class="${color}">
|
||||
<th scope="row">${ii}</th>
|
||||
<td>${owner}</td>
|
||||
<td class="text-start">
|
||||
<a href="/archived-device-view.html?id=${devId}">${devName}</a>
|
||||
</td>
|
||||
<td>${ipAddress}</td>
|
||||
<td>${dateStr}</td>
|
||||
<td>${status}</td>
|
||||
<td>
|
||||
<button type="button" class="btn btn-success btn-sm" onClick="document.location='/archived-device-view.html?id=${devId}'; ">
|
||||
<i class="fas fa-eye"></i>
|
||||
</button>
|
||||
${adminActions}
|
||||
</td>
|
||||
</tr> `
|
||||
) );
|
||||
});
|
||||
})
|
||||
.fail(function(xhr, status, error) {
|
||||
console.error('updateRequestsList: ERROR: ', status, error);
|
||||
})
|
||||
;
|
||||
}
|
||||
|
||||
function getDeviceStatusColor(status) {
|
||||
if (status=='NEW_DEVICE') return '';
|
||||
if (status.indexOf('ERROR')>0) return 'table-danger';
|
||||
if (status=='FAILED') return 'table-danger';
|
||||
if (status=='SUSPECT') return 'table-warning';
|
||||
if (status.indexOf('BOARDING')>0) return 'table-warning';
|
||||
if (status=='ONBOARDED') return 'table-success';
|
||||
if (status=='HEALTHY' || status=='BUSY' || status=='IDLE') return 'table-success';
|
||||
if (status=='OFFBOARDED' || status=='ON_HOLD') return 'table-secondary';
|
||||
return 'table-info';
|
||||
}
|
||||
|
||||
function unarchiveDevice(devId, ipAddress) {
|
||||
if (! confirm('Restore device?')) return;
|
||||
showModal('Device: '+ipAddress+' ['+devId+']', 'DEVICE', devId);
|
||||
}
|
||||
|
||||
function _unarchiveDevice(devId, credentials) {
|
||||
//if (! confirm('Restore device?')) return;
|
||||
//if (! confirm('You will need to manually provide Device credentials in Devices page')) return;
|
||||
|
||||
$.ajax({
|
||||
type: 'post',
|
||||
url: `/monitor/device/${devId}/unarchive`,
|
||||
contentType:"application/json; charset=utf-8",
|
||||
data: credentials,
|
||||
dataType: 'text'
|
||||
})
|
||||
.done(function(data, status) {
|
||||
console.log('unarchiveDevice: OK: ', data);
|
||||
updateDevicesList(true);
|
||||
})
|
||||
.fail(function(xhr, status, error) {
|
||||
console.error('unarchiveDevice: ERROR: ', status, error);
|
||||
});
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<div style="position: absolute; left: 10px; top: 10px;">
|
||||
<a href="index.html"><img src="img/nebulous-logo-basic.png" width="155px" height="155px" alt=""></a>
|
||||
</div>
|
||||
<div class="text-end text-secondary nowrap">
|
||||
<a href="requests.html"><img src="img/icon/Group 1802.svg" width="32px" height="32px" /></a>
|
||||
<a href="devices.html"><img src="img/icon/Group 1953.svg" width="32px" height="32px" /></a>
|
||||
<div style="display: inline-block; border: 2px solid red; border-radius: 30%;"><img src="img/icon/Group 1954.svg" width="32px" height="32px"/></div>
|
||||
<!--<a href="archived.html"><img src="img/icon/Group 1954.svg" width="32px" height="32px" /></a>-->
|
||||
<a xxxhref="#" style="-webkit-filter: grayscale(100%); /* Safari 6.0 - 9.0 */ filter: grayscale(100%);"><img src="img/icon/Group 1955.svg" width="32px" height="32px" /></a>
|
||||
|
||||
<img src="img/user-icon.png" width="24" height="auto">
|
||||
<span id="whoami"><i class="fas fa-spinner fa-spin"></i></span>
|
||||
|
||||
<span onClick="document.location = '/logout';">
|
||||
<i class="fas fa-sign-out-alt"></i>
|
||||
</span>
|
||||
|
||||
<script>
|
||||
$(function() {
|
||||
$.ajax({ url: '/discovery/whoami', dataType: 'json' })
|
||||
.done(function(data) {
|
||||
isAdmin = data.admin;
|
||||
data.admin ? $('#whoami').html( $(`<span class="text-primary fw-bold">${data.user}</span>`) ) : $('#whoami').html( data.user );
|
||||
if (isAdmin) $('.adminOnly').toggleClass('d-none');
|
||||
})
|
||||
.fail(function(xhr, status, error) { $('#whoami').html( $(`Error: ${status} ${JSON.stringify(error)}`) ); });
|
||||
});
|
||||
</script>
|
||||
</div>
|
||||
|
||||
<section class="light-section pb-0">
|
||||
<div class="container">
|
||||
<div class="text-center">
|
||||
<h2>Archived Registration Requests</h2>
|
||||
<!--<p class="sub-header">Device registration requests</p>-->
|
||||
|
||||
<button type="button" class="btn btn-primary" onClick="document.location = 'index.html';">
|
||||
<i class="fa fa-home"></i>
|
||||
</button>
|
||||
|
||||
<span class="adminOnly d-none"> </span>
|
||||
<button type="button" class="adminOnly btn btn-danger d-none" onClick="updateRequestsList(true)">
|
||||
<i class="fa fa-refresh"></i>
|
||||
</button>
|
||||
<span class="adminOnly d-none"> </span>
|
||||
|
||||
<button type="button" class="btn btn-primary" onClick="updateRequestsList(false)">
|
||||
<i class="fa fa-refresh"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Query server for stored requests -->
|
||||
<div class="table-responsive text-nowrap">
|
||||
<table class="table table-hover table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">#</th>
|
||||
<th scope="col">Requester</th>
|
||||
<th scope="col" class="w-50">Device name</th>
|
||||
<th scope="col">IP Address</th>
|
||||
<th scope="col">Reg. Date</th>
|
||||
<th scope="col">Status</th>
|
||||
<th scope="col">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="requestsTable-tbody">
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="light-section">
|
||||
<div class="container">
|
||||
<div class="text-center">
|
||||
<h2>Archived Devices</h2>
|
||||
<!--<p class="sub-header">Devices</p>-->
|
||||
|
||||
<button type="button" class="btn btn-primary" onClick="document.location = 'index.html';">
|
||||
<i class="fa fa-home"></i>
|
||||
</button>
|
||||
|
||||
<span class="adminOnly d-none"> </span>
|
||||
<button type="button" class="adminOnly btn btn-danger d-none" onClick="updateDevicesList(true)">
|
||||
<i class="fa fa-refresh"></i>
|
||||
</button>
|
||||
<span class="adminOnly d-none"> </span>
|
||||
|
||||
<button type="button" class="btn btn-primary" onClick="updateDevicesList(false)">
|
||||
<i class="fa fa-refresh"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Query server for stored requests -->
|
||||
<div class="table-responsive text-nowrap">
|
||||
<table class="table table-hover table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">#</th>
|
||||
<th scope="col">Owner</th>
|
||||
<th scope="col" class="w-50">Device name</th>
|
||||
<th scope="col">IP Address</th>
|
||||
<th scope="col">Reg. Date</th>
|
||||
<th scope="col">Status</th>
|
||||
<th scope="col">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="devicesTable-tbody">
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Vertically centered scrollable modal -->
|
||||
<div class="modal fade" id="credentialsModal" tabindex="-1" aria-labelledby="credentialsModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered modal-dialog-scrollable">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="credentialsModalLabel">Device Credentials</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>Device credentials are not stored when a <i>device</i> or a <i>device registration request</i>
|
||||
is archived. In order to restore an archived device or request you will need to provide
|
||||
the device credentials.
|
||||
</p>
|
||||
<p><i><span id="credentialsModal_info"></span></i></p>
|
||||
<form>
|
||||
<input type="hidden" class="form-control" id="device#type">
|
||||
<input type="hidden" class="form-control" id="device#id">
|
||||
<div class="mb-3">
|
||||
<label for="device#username" class="col-form-label"><b>Username:</b></label>
|
||||
<input type="text" class="form-control" id="device#username">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="device#password" class="col-form-label"><b>Password:</b></label>
|
||||
<input type="text" class="form-control" id="device#password">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="device#publicKey" class="col-form-label"><b>SSH Key:</b></label>
|
||||
<textarea class="form-control" id="device#publicKey"></textarea>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
||||
<button type="button" class="btn btn-primary" id="btn-restore-req-or-dev" onclick="doRestore()">Restore Device</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer class="py-5">
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="footer-item col-md-8">
|
||||
<p class="footer-item-title">Links</p>
|
||||
<a href="">About Us</a>
|
||||
<a href="">Portfolio</a>
|
||||
<a href="">Blog</a>
|
||||
<a href="">Sing In</a>
|
||||
</div>
|
||||
<div class="footer-item col-md-4">
|
||||
<p class="footer-item-title">Get In Touch</p>
|
||||
<form>
|
||||
<div class="mb-3 pb-3">
|
||||
<label for="exampleInputEmail1" class="form-label pb-3">Enter your email and we'll send you more information.</label>
|
||||
<input type="email" placeholder="Your Email" class="form-control" id="exampleInputEmail1" aria-describedby="emailHelp">
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">Subscribe</button>
|
||||
</form>
|
||||
</div>
|
||||
<div class="copyright pt-4 text-center text-muted">
|
||||
<p>© 2022 YOUR-DOMAIN | Created by <a href="https://firmbee.com/solutions/to-do-list/" title="Firmbee - Free To-do list App" target="_blank">Firmbee.com</a></p>
|
||||
<!--
|
||||
This template is licenced under Attribution 3.0 (CC BY 3.0 PL),
|
||||
You are free to: Share and Adapt. You must give appropriate credit, you may do so in any reasonable manner, but not in any way that suggests the licensor endorses you or your use.
|
||||
-->
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</main>
|
||||
<div class="fb2022-copy">Fbee 2022 copyright</div>
|
||||
</body>
|
||||
</html>
|
@ -144,6 +144,51 @@ function updateFormData(data) {
|
||||
} else {
|
||||
$(`[id="device#messages"]`).val( '' ).attr( 'rows', 1 );
|
||||
}
|
||||
|
||||
// Update device metrics
|
||||
if (data.metrics) {
|
||||
// Clear contents of 'Device metrics' form part
|
||||
var target = $('#device-metrics');
|
||||
target.html('');
|
||||
|
||||
// Add timestamp in 'Device metrics' form part
|
||||
var timestamp = data.metrics.timestamp;
|
||||
//console.log(`TIMESTAMP: ${timestamp}`);
|
||||
target.append(`
|
||||
<div class="form-group row">
|
||||
<label for="metric#timestamp" class="col-sm-6 col-form-label"><b>Timestamp</b></label>
|
||||
<div class="col-sm-6">
|
||||
<input type="text" readonly class="form-control-plaintext" id="metric#timestamp" value="${timestamp}">
|
||||
</div>
|
||||
</div>
|
||||
`);
|
||||
|
||||
// Order metrics by key
|
||||
const ordered = Object.keys(data.metrics.metrics).sort().reduce(
|
||||
(obj, key) => {
|
||||
obj[key] = data.metrics.metrics[key];
|
||||
return obj;
|
||||
},
|
||||
{}
|
||||
);
|
||||
// Move 'counts' to the end
|
||||
const countsMap = Object.entries(ordered).filter(([k, v]) => k.startsWith('count-'));
|
||||
const othersMap = Object.entries(ordered).filter(([k, v]) => ! k.startsWith('count-'));
|
||||
const finalMap = Object.fromEntries([...othersMap, ...countsMap]);
|
||||
|
||||
// Append metrics in 'Device metrics' form part
|
||||
for (const [key, value] of Object.entries(finalMap)) {
|
||||
//console.log(`${key}: ${value}`);
|
||||
target.append(`
|
||||
<div class="form-group row border-top">
|
||||
<label for="metric#metric#${key}" class="col-sm-6 col-form-label"><b>${key}</b></label>
|
||||
<div class="col-sm-6">
|
||||
<input type="text" readonly class="form-control-plaintext" id="metric#tmetric#${key}" value="${value}">
|
||||
</div>
|
||||
</div>
|
||||
`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function flattenObject(ob) {
|
||||
@ -259,6 +304,11 @@ function sendDeviceData(deviceData) {
|
||||
<a href="index.html"><img src="img/nebulous-logo-basic.png" width="155px" height="155px" alt=""></a>
|
||||
</div>
|
||||
<div class="text-end text-secondary nowrap">
|
||||
<a href="requests.html"><img src="img/icon/Group 1802.svg" width="32px" height="32px" /></a>
|
||||
<a href="devices.html"><img src="img/icon/Group 1953.svg" width="32px" height="32px" /></a>
|
||||
<a href="archived.html"><img src="img/icon/Group 1954.svg" width="32px" height="32px" /></a>
|
||||
<a xxxhref="#" style="-webkit-filter: grayscale(100%); /* Safari 6.0 - 9.0 */ filter: grayscale(100%);"><img src="img/icon/Group 1955.svg" width="32px" height="32px" /></a>
|
||||
|
||||
<img src="img/user-icon.png" width="24" height="auto">
|
||||
<span id="whoami"><i class="fas fa-spinner fa-spin"></i></span>
|
||||
|
||||
@ -311,125 +361,156 @@ function sendDeviceData(deviceData) {
|
||||
</button>
|
||||
<p> </p>
|
||||
|
||||
<form>
|
||||
<div class="form-group row text-center bg-dark bg-opacity-25">
|
||||
<h5>Device details</h5>
|
||||
<div class="row">
|
||||
<div id="device-info-part" class="col-8 p-3">
|
||||
|
||||
<form>
|
||||
<div class="form-group row text-center bg-dark bg-opacity-25">
|
||||
<h5>Device details</h5>
|
||||
</div>
|
||||
|
||||
<!-- Device id -->
|
||||
<div class="form-group row">
|
||||
<label for="device#id" class="col-sm-2 col-form-label"><b>Device Id</b></label>
|
||||
<div class="col-sm-10">
|
||||
<input type="text" readonly class="form-control-plaintext" id="device#id" value="">
|
||||
</div>
|
||||
</div>
|
||||
<!-- Device owner -->
|
||||
<div class="form-group row">
|
||||
<label for="device#owner" class="col-sm-2 col-form-label"><b>Owner</b></label>
|
||||
<div class="col-sm-10">
|
||||
<input type="text" readonly class="form-control-plaintext" id="device#owner" value="">
|
||||
</div>
|
||||
</div>
|
||||
<!-- Device Name -->
|
||||
<div class="form-group row">
|
||||
<label for="device#name" class="col-sm-2 col-form-label"><b>Device Name</b></label>
|
||||
<div class="col-sm-10">
|
||||
<input type="text" readonly class="form-control-plaintext" id="device#name" value="" placeholder="Device name">
|
||||
</div>
|
||||
</div>
|
||||
<!-- Device OS -->
|
||||
<div class="form-group row">
|
||||
<label for="device#os" class="col-sm-2 col-form-label"><b>Device OS</b></label>
|
||||
<div class="col-sm-10">
|
||||
<input type="text" readonly class="form-control-plaintext" id="device#os" value="LINUX" placeholder="Device OS">
|
||||
</div>
|
||||
</div>
|
||||
<!-- Device IP Address -->
|
||||
<div class="form-group row">
|
||||
<label for="device#ipAddress" class="col-sm-2 col-form-label"><b>IP address</b></label>
|
||||
<div class="col-sm-10">
|
||||
<input type="text" readonly class="form-control-plaintext" id="device#ipAddress" value="" placeholder="Device IP address">
|
||||
</div>
|
||||
</div>
|
||||
<!-- Device Location -->
|
||||
<div class="form-group row">
|
||||
<label for="device#location#name" class="col-sm-2 col-form-label"><b>Location</b></label>
|
||||
<div class="col-sm-4">
|
||||
<input type="text" readonly class="form-control-plaintext" id="device#location#name" value="" placeholder="Device Location">
|
||||
</div>
|
||||
<label for="device#location#latitude" class="col-sm-1 col-form-label"><b>Lat.:</b></label>
|
||||
<div class="col-sm-2">
|
||||
<input type="text" readonly class="form-control-plaintext" id="device#location#latitude" value="" placeholder="Device Latitude">
|
||||
</div>
|
||||
<label for="device#location#longitude" class="col-sm-1 col-form-label"><b>Long.:</b></label>
|
||||
<div class="col-sm-2">
|
||||
<input type="text" readonly class="form-control-plaintext" id="device#location#longitude" value="" placeholder="Device Longitude">
|
||||
</div>
|
||||
</div>
|
||||
<!-- Device Username -->
|
||||
<div class="form-group row">
|
||||
<label for="device#username" class="col-sm-2 col-form-label"><b>SSH Username</b></label>
|
||||
<div class="col-sm-10">
|
||||
<input type="text" readonly class="form-control-plaintext" id="device#username" value="" placeholder="SSH username">
|
||||
</div>
|
||||
</div>
|
||||
<!-- Device Password -->
|
||||
<div class="form-group row">
|
||||
<label for="device#password" class="col-sm-2 col-form-label"><b>SSH Password</b></label>
|
||||
<div class="col-sm-10">
|
||||
<input type="password" readonly class="form-control-plaintext" id="device#password" value="" placeholder="*** SSH password - Not exposed ***">
|
||||
</div>
|
||||
</div>
|
||||
<!-- Device Public Key -->
|
||||
<div class="form-group row">
|
||||
<label for="device#publicKey" class="col-sm-2 col-form-label"><b>SSH Public Key</b></label>
|
||||
<div class="col-sm-10">
|
||||
<textarea readonly class="form-control-plaintext" id="device#publicKey" placeholder="*** SSH public key - Not exposed ***"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Node reference (available after onboarding) -->
|
||||
<div class="form-group row">
|
||||
<label for="device#nodeReference" class="col-sm-2 col-form-label"><b>Node Reference</b></label>
|
||||
<div class="col-sm-10">
|
||||
<input type="text" readonly class="form-control-plaintext" id="device#nodeReference" value="">
|
||||
</div>
|
||||
</div>
|
||||
<!-- Device status -->
|
||||
<div class="form-group row">
|
||||
<label for="device#status" class="col-sm-2 col-form-label"><b>Device Status</b></label>
|
||||
<div class="col-sm-10">
|
||||
<input type="text" readonly class="form-control-plaintext" id="device#status" value="na">
|
||||
</div>
|
||||
</div>
|
||||
<!-- EMS Device status -->
|
||||
<div class="form-group row">
|
||||
<label for="device#statusUpdate#state" class="col-sm-2 col-form-label"><b>EMS Device Status</b></label>
|
||||
<div class="col-sm-10">
|
||||
<input type="text" readonly class="form-control-plaintext" id="device#statusUpdate#state" value="na">
|
||||
</div>
|
||||
</div>
|
||||
<!-- Device creation data -->
|
||||
<div class="form-group row">
|
||||
<label for="device#creationDate" class="col-sm-2 col-form-label"><b>Creation Date</b></label>
|
||||
<div class="col-sm-10">
|
||||
<input type="text" readonly class="form-control-plaintext" id="device#creationDate" value="">
|
||||
</div>
|
||||
</div>
|
||||
<!-- Device last update date -->
|
||||
<div class="form-group row">
|
||||
<label for="device#lastUpdateDate" class="col-sm-2 col-form-label"><b>Last Updated</b></label>
|
||||
<div class="col-sm-10">
|
||||
<input type="text" readonly class="form-control-plaintext" id="device#lastUpdateDate" value="">
|
||||
</div>
|
||||
</div>
|
||||
<!-- Device messages -->
|
||||
<div class="form-group row">
|
||||
<label for="device#messages" class="col-sm-2 col-form-label"><b>Messages</b></label>
|
||||
<div class="col-sm-10">
|
||||
<textarea readonly class="form-control-plaintext" id="device#messages"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Device Additional Info -->
|
||||
<div class="form-group row">
|
||||
<label for="device#deviceInfo" class="col-sm-2 col-form-label"><b>Additional Info</b></label>
|
||||
<div class="col-sm-10">
|
||||
<textarea readonly class="form-control-plaintext" id="device#deviceInfo" placeholder="Additional device info"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!--<div class="text-center">
|
||||
<p> </p>
|
||||
<input class="btn btn-primary" type="submit" value="Submit">
|
||||
<input class="btn btn-primary" type="reset" value="Reset">
|
||||
</div>-->
|
||||
|
||||
</form>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Device id -->
|
||||
<div class="form-group row">
|
||||
<label for="device#id" class="col-sm-2 col-form-label"><b>Device Id</b></label>
|
||||
<div class="col-sm-10">
|
||||
<input type="text" readonly class="form-control-plaintext" id="device#id" value="">
|
||||
<div id="device-metrics-part" class="text-center col-4 p-3">
|
||||
<div class="form-group row text-center bg-dark bg-opacity-25">
|
||||
<h5>Device metrics</h5>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Device owner -->
|
||||
<div class="form-group row">
|
||||
<label for="device#owner" class="col-sm-2 col-form-label"><b>Owner</b></label>
|
||||
<div class="col-sm-10">
|
||||
<input type="text" readonly class="form-control-plaintext" id="device#owner" value="">
|
||||
</div>
|
||||
</div>
|
||||
<!-- Device Name -->
|
||||
<div class="form-group row">
|
||||
<label for="device#name" class="col-sm-2 col-form-label"><b>Device Name</b></label>
|
||||
<div class="col-sm-10">
|
||||
<input type="text" readonly class="form-control-plaintext" id="device#name" value="" placeholder="Device name">
|
||||
</div>
|
||||
</div>
|
||||
<!-- Device OS -->
|
||||
<div class="form-group row">
|
||||
<label for="device#os" class="col-sm-2 col-form-label"><b>Device OS</b></label>
|
||||
<div class="col-sm-10">
|
||||
<input type="text" readonly class="form-control-plaintext" id="device#os" value="LINUX" placeholder="Device OS">
|
||||
</div>
|
||||
</div>
|
||||
<!-- Device IP Address -->
|
||||
<div class="form-group row">
|
||||
<label for="device#ipAddress" class="col-sm-2 col-form-label"><b>IP address</b></label>
|
||||
<div class="col-sm-10">
|
||||
<input type="text" readonly class="form-control-plaintext" id="device#ipAddress" value="" placeholder="Device IP address">
|
||||
</div>
|
||||
</div>
|
||||
<!-- Device Username -->
|
||||
<div class="form-group row">
|
||||
<label for="device#username" class="col-sm-2 col-form-label"><b>SSH Username</b></label>
|
||||
<div class="col-sm-10">
|
||||
<input type="text" readonly class="form-control-plaintext" id="device#username" value="" placeholder="SSH username">
|
||||
</div>
|
||||
</div>
|
||||
<!-- Device Password -->
|
||||
<div class="form-group row">
|
||||
<label for="device#password" class="col-sm-2 col-form-label"><b>SSH Password</b></label>
|
||||
<div class="col-sm-10">
|
||||
<input type="password" readonly class="form-control-plaintext" id="device#password" value="" placeholder="*** SSH password - Not exposed ***">
|
||||
</div>
|
||||
</div>
|
||||
<!-- Device Public Key -->
|
||||
<div class="form-group row">
|
||||
<label for="device#publicKey" class="col-sm-2 col-form-label"><b>SSH Public Key</b></label>
|
||||
<div class="col-sm-10">
|
||||
<textarea readonly class="form-control-plaintext" id="device#publicKey" placeholder="*** SSH public key - Not exposed ***"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Node reference (available after onboarding) -->
|
||||
<div class="form-group row">
|
||||
<label for="device#nodeReference" class="col-sm-2 col-form-label"><b>Node Reference</b></label>
|
||||
<div class="col-sm-10">
|
||||
<input type="text" readonly class="form-control-plaintext" id="device#nodeReference" value="">
|
||||
</div>
|
||||
</div>
|
||||
<!-- Device status -->
|
||||
<div class="form-group row">
|
||||
<label for="device#status" class="col-sm-2 col-form-label"><b>Device Status</b></label>
|
||||
<div class="col-sm-10">
|
||||
<input type="text" readonly class="form-control-plaintext" id="device#status" value="NEW_DEVICE">
|
||||
</div>
|
||||
</div>
|
||||
<!-- Device creation data -->
|
||||
<div class="form-group row">
|
||||
<label for="device#creationDate" class="col-sm-2 col-form-label"><b>Creation Date</b></label>
|
||||
<div class="col-sm-10">
|
||||
<input type="text" readonly class="form-control-plaintext" id="device#creationDate" value="">
|
||||
</div>
|
||||
</div>
|
||||
<!-- Device last update date -->
|
||||
<div class="form-group row">
|
||||
<label for="device#lastUpdateDate" class="col-sm-2 col-form-label"><b>Last Updated</b></label>
|
||||
<div class="col-sm-10">
|
||||
<input type="text" readonly class="form-control-plaintext" id="device#lastUpdateDate" value="">
|
||||
</div>
|
||||
</div>
|
||||
<!-- Device messages -->
|
||||
<div class="form-group row">
|
||||
<label for="device#messages" class="col-sm-2 col-form-label"><b>Messages</b></label>
|
||||
<div class="col-sm-10">
|
||||
<textarea readonly class="form-control-plaintext" id="device#messages"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div id="device-metrics"></div>
|
||||
|
||||
<!-- Device Additional Info -->
|
||||
<div class="form-group row">
|
||||
<label for="device#deviceInfo" class="col-sm-2 col-form-label"><b>Additional Info</b></label>
|
||||
<div class="col-sm-10">
|
||||
<textarea readonly class="form-control-plaintext" id="device#deviceInfo" placeholder="Additional device info"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row text-center bg-dark bg-opacity-25">
|
||||
<h5>Device metrics</h5>
|
||||
</div>
|
||||
|
||||
++++ TODO ++++
|
||||
|
||||
<!--<div class="text-center">
|
||||
<p> </p>
|
||||
<input class="btn btn-primary" type="submit" value="Submit">
|
||||
<input class="btn btn-primary" type="reset" value="Reset">
|
||||
</div>-->
|
||||
|
||||
</form>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
@ -55,16 +55,26 @@ function updateDevicesList(asAdmin) {
|
||||
var owner = item.owner;
|
||||
var devName = (item.name && item.name.trim()!=='') ? item.name.trim() : `(No name - Id ${devId})`;
|
||||
var ipAddress = item.ipAddress;
|
||||
var load = 'TODO';
|
||||
var status = item.status;
|
||||
var color = getStatusColor(status);
|
||||
var load = getLoadStr(item);
|
||||
var status = `${ ((item.statusUpdate && item.statusUpdate.state) ? item.statusUpdate.state : 'na') }
|
||||
<br/> (<i><span class="small">${item.status}</span></i>) `;
|
||||
var color = getStatusColor(item.status);
|
||||
var isOffboarded = item.status==='OFFBOARDED' || item.status==='OFFBOARD_ERROR';
|
||||
|
||||
var adminActions = (isAdmin) ? `
|
||||
<button type="button" class="btn btn-primary btn-sm" onClick="if (confirm('Onboard Device again?')) manageDevice('${devId}', 'onboard');">
|
||||
<i class="fas fa-redo"></i>
|
||||
</button>
|
||||
<button type="button" class="btn btn-danger btn-sm" onClick="if (confirm('Remove Device?')) manageDevice('${devId}', 'offboard');">
|
||||
<i class="fas fa-ban"></i>
|
||||
var userActions = (isOffboarded)
|
||||
? ''
|
||||
: `
|
||||
<button type="button" class="btn btn-primary btn-sm" onClick="if (confirm('Onboard Device again?')) manageDevice('${devId}', 'onboard');">
|
||||
<i class="fas fa-backward"></i>
|
||||
</button>
|
||||
<button type="button" class="btn btn-danger btn-sm" onClick="if (confirm('Remove Device?')) manageDevice('${devId}', 'offboard');">
|
||||
<i class="fas fa-ban"></i>
|
||||
</button>
|
||||
`;
|
||||
var adminActions = (isAdmin)
|
||||
? `
|
||||
<button class="btn btn-warning btn-sm" onClick="if (confirm('Archive Device?')) archiveDevice('${devId}')">
|
||||
<i class="fas fa-box"></i>
|
||||
</button>
|
||||
` : '';
|
||||
ii++;
|
||||
@ -77,10 +87,11 @@ function updateDevicesList(asAdmin) {
|
||||
<td>${load}</td>
|
||||
<td>${status}</td>
|
||||
<td>
|
||||
${adminActions}
|
||||
<button type="button" class="btn btn-success btn-sm" onClick="document.location='/device-view.html?id=${devId}${urlAppend}'; ">
|
||||
<i class="fas fa-eye"></i>
|
||||
</button>
|
||||
${userActions}
|
||||
${adminActions}
|
||||
</td>
|
||||
</tr> `
|
||||
) );
|
||||
@ -92,13 +103,33 @@ function updateDevicesList(asAdmin) {
|
||||
;
|
||||
}
|
||||
|
||||
function getLoadStr(item) {
|
||||
var cpu = 'cpu: -';
|
||||
var ram = 'ram: -';
|
||||
if (item && item.metrics && item.metrics.metrics) {
|
||||
if (item.metrics.metrics.cpu) {
|
||||
var val = Math.round(item.metrics.metrics.cpu);
|
||||
var color = (val>80) ? 'bg-danger text-white' : '';
|
||||
cpu = `<span class="${color}">cpu: ${val}%</span>`;
|
||||
}
|
||||
if (item.metrics.metrics.ram) {
|
||||
var val = Math.round(item.metrics.metrics.ram);
|
||||
var color = (val>80) ? 'bg-danger text-white' : '';
|
||||
ram = `<span class="${color}">ram: ${val}%</span>`;
|
||||
}
|
||||
}
|
||||
return `${cpu}<br/>${ram}`;
|
||||
}
|
||||
|
||||
function getStatusColor(status) {
|
||||
if (status.indexOf('ERROR')>0) return 'table-danger';
|
||||
if (status.indexOf('REJECT')>0) return 'bg-danger';
|
||||
if (status.indexOf('PENDING')>=0) return 'table-warning';
|
||||
if (status=='NEW_DEVICE') return '';
|
||||
if (status=='BUSY') return 'table-success';
|
||||
if (status=='IDLE') return 'table-warning';
|
||||
if (status.indexOf('ERROR')>0) return 'table-danger';
|
||||
if (status=='FAILED') return 'table-danger';
|
||||
if (status=='SUSPECT') return 'table-warning';
|
||||
if (status.indexOf('BOARDING')>0) return 'table-warning';
|
||||
if (status=='ONBOARDED') return 'table-success';
|
||||
if (status=='HEALTHY' || status=='BUSY' || status=='IDLE') return 'table-success';
|
||||
if (status=='OFFBOARDED' || status=='ON_HOLD') return 'table-secondary';
|
||||
return 'table-info';
|
||||
}
|
||||
|
||||
@ -120,6 +151,53 @@ function manageDevice(id, action) {
|
||||
})
|
||||
;
|
||||
}
|
||||
|
||||
function requestUpdate() {
|
||||
$.ajax({
|
||||
url: '/monitor/request-update',
|
||||
method: 'GET',
|
||||
async: 'true'
|
||||
|
||||
})
|
||||
.done(function(data, status) {
|
||||
console.log('requestUpdate: OK');
|
||||
})
|
||||
.fail(function(xhr, status, error) {
|
||||
console.error('requestUpdate: ERROR: ', status, error);
|
||||
})
|
||||
.always(function(data, status) {
|
||||
updateDevicesList();
|
||||
})
|
||||
;
|
||||
}
|
||||
|
||||
function processDevices() {
|
||||
$.ajax({ url: '/monitor/device/process' })
|
||||
.done(function(data, status) {
|
||||
//console.log('processDevices: OK: ', data);
|
||||
})
|
||||
.fail(function(xhr, status, error) {
|
||||
console.error('processDevices: ERROR: ', status, error);
|
||||
})
|
||||
.always(function(data, status) {
|
||||
setTimeout(updateDevicesList, 500);
|
||||
})
|
||||
;
|
||||
}
|
||||
|
||||
function archiveDevice(devId) {
|
||||
$.ajax({
|
||||
url: `/monitor/device/${devId}/archive`,
|
||||
dataType: 'text'
|
||||
})
|
||||
.done(function(data, status) {
|
||||
console.log('archiveDevice: OK: ', devId, data);
|
||||
updateDevicesList(true);
|
||||
})
|
||||
.fail(function(xhr, status, error) {
|
||||
console.error('archiveDevice: ERROR: ', status, error);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
</head>
|
||||
@ -129,6 +207,12 @@ function manageDevice(id, action) {
|
||||
<a href="index.html"><img src="img/nebulous-logo-basic.png" width="155px" height="155px" alt=""></a>
|
||||
</div>
|
||||
<div class="text-end text-secondary nowrap">
|
||||
<a href="requests.html"><img src="img/icon/Group 1802.svg" width="32px" height="32px" /></a>
|
||||
<div style="display: inline-block; border: 2px solid red; border-radius: 30%;"><img src="img/icon/Group 1953.svg" width="32px" height="32px"/></div>
|
||||
<!--<a href="devices.html"><img src="img/icon/Group 1953.svg" width="32px" height="32px" /></a>-->
|
||||
<a href="archived.html"><img src="img/icon/Group 1954.svg" width="32px" height="32px" /></a>
|
||||
<a xxxhref="#" style="-webkit-filter: grayscale(100%); /* Safari 6.0 - 9.0 */ filter: grayscale(100%);"><img src="img/icon/Group 1955.svg" width="32px" height="32px" /></a>
|
||||
|
||||
<img src="img/user-icon.png" width="24" height="auto">
|
||||
<span id="whoami"><i class="fas fa-spinner fa-spin"></i></span>
|
||||
|
||||
@ -158,6 +242,13 @@ function manageDevice(id, action) {
|
||||
<i class="fa fa-home"></i>
|
||||
</button>
|
||||
|
||||
<button type="button" class="btn btn-danger" onClick="requestUpdate()">
|
||||
<i class="fa fa-tachometer-alt"></i>
|
||||
</button>
|
||||
|
||||
<button type="button" class="adminOnly btn btn-danger d-none" onClick="processDevices()">
|
||||
<i class="fa fa-random"></i>
|
||||
</button>
|
||||
<span class="adminOnly d-none"> </span>
|
||||
<button type="button" class="adminOnly btn btn-danger d-none" onClick="updateDevicesList(true)">
|
||||
<i class="fa fa-refresh"></i>
|
Before Width: | Height: | Size: 311 B After Width: | Height: | Size: 311 B |
Before Width: | Height: | Size: 668 B After Width: | Height: | Size: 668 B |
Before Width: | Height: | Size: 312 B After Width: | Height: | Size: 312 B |
Before Width: | Height: | Size: 773 B After Width: | Height: | Size: 773 B |
Before Width: | Height: | Size: 4.1 KiB After Width: | Height: | Size: 4.1 KiB |
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.4 KiB |
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 2.3 KiB |
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 24 KiB |
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 27 KiB |
Before Width: | Height: | Size: 4.0 KiB After Width: | Height: | Size: 4.0 KiB |
@ -68,7 +68,7 @@
|
||||
<a href="archived.html">More <img src="img/arrow-right.svg" alt="" style="width:20px"></a>
|
||||
</div>
|
||||
|
||||
<div class="light-section-item col-sm-6 col-md-3">
|
||||
<div class="light-section-item col-sm-6 col-md-3" style="-webkit-filter: grayscale(100%); /* Safari 6.0 - 9.0 */ filter: grayscale(100%);">
|
||||
<img src="img/icon/Group 1955.svg" alt="">
|
||||
<p class="title">Settings</p>
|
||||
<p>View the Resource Discovery service settings. Admins can also manage the service,
|
@ -252,8 +252,9 @@ function saveRequestInfo() {
|
||||
|
||||
// Fix messages
|
||||
var messages = $(`[id="request#messages"]`).val().trim();
|
||||
if (messages==='') messages = '';
|
||||
root['messages'] = messages.split(/\r\n|\r|\n/);
|
||||
root['messages'] = (messages!=='')
|
||||
? messages.split(/\r\n|\r|\n/)
|
||||
: [];
|
||||
|
||||
// Check request Id
|
||||
if (requestId!=='' && requestId!==root.id) {
|
||||
@ -283,9 +284,11 @@ function sendRequestData(requestData) {
|
||||
refreshRequestInfo(requestId);
|
||||
})
|
||||
.fail(function(xhr, status, error) {
|
||||
console.error('sendRequestData: ERROR: ', status, error);
|
||||
var data = {};
|
||||
try { data = JSON.parse(xhr.responseText); } catch (e) { }
|
||||
console.log('sendRequestData: ERROR: ', status, error, xhr.responseText);
|
||||
$('#page_title').html(
|
||||
$(`<div class="text-warning bg-danger">Error: ${status}: ${error}</div>`)
|
||||
$(`<div class="text-warning bg-danger">Error: ${status}: ${error ?? ''} ${data ? data.message : ''}</div>`)
|
||||
);
|
||||
})
|
||||
.always(function(data, status) {
|
||||
@ -304,6 +307,11 @@ function sendRequestData(requestData) {
|
||||
<a href="index.html"><img src="img/nebulous-logo-basic.png" width="155px" height="155px" alt=""></a>
|
||||
</div>
|
||||
<div class="text-end text-secondary nowrap">
|
||||
<a href="requests.html"><img src="img/icon/Group 1802.svg" width="32px" height="32px" /></a>
|
||||
<a href="devices.html"><img src="img/icon/Group 1953.svg" width="32px" height="32px" /></a>
|
||||
<a href="archived.html"><img src="img/icon/Group 1954.svg" width="32px" height="32px" /></a>
|
||||
<a xxxhref="#" style="-webkit-filter: grayscale(100%); /* Safari 6.0 - 9.0 */ filter: grayscale(100%);"><img src="img/icon/Group 1955.svg" width="32px" height="32px" /></a>
|
||||
|
||||
<img src="img/user-icon.png" width="24" height="auto">
|
||||
<span id="whoami"><i class="fas fa-spinner fa-spin"></i></span>
|
||||
|
||||
@ -450,6 +458,21 @@ function sendRequestData(requestData) {
|
||||
<input type="text" class="form-control" id="request#device#ipAddress" value="" placeholder="Device IP address">
|
||||
</div>
|
||||
</div>
|
||||
<!-- Device Location -->
|
||||
<div class="form-group row">
|
||||
<label for="request#device#location#name" class="col-sm-2 col-form-label"><b>Location</b></label>
|
||||
<div class="col-sm-4">
|
||||
<input type="text" class="form-control" id="request#device#location#name" value="" placeholder="Device Location">
|
||||
</div>
|
||||
<label for="request#device#location#latitude" class="col-sm-1 col-form-label">Latitude</label>
|
||||
<div class="col-sm-2">
|
||||
<input type="text" class="form-control" id="request#device#location#latitude" value="" placeholder="Device Latitude">
|
||||
</div>
|
||||
<label for="request#device#location#longitude" class="col-sm-1 col-form-label">Longitude</label>
|
||||
<div class="col-sm-2">
|
||||
<input type="text" class="form-control" id="request#device#location#longitude" value="" placeholder="Device Longitude">
|
||||
</div>
|
||||
</div>
|
||||
<!-- Device Username -->
|
||||
<div class="form-group row">
|
||||
<label for="request#device#username" class="col-sm-2 col-form-label"><b>SSH Username</b></label>
|
||||
@ -461,14 +484,14 @@ function sendRequestData(requestData) {
|
||||
<div class="form-group row">
|
||||
<label for="request#device#password" class="col-sm-2 col-form-label"><b>SSH Password</b></label>
|
||||
<div class="col-sm-10">
|
||||
<input type="text" class="form-control" id="request#device#password" value="" placeholder="SSH password">
|
||||
<input type="text" class="form-control" id="request#device#password" value="" placeholder="*** SSH password - Not exposed ***">
|
||||
</div>
|
||||
</div>
|
||||
<!-- Device Public Key -->
|
||||
<div class="form-group row">
|
||||
<label for="request#device#publicKey" class="col-sm-2 col-form-label"><b>SSH Public Key</b></label>
|
||||
<div class="col-sm-10">
|
||||
<textarea class="form-control" id="request#device#publicKey" placeholder="SSH public key"></textarea>
|
||||
<textarea class="form-control" id="request#device#publicKey" placeholder="*** SSH public key - Not exposed ***"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Device Additional Info -->
|
@ -80,13 +80,9 @@ function updateRequestsList(asAdmin) {
|
||||
</button>
|
||||
` : '';
|
||||
adminActions += (isAdmin) ? `
|
||||
<a href="#" target="_blank" onClick="
|
||||
this.href = '/discovery/request/${reqId}/status/'+prompt('Change status to:', 'NEW_REQUEST');
|
||||
">
|
||||
<button class="btn btn-outline-danger btn-sm">
|
||||
<i class="fas fa-fast-backward"></i>
|
||||
</button>
|
||||
</a>
|
||||
<button class="btn btn-outline-danger btn-sm" onClick="changeStatus('${reqId}')">
|
||||
<i class="fas fa-fast-backward"></i>
|
||||
</button>
|
||||
`: '';
|
||||
ii++;
|
||||
tbody.append( $(`
|
||||
@ -148,7 +144,11 @@ function processRequests() {
|
||||
})
|
||||
.fail(function(xhr, status, error) {
|
||||
console.error('processRequests: ERROR: ', status, error);
|
||||
});
|
||||
})
|
||||
.always(function(data, status) {
|
||||
setTimeout(updateRequestsList, 500);
|
||||
})
|
||||
;
|
||||
}
|
||||
|
||||
function authorizeRequest(reqId, authorize) {
|
||||
@ -179,6 +179,20 @@ function archiveRequest(reqId) {
|
||||
console.error('archiveRequest: ERROR: ', status, error);
|
||||
});
|
||||
}
|
||||
|
||||
function changeStatus(reqId) {
|
||||
var newStatus = prompt('Change status to:', 'NEW_REQUEST');
|
||||
if (newStatus!=null && newStatus.trim()!=='') {
|
||||
$.ajax({
|
||||
url: '/discovery/request/'+reqId+'/status/'+newStatus.trim().toUpperCase(),
|
||||
dataType: 'json'
|
||||
})
|
||||
.always(function(data, status) {
|
||||
console.log('changeStatus: OK: ', data);
|
||||
updateRequestsList(true);
|
||||
})
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
</head>
|
||||
@ -188,6 +202,12 @@ function archiveRequest(reqId) {
|
||||
<a href="index.html"><img src="img/nebulous-logo-basic.png" width="155px" height="155px" alt=""></a>
|
||||
</div>
|
||||
<div class="text-end text-secondary nowrap">
|
||||
<div style="display: inline-block; border: 2px solid red; border-radius: 30%;"><img src="img/icon/Group 1802.svg" width="32px" height="32px"/></div>
|
||||
<!--<a href="requests.html"><img src="img/icon/Group 1802.svg" width="32px" height="32px" /></a>-->
|
||||
<a href="devices.html"><img src="img/icon/Group 1953.svg" width="32px" height="32px" /></a>
|
||||
<a href="archived.html"><img src="img/icon/Group 1954.svg" width="32px" height="32px" /></a>
|
||||
<a xxxhref="#" style="-webkit-filter: grayscale(100%); /* Safari 6.0 - 9.0 */ filter: grayscale(100%);"><img src="img/icon/Group 1955.svg" width="32px" height="32px" /></a>
|
||||
|
||||
<img src="img/user-icon.png" width="24" height="auto">
|
||||
<span id="whoami"><i class="fas fa-spinner fa-spin"></i></span>
|
||||
|