RD: Squashed changes from 2023-10-06 to 2023-10-30

Change-Id: I8acdbaf7414e40a928de79b48ca8c996b154d62d
This commit is contained in:
ipatini 2023-10-30 16:30:10 +02:00
parent 5b7ca47065
commit 0c05760636
81 changed files with 3343 additions and 576 deletions

View File

@ -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;
}
}

View File

@ -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 {
}

View File

@ -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 ]

View File

@ -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>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
<span onClick="document.location = '/logout';">
<i class="fas fa-sign-out-alt"></i>
</span>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
<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>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
<span class="adminOnly d-none">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</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">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</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>&copy; 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>

View File

@ -10,14 +10,15 @@
<relativePath/> <!-- lookup parent from repository --> <relativePath/> <!-- lookup parent from repository -->
</parent> </parent>
<groupId>eu.nebulous.resource-discovery</groupId> <groupId>eu.nebulous.resource-management</groupId>
<artifactId>management</artifactId> <artifactId>resource-discovery</artifactId>
<version>1.0.0-SNAPSHOT</version> <version>1.0.0-SNAPSHOT</version>
<name>Resource discovery management service</name> <name>Resource discovery service</name>
<description>Nebulous resource discovery management service</description> <description>Nebulous resource discovery service</description>
<properties> <properties>
<java.version>17</java.version> <java.version>17</java.version>
<imageName>${project.artifactId}:${project.version}</imageName>
</properties> </properties>
<dependencies> <dependencies>
@ -88,6 +89,21 @@
<plugin> <plugin>
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId> <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> </plugin>
</plugins> </plugins>
</build> </build>

View File

@ -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;
}
}

View File

@ -4,18 +4,18 @@ import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; 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.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity; 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.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.core.userdetails.User; 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.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager; import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.SecurityFilterChain;
import java.util.ArrayList; import java.security.SecureRandom;
import java.util.List;
import static org.springframework.security.config.Customizer.withDefaults; import static org.springframework.security.config.Customizer.withDefaults;
@ -39,21 +39,31 @@ public class SecurityConfig {
return httpSecurity.build(); return httpSecurity.build();
} }
@Bean public void configAuthentication(AuthenticationManagerBuilder auth) throws Exception {
public InMemoryUserDetailsManager userDetailsService() { auth.inMemoryAuthentication().passwordEncoder(passwordEncoder());
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]));
} }
@Bean @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() { public PasswordEncoder encoder() {
// Clear-text password encoder
return new PasswordEncoder() { return new PasswordEncoder() {
@Override @Override
public String encode(CharSequence rawPassword) { public String encode(CharSequence rawPassword) {
@ -65,5 +75,5 @@ public class SecurityConfig {
return rawPassword.toString().equals(encodedPassword); return rawPassword.toString().equals(encodedPassword);
} }
}; };
} }*/
} }

View File

@ -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";
}
}

View File

@ -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);
}
}

View File

@ -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;
}

View File

@ -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));
}
}

View File

@ -0,0 +1,5 @@
package eu.nebulous.resource.discovery.common;
public enum REQUEST_TYPE {
INSTALL, REINSTALL, UNINSTALL, NODE_DETAILS, INFO, DIAGNOSTICS, OTHER
}

View File

@ -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");
}
}

View File

@ -6,12 +6,12 @@ import eu.nebulous.resource.discovery.monitor.model.DeviceException;
import eu.nebulous.resource.discovery.monitor.service.DeviceManagementService; import eu.nebulous.resource.discovery.monitor.service.DeviceManagementService;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import java.util.List; import java.util.List;
import java.util.Map;
@Slf4j @Slf4j
@RestController @RestController
@ -42,9 +42,10 @@ public class ArchivedDeviceManagementController {
return deviceService.getArchivedByIpAddress(ipAddress); return deviceService.getArchivedByIpAddress(ipAddress);
} }
@GetMapping(value = "/device/{id}/unarchive", produces = MediaType.APPLICATION_JSON_VALUE) @PostMapping(value = "/device/{id}/unarchive",
public String unarchiveDevice(@PathVariable String id) { consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
deviceService.unarchiveDevice(id); public String unarchiveDevice(@PathVariable String id, @RequestBody Map<String,String> credentials) {
deviceService.unarchiveDevice(id, credentials);
return "UNARCHIVED"; return "UNARCHIVED";
} }
} }

View File

@ -1,11 +1,16 @@
package eu.nebulous.resource.discovery.monitor.controller; 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.Device;
import eu.nebulous.resource.discovery.monitor.model.DeviceException; 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.monitor.service.DeviceManagementService;
import eu.nebulous.resource.discovery.registration.model.RegistrationRequestException;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.Authentication; import org.springframework.security.core.Authentication;
@ -13,6 +18,9 @@ import org.springframework.security.core.GrantedAuthority;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
@Slf4j @Slf4j
@RestController @RestController
@ -20,7 +28,9 @@ import java.util.List;
@RequestMapping("/monitor") @RequestMapping("/monitor")
@PreAuthorize("hasAuthority('ROLE_ADMIN')") @PreAuthorize("hasAuthority('ROLE_ADMIN')")
public class DeviceManagementController { public class DeviceManagementController {
private final DeviceProcessor deviceProcessor;
private final DeviceManagementService deviceService; private final DeviceManagementService deviceService;
private final DeviceLifeCycleRequestService deviceLifeCycleRequestService;
private boolean isAuthenticated(Authentication authentication) { private boolean isAuthenticated(Authentication authentication) {
return authentication!=null && StringUtils.isNotBlank(authentication.getName()); return authentication!=null && StringUtils.isNotBlank(authentication.getName());
@ -89,9 +99,76 @@ public class DeviceManagementController {
deviceService.deleteById(id); 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) @GetMapping(value = "/device/{id}/archive", produces = MediaType.APPLICATION_JSON_VALUE)
public String archiveDevice(@PathVariable String id) { public String archiveDevice(@PathVariable String id) {
deviceService.archiveDevice(id); deviceService.archiveDevice(id);
return "ARCHIVED"; 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()
);
}
} }

View File

@ -1,10 +1,8 @@
package eu.nebulous.resource.discovery.monitor.model; package eu.nebulous.resource.discovery.monitor.model;
import eu.nebulous.resource.discovery.registration.model.RegistrationRequest; import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.AccessLevel; import eu.nebulous.resource.discovery.common.DeviceLocation;
import lombok.Data; import lombok.*;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.experimental.SuperBuilder; import lombok.experimental.SuperBuilder;
import org.springframework.data.mongodb.core.mapping.Document; import org.springframework.data.mongodb.core.mapping.Document;
@ -23,12 +21,17 @@ public class Device {
private String name; private String name;
private String owner; private String owner;
private String ipAddress; private String ipAddress;
private DeviceLocation location;
private String username; private String username;
@ToString.Exclude
@JsonProperty(access = JsonProperty.Access.WRITE_ONLY)
private char[] password; private char[] password;
@ToString.Exclude
@JsonProperty(access = JsonProperty.Access.WRITE_ONLY)
private char[] publicKey; private char[] publicKey;
private Map<String, String> deviceInfo; private Map<String, String> deviceInfo;
private RegistrationRequest request; //private RegistrationRequest request;
private String requestId; private String requestId;
private Instant creationDate; private Instant creationDate;
private Instant lastUpdateDate; private Instant lastUpdateDate;
@ -38,4 +41,14 @@ public class Device {
private String nodeReference; private String nodeReference;
@Setter(AccessLevel.NONE) @Setter(AccessLevel.NONE)
private List<String> messages = new ArrayList<>(); private List<String> messages = new ArrayList<>();
private DeviceStatusUpdate statusUpdate;
private DeviceMetrics metrics;
private Instant suspectTimestamp;
private int retries;
public void incrementRetries() {
retries++;
}
} }

View File

@ -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;
}

View File

@ -3,6 +3,7 @@ package eu.nebulous.resource.discovery.monitor.model;
public enum DeviceStatus { public enum DeviceStatus {
NEW_DEVICE, ON_HOLD, NEW_DEVICE, ON_HOLD,
ONBOARDING, ONBOARDED, ONBOARD_ERROR, ONBOARDING, ONBOARDED, ONBOARD_ERROR,
HEALTHY, BUSY, IDLE, HEALTHY, SUSPECT, FAILED,
BUSY, IDLE,
OFFBOARDING, OFFBOARDED, OFFBOARD_ERROR OFFBOARDING, OFFBOARDED, OFFBOARD_ERROR
} }

View File

@ -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;
}

View File

@ -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);
}

View File

@ -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;
}
}
}

View File

@ -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());
}
}
}

View File

@ -1,15 +1,19 @@
package eu.nebulous.resource.discovery.monitor.service; 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.ArchivedDevice;
import eu.nebulous.resource.discovery.monitor.model.Device; import eu.nebulous.resource.discovery.monitor.model.Device;
import eu.nebulous.resource.discovery.monitor.model.DeviceException; import eu.nebulous.resource.discovery.monitor.model.DeviceException;
import eu.nebulous.resource.discovery.monitor.model.DeviceStatus; import eu.nebulous.resource.discovery.monitor.model.DeviceStatus;
import eu.nebulous.resource.discovery.monitor.repository.ArchivedDeviceRepository; import eu.nebulous.resource.discovery.monitor.repository.ArchivedDeviceRepository;
import eu.nebulous.resource.discovery.monitor.repository.DeviceRepository; import eu.nebulous.resource.discovery.monitor.repository.DeviceRepository;
import eu.nebulous.resource.discovery.registration.model.RegistrationRequestException;
import lombok.NonNull; import lombok.NonNull;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import java.time.Instant; import java.time.Instant;
@ -19,6 +23,7 @@ import java.util.*;
@Service @Service
@RequiredArgsConstructor @RequiredArgsConstructor
public class DeviceManagementService { public class DeviceManagementService {
private final ResourceDiscoveryProperties properties;
private final DeviceRepository deviceRepository; private final DeviceRepository deviceRepository;
private final ArchivedDeviceRepository archivedDeviceRepository; private final ArchivedDeviceRepository archivedDeviceRepository;
private final DeviceConversionService deviceConversionService; private final DeviceConversionService deviceConversionService;
@ -41,6 +46,10 @@ public class DeviceManagementService {
return deviceRepository.findByIpAddress(ipAddress); return deviceRepository.findByIpAddress(ipAddress);
} }
public boolean isIpAddressInUse(@NonNull String ipAddress) {
return deviceRepository.findByIpAddress(ipAddress).isPresent();
}
public @NonNull Device save(@NonNull Device device) { public @NonNull Device save(@NonNull Device device) {
DeviceStatus status = device.getStatus(); DeviceStatus status = device.getStatus();
checkDevice(device, true); 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() { public List<ArchivedDevice> getArchivedAll() {
return Collections.unmodifiableList(archivedDeviceRepository.findAll()); return Collections.unmodifiableList(archivedDeviceRepository.findAll());
} }
@ -126,20 +151,40 @@ public class DeviceManagementService {
public List<ArchivedDevice> getArchivedByOwner(@NonNull String owner) { public List<ArchivedDevice> getArchivedByOwner(@NonNull String owner) {
return archivedDeviceRepository.findByOwner(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) { public Optional<ArchivedDevice> getArchivedById(@NonNull String id) {
return archivedDeviceRepository.findById(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) { public List<ArchivedDevice> getArchivedByIpAddress(@NonNull String ipAddress) {
return archivedDeviceRepository.findByIpAddress(ipAddress); return archivedDeviceRepository.findByIpAddress(ipAddress);
} }
public void archiveDevice(String id) { public void archiveDevice(String id) {
archiveRequestBySystem(id); archiveDeviceBySystem(id);
} }
public void archiveRequestBySystem(String id) { public void archiveDeviceBySystem(String id) {
Optional<Device> result = getById(id); Optional<Device> result = getById(id);
if (result.isEmpty()) if (result.isEmpty())
throw new DeviceException( throw new DeviceException(
@ -149,14 +194,32 @@ public class DeviceManagementService {
deviceRepository.delete(result.get()); deviceRepository.delete(result.get());
} }
public void unarchiveDevice(String id) { public void unarchiveDevice(String id, Map<String,String> credentials) {
Optional<ArchivedDevice> result = getArchivedById(id); Optional<ArchivedDevice> result = getArchivedById(id);
if (result.isEmpty()) if (result.isEmpty())
throw new DeviceException( throw new DeviceException(
"Archived device with Id does not exists in repository: "+id); "Archived device with Id does not exists in repository: "+id);
checkCredentials(result.get().getId(), credentials);
result.get().setArchiveDate(null); 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()); 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);
}
} }

View File

@ -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();
}
}

View File

@ -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);
}
}
}

View File

@ -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");
}
}

View File

@ -1,23 +1,17 @@
package eu.nebulous.resource.discovery.registration; 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 com.fasterxml.jackson.databind.ObjectMapper;
import eu.nebulous.resource.discovery.ResourceDiscoveryProperties; 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.Device;
import eu.nebulous.resource.discovery.monitor.service.DeviceManagementService; import eu.nebulous.resource.discovery.monitor.service.DeviceManagementService;
import eu.nebulous.resource.discovery.registration.model.RegistrationRequest; import eu.nebulous.resource.discovery.registration.model.RegistrationRequest;
import eu.nebulous.resource.discovery.registration.model.RegistrationRequestStatus; import eu.nebulous.resource.discovery.registration.model.RegistrationRequestStatus;
import eu.nebulous.resource.discovery.registration.service.RegistrationRequestService; import eu.nebulous.resource.discovery.registration.service.RegistrationRequestService;
import jakarta.jms.*;
import lombok.NonNull; import lombok.NonNull;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; 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.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.InitializingBean; import org.springframework.beans.factory.InitializingBean;
import org.springframework.scheduling.TaskScheduler; import org.springframework.scheduling.TaskScheduler;
@ -42,10 +36,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
@EnableAsync @EnableAsync
@EnableScheduling @EnableScheduling
@RequiredArgsConstructor @RequiredArgsConstructor
public class RegistrationRequestProcessor implements IRegistrationRequestProcessor, InitializingBean, MessageListener { public class RegistrationRequestProcessor implements IRegistrationRequestProcessor, InitializingBean, BrokerUtil.Listener {
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
private final static List<RegistrationRequestStatus> STATUSES_TO_ARCHIVE = List.of( private final static List<RegistrationRequestStatus> STATUSES_TO_ARCHIVE = List.of(
RegistrationRequestStatus.PRE_AUTHORIZATION_REJECT, RegistrationRequestStatus.PRE_AUTHORIZATION_REJECT,
RegistrationRequestStatus.PRE_AUTHORIZATION_ERROR, RegistrationRequestStatus.PRE_AUTHORIZATION_ERROR,
@ -61,10 +52,11 @@ public class RegistrationRequestProcessor implements IRegistrationRequestProcess
private final DeviceManagementService deviceManagementService; private final DeviceManagementService deviceManagementService;
private final TaskScheduler taskScheduler; private final TaskScheduler taskScheduler;
private final ObjectMapper objectMapper; private final ObjectMapper objectMapper;
private final BrokerUtil brokerUtil;
private final AtomicBoolean isRunning = new AtomicBoolean(false); private final AtomicBoolean isRunning = new AtomicBoolean(false);
@Override @Override
public void afterPropertiesSet() throws Exception { public void afterPropertiesSet() {
// Initialize request processing results listener // Initialize request processing results listener
taskScheduler.schedule(this::initializeResultsListener, Instant.now().plusSeconds(processorProperties.getSubscriptionStartupDelay())); taskScheduler.schedule(this::initializeResultsListener, Instant.now().plusSeconds(processorProperties.getSubscriptionStartupDelay()));
@ -91,28 +83,16 @@ public class RegistrationRequestProcessor implements IRegistrationRequestProcess
} }
log.debug("processRequests: Processing registration requests"); 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 // Process requests
try { try {
processNewRequests(producer); processNewRequests();
processOnboardingRequests(producer); processOnboardingRequests();
if (processorProperties.isAutomaticArchivingEnabled()) if (processorProperties.isAutomaticArchivingEnabled())
archiveRequests(); archiveRequests();
} catch (Throwable t) { } catch (Throwable t) {
log.error("processRequests: ERROR processing requests: ", t); log.error("processRequests: ERROR processing requests: ", t);
} }
// Close connection to Message broker
conn.close();
log.debug("processRequests: Processing completed"); log.debug("processRequests: Processing completed");
return CompletableFuture.completedFuture("DONE"); return CompletableFuture.completedFuture("DONE");
@ -125,8 +105,8 @@ public class RegistrationRequestProcessor implements IRegistrationRequestProcess
} }
} }
private void processNewRequests(MessageProducer producer) throws JsonProcessingException, JMSException { private void processNewRequests() {
log.trace("processNewRequests: BEGIN: {}", producer); log.trace("processNewRequests: BEGIN");
List<RegistrationRequest> newRequests = registrationRequestService.getAll().stream() List<RegistrationRequest> newRequests = registrationRequestService.getAll().stream()
.filter(r -> r.getStatus() == RegistrationRequestStatus.NEW_REQUEST).toList(); .filter(r -> r.getStatus() == RegistrationRequestStatus.NEW_REQUEST).toList();
@ -136,10 +116,11 @@ public class RegistrationRequestProcessor implements IRegistrationRequestProcess
for (RegistrationRequest registrationRequest : newRequests) { for (RegistrationRequest registrationRequest : newRequests) {
try { try {
log.debug("processNewRequests: Requesting collection of device data for request with Id: {}", registrationRequest.getId()); log.debug("processNewRequests: Requesting collection of device data for request with Id: {}", registrationRequest.getId());
Map<String, String> dataCollectionRequest = prepareRequestPayload(REQUEST_TYPE_DATA_COLLECTION, registrationRequest); Map<String, String> dataCollectionRequest = prepareRequestPayload(REQUEST_TYPE.DIAGNOSTICS, registrationRequest);
String jsonMessage = objectMapper.writer().writeValueAsString(dataCollectionRequest); brokerUtil.sendMessage(processorProperties.getDataCollectionRequestTopic(), dataCollectionRequest);
producer.send(createMessage(jsonMessage));
registrationRequest.setStatus(RegistrationRequestStatus.DATA_COLLECTION_REQUESTED); registrationRequest.setStatus(RegistrationRequestStatus.DATA_COLLECTION_REQUESTED);
log.debug("processNewRequests: Save updated request: id={}, request={}", registrationRequest.getId(), registrationRequest);
registrationRequestService.update(registrationRequest); registrationRequestService.update(registrationRequest);
log.debug("processNewRequests: Data collection request sent for request with Id: {}", registrationRequest.getId()); log.debug("processNewRequests: Data collection request sent for request with Id: {}", registrationRequest.getId());
} catch (Exception e) { } catch (Exception e) {
@ -152,8 +133,8 @@ public class RegistrationRequestProcessor implements IRegistrationRequestProcess
log.trace("processNewRequests: END"); log.trace("processNewRequests: END");
} }
private void processOnboardingRequests(MessageProducer producer) throws JsonProcessingException, JMSException { private void processOnboardingRequests() {
log.trace("processOnboardingRequests: BEGIN: {}", producer); log.trace("processOnboardingRequests: BEGIN");
List<RegistrationRequest> onboardingRequests = registrationRequestService.getAll().stream() List<RegistrationRequest> onboardingRequests = registrationRequestService.getAll().stream()
.filter(r -> r.getStatus() == RegistrationRequestStatus.PENDING_ONBOARDING).toList(); .filter(r -> r.getStatus() == RegistrationRequestStatus.PENDING_ONBOARDING).toList();
@ -162,33 +143,36 @@ public class RegistrationRequestProcessor implements IRegistrationRequestProcess
for (RegistrationRequest registrationRequest : onboardingRequests) { for (RegistrationRequest registrationRequest : onboardingRequests) {
try { try {
log.debug("processOnboardingRequests: Checking device before requesting onboarding for request with Id: {}", registrationRequest.getId()); log.debug("processOnboardingRequests: Checking device data before requesting onboarding, for request with Id: {}", registrationRequest.getId());
deviceManagementService.checkDevice( Device deviceForMonitoring = objectMapper.convertValue(registrationRequest.getDevice(), Device.class);
objectMapper.convertValue(registrationRequest.getDevice(), Device.class), deviceForMonitoring.setPassword(registrationRequest.getDevice().getPassword()); // ignored by 'objectMapper', so we've to copy them
true); 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()); log.debug("processOnboardingRequests: Requesting device onboarding for request with Id: {}", registrationRequest.getId());
Map<String, String> dataCollectionRequest = prepareRequestPayload(REQUEST_TYPE_ONBOARDING, registrationRequest); Map<String, String> dataCollectionRequest = prepareRequestPayload(REQUEST_TYPE.INSTALL, registrationRequest);
String jsonMessage = objectMapper.writer().writeValueAsString(dataCollectionRequest); brokerUtil.sendMessage(processorProperties.getDataCollectionRequestTopic(), dataCollectionRequest);
producer.send(createMessage(jsonMessage));
registrationRequest.setStatus(RegistrationRequestStatus.ONBOARDING_REQUESTED); 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()); log.debug("processOnboardingRequests: Onboarding request sent for request with Id: {}", registrationRequest.getId());
} catch (Exception e) { } catch (Exception e) {
log.warn("processOnboardingRequests: EXCEPTION while sending onboarding request for request with Id: {}\n", registrationRequest.getId(), e); log.warn("processOnboardingRequests: EXCEPTION while sending onboarding request for request with Id: {}\n", registrationRequest.getId(), e);
registrationRequest.setStatus(RegistrationRequestStatus.ONBOARDING_ERROR); registrationRequest.setStatus(RegistrationRequestStatus.ONBOARDING_ERROR);
registrationRequestService.update(registrationRequest); registrationRequest.getMessages().add("EXCEPTION "+e.getMessage());
registrationRequestService.update(registrationRequest, false);
} }
} }
log.trace("processOnboardingRequests: END"); 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 { try {
Map<String, String> payload = new LinkedHashMap<>(Map.of( Map<String, String> payload = new LinkedHashMap<>(Map.of(
"requestId", registrationRequest.getId(), "requestId", registrationRequest.getId(),
"requestType", requestType, "requestType", requestType.name(),
"deviceId", registrationRequest.getDevice().getId(), "deviceId", registrationRequest.getDevice().getId(),
"deviceOs", registrationRequest.getDevice().getOs(), "deviceOs", registrationRequest.getDevice().getOs(),
"deviceName", registrationRequest.getDevice().getName(), "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() { private void archiveRequests() {
Instant archiveThreshold = Instant.now().minus(processorProperties.getArchivingThreshold(), ChronoUnit.MINUTES); Instant archiveThreshold = Instant.now().minus(processorProperties.getArchivingThreshold(), ChronoUnit.MINUTES);
log.trace("archiveRequests: BEGIN: archive-threshold: {}", archiveThreshold); log.trace("archiveRequests: BEGIN: archive-threshold: {}", archiveThreshold);
@ -238,46 +216,22 @@ public class RegistrationRequestProcessor implements IRegistrationRequestProcess
protected void initializeResultsListener() { protected void initializeResultsListener() {
try { try {
ActiveMQConnectionFactory connectionFactory = new ActiveMQConnectionFactory( brokerUtil.subscribe(processorProperties.getDataCollectionResponseTopic(), this);
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();
} catch (Exception e) { } catch (Exception e) {
log.error("RegistrationRequestProcessor: ERROR while subscribing to Message broker for Device info announcements: ", 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(Map message) {
public void onMessage(Message message) { processResponse(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);
}
} }
private void processResponse(@NonNull Map<String, Object> response) { private void processResponse(@NonNull Map<String, Object> response) {
String requestType = response.getOrDefault("requestType", "").toString().trim();
String requestId = response.getOrDefault("requestId", "").toString().trim(); String requestId = response.getOrDefault("requestId", "").toString().trim();
String reference = response.getOrDefault("reference", "").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(); String deviceIpAddress = response.getOrDefault("deviceIpAddress", "").toString().trim();
long timestamp = Long.parseLong(response.getOrDefault("timestamp", "-1").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; case ONBOARDING_REQUESTED -> RegistrationRequestStatus.ONBOARDING_ERROR;
default -> currStatus; default -> currStatus;
}; };
log.debug("processResponse: Temporary status change: {} --> {}", currStatus, newStatus);
registrationRequest.setStatus(newStatus); registrationRequest.setStatus(newStatus);
if (currStatus==RegistrationRequestStatus.SUCCESS) { if (currStatus==RegistrationRequestStatus.SUCCESS) {
@ -301,8 +256,8 @@ public class RegistrationRequestProcessor implements IRegistrationRequestProcess
String ipAddress = registrationRequest.getDevice().getIpAddress(); String ipAddress = registrationRequest.getDevice().getIpAddress();
boolean isError = false; boolean isError = false;
if (StringUtils.equals(ipAddress, deviceIpAddress)) { if (StringUtils.isNotBlank(deviceIpAddress) && ! 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); 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); log.warn("processResponse: {}", mesg);
registrationRequest.getMessages().add(mesg); registrationRequest.getMessages().add(mesg);
isError = true; isError = true;
@ -313,17 +268,21 @@ public class RegistrationRequestProcessor implements IRegistrationRequestProcess
registrationRequest.getMessages().add(mesg); registrationRequest.getMessages().add(mesg);
isError = true; isError = true;
} }
if (! "SUCCESS".equals(status)) { if (! "SUCCESS".equals(responseStatus)) {
String mesg = String.format("Request status is not SUCCESS: id=%s, timestamp=%d, status=%s", requestId, timestamp, status); String mesg = String.format("RESPONSE status is not SUCCESS: id=%s, timestamp=%d, status=%s", requestId, timestamp, responseStatus);
log.warn("processResponse: {}", mesg); log.warn("processResponse: {}", mesg);
registrationRequest.getMessages().add(mesg); registrationRequest.getMessages().add(mesg);
isError = true; isError = true;
} }
if (isError) { 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; return;
} }
boolean doArchive = false;
Object obj = response.get("nodeInfo"); Object obj = response.get("nodeInfo");
if (obj instanceof Map devInfo) { if (obj instanceof Map devInfo) {
// Update request info // Update request info
@ -358,8 +317,10 @@ public class RegistrationRequestProcessor implements IRegistrationRequestProcess
// Set new status // Set new status
if (currStatus==RegistrationRequestStatus.DATA_COLLECTION_REQUESTED) if (currStatus==RegistrationRequestStatus.DATA_COLLECTION_REQUESTED)
registrationRequest.setStatus(RegistrationRequestStatus.PENDING_AUTHORIZATION); registrationRequest.setStatus(RegistrationRequestStatus.PENDING_AUTHORIZATION);
if (currStatus==RegistrationRequestStatus.ONBOARDING_REQUESTED) if (currStatus==RegistrationRequestStatus.ONBOARDING_REQUESTED) {
registrationRequest.setStatus(RegistrationRequestStatus.SUCCESS); registrationRequest.setStatus(RegistrationRequestStatus.SUCCESS);
doArchive = processorProperties.isImmediatelyArchiveSuccessRequests();
}
log.debug("processResponse: Done processing response for request: id={}, timestamp={}", requestId, timestamp); log.debug("processResponse: Done processing response for request: id={}, timestamp={}", requestId, timestamp);
} else { } else {
@ -378,19 +339,32 @@ public class RegistrationRequestProcessor implements IRegistrationRequestProcess
} }
// Store changes // 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 { } else {
log.warn("processResponse: Request not found: id={}", requestId); log.debug("processResponse: Request not found: id={}, requestType={}", requestId, requestType);
} }
} }
private void copyDeviceToMonitoring(RegistrationRequest registrationRequest) { private void copyDeviceToMonitoring(RegistrationRequest registrationRequest) {
Device device = objectMapper.convertValue(registrationRequest.getDevice(), Device.class); Device device = objectMapper.convertValue(registrationRequest.getDevice(), Device.class);
// override values
device.setId(null); device.setId(null);
device.setStatus(null); device.setStatus(null);
device.setRequest(registrationRequest);
device.setRequestId(registrationRequest.getId());
device.getMessages().clear(); 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()); device.setNodeReference(registrationRequest.getNodeReference());
deviceManagementService.save(device); deviceManagementService.save(device);
} }

View File

@ -1,14 +1,15 @@
package eu.nebulous.resource.discovery.registration.controller; package eu.nebulous.resource.discovery.registration.controller;
import eu.nebulous.resource.discovery.registration.IRegistrationRequestProcessor; 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.ArchivedRegistrationRequest;
import eu.nebulous.resource.discovery.registration.model.RegistrationRequest; import eu.nebulous.resource.discovery.registration.model.RegistrationRequest;
import eu.nebulous.resource.discovery.registration.model.RegistrationRequestException; import eu.nebulous.resource.discovery.registration.model.RegistrationRequestException;
import eu.nebulous.resource.discovery.registration.model.RegistrationRequestStatus; import eu.nebulous.resource.discovery.registration.model.RegistrationRequestStatus;
import eu.nebulous.resource.discovery.registration.service.RegistrationRequestService;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.Authentication; import org.springframework.security.core.Authentication;
@ -118,9 +119,13 @@ public class RegistrationRequestController {
} }
@PreAuthorize("hasAuthority('ROLE_ADMIN')") @PreAuthorize("hasAuthority('ROLE_ADMIN')")
@GetMapping(value = "/request/{id}/unarchive", produces = MediaType.APPLICATION_JSON_VALUE) @PostMapping(value = "/request/{id}/unarchive",
public RegistrationRequest unarchiveRequest(@PathVariable String id, Authentication authentication) { consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
registrationRequestService.unarchiveRequest(id, authentication); public RegistrationRequest unarchiveRequest(@PathVariable String id,
@RequestBody Map<String,String> credentials,
Authentication authentication)
{
registrationRequestService.unarchiveRequest(id, credentials, authentication);
return registrationRequestService.getById(id) return registrationRequestService.getById(id)
.orElseThrow(() -> new RegistrationRequestException("Failed to unarchive registration request with id: " + id)); .orElseThrow(() -> new RegistrationRequestException("Failed to unarchive registration request with id: " + id));
} }
@ -141,4 +146,15 @@ public class RegistrationRequestController {
return registrationRequestService.getArchivedByIdAsUser(id, authentication) return registrationRequestService.getArchivedByIdAsUser(id, authentication)
.orElseThrow(() -> new RegistrationRequestException("Not found archived registration request with id: "+id)); .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()
);
}
} }

View File

@ -1,7 +1,10 @@
package eu.nebulous.resource.discovery.registration.model; 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.Data;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
import lombok.ToString;
import lombok.experimental.SuperBuilder; import lombok.experimental.SuperBuilder;
import java.util.Map; import java.util.Map;
@ -15,8 +18,13 @@ public class Device {
private String name; private String name;
private String owner; private String owner;
private String ipAddress; private String ipAddress;
private DeviceLocation location;
private String username; private String username;
@ToString.Exclude
@JsonProperty(access = JsonProperty.Access.WRITE_ONLY)
private char[] password; private char[] password;
@ToString.Exclude
@JsonProperty(access = JsonProperty.Access.WRITE_ONLY)
private char[] publicKey; private char[] publicKey;
private Map<String, String> deviceInfo; private Map<String, String> deviceInfo;
} }

View File

@ -25,8 +25,18 @@ public class RegistrationRequest {
private Instant lastUpdateDate; private Instant lastUpdateDate;
private Instant archiveDate; private Instant archiveDate;
private RegistrationRequestStatus status; private RegistrationRequestStatus status;
private List<RegistrationRequestHistoryEntry> history; private List<RegistrationRequestHistoryEntry> history = new ArrayList<>();
private String nodeReference; private String nodeReference;
@Setter(AccessLevel.NONE) @Setter(AccessLevel.NONE)
private List<String> messages = new ArrayList<>(); 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);
}
} }

View File

@ -7,4 +7,5 @@ import java.util.List;
public interface RegistrationRequestRepository extends MongoRepository<RegistrationRequest, String> { public interface RegistrationRequestRepository extends MongoRepository<RegistrationRequest, String> {
List<RegistrationRequest> findByRequester(String requester); List<RegistrationRequest> findByRequester(String requester);
List<RegistrationRequest> findByDeviceIpAddress(String ipAddress);
} }

View File

@ -1,5 +1,6 @@
package eu.nebulous.resource.discovery.registration.service; 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.model.*;
import eu.nebulous.resource.discovery.registration.repository.ArchivedRegistrationRequestRepository; import eu.nebulous.resource.discovery.registration.repository.ArchivedRegistrationRequestRepository;
import eu.nebulous.resource.discovery.registration.repository.RegistrationRequestRepository; import eu.nebulous.resource.discovery.registration.repository.RegistrationRequestRepository;
@ -7,6 +8,7 @@ import lombok.NonNull;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.BeanUtils;
import org.springframework.security.core.Authentication; import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.GrantedAuthority;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
@ -21,6 +23,7 @@ public class RegistrationRequestService {
private final RegistrationRequestRepository registrationRequestRepository; private final RegistrationRequestRepository registrationRequestRepository;
private final ArchivedRegistrationRequestRepository archivedRegistrationRequestRepository; private final ArchivedRegistrationRequestRepository archivedRegistrationRequestRepository;
private final RegistrationRequestConversionService registrationRequestConversionService; private final RegistrationRequestConversionService registrationRequestConversionService;
private final DeviceManagementService deviceManagementService;
/*private final InMemoryRegistrationRequestRepository<RegistrationRequest> /*private final InMemoryRegistrationRequestRepository<RegistrationRequest>
registrationRequestRepository = new InMemoryRegistrationRequestRepository<>(); registrationRequestRepository = new InMemoryRegistrationRequestRepository<>();
@ -38,10 +41,19 @@ public class RegistrationRequestService {
return registrationRequestRepository.findById(id); return registrationRequestRepository.findById(id);
} }
public List<RegistrationRequest> getByDeviceIpAddress(@NonNull String ipAddress) {
return registrationRequestRepository.findByDeviceIpAddress(ipAddress);
}
public List<RegistrationRequest> getAll() { public List<RegistrationRequest> getAll() {
return Collections.unmodifiableList(registrationRequestRepository.findAll()); 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) { public @NonNull RegistrationRequest save(@NonNull RegistrationRequest registrationRequest) {
RegistrationRequestStatus status = registrationRequest.getStatus(); RegistrationRequestStatus status = registrationRequest.getStatus();
if (status == null) { if (status == null) {
@ -62,6 +74,9 @@ public class RegistrationRequestService {
registrationRequest.setRequestDate(Instant.now()); registrationRequest.setRequestDate(Instant.now());
checkRegistrationRequest(registrationRequest); checkRegistrationRequest(registrationRequest);
// check IP address uniqueness
checkIpAddressUniqueness(registrationRequest);
registrationRequestRepository.save(registrationRequest); registrationRequestRepository.save(registrationRequest);
return registrationRequest; return registrationRequest;
} }
@ -71,6 +86,10 @@ public class RegistrationRequestService {
} }
public RegistrationRequest update(@NonNull RegistrationRequest registrationRequest, boolean checkEditDel) { 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()); Optional<RegistrationRequest> result = getById(registrationRequest.getId());
if (result.isEmpty()) if (result.isEmpty())
throw new RegistrationRequestException( throw new RegistrationRequestException(
@ -79,13 +98,45 @@ public class RegistrationRequestService {
if (checkEditDel) if (checkEditDel)
canEditOrDelete(result.get()); canEditOrDelete(result.get());
registrationRequest.setLastUpdateDate(Instant.now()); // check IP address uniqueness
registrationRequestRepository.save(registrationRequest); 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(() -> return getById(registrationRequest.getId()).orElseThrow(() ->
new RegistrationRequestException("Request update failed for Id: "+registrationRequest.getId())); 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) { private void checkRegistrationRequest(@NonNull RegistrationRequest registrationRequest) {
List<String> errors = new ArrayList<>(); List<String> errors = new ArrayList<>();
if (StringUtils.isBlank(registrationRequest.getId())) errors.add("Null or blank Id"); if (StringUtils.isBlank(registrationRequest.getId())) errors.add("Null or blank Id");
@ -105,6 +156,17 @@ public class RegistrationRequestService {
//XXX:TODO //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) { private void canEditOrDelete(RegistrationRequest registrationRequest) {
RegistrationRequestStatus status = registrationRequest.getStatus(); RegistrationRequestStatus status = registrationRequest.getStatus();
if (status==RegistrationRequestStatus.ONBOARDING_REQUESTED || status== RegistrationRequestStatus.SUCCESS) if (status==RegistrationRequestStatus.ONBOARDING_REQUESTED || status== RegistrationRequestStatus.SUCCESS)
@ -248,16 +310,34 @@ public class RegistrationRequestService {
registrationRequestRepository.delete(result.get()); 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); Optional<ArchivedRegistrationRequest> result = getArchivedById(id);
if (result.isEmpty()) if (result.isEmpty())
throw new RegistrationRequestException( throw new RegistrationRequestException(
"Archived registration request with Id does not exists in repository: "+id); "Archived registration request with Id does not exists in repository: "+id);
checkAdmin(result.get().getId(), authentication); checkAdmin(result.get().getId(), authentication);
checkCredentials(result.get().getId(), credentials);
result.get().setArchiveDate(null); result.get().setArchiveDate(null);
registrationRequestRepository.save( RegistrationRequest restoredRequest = registrationRequestConversionService.toRegistrationRequest(result.get());
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()); 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);
}
} }

View 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

View 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})

View File

@ -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>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
<img src="img/user-icon.png" width="24" height="auto">
<span id="whoami"><i class="fas fa-spinner fa-spin"></i></span>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
<span onClick="document.location = '/logout';">
<i class="fas fa-sign-out-alt"></i>
</span>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
<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>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
<button type="button" class="btn btn-primary" onClick="document.location = 'archived.html';">
<i class="fa fa-arrow-left"></i>
</button>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
<button type="button" class="btn btn-primary" onClick="refreshDeviceInfo(deviceId);">
<i class="fa fa-refresh"></i>
</button>
<p>&nbsp;</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>&nbsp;</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>&copy; 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>

View File

@ -146,6 +146,11 @@ function flattenObject(ob) {
<a href="index.html"><img src="img/nebulous-logo-basic.png" width="155px" height="155px" alt=""></a> <a href="index.html"><img src="img/nebulous-logo-basic.png" width="155px" height="155px" alt=""></a>
</div> </div>
<div class="text-end text-secondary nowrap"> <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>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
<img src="img/user-icon.png" width="24" height="auto"> <img src="img/user-icon.png" width="24" height="auto">
<span id="whoami"><i class="fas fa-spinner fa-spin"></i></span> <span id="whoami"><i class="fas fa-spinner fa-spin"></i></span>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
@ -295,6 +300,21 @@ function flattenObject(ob) {
<input type="text" readonly class="form-control" id="request#device#ipAddress" value="" placeholder="Device IP address"> <input type="text" readonly class="form-control" id="request#device#ipAddress" value="" placeholder="Device IP address">
</div> </div>
</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 --> <!-- Device Username -->
<div class="form-group row"> <div class="form-group row">
<label for="request#device#username" class="col-sm-2 col-form-label"><b>SSH Username</b></label> <label for="request#device#username" class="col-sm-2 col-form-label"><b>SSH Username</b></label>

View File

@ -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>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
<img src="img/user-icon.png" width="24" height="auto">
<span id="whoami"><i class="fas fa-spinner fa-spin"></i></span>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
<span onClick="document.location = '/logout';">
<i class="fas fa-sign-out-alt"></i>
</span>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
<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>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
<span class="adminOnly d-none">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</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">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</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>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
<span class="adminOnly d-none">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</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">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</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>&copy; 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>

View File

@ -144,6 +144,51 @@ function updateFormData(data) {
} else { } else {
$(`[id="device#messages"]`).val( '' ).attr( 'rows', 1 ); $(`[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) { 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> <a href="index.html"><img src="img/nebulous-logo-basic.png" width="155px" height="155px" alt=""></a>
</div> </div>
<div class="text-end text-secondary nowrap"> <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>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
<img src="img/user-icon.png" width="24" height="auto"> <img src="img/user-icon.png" width="24" height="auto">
<span id="whoami"><i class="fas fa-spinner fa-spin"></i></span> <span id="whoami"><i class="fas fa-spinner fa-spin"></i></span>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
@ -311,125 +361,156 @@ function sendDeviceData(deviceData) {
</button> </button>
<p>&nbsp;</p> <p>&nbsp;</p>
<form> <div class="row">
<div class="form-group row text-center bg-dark bg-opacity-25"> <div id="device-info-part" class="col-8 p-3">
<h5>Device details</h5>
<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>&nbsp;</p>
<input class="btn btn-primary" type="submit" value="Submit">
<input class="btn btn-primary" type="reset" value="Reset">
</div>-->
</form>
</div> </div>
<!-- Device id --> <div id="device-metrics-part" class="text-center col-4 p-3">
<div class="form-group row"> <div class="form-group row text-center bg-dark bg-opacity-25">
<label for="device#id" class="col-sm-2 col-form-label"><b>Device Id</b></label> <h5>Device metrics</h5>
<div class="col-sm-10">
<input type="text" readonly class="form-control-plaintext" id="device#id" value="">
</div> </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 id="device-metrics"></div>
<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>
<!-- 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>
</div>
<div class="form-group row text-center bg-dark bg-opacity-25">
<h5>Device metrics</h5>
</div>
++++ TODO ++++
<!--<div class="text-center">
<p>&nbsp;</p>
<input class="btn btn-primary" type="submit" value="Submit">
<input class="btn btn-primary" type="reset" value="Reset">
</div>-->
</form>
</div> </div>

View File

@ -55,16 +55,26 @@ function updateDevicesList(asAdmin) {
var owner = item.owner; var owner = item.owner;
var devName = (item.name && item.name.trim()!=='') ? item.name.trim() : `(No name - Id ${devId})`; var devName = (item.name && item.name.trim()!=='') ? item.name.trim() : `(No name - Id ${devId})`;
var ipAddress = item.ipAddress; var ipAddress = item.ipAddress;
var load = 'TODO'; var load = getLoadStr(item);
var status = item.status; var status = `${ ((item.statusUpdate && item.statusUpdate.state) ? item.statusUpdate.state : 'na') }
var color = getStatusColor(status); <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) ? ` 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-redo"></i> : `
</button> <button type="button" class="btn btn-primary btn-sm" onClick="if (confirm('Onboard Device again?')) manageDevice('${devId}', 'onboard');">
<button type="button" class="btn btn-danger btn-sm" onClick="if (confirm('Remove Device?')) manageDevice('${devId}', 'offboard');"> <i class="fas fa-backward"></i>
<i class="fas fa-ban"></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> </button>
` : ''; ` : '';
ii++; ii++;
@ -77,10 +87,11 @@ function updateDevicesList(asAdmin) {
<td>${load}</td> <td>${load}</td>
<td>${status}</td> <td>${status}</td>
<td> <td>
${adminActions}
<button type="button" class="btn btn-success btn-sm" onClick="document.location='/device-view.html?id=${devId}${urlAppend}'; "> <button type="button" class="btn btn-success btn-sm" onClick="document.location='/device-view.html?id=${devId}${urlAppend}'; ">
<i class="fas fa-eye"></i> <i class="fas fa-eye"></i>
</button> </button>
${userActions}
${adminActions}
</td> </td>
</tr> ` </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) { 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=='NEW_DEVICE') return '';
if (status=='BUSY') return 'table-success'; if (status.indexOf('ERROR')>0) return 'table-danger';
if (status=='IDLE') return 'table-warning'; 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'; 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> </script>
</head> </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> <a href="index.html"><img src="img/nebulous-logo-basic.png" width="155px" height="155px" alt=""></a>
</div> </div>
<div class="text-end text-secondary nowrap"> <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>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
<img src="img/user-icon.png" width="24" height="auto"> <img src="img/user-icon.png" width="24" height="auto">
<span id="whoami"><i class="fas fa-spinner fa-spin"></i></span> <span id="whoami"><i class="fas fa-spinner fa-spin"></i></span>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
@ -158,6 +242,13 @@ function manageDevice(id, action) {
<i class="fa fa-home"></i> <i class="fa fa-home"></i>
</button> </button>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
<button type="button" class="btn btn-danger" onClick="requestUpdate()">
<i class="fa fa-tachometer-alt"></i>
</button>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
<button type="button" class="adminOnly btn btn-danger d-none" onClick="processDevices()">
<i class="fa fa-random"></i>
</button>
<span class="adminOnly d-none">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</span> <span class="adminOnly d-none">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</span>
<button type="button" class="adminOnly btn btn-danger d-none" onClick="updateDevicesList(true)"> <button type="button" class="adminOnly btn btn-danger d-none" onClick="updateDevicesList(true)">
<i class="fa fa-refresh"></i> <i class="fa fa-refresh"></i>

View File

@ -68,7 +68,7 @@
<a href="archived.html">More <img src="img/arrow-right.svg" alt="" style="width:20px"></a> <a href="archived.html">More <img src="img/arrow-right.svg" alt="" style="width:20px"></a>
</div> </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=""> <img src="img/icon/Group 1955.svg" alt="">
<p class="title">Settings</p> <p class="title">Settings</p>
<p>View the Resource Discovery service settings. Admins can also manage the service, <p>View the Resource Discovery service settings. Admins can also manage the service,

View File

@ -252,8 +252,9 @@ function saveRequestInfo() {
// Fix messages // Fix messages
var messages = $(`[id="request#messages"]`).val().trim(); var messages = $(`[id="request#messages"]`).val().trim();
if (messages==='') messages = ''; root['messages'] = (messages!=='')
root['messages'] = messages.split(/\r\n|\r|\n/); ? messages.split(/\r\n|\r|\n/)
: [];
// Check request Id // Check request Id
if (requestId!=='' && requestId!==root.id) { if (requestId!=='' && requestId!==root.id) {
@ -283,9 +284,11 @@ function sendRequestData(requestData) {
refreshRequestInfo(requestId); refreshRequestInfo(requestId);
}) })
.fail(function(xhr, status, error) { .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( $('#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) { .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> <a href="index.html"><img src="img/nebulous-logo-basic.png" width="155px" height="155px" alt=""></a>
</div> </div>
<div class="text-end text-secondary nowrap"> <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>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
<img src="img/user-icon.png" width="24" height="auto"> <img src="img/user-icon.png" width="24" height="auto">
<span id="whoami"><i class="fas fa-spinner fa-spin"></i></span> <span id="whoami"><i class="fas fa-spinner fa-spin"></i></span>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
@ -450,6 +458,21 @@ function sendRequestData(requestData) {
<input type="text" class="form-control" id="request#device#ipAddress" value="" placeholder="Device IP address"> <input type="text" class="form-control" id="request#device#ipAddress" value="" placeholder="Device IP address">
</div> </div>
</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 --> <!-- Device Username -->
<div class="form-group row"> <div class="form-group row">
<label for="request#device#username" class="col-sm-2 col-form-label"><b>SSH Username</b></label> <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"> <div class="form-group row">
<label for="request#device#password" class="col-sm-2 col-form-label"><b>SSH Password</b></label> <label for="request#device#password" class="col-sm-2 col-form-label"><b>SSH Password</b></label>
<div class="col-sm-10"> <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>
</div> </div>
<!-- Device Public Key --> <!-- Device Public Key -->
<div class="form-group row"> <div class="form-group row">
<label for="request#device#publicKey" class="col-sm-2 col-form-label"><b>SSH Public Key</b></label> <label for="request#device#publicKey" class="col-sm-2 col-form-label"><b>SSH Public Key</b></label>
<div class="col-sm-10"> <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>
</div> </div>
<!-- Device Additional Info --> <!-- Device Additional Info -->

View File

@ -80,13 +80,9 @@ function updateRequestsList(asAdmin) {
</button> </button>
` : ''; ` : '';
adminActions += (isAdmin) ? ` adminActions += (isAdmin) ? `
<a href="#" target="_blank" onClick=" <button class="btn btn-outline-danger btn-sm" onClick="changeStatus('${reqId}')">
this.href = '/discovery/request/${reqId}/status/'+prompt('Change status to:', 'NEW_REQUEST'); <i class="fas fa-fast-backward"></i>
"> </button>
<button class="btn btn-outline-danger btn-sm">
<i class="fas fa-fast-backward"></i>
</button>
</a>
`: ''; `: '';
ii++; ii++;
tbody.append( $(` tbody.append( $(`
@ -148,7 +144,11 @@ function processRequests() {
}) })
.fail(function(xhr, status, error) { .fail(function(xhr, status, error) {
console.error('processRequests: ERROR: ', status, error); console.error('processRequests: ERROR: ', status, error);
}); })
.always(function(data, status) {
setTimeout(updateRequestsList, 500);
})
;
} }
function authorizeRequest(reqId, authorize) { function authorizeRequest(reqId, authorize) {
@ -179,6 +179,20 @@ function archiveRequest(reqId) {
console.error('archiveRequest: ERROR: ', status, error); 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> </script>
</head> </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> <a href="index.html"><img src="img/nebulous-logo-basic.png" width="155px" height="155px" alt=""></a>
</div> </div>
<div class="text-end text-secondary nowrap"> <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>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
<img src="img/user-icon.png" width="24" height="auto"> <img src="img/user-icon.png" width="24" height="auto">
<span id="whoami"><i class="fas fa-spinner fa-spin"></i></span> <span id="whoami"><i class="fas fa-spinner fa-spin"></i></span>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;