From 0c05760636652e0c34e6aac50e1333b6bb9e117e Mon Sep 17 00:00:00 2001 From: ipatini Date: Mon, 30 Oct 2023 16:30:10 +0200 Subject: [PATCH] RD: Squashed changes from 2023-10-06 to 2023-10-30 Change-Id: I8acdbaf7414e40a928de79b48ca8c996b154d62d --- .../ResourceDiscoveryProperties.java | 50 -- .../discovery/monitor/DeviceProcessor.java | 14 - management/src/main/resources/application.yml | 27 -- .../static/freebees_webdesign_6/archived.html | 207 -------- {management => resource-discovery}/.gitignore | 0 .../.mvn/wrapper/maven-wrapper.jar | Bin .../.mvn/wrapper/maven-wrapper.properties | 0 {management => resource-discovery}/mvnw | 0 {management => resource-discovery}/mvnw.cmd | 0 {management => resource-discovery}/pom.xml | 24 +- .../ResourceDiscoveryApplication.java | 0 .../discovery/ResourceDiscoveryConfig.java | 0 .../ResourceDiscoveryProperties.java | 95 ++++ .../resource/discovery/SecurityConfig.java | 40 +- .../resource/discovery/StatusController.java | 14 + .../resource/discovery/common/BrokerUtil.java | 231 +++++++++ .../discovery/common/DeviceLocation.java | 25 + .../discovery/common/EncryptionUtil.java | 231 +++++++++ .../discovery/common/REQUEST_TYPE.java | 5 + .../discovery/monitor/DeviceProcessor.java | 161 +++++++ .../ArchivedDeviceManagementController.java | 9 +- .../DeviceManagementController.java | 77 +++ .../monitor/model/ArchivedDevice.java | 0 .../discovery/monitor/model/Device.java | 25 +- .../monitor/model/DeviceException.java | 0 .../monitor/model/DeviceMetrics.java | 18 + .../discovery/monitor/model/DeviceStatus.java | 3 +- .../monitor/model/DeviceStatusUpdate.java | 18 + .../repository/ArchivedDeviceRepository.java | 0 .../monitor/repository/DeviceRepository.java | 0 .../service/AbstractMonitorService.java | 72 +++ .../service/DeviceConversionService.java | 0 .../DeviceLifeCycleRequestService.java | 137 ++++++ .../DeviceLifeCycleResponseService.java | 137 ++++++ .../service/DeviceManagementService.java | 71 ++- .../service/DeviceMetricsMonitorService.java | 127 +++++ .../service/DeviceStatusMonitorService.java | 99 ++++ .../UnknownDeviceRegistrationService.java | 253 ++++++++++ .../IRegistrationRequestProcessor.java | 0 .../RegistrationRequestProcessor.java | 156 +++--- ...ationRequestService_SampleDataCreator.java | 0 .../RegistrationRequestController.java | 24 +- .../model/ArchivedRegistrationRequest.java | 0 .../discovery/registration/model/Device.java | 8 + .../model/RegistrationRequest.java | 12 +- .../model/RegistrationRequestException.java | 0 .../RegistrationRequestHistoryEntry.java | 0 .../model/RegistrationRequestStatus.java | 0 ...ArchivedRegistrationRequestRepository.java | 0 ...InMemoryRegistrationRequestRepository.java | 0 .../RegistrationRequestRepository.java | 1 + .../RegistrationRequestConversionService.java | 0 .../service/RegistrationRequestService.java | 90 +++- .../src/main/resources/application.yml | 38 ++ .../src/main/resources/banner.txt | 11 + .../archived-device-view.html | 434 +++++++++++++++++ .../archived-request-view.html | 20 + .../static/freebees_webdesign_6/archived.html | 454 ++++++++++++++++++ .../static/freebees_webdesign_6/css/style.css | 0 .../freebees_webdesign_6/css/style.css.map | 0 .../freebees_webdesign_6/device-view.html | 307 +++++++----- .../static/freebees_webdesign_6/devices.html | 121 ++++- .../freebees_webdesign_6/img/arrow-left.svg | 0 .../img/arrow-right-color.svg | 0 .../freebees_webdesign_6/img/arrow-right.svg | 0 .../freebees_webdesign_6/img/circle.svg | 0 .../img/icon/Group 1802.svg | 0 .../img/icon/Group 1953.svg | 0 .../img/icon/Group 1954.svg | 0 .../img/icon/Group 1955.svg | 0 .../img/nebulous-logo-basic.png | Bin .../img/nebulous-logo-white.png | Bin .../freebees_webdesign_6/img/user-icon.png | Bin .../static/freebees_webdesign_6/index.html | 2 +- .../freebees_webdesign_6/js/addshadow.js | 0 .../freebees_webdesign_6/request-edit.html | 35 +- .../static/freebees_webdesign_6/requests.html | 36 +- .../freebees_webdesign_6/sass/_colors.scss | 0 .../freebees_webdesign_6/sass/style.scss | 0 .../src/main/resources/static/index.html | 0 .../ResourceManagementApplicationTests.java | 0 81 files changed, 3343 insertions(+), 576 deletions(-) delete mode 100644 management/src/main/java/eu/nebulous/resource/discovery/ResourceDiscoveryProperties.java delete mode 100644 management/src/main/java/eu/nebulous/resource/discovery/monitor/DeviceProcessor.java delete mode 100644 management/src/main/resources/application.yml delete mode 100644 management/src/main/resources/static/freebees_webdesign_6/archived.html rename {management => resource-discovery}/.gitignore (100%) rename {management => resource-discovery}/.mvn/wrapper/maven-wrapper.jar (100%) rename {management => resource-discovery}/.mvn/wrapper/maven-wrapper.properties (100%) rename {management => resource-discovery}/mvnw (100%) rename {management => resource-discovery}/mvnw.cmd (100%) rename {management => resource-discovery}/pom.xml (81%) rename {management => resource-discovery}/src/main/java/eu/nebulous/resource/discovery/ResourceDiscoveryApplication.java (100%) rename {management => resource-discovery}/src/main/java/eu/nebulous/resource/discovery/ResourceDiscoveryConfig.java (100%) create mode 100644 resource-discovery/src/main/java/eu/nebulous/resource/discovery/ResourceDiscoveryProperties.java rename {management => resource-discovery}/src/main/java/eu/nebulous/resource/discovery/SecurityConfig.java (65%) create mode 100644 resource-discovery/src/main/java/eu/nebulous/resource/discovery/StatusController.java create mode 100644 resource-discovery/src/main/java/eu/nebulous/resource/discovery/common/BrokerUtil.java create mode 100644 resource-discovery/src/main/java/eu/nebulous/resource/discovery/common/DeviceLocation.java create mode 100644 resource-discovery/src/main/java/eu/nebulous/resource/discovery/common/EncryptionUtil.java create mode 100644 resource-discovery/src/main/java/eu/nebulous/resource/discovery/common/REQUEST_TYPE.java create mode 100644 resource-discovery/src/main/java/eu/nebulous/resource/discovery/monitor/DeviceProcessor.java rename {management => resource-discovery}/src/main/java/eu/nebulous/resource/discovery/monitor/controller/ArchivedDeviceManagementController.java (84%) rename {management => resource-discovery}/src/main/java/eu/nebulous/resource/discovery/monitor/controller/DeviceManagementController.java (53%) rename {management => resource-discovery}/src/main/java/eu/nebulous/resource/discovery/monitor/model/ArchivedDevice.java (100%) rename {management => resource-discovery}/src/main/java/eu/nebulous/resource/discovery/monitor/model/Device.java (60%) rename {management => resource-discovery}/src/main/java/eu/nebulous/resource/discovery/monitor/model/DeviceException.java (100%) create mode 100644 resource-discovery/src/main/java/eu/nebulous/resource/discovery/monitor/model/DeviceMetrics.java rename {management => resource-discovery}/src/main/java/eu/nebulous/resource/discovery/monitor/model/DeviceStatus.java (80%) create mode 100644 resource-discovery/src/main/java/eu/nebulous/resource/discovery/monitor/model/DeviceStatusUpdate.java rename {management => resource-discovery}/src/main/java/eu/nebulous/resource/discovery/monitor/repository/ArchivedDeviceRepository.java (100%) rename {management => resource-discovery}/src/main/java/eu/nebulous/resource/discovery/monitor/repository/DeviceRepository.java (100%) create mode 100644 resource-discovery/src/main/java/eu/nebulous/resource/discovery/monitor/service/AbstractMonitorService.java rename {management => resource-discovery}/src/main/java/eu/nebulous/resource/discovery/monitor/service/DeviceConversionService.java (100%) create mode 100644 resource-discovery/src/main/java/eu/nebulous/resource/discovery/monitor/service/DeviceLifeCycleRequestService.java create mode 100644 resource-discovery/src/main/java/eu/nebulous/resource/discovery/monitor/service/DeviceLifeCycleResponseService.java rename {management => resource-discovery}/src/main/java/eu/nebulous/resource/discovery/monitor/service/DeviceManagementService.java (65%) create mode 100644 resource-discovery/src/main/java/eu/nebulous/resource/discovery/monitor/service/DeviceMetricsMonitorService.java create mode 100644 resource-discovery/src/main/java/eu/nebulous/resource/discovery/monitor/service/DeviceStatusMonitorService.java create mode 100644 resource-discovery/src/main/java/eu/nebulous/resource/discovery/monitor/service/UnknownDeviceRegistrationService.java rename {management => resource-discovery}/src/main/java/eu/nebulous/resource/discovery/registration/IRegistrationRequestProcessor.java (100%) rename {management => resource-discovery}/src/main/java/eu/nebulous/resource/discovery/registration/RegistrationRequestProcessor.java (73%) rename {management => resource-discovery}/src/main/java/eu/nebulous/resource/discovery/registration/RegistrationRequestService_SampleDataCreator.java (100%) rename {management => resource-discovery}/src/main/java/eu/nebulous/resource/discovery/registration/controller/RegistrationRequestController.java (90%) rename {management => resource-discovery}/src/main/java/eu/nebulous/resource/discovery/registration/model/ArchivedRegistrationRequest.java (100%) rename {management => resource-discovery}/src/main/java/eu/nebulous/resource/discovery/registration/model/Device.java (59%) rename {management => resource-discovery}/src/main/java/eu/nebulous/resource/discovery/registration/model/RegistrationRequest.java (65%) rename {management => resource-discovery}/src/main/java/eu/nebulous/resource/discovery/registration/model/RegistrationRequestException.java (100%) rename {management => resource-discovery}/src/main/java/eu/nebulous/resource/discovery/registration/model/RegistrationRequestHistoryEntry.java (100%) rename {management => resource-discovery}/src/main/java/eu/nebulous/resource/discovery/registration/model/RegistrationRequestStatus.java (100%) rename {management => resource-discovery}/src/main/java/eu/nebulous/resource/discovery/registration/repository/ArchivedRegistrationRequestRepository.java (100%) rename {management => resource-discovery}/src/main/java/eu/nebulous/resource/discovery/registration/repository/InMemoryRegistrationRequestRepository.java (100%) rename {management => resource-discovery}/src/main/java/eu/nebulous/resource/discovery/registration/repository/RegistrationRequestRepository.java (85%) rename {management => resource-discovery}/src/main/java/eu/nebulous/resource/discovery/registration/service/RegistrationRequestConversionService.java (100%) rename {management => resource-discovery}/src/main/java/eu/nebulous/resource/discovery/registration/service/RegistrationRequestService.java (74%) create mode 100644 resource-discovery/src/main/resources/application.yml create mode 100644 resource-discovery/src/main/resources/banner.txt create mode 100644 resource-discovery/src/main/resources/static/freebees_webdesign_6/archived-device-view.html rename management/src/main/resources/static/freebees_webdesign_6/archived-view.html => resource-discovery/src/main/resources/static/freebees_webdesign_6/archived-request-view.html (91%) create mode 100644 resource-discovery/src/main/resources/static/freebees_webdesign_6/archived.html rename {management => resource-discovery}/src/main/resources/static/freebees_webdesign_6/css/style.css (100%) rename {management => resource-discovery}/src/main/resources/static/freebees_webdesign_6/css/style.css.map (100%) rename {management => resource-discovery}/src/main/resources/static/freebees_webdesign_6/device-view.html (50%) rename {management => resource-discovery}/src/main/resources/static/freebees_webdesign_6/devices.html (66%) rename {management => resource-discovery}/src/main/resources/static/freebees_webdesign_6/img/arrow-left.svg (100%) rename {management => resource-discovery}/src/main/resources/static/freebees_webdesign_6/img/arrow-right-color.svg (100%) rename {management => resource-discovery}/src/main/resources/static/freebees_webdesign_6/img/arrow-right.svg (100%) rename {management => resource-discovery}/src/main/resources/static/freebees_webdesign_6/img/circle.svg (100%) rename {management => resource-discovery}/src/main/resources/static/freebees_webdesign_6/img/icon/Group 1802.svg (100%) rename {management => resource-discovery}/src/main/resources/static/freebees_webdesign_6/img/icon/Group 1953.svg (100%) rename {management => resource-discovery}/src/main/resources/static/freebees_webdesign_6/img/icon/Group 1954.svg (100%) rename {management => resource-discovery}/src/main/resources/static/freebees_webdesign_6/img/icon/Group 1955.svg (100%) rename {management => resource-discovery}/src/main/resources/static/freebees_webdesign_6/img/nebulous-logo-basic.png (100%) rename {management => resource-discovery}/src/main/resources/static/freebees_webdesign_6/img/nebulous-logo-white.png (100%) rename {management => resource-discovery}/src/main/resources/static/freebees_webdesign_6/img/user-icon.png (100%) rename {management => resource-discovery}/src/main/resources/static/freebees_webdesign_6/index.html (98%) rename {management => resource-discovery}/src/main/resources/static/freebees_webdesign_6/js/addshadow.js (100%) rename {management => resource-discovery}/src/main/resources/static/freebees_webdesign_6/request-edit.html (90%) rename {management => resource-discovery}/src/main/resources/static/freebees_webdesign_6/requests.html (89%) rename {management => resource-discovery}/src/main/resources/static/freebees_webdesign_6/sass/_colors.scss (100%) rename {management => resource-discovery}/src/main/resources/static/freebees_webdesign_6/sass/style.scss (100%) rename {management => resource-discovery}/src/main/resources/static/index.html (100%) rename {management => resource-discovery}/src/test/java/eu/nebulous/resource/discovery/registration/ResourceManagementApplicationTests.java (100%) diff --git a/management/src/main/java/eu/nebulous/resource/discovery/ResourceDiscoveryProperties.java b/management/src/main/java/eu/nebulous/resource/discovery/ResourceDiscoveryProperties.java deleted file mode 100644 index c9bcc87..0000000 --- a/management/src/main/java/eu/nebulous/resource/discovery/ResourceDiscoveryProperties.java +++ /dev/null @@ -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 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 users; - - @Data - public static class UserData { - private final String username; - @ToString.Exclude - private final String password; - private final List roles; - } -} diff --git a/management/src/main/java/eu/nebulous/resource/discovery/monitor/DeviceProcessor.java b/management/src/main/java/eu/nebulous/resource/discovery/monitor/DeviceProcessor.java deleted file mode 100644 index 3a97f7d..0000000 --- a/management/src/main/java/eu/nebulous/resource/discovery/monitor/DeviceProcessor.java +++ /dev/null @@ -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 { -} diff --git a/management/src/main/resources/application.yml b/management/src/main/resources/application.yml deleted file mode 100644 index 8c2691d..0000000 --- a/management/src/main/resources/application.yml +++ /dev/null @@ -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 ] diff --git a/management/src/main/resources/static/freebees_webdesign_6/archived.html b/management/src/main/resources/static/freebees_webdesign_6/archived.html deleted file mode 100644 index 7b357d5..0000000 --- a/management/src/main/resources/static/freebees_webdesign_6/archived.html +++ /dev/null @@ -1,207 +0,0 @@ - - - - - - - - NebulOuS Resource Discovery - Management page - - - - - - - - - - - - - - - -
-
- -
-
- - -       - - - -       - -
-
-
-
-

Archived Registration Requests

- - - -       -       - -       - - -
- - -
- - - - - - - - - - - - - - -
#RequesterDevice nameIP AddressReg. DateStatusActions
-
- -
-
-
-
-
- - - -
-
-
-
Fbee 2022 copyright
- - \ No newline at end of file diff --git a/management/.gitignore b/resource-discovery/.gitignore similarity index 100% rename from management/.gitignore rename to resource-discovery/.gitignore diff --git a/management/.mvn/wrapper/maven-wrapper.jar b/resource-discovery/.mvn/wrapper/maven-wrapper.jar similarity index 100% rename from management/.mvn/wrapper/maven-wrapper.jar rename to resource-discovery/.mvn/wrapper/maven-wrapper.jar diff --git a/management/.mvn/wrapper/maven-wrapper.properties b/resource-discovery/.mvn/wrapper/maven-wrapper.properties similarity index 100% rename from management/.mvn/wrapper/maven-wrapper.properties rename to resource-discovery/.mvn/wrapper/maven-wrapper.properties diff --git a/management/mvnw b/resource-discovery/mvnw similarity index 100% rename from management/mvnw rename to resource-discovery/mvnw diff --git a/management/mvnw.cmd b/resource-discovery/mvnw.cmd similarity index 100% rename from management/mvnw.cmd rename to resource-discovery/mvnw.cmd diff --git a/management/pom.xml b/resource-discovery/pom.xml similarity index 81% rename from management/pom.xml rename to resource-discovery/pom.xml index e055c31..01a052d 100644 --- a/management/pom.xml +++ b/resource-discovery/pom.xml @@ -10,14 +10,15 @@ - eu.nebulous.resource-discovery - management + eu.nebulous.resource-management + resource-discovery 1.0.0-SNAPSHOT - Resource discovery management service - Nebulous resource discovery management service + Resource discovery service + Nebulous resource discovery service 17 + ${project.artifactId}:${project.version} @@ -88,6 +89,21 @@ org.springframework.boot spring-boot-maven-plugin + + + + build-image + + + + + + ${imageName} + + C.UTF-8 + + + diff --git a/management/src/main/java/eu/nebulous/resource/discovery/ResourceDiscoveryApplication.java b/resource-discovery/src/main/java/eu/nebulous/resource/discovery/ResourceDiscoveryApplication.java similarity index 100% rename from management/src/main/java/eu/nebulous/resource/discovery/ResourceDiscoveryApplication.java rename to resource-discovery/src/main/java/eu/nebulous/resource/discovery/ResourceDiscoveryApplication.java diff --git a/management/src/main/java/eu/nebulous/resource/discovery/ResourceDiscoveryConfig.java b/resource-discovery/src/main/java/eu/nebulous/resource/discovery/ResourceDiscoveryConfig.java similarity index 100% rename from management/src/main/java/eu/nebulous/resource/discovery/ResourceDiscoveryConfig.java rename to resource-discovery/src/main/java/eu/nebulous/resource/discovery/ResourceDiscoveryConfig.java diff --git a/resource-discovery/src/main/java/eu/nebulous/resource/discovery/ResourceDiscoveryProperties.java b/resource-discovery/src/main/java/eu/nebulous/resource/discovery/ResourceDiscoveryProperties.java new file mode 100644 index 0000000..1a9ba44 --- /dev/null +++ b/resource-discovery/src/main/java/eu/nebulous/resource/discovery/ResourceDiscoveryProperties.java @@ -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 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 users; + + @Data + public static class UserData { + private final String username; + @ToString.Exclude + private final String password; + private final List roles; + } +} diff --git a/management/src/main/java/eu/nebulous/resource/discovery/SecurityConfig.java b/resource-discovery/src/main/java/eu/nebulous/resource/discovery/SecurityConfig.java similarity index 65% rename from management/src/main/java/eu/nebulous/resource/discovery/SecurityConfig.java rename to resource-discovery/src/main/java/eu/nebulous/resource/discovery/SecurityConfig.java index 07b2216..399e40e 100644 --- a/management/src/main/java/eu/nebulous/resource/discovery/SecurityConfig.java +++ b/resource-discovery/src/main/java/eu/nebulous/resource/discovery/SecurityConfig.java @@ -4,18 +4,18 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; import org.springframework.security.core.userdetails.User; -import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.provisioning.InMemoryUserDetailsManager; import org.springframework.security.web.SecurityFilterChain; -import java.util.ArrayList; -import java.util.List; +import java.security.SecureRandom; import static org.springframework.security.config.Customizer.withDefaults; @@ -39,21 +39,31 @@ public class SecurityConfig { return httpSecurity.build(); } - @Bean - public InMemoryUserDetailsManager userDetailsService() { - List users = new ArrayList<>(); - properties.getUsers().forEach(userData -> { - UserDetails user = User.withUsername(userData.getUsername()) - .password(encoder().encode(userData.getPassword())) - .roles(userData.getRoles().toArray(new String[0])) - .build(); - users.add(user); - }); - return new InMemoryUserDetailsManager(users.toArray(new UserDetails[0])); + public void configAuthentication(AuthenticationManagerBuilder auth) throws Exception { + auth.inMemoryAuthentication().passwordEncoder(passwordEncoder()); } @Bean + public InMemoryUserDetailsManager inMemoryUserDetailsManager() { + return new InMemoryUserDetailsManager( + properties.getUsers().stream() + .map(userData -> User.builder() + .username(userData.getUsername()) + .password(userData.getPassword()) + .roles(userData.getRoles().toArray(new String[0])) + .build()) + .toList()); + } + + @Bean + public PasswordEncoder passwordEncoder() { + int strength = 10; // iterations + return new BCryptPasswordEncoder(strength, new SecureRandom()); + } + + /*@Bean public PasswordEncoder encoder() { + // Clear-text password encoder return new PasswordEncoder() { @Override public String encode(CharSequence rawPassword) { @@ -65,5 +75,5 @@ public class SecurityConfig { return rawPassword.toString().equals(encodedPassword); } }; - } + }*/ } diff --git a/resource-discovery/src/main/java/eu/nebulous/resource/discovery/StatusController.java b/resource-discovery/src/main/java/eu/nebulous/resource/discovery/StatusController.java new file mode 100644 index 0000000..2158ff8 --- /dev/null +++ b/resource-discovery/src/main/java/eu/nebulous/resource/discovery/StatusController.java @@ -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"; + } +} diff --git a/resource-discovery/src/main/java/eu/nebulous/resource/discovery/common/BrokerUtil.java b/resource-discovery/src/main/java/eu/nebulous/resource/discovery/common/BrokerUtil.java new file mode 100644 index 0000000..d308d64 --- /dev/null +++ b/resource-discovery/src/main/java/eu/nebulous/resource/discovery/common/BrokerUtil.java @@ -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 producers = new HashMap<>(); + private final Map consumers = new HashMap<>(); + private final Map> 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 message) throws JMSException, JsonProcessingException { + sendMessage(topic, message, false); + } + + public void sendMessage(@NonNull String topic, @NonNull Map 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 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> 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 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); + } +} diff --git a/resource-discovery/src/main/java/eu/nebulous/resource/discovery/common/DeviceLocation.java b/resource-discovery/src/main/java/eu/nebulous/resource/discovery/common/DeviceLocation.java new file mode 100644 index 0000000..dff7074 --- /dev/null +++ b/resource-discovery/src/main/java/eu/nebulous/resource/discovery/common/DeviceLocation.java @@ -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; +} diff --git a/resource-discovery/src/main/java/eu/nebulous/resource/discovery/common/EncryptionUtil.java b/resource-discovery/src/main/java/eu/nebulous/resource/discovery/common/EncryptionUtil.java new file mode 100644 index 0000000..c33c999 --- /dev/null +++ b/resource-discovery/src/main/java/eu/nebulous/resource/discovery/common/EncryptionUtil.java @@ -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 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> 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)); + } +} diff --git a/resource-discovery/src/main/java/eu/nebulous/resource/discovery/common/REQUEST_TYPE.java b/resource-discovery/src/main/java/eu/nebulous/resource/discovery/common/REQUEST_TYPE.java new file mode 100644 index 0000000..25bfd39 --- /dev/null +++ b/resource-discovery/src/main/java/eu/nebulous/resource/discovery/common/REQUEST_TYPE.java @@ -0,0 +1,5 @@ +package eu.nebulous.resource.discovery.common; + +public enum REQUEST_TYPE { + INSTALL, REINSTALL, UNINSTALL, NODE_DETAILS, INFO, DIAGNOSTICS, OTHER +} diff --git a/resource-discovery/src/main/java/eu/nebulous/resource/discovery/monitor/DeviceProcessor.java b/resource-discovery/src/main/java/eu/nebulous/resource/discovery/monitor/DeviceProcessor.java new file mode 100644 index 0000000..054135e --- /dev/null +++ b/resource-discovery/src/main/java/eu/nebulous/resource/discovery/monitor/DeviceProcessor.java @@ -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 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 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 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 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 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"); + } +} diff --git a/management/src/main/java/eu/nebulous/resource/discovery/monitor/controller/ArchivedDeviceManagementController.java b/resource-discovery/src/main/java/eu/nebulous/resource/discovery/monitor/controller/ArchivedDeviceManagementController.java similarity index 84% rename from management/src/main/java/eu/nebulous/resource/discovery/monitor/controller/ArchivedDeviceManagementController.java rename to resource-discovery/src/main/java/eu/nebulous/resource/discovery/monitor/controller/ArchivedDeviceManagementController.java index 5b2724d..d4e8a9d 100644 --- a/management/src/main/java/eu/nebulous/resource/discovery/monitor/controller/ArchivedDeviceManagementController.java +++ b/resource-discovery/src/main/java/eu/nebulous/resource/discovery/monitor/controller/ArchivedDeviceManagementController.java @@ -6,12 +6,12 @@ import eu.nebulous.resource.discovery.monitor.model.DeviceException; import eu.nebulous.resource.discovery.monitor.service.DeviceManagementService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.apache.commons.lang3.StringUtils; import org.springframework.http.MediaType; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.*; import java.util.List; +import java.util.Map; @Slf4j @RestController @@ -42,9 +42,10 @@ public class ArchivedDeviceManagementController { return deviceService.getArchivedByIpAddress(ipAddress); } - @GetMapping(value = "/device/{id}/unarchive", produces = MediaType.APPLICATION_JSON_VALUE) - public String unarchiveDevice(@PathVariable String id) { - deviceService.unarchiveDevice(id); + @PostMapping(value = "/device/{id}/unarchive", + consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE) + public String unarchiveDevice(@PathVariable String id, @RequestBody Map credentials) { + deviceService.unarchiveDevice(id, credentials); return "UNARCHIVED"; } } diff --git a/management/src/main/java/eu/nebulous/resource/discovery/monitor/controller/DeviceManagementController.java b/resource-discovery/src/main/java/eu/nebulous/resource/discovery/monitor/controller/DeviceManagementController.java similarity index 53% rename from management/src/main/java/eu/nebulous/resource/discovery/monitor/controller/DeviceManagementController.java rename to resource-discovery/src/main/java/eu/nebulous/resource/discovery/monitor/controller/DeviceManagementController.java index 34caf74..8b2c213 100644 --- a/management/src/main/java/eu/nebulous/resource/discovery/monitor/controller/DeviceManagementController.java +++ b/resource-discovery/src/main/java/eu/nebulous/resource/discovery/monitor/controller/DeviceManagementController.java @@ -1,11 +1,16 @@ package eu.nebulous.resource.discovery.monitor.controller; +import eu.nebulous.resource.discovery.monitor.DeviceProcessor; +import eu.nebulous.resource.discovery.monitor.model.ArchivedDevice; import eu.nebulous.resource.discovery.monitor.model.Device; import eu.nebulous.resource.discovery.monitor.model.DeviceException; +import eu.nebulous.resource.discovery.monitor.service.DeviceLifeCycleRequestService; import eu.nebulous.resource.discovery.monitor.service.DeviceManagementService; +import eu.nebulous.resource.discovery.registration.model.RegistrationRequestException; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; +import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.core.Authentication; @@ -13,6 +18,9 @@ import org.springframework.security.core.GrantedAuthority; import org.springframework.web.bind.annotation.*; import java.util.List; +import java.util.Map; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; @Slf4j @RestController @@ -20,7 +28,9 @@ import java.util.List; @RequestMapping("/monitor") @PreAuthorize("hasAuthority('ROLE_ADMIN')") public class DeviceManagementController { + private final DeviceProcessor deviceProcessor; private final DeviceManagementService deviceService; + private final DeviceLifeCycleRequestService deviceLifeCycleRequestService; private boolean isAuthenticated(Authentication authentication) { return authentication!=null && StringUtils.isNotBlank(authentication.getName()); @@ -89,9 +99,76 @@ public class DeviceManagementController { deviceService.deleteById(id); } + // ------------------------------------------------------------------------ + + @PreAuthorize("hasAuthority('ROLE_ADMIN') || hasAuthority('ROLE_USER')") + @GetMapping(value = "/device/{id}/onboard") + public void onboardDevice(@PathVariable String id) { + deviceLifeCycleRequestService.reinstallRequest(id); + } + + @PreAuthorize("hasAuthority('ROLE_ADMIN') || hasAuthority('ROLE_USER')") + @GetMapping(value = "/device/{id}/offboard") + public void offboardDevice(@PathVariable String id) { + deviceLifeCycleRequestService.uninstallRequest(id); + } + + @PreAuthorize("hasAuthority('ROLE_ADMIN') || hasAuthority('ROLE_USER')") + @GetMapping(value = "/request-update") + public String requestUpdate() { + deviceLifeCycleRequestService.requestInfoUpdate(); + return "REQUESTED-UPDATE"; + } + + @GetMapping(value = "/device/process") + public Map processDevices() throws ExecutionException, InterruptedException { + Future future = deviceProcessor.processDevices(); + return Map.of("result", future.isDone() ? future.get() : "STARTED"); + } + @GetMapping(value = "/device/{id}/archive", produces = MediaType.APPLICATION_JSON_VALUE) public String archiveDevice(@PathVariable String id) { deviceService.archiveDevice(id); return "ARCHIVED"; } + + @PostMapping(value = "/device/{id}/unarchive", + consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE) + public String unarchiveDevice(@PathVariable String id, @RequestBody Map 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 listArchivedRequests(Authentication authentication) { + return deviceService.getArchivedByOwner(authentication); + } + + @GetMapping(value = "/device/archived/all", produces = MediaType.APPLICATION_JSON_VALUE) + public List 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 handleRegistrationRequestException(DeviceException exception) { + return Map.of( + "status", HttpStatus.BAD_REQUEST.value(), + "timestamp", System.currentTimeMillis(), + "exception", exception.getClass().getName(), + "message", exception.getMessage() + ); + } } diff --git a/management/src/main/java/eu/nebulous/resource/discovery/monitor/model/ArchivedDevice.java b/resource-discovery/src/main/java/eu/nebulous/resource/discovery/monitor/model/ArchivedDevice.java similarity index 100% rename from management/src/main/java/eu/nebulous/resource/discovery/monitor/model/ArchivedDevice.java rename to resource-discovery/src/main/java/eu/nebulous/resource/discovery/monitor/model/ArchivedDevice.java diff --git a/management/src/main/java/eu/nebulous/resource/discovery/monitor/model/Device.java b/resource-discovery/src/main/java/eu/nebulous/resource/discovery/monitor/model/Device.java similarity index 60% rename from management/src/main/java/eu/nebulous/resource/discovery/monitor/model/Device.java rename to resource-discovery/src/main/java/eu/nebulous/resource/discovery/monitor/model/Device.java index 187fad4..7aa5ded 100644 --- a/management/src/main/java/eu/nebulous/resource/discovery/monitor/model/Device.java +++ b/resource-discovery/src/main/java/eu/nebulous/resource/discovery/monitor/model/Device.java @@ -1,10 +1,8 @@ package eu.nebulous.resource.discovery.monitor.model; -import eu.nebulous.resource.discovery.registration.model.RegistrationRequest; -import lombok.AccessLevel; -import lombok.Data; -import lombok.NoArgsConstructor; -import lombok.Setter; +import com.fasterxml.jackson.annotation.JsonProperty; +import eu.nebulous.resource.discovery.common.DeviceLocation; +import lombok.*; import lombok.experimental.SuperBuilder; import org.springframework.data.mongodb.core.mapping.Document; @@ -23,12 +21,17 @@ public class Device { private String name; private String owner; private String ipAddress; + private DeviceLocation location; private String username; + @ToString.Exclude + @JsonProperty(access = JsonProperty.Access.WRITE_ONLY) private char[] password; + @ToString.Exclude + @JsonProperty(access = JsonProperty.Access.WRITE_ONLY) private char[] publicKey; private Map deviceInfo; - private RegistrationRequest request; + //private RegistrationRequest request; private String requestId; private Instant creationDate; private Instant lastUpdateDate; @@ -38,4 +41,14 @@ public class Device { private String nodeReference; @Setter(AccessLevel.NONE) private List messages = new ArrayList<>(); + + private DeviceStatusUpdate statusUpdate; + private DeviceMetrics metrics; + + private Instant suspectTimestamp; + private int retries; + + public void incrementRetries() { + retries++; + } } diff --git a/management/src/main/java/eu/nebulous/resource/discovery/monitor/model/DeviceException.java b/resource-discovery/src/main/java/eu/nebulous/resource/discovery/monitor/model/DeviceException.java similarity index 100% rename from management/src/main/java/eu/nebulous/resource/discovery/monitor/model/DeviceException.java rename to resource-discovery/src/main/java/eu/nebulous/resource/discovery/monitor/model/DeviceException.java diff --git a/resource-discovery/src/main/java/eu/nebulous/resource/discovery/monitor/model/DeviceMetrics.java b/resource-discovery/src/main/java/eu/nebulous/resource/discovery/monitor/model/DeviceMetrics.java new file mode 100644 index 0000000..a5e6cc6 --- /dev/null +++ b/resource-discovery/src/main/java/eu/nebulous/resource/discovery/monitor/model/DeviceMetrics.java @@ -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 metrics; + private List latestEvents; +} diff --git a/management/src/main/java/eu/nebulous/resource/discovery/monitor/model/DeviceStatus.java b/resource-discovery/src/main/java/eu/nebulous/resource/discovery/monitor/model/DeviceStatus.java similarity index 80% rename from management/src/main/java/eu/nebulous/resource/discovery/monitor/model/DeviceStatus.java rename to resource-discovery/src/main/java/eu/nebulous/resource/discovery/monitor/model/DeviceStatus.java index 1b45cf7..e124edf 100644 --- a/management/src/main/java/eu/nebulous/resource/discovery/monitor/model/DeviceStatus.java +++ b/resource-discovery/src/main/java/eu/nebulous/resource/discovery/monitor/model/DeviceStatus.java @@ -3,6 +3,7 @@ package eu.nebulous.resource.discovery.monitor.model; public enum DeviceStatus { NEW_DEVICE, ON_HOLD, ONBOARDING, ONBOARDED, ONBOARD_ERROR, - HEALTHY, BUSY, IDLE, + HEALTHY, SUSPECT, FAILED, + BUSY, IDLE, OFFBOARDING, OFFBOARDED, OFFBOARD_ERROR } diff --git a/resource-discovery/src/main/java/eu/nebulous/resource/discovery/monitor/model/DeviceStatusUpdate.java b/resource-discovery/src/main/java/eu/nebulous/resource/discovery/monitor/model/DeviceStatusUpdate.java new file mode 100644 index 0000000..0bd8683 --- /dev/null +++ b/resource-discovery/src/main/java/eu/nebulous/resource/discovery/monitor/model/DeviceStatusUpdate.java @@ -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 errors; +} diff --git a/management/src/main/java/eu/nebulous/resource/discovery/monitor/repository/ArchivedDeviceRepository.java b/resource-discovery/src/main/java/eu/nebulous/resource/discovery/monitor/repository/ArchivedDeviceRepository.java similarity index 100% rename from management/src/main/java/eu/nebulous/resource/discovery/monitor/repository/ArchivedDeviceRepository.java rename to resource-discovery/src/main/java/eu/nebulous/resource/discovery/monitor/repository/ArchivedDeviceRepository.java diff --git a/management/src/main/java/eu/nebulous/resource/discovery/monitor/repository/DeviceRepository.java b/resource-discovery/src/main/java/eu/nebulous/resource/discovery/monitor/repository/DeviceRepository.java similarity index 100% rename from management/src/main/java/eu/nebulous/resource/discovery/monitor/repository/DeviceRepository.java rename to resource-discovery/src/main/java/eu/nebulous/resource/discovery/monitor/repository/DeviceRepository.java diff --git a/resource-discovery/src/main/java/eu/nebulous/resource/discovery/monitor/service/AbstractMonitorService.java b/resource-discovery/src/main/java/eu/nebulous/resource/discovery/monitor/service/AbstractMonitorService.java new file mode 100644 index 0000000..f36e530 --- /dev/null +++ b/resource-discovery/src/main/java/eu/nebulous/resource/discovery/monitor/service/AbstractMonitorService.java @@ -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 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); +} diff --git a/management/src/main/java/eu/nebulous/resource/discovery/monitor/service/DeviceConversionService.java b/resource-discovery/src/main/java/eu/nebulous/resource/discovery/monitor/service/DeviceConversionService.java similarity index 100% rename from management/src/main/java/eu/nebulous/resource/discovery/monitor/service/DeviceConversionService.java rename to resource-discovery/src/main/java/eu/nebulous/resource/discovery/monitor/service/DeviceConversionService.java diff --git a/resource-discovery/src/main/java/eu/nebulous/resource/discovery/monitor/service/DeviceLifeCycleRequestService.java b/resource-discovery/src/main/java/eu/nebulous/resource/discovery/monitor/service/DeviceLifeCycleRequestService.java new file mode 100644 index 0000000..160a321 --- /dev/null +++ b/resource-discovery/src/main/java/eu/nebulous/resource/discovery/monitor/service/DeviceLifeCycleRequestService.java @@ -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 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 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 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 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 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 prepareRequestPayload(@NonNull REQUEST_TYPE requestType, Device device) { + try { + Map 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; + } + } +} diff --git a/resource-discovery/src/main/java/eu/nebulous/resource/discovery/monitor/service/DeviceLifeCycleResponseService.java b/resource-discovery/src/main/java/eu/nebulous/resource/discovery/monitor/service/DeviceLifeCycleResponseService.java new file mode 100644 index 0000000..498202e --- /dev/null +++ b/resource-discovery/src/main/java/eu/nebulous/resource/discovery/monitor/service/DeviceLifeCycleResponseService.java @@ -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 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 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 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 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 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()); + } + } +} diff --git a/management/src/main/java/eu/nebulous/resource/discovery/monitor/service/DeviceManagementService.java b/resource-discovery/src/main/java/eu/nebulous/resource/discovery/monitor/service/DeviceManagementService.java similarity index 65% rename from management/src/main/java/eu/nebulous/resource/discovery/monitor/service/DeviceManagementService.java rename to resource-discovery/src/main/java/eu/nebulous/resource/discovery/monitor/service/DeviceManagementService.java index 736ab44..8bf7802 100644 --- a/management/src/main/java/eu/nebulous/resource/discovery/monitor/service/DeviceManagementService.java +++ b/resource-discovery/src/main/java/eu/nebulous/resource/discovery/monitor/service/DeviceManagementService.java @@ -1,15 +1,19 @@ package eu.nebulous.resource.discovery.monitor.service; +import eu.nebulous.resource.discovery.ResourceDiscoveryProperties; import eu.nebulous.resource.discovery.monitor.model.ArchivedDevice; import eu.nebulous.resource.discovery.monitor.model.Device; import eu.nebulous.resource.discovery.monitor.model.DeviceException; import eu.nebulous.resource.discovery.monitor.model.DeviceStatus; import eu.nebulous.resource.discovery.monitor.repository.ArchivedDeviceRepository; import eu.nebulous.resource.discovery.monitor.repository.DeviceRepository; +import eu.nebulous.resource.discovery.registration.model.RegistrationRequestException; import lombok.NonNull; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; import org.springframework.stereotype.Service; import java.time.Instant; @@ -19,6 +23,7 @@ import java.util.*; @Service @RequiredArgsConstructor public class DeviceManagementService { + private final ResourceDiscoveryProperties properties; private final DeviceRepository deviceRepository; private final ArchivedDeviceRepository archivedDeviceRepository; private final DeviceConversionService deviceConversionService; @@ -41,6 +46,10 @@ public class DeviceManagementService { return deviceRepository.findByIpAddress(ipAddress); } + public boolean isIpAddressInUse(@NonNull String ipAddress) { + return deviceRepository.findByIpAddress(ipAddress).isPresent(); + } + public @NonNull Device save(@NonNull Device device) { DeviceStatus status = device.getStatus(); checkDevice(device, true); @@ -119,6 +128,22 @@ public class DeviceManagementService { // ------------------------------------------------------------------------ + private boolean canAccess(@NonNull Device device, Authentication authentication) { + return canAccess(device, authentication, false); + } + + private boolean canAccess(@NonNull Device device, Authentication authentication, boolean sameUserOnly) { + String owner = device.getOwner(); + if (owner == null && authentication.getName() == null) return true; + return owner != null && ( + owner.equals(authentication.getName()) || + !sameUserOnly && authentication.getAuthorities().stream() + .map(GrantedAuthority::getAuthority).toList().contains("ROLE_ADMIN") + ); + } + + // ------------------------------------------------------------------------ + public List getArchivedAll() { return Collections.unmodifiableList(archivedDeviceRepository.findAll()); } @@ -126,20 +151,40 @@ public class DeviceManagementService { public List getArchivedByOwner(@NonNull String owner) { return archivedDeviceRepository.findByOwner(owner); } + public List getArchivedByOwner(Authentication authentication) { + return getArchivedAll().stream() + .filter(dev -> canAccess(dev, authentication, true)) + .toList(); + } + + public Optional getArchivedById(@NonNull String id, Authentication authentication) { + Optional 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 getArchivedById(@NonNull String id) { return archivedDeviceRepository.findById(id); } + public List getArchivedByIpAddress(@NonNull String ipAddress, Authentication authentication) { + return getArchivedByIpAddress(ipAddress).stream() + .filter(dev -> canAccess(dev, authentication, true)) + .toList(); + } + public List getArchivedByIpAddress(@NonNull String ipAddress) { return archivedDeviceRepository.findByIpAddress(ipAddress); } public void archiveDevice(String id) { - archiveRequestBySystem(id); + archiveDeviceBySystem(id); } - public void archiveRequestBySystem(String id) { + public void archiveDeviceBySystem(String id) { Optional result = getById(id); if (result.isEmpty()) throw new DeviceException( @@ -149,14 +194,32 @@ public class DeviceManagementService { deviceRepository.delete(result.get()); } - public void unarchiveDevice(String id) { + public void unarchiveDevice(String id, Map credentials) { Optional result = getArchivedById(id); if (result.isEmpty()) throw new DeviceException( "Archived device with Id does not exists in repository: "+id); + checkCredentials(result.get().getId(), credentials); result.get().setArchiveDate(null); - deviceRepository.save(deviceConversionService.toDevice(result.get())); + Device restoredDevice = deviceConversionService.toDevice(result.get()); + restoredDevice.setUsername(credentials.get("username")); + restoredDevice.setPassword(credentials.get("password").toCharArray()); + restoredDevice.setPublicKey(credentials.get("publicKey").toCharArray()); + deviceRepository.save(restoredDevice); archivedDeviceRepository.deleteById(result.get().getId()); } + + private void checkCredentials(String id, Map 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); + } } diff --git a/resource-discovery/src/main/java/eu/nebulous/resource/discovery/monitor/service/DeviceMetricsMonitorService.java b/resource-discovery/src/main/java/eu/nebulous/resource/discovery/monitor/service/DeviceMetricsMonitorService.java new file mode 100644 index 0000000..0b355df --- /dev/null +++ b/resource-discovery/src/main/java/eu/nebulous/resource/discovery/monitor/service/DeviceMetricsMonitorService.java @@ -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 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 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 latestEvents = (latestEventsObj instanceof List list) ? list : Collections.emptyList(); + Map 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(); + } +} diff --git a/resource-discovery/src/main/java/eu/nebulous/resource/discovery/monitor/service/DeviceStatusMonitorService.java b/resource-discovery/src/main/java/eu/nebulous/resource/discovery/monitor/service/DeviceStatusMonitorService.java new file mode 100644 index 0000000..dcd7b21 --- /dev/null +++ b/resource-discovery/src/main/java/eu/nebulous/resource/discovery/monitor/service/DeviceStatusMonitorService.java @@ -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 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 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); + } + } +} diff --git a/resource-discovery/src/main/java/eu/nebulous/resource/discovery/monitor/service/UnknownDeviceRegistrationService.java b/resource-discovery/src/main/java/eu/nebulous/resource/discovery/monitor/service/UnknownDeviceRegistrationService.java new file mode 100644 index 0000000..7bb4fc1 --- /dev/null +++ b/resource-discovery/src/main/java/eu/nebulous/resource/discovery/monitor/service/UnknownDeviceRegistrationService.java @@ -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 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 detectedDevices = Collections.synchronizedMap(new LinkedHashMap<>()); + private final List 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 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 map; + synchronized (detectedDevices) { + map = new LinkedHashMap<>(detectedDevices); + detectedDevices.clear(); + } + + // Process detected devices + LinkedHashMap 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 requests = registrationRequestService.getByDeviceIpAddress(ipAddress.trim()); + if (requests.isEmpty()) { + // No registration request found with this IP address + + // Check if device is registered + Optional 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 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 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 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 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"); + } +} diff --git a/management/src/main/java/eu/nebulous/resource/discovery/registration/IRegistrationRequestProcessor.java b/resource-discovery/src/main/java/eu/nebulous/resource/discovery/registration/IRegistrationRequestProcessor.java similarity index 100% rename from management/src/main/java/eu/nebulous/resource/discovery/registration/IRegistrationRequestProcessor.java rename to resource-discovery/src/main/java/eu/nebulous/resource/discovery/registration/IRegistrationRequestProcessor.java diff --git a/management/src/main/java/eu/nebulous/resource/discovery/registration/RegistrationRequestProcessor.java b/resource-discovery/src/main/java/eu/nebulous/resource/discovery/registration/RegistrationRequestProcessor.java similarity index 73% rename from management/src/main/java/eu/nebulous/resource/discovery/registration/RegistrationRequestProcessor.java rename to resource-discovery/src/main/java/eu/nebulous/resource/discovery/registration/RegistrationRequestProcessor.java index 39dfbf5..f8871f7 100644 --- a/management/src/main/java/eu/nebulous/resource/discovery/registration/RegistrationRequestProcessor.java +++ b/resource-discovery/src/main/java/eu/nebulous/resource/discovery/registration/RegistrationRequestProcessor.java @@ -1,23 +1,17 @@ package eu.nebulous.resource.discovery.registration; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; import eu.nebulous.resource.discovery.ResourceDiscoveryProperties; +import eu.nebulous.resource.discovery.common.BrokerUtil; +import eu.nebulous.resource.discovery.common.REQUEST_TYPE; import eu.nebulous.resource.discovery.monitor.model.Device; import eu.nebulous.resource.discovery.monitor.service.DeviceManagementService; import eu.nebulous.resource.discovery.registration.model.RegistrationRequest; import eu.nebulous.resource.discovery.registration.model.RegistrationRequestStatus; import eu.nebulous.resource.discovery.registration.service.RegistrationRequestService; -import jakarta.jms.*; import lombok.NonNull; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.apache.activemq.ActiveMQConnection; -import org.apache.activemq.ActiveMQConnectionFactory; -import org.apache.activemq.command.ActiveMQMessage; -import org.apache.activemq.command.ActiveMQTextMessage; -import org.apache.activemq.command.ActiveMQTopic; import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.InitializingBean; import org.springframework.scheduling.TaskScheduler; @@ -42,10 +36,7 @@ import java.util.concurrent.atomic.AtomicBoolean; @EnableAsync @EnableScheduling @RequiredArgsConstructor -public class RegistrationRequestProcessor implements IRegistrationRequestProcessor, InitializingBean, MessageListener { - private static final String REQUEST_TYPE_DATA_COLLECTION = "DIAGNOSTICS"; // EMS task type for collecting node info - private static final String REQUEST_TYPE_ONBOARDING = "VM"; // EMS task type for installing EMS client - +public class RegistrationRequestProcessor implements IRegistrationRequestProcessor, InitializingBean, BrokerUtil.Listener { private final static List STATUSES_TO_ARCHIVE = List.of( RegistrationRequestStatus.PRE_AUTHORIZATION_REJECT, RegistrationRequestStatus.PRE_AUTHORIZATION_ERROR, @@ -61,10 +52,11 @@ public class RegistrationRequestProcessor implements IRegistrationRequestProcess private final DeviceManagementService deviceManagementService; private final TaskScheduler taskScheduler; private final ObjectMapper objectMapper; + private final BrokerUtil brokerUtil; private final AtomicBoolean isRunning = new AtomicBoolean(false); @Override - public void afterPropertiesSet() throws Exception { + public void afterPropertiesSet() { // Initialize request processing results listener taskScheduler.schedule(this::initializeResultsListener, Instant.now().plusSeconds(processorProperties.getSubscriptionStartupDelay())); @@ -91,28 +83,16 @@ public class RegistrationRequestProcessor implements IRegistrationRequestProcess } log.debug("processRequests: Processing registration requests"); - // Connect to Message broker - ActiveMQConnectionFactory connectionFactory = new ActiveMQConnectionFactory( - processorProperties.getBrokerUsername(), processorProperties.getBrokerPassword(), - processorProperties.getBrokerURL()); - ActiveMQConnection conn = (ActiveMQConnection) connectionFactory.createConnection(); - Session session = conn.createSession(); - MessageProducer producer = session.createProducer( - new ActiveMQTopic(processorProperties.getDataCollectionRequestTopic())); - // Process requests try { - processNewRequests(producer); - processOnboardingRequests(producer); + processNewRequests(); + processOnboardingRequests(); if (processorProperties.isAutomaticArchivingEnabled()) archiveRequests(); } catch (Throwable t) { log.error("processRequests: ERROR processing requests: ", t); } - // Close connection to Message broker - conn.close(); - log.debug("processRequests: Processing completed"); return CompletableFuture.completedFuture("DONE"); @@ -125,8 +105,8 @@ public class RegistrationRequestProcessor implements IRegistrationRequestProcess } } - private void processNewRequests(MessageProducer producer) throws JsonProcessingException, JMSException { - log.trace("processNewRequests: BEGIN: {}", producer); + private void processNewRequests() { + log.trace("processNewRequests: BEGIN"); List newRequests = registrationRequestService.getAll().stream() .filter(r -> r.getStatus() == RegistrationRequestStatus.NEW_REQUEST).toList(); @@ -136,10 +116,11 @@ public class RegistrationRequestProcessor implements IRegistrationRequestProcess for (RegistrationRequest registrationRequest : newRequests) { try { log.debug("processNewRequests: Requesting collection of device data for request with Id: {}", registrationRequest.getId()); - Map dataCollectionRequest = prepareRequestPayload(REQUEST_TYPE_DATA_COLLECTION, registrationRequest); - String jsonMessage = objectMapper.writer().writeValueAsString(dataCollectionRequest); - producer.send(createMessage(jsonMessage)); + Map dataCollectionRequest = prepareRequestPayload(REQUEST_TYPE.DIAGNOSTICS, registrationRequest); + brokerUtil.sendMessage(processorProperties.getDataCollectionRequestTopic(), dataCollectionRequest); registrationRequest.setStatus(RegistrationRequestStatus.DATA_COLLECTION_REQUESTED); + + log.debug("processNewRequests: Save updated request: id={}, request={}", registrationRequest.getId(), registrationRequest); registrationRequestService.update(registrationRequest); log.debug("processNewRequests: Data collection request sent for request with Id: {}", registrationRequest.getId()); } catch (Exception e) { @@ -152,8 +133,8 @@ public class RegistrationRequestProcessor implements IRegistrationRequestProcess log.trace("processNewRequests: END"); } - private void processOnboardingRequests(MessageProducer producer) throws JsonProcessingException, JMSException { - log.trace("processOnboardingRequests: BEGIN: {}", producer); + private void processOnboardingRequests() { + log.trace("processOnboardingRequests: BEGIN"); List onboardingRequests = registrationRequestService.getAll().stream() .filter(r -> r.getStatus() == RegistrationRequestStatus.PENDING_ONBOARDING).toList(); @@ -162,33 +143,36 @@ public class RegistrationRequestProcessor implements IRegistrationRequestProcess for (RegistrationRequest registrationRequest : onboardingRequests) { try { - log.debug("processOnboardingRequests: Checking device before requesting onboarding for request with Id: {}", registrationRequest.getId()); - deviceManagementService.checkDevice( - objectMapper.convertValue(registrationRequest.getDevice(), Device.class), - true); + log.debug("processOnboardingRequests: Checking device data before requesting onboarding, for request with Id: {}", registrationRequest.getId()); + Device deviceForMonitoring = objectMapper.convertValue(registrationRequest.getDevice(), Device.class); + deviceForMonitoring.setPassword(registrationRequest.getDevice().getPassword()); // ignored by 'objectMapper', so we've to copy them + deviceForMonitoring.setPublicKey(registrationRequest.getDevice().getPublicKey()); // ignored by 'objectMapper', so we've to copy them + deviceManagementService.checkDevice(deviceForMonitoring, true); log.debug("processOnboardingRequests: Requesting device onboarding for request with Id: {}", registrationRequest.getId()); - Map dataCollectionRequest = prepareRequestPayload(REQUEST_TYPE_ONBOARDING, registrationRequest); - String jsonMessage = objectMapper.writer().writeValueAsString(dataCollectionRequest); - producer.send(createMessage(jsonMessage)); + Map dataCollectionRequest = prepareRequestPayload(REQUEST_TYPE.INSTALL, registrationRequest); + brokerUtil.sendMessage(processorProperties.getDataCollectionRequestTopic(), dataCollectionRequest); registrationRequest.setStatus(RegistrationRequestStatus.ONBOARDING_REQUESTED); - registrationRequestService.update(registrationRequest); + + log.debug("processOnboardingRequests: Save updated request: id={}, request={}", registrationRequest.getId(), registrationRequest); + registrationRequestService.update(registrationRequest, false); log.debug("processOnboardingRequests: Onboarding request sent for request with Id: {}", registrationRequest.getId()); } catch (Exception e) { log.warn("processOnboardingRequests: EXCEPTION while sending onboarding request for request with Id: {}\n", registrationRequest.getId(), e); registrationRequest.setStatus(RegistrationRequestStatus.ONBOARDING_ERROR); - registrationRequestService.update(registrationRequest); + registrationRequest.getMessages().add("EXCEPTION "+e.getMessage()); + registrationRequestService.update(registrationRequest, false); } } log.trace("processOnboardingRequests: END"); } - private static Map prepareRequestPayload(@NonNull String requestType, RegistrationRequest registrationRequest) { + private static Map prepareRequestPayload(@NonNull REQUEST_TYPE requestType, RegistrationRequest registrationRequest) { try { Map payload = new LinkedHashMap<>(Map.of( "requestId", registrationRequest.getId(), - "requestType", requestType, + "requestType", requestType.name(), "deviceId", registrationRequest.getDevice().getId(), "deviceOs", registrationRequest.getDevice().getOs(), "deviceName", registrationRequest.getDevice().getName(), @@ -208,12 +192,6 @@ public class RegistrationRequestProcessor implements IRegistrationRequestProcess } } - protected ActiveMQMessage createMessage(String message) throws MessageNotWriteableException { - ActiveMQTextMessage textMessage = new ActiveMQTextMessage(); - textMessage.setText(message); - return textMessage; - } - private void archiveRequests() { Instant archiveThreshold = Instant.now().minus(processorProperties.getArchivingThreshold(), ChronoUnit.MINUTES); log.trace("archiveRequests: BEGIN: archive-threshold: {}", archiveThreshold); @@ -238,46 +216,22 @@ public class RegistrationRequestProcessor implements IRegistrationRequestProcess protected void initializeResultsListener() { try { - ActiveMQConnectionFactory connectionFactory = new ActiveMQConnectionFactory( - processorProperties.getBrokerUsername(), processorProperties.getBrokerPassword(), processorProperties.getBrokerURL()); - ActiveMQConnection conn = (ActiveMQConnection) connectionFactory.createConnection(); - Session session = conn.createSession(); - MessageConsumer consumer = session.createConsumer( - new ActiveMQTopic(processorProperties.getDataCollectionResponseTopic())); - consumer.setMessageListener(this); - conn.start(); + brokerUtil.subscribe(processorProperties.getDataCollectionResponseTopic(), this); } catch (Exception e) { log.error("RegistrationRequestProcessor: ERROR while subscribing to Message broker for Device info announcements: ", e); - taskScheduler.schedule(this::initializeResultsListener, Instant.now().plusSeconds(processorProperties.getSubscriptionRetry())); + taskScheduler.schedule(this::initializeResultsListener, Instant.now().plusSeconds(processorProperties.getSubscriptionRetryDelay())); } } - @Override - public void onMessage(Message message) { - try { - log.debug("RegistrationRequestProcessor: Received a JMS message: {}", message); - if (message instanceof ActiveMQTextMessage textMessage) { - String payload = textMessage.getText(); - log.trace("RegistrationRequestProcessor: Message payload: {}", payload); - TypeReference> typeRef = new TypeReference<>() { }; - Object obj = objectMapper.readerFor(typeRef).readValue(payload); - if (obj instanceof Map response) { - processResponse(response); - } else { - log.debug("RegistrationRequestProcessor: Message payload is not recognized. Expected Map: type={}, object={}", obj.getClass().getName(), obj); - } - } else { - log.debug("RegistrationRequestProcessor: Message type is not supported: {}", message); - } - } catch (Exception e) { - log.warn("RegistrationRequestProcessor: ERROR while processing message: {}\nException: ", message, e); - } + public void onMessage(Map message) { + processResponse(message); } private void processResponse(@NonNull Map response) { + String requestType = response.getOrDefault("requestType", "").toString().trim(); String requestId = response.getOrDefault("requestId", "").toString().trim(); String reference = response.getOrDefault("reference", "").toString().trim(); - String status = response.getOrDefault("status", "").toString().trim(); + String responseStatus = response.getOrDefault("status", "").toString().trim(); String deviceIpAddress = response.getOrDefault("deviceIpAddress", "").toString().trim(); long timestamp = Long.parseLong(response.getOrDefault("timestamp", "-1").toString().trim()); @@ -291,6 +245,7 @@ public class RegistrationRequestProcessor implements IRegistrationRequestProcess case ONBOARDING_REQUESTED -> RegistrationRequestStatus.ONBOARDING_ERROR; default -> currStatus; }; + log.debug("processResponse: Temporary status change: {} --> {}", currStatus, newStatus); registrationRequest.setStatus(newStatus); if (currStatus==RegistrationRequestStatus.SUCCESS) { @@ -301,8 +256,8 @@ public class RegistrationRequestProcessor implements IRegistrationRequestProcess String ipAddress = registrationRequest.getDevice().getIpAddress(); boolean isError = false; - if (StringUtils.equals(ipAddress, deviceIpAddress)) { - String mesg = String.format("Device IP address do not match with that in request: id=%s, ip-address=%s != %s", requestId, ipAddress, deviceIpAddress); + if (StringUtils.isNotBlank(deviceIpAddress) && ! StringUtils.equals(ipAddress, deviceIpAddress)) { + String mesg = String.format("Device IP address in RESPONSE does not match with that in the request: id=%s, ip-address-response=%s != ip-address-in-request%s", requestId, deviceIpAddress, ipAddress); log.warn("processResponse: {}", mesg); registrationRequest.getMessages().add(mesg); isError = true; @@ -313,17 +268,21 @@ public class RegistrationRequestProcessor implements IRegistrationRequestProcess registrationRequest.getMessages().add(mesg); isError = true; } - if (! "SUCCESS".equals(status)) { - String mesg = String.format("Request status is not SUCCESS: id=%s, timestamp=%d, status=%s", requestId, timestamp, status); + if (! "SUCCESS".equals(responseStatus)) { + String mesg = String.format("RESPONSE status is not SUCCESS: id=%s, timestamp=%d, status=%s", requestId, timestamp, responseStatus); log.warn("processResponse: {}", mesg); registrationRequest.getMessages().add(mesg); isError = true; } if (isError) { - registrationRequestService.update(registrationRequest); + if (log.isDebugEnabled()) + log.debug("processResponse: Save request with errors: id={}, errors={}, request={}", requestId, registrationRequest.getMessages(), registrationRequest); + log.warn("processResponse: Save request with errors: id={}, errors={}", requestId, registrationRequest.getMessages()); + registrationRequestService.update(registrationRequest, false, true); return; } + boolean doArchive = false; Object obj = response.get("nodeInfo"); if (obj instanceof Map devInfo) { // Update request info @@ -358,8 +317,10 @@ public class RegistrationRequestProcessor implements IRegistrationRequestProcess // Set new status if (currStatus==RegistrationRequestStatus.DATA_COLLECTION_REQUESTED) registrationRequest.setStatus(RegistrationRequestStatus.PENDING_AUTHORIZATION); - if (currStatus==RegistrationRequestStatus.ONBOARDING_REQUESTED) + if (currStatus==RegistrationRequestStatus.ONBOARDING_REQUESTED) { registrationRequest.setStatus(RegistrationRequestStatus.SUCCESS); + doArchive = processorProperties.isImmediatelyArchiveSuccessRequests(); + } log.debug("processResponse: Done processing response for request: id={}, timestamp={}", requestId, timestamp); } else { @@ -378,19 +339,32 @@ public class RegistrationRequestProcessor implements IRegistrationRequestProcess } // Store changes - registrationRequestService.update(registrationRequest, false); + log.debug("processResponse: Save updated request: id={}, request={}", requestId, registrationRequest); + registrationRequestService.update(registrationRequest, false, true); + + // Archive success requests + if (doArchive) { + registrationRequestService.archiveRequestBySystem(registrationRequest.getId()); + } } else { - log.warn("processResponse: Request not found: id={}", requestId); + log.debug("processResponse: Request not found: id={}, requestType={}", requestId, requestType); } } private void copyDeviceToMonitoring(RegistrationRequest registrationRequest) { Device device = objectMapper.convertValue(registrationRequest.getDevice(), Device.class); + // override values device.setId(null); device.setStatus(null); - device.setRequest(registrationRequest); - device.setRequestId(registrationRequest.getId()); device.getMessages().clear(); + + // copy credentials + device.setPassword(registrationRequest.getDevice().getPassword()); // ignored by 'objectMapper', so we've to copy them + device.setPublicKey(registrationRequest.getDevice().getPublicKey()); // ignored by 'objectMapper', so we've to copy them + + // fields specific to Monitoring Device + //device.setRequest(registrationRequest); + device.setRequestId(registrationRequest.getId()); device.setNodeReference(registrationRequest.getNodeReference()); deviceManagementService.save(device); } diff --git a/management/src/main/java/eu/nebulous/resource/discovery/registration/RegistrationRequestService_SampleDataCreator.java b/resource-discovery/src/main/java/eu/nebulous/resource/discovery/registration/RegistrationRequestService_SampleDataCreator.java similarity index 100% rename from management/src/main/java/eu/nebulous/resource/discovery/registration/RegistrationRequestService_SampleDataCreator.java rename to resource-discovery/src/main/java/eu/nebulous/resource/discovery/registration/RegistrationRequestService_SampleDataCreator.java diff --git a/management/src/main/java/eu/nebulous/resource/discovery/registration/controller/RegistrationRequestController.java b/resource-discovery/src/main/java/eu/nebulous/resource/discovery/registration/controller/RegistrationRequestController.java similarity index 90% rename from management/src/main/java/eu/nebulous/resource/discovery/registration/controller/RegistrationRequestController.java rename to resource-discovery/src/main/java/eu/nebulous/resource/discovery/registration/controller/RegistrationRequestController.java index ec05305..7085470 100644 --- a/management/src/main/java/eu/nebulous/resource/discovery/registration/controller/RegistrationRequestController.java +++ b/resource-discovery/src/main/java/eu/nebulous/resource/discovery/registration/controller/RegistrationRequestController.java @@ -1,14 +1,15 @@ package eu.nebulous.resource.discovery.registration.controller; import eu.nebulous.resource.discovery.registration.IRegistrationRequestProcessor; -import eu.nebulous.resource.discovery.registration.service.RegistrationRequestService; import eu.nebulous.resource.discovery.registration.model.ArchivedRegistrationRequest; import eu.nebulous.resource.discovery.registration.model.RegistrationRequest; import eu.nebulous.resource.discovery.registration.model.RegistrationRequestException; import eu.nebulous.resource.discovery.registration.model.RegistrationRequestStatus; +import eu.nebulous.resource.discovery.registration.service.RegistrationRequestService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; +import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.core.Authentication; @@ -118,9 +119,13 @@ public class RegistrationRequestController { } @PreAuthorize("hasAuthority('ROLE_ADMIN')") - @GetMapping(value = "/request/{id}/unarchive", produces = MediaType.APPLICATION_JSON_VALUE) - public RegistrationRequest unarchiveRequest(@PathVariable String id, Authentication authentication) { - registrationRequestService.unarchiveRequest(id, authentication); + @PostMapping(value = "/request/{id}/unarchive", + consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE) + public RegistrationRequest unarchiveRequest(@PathVariable String id, + @RequestBody Map credentials, + Authentication authentication) + { + registrationRequestService.unarchiveRequest(id, credentials, authentication); return registrationRequestService.getById(id) .orElseThrow(() -> new RegistrationRequestException("Failed to unarchive registration request with id: " + id)); } @@ -141,4 +146,15 @@ public class RegistrationRequestController { return registrationRequestService.getArchivedByIdAsUser(id, authentication) .orElseThrow(() -> new RegistrationRequestException("Not found archived registration request with id: "+id)); } + + @ResponseStatus(value = HttpStatus.BAD_REQUEST) + @ExceptionHandler(RegistrationRequestException.class) + public Map handleRegistrationRequestException(RegistrationRequestException exception) { + return Map.of( + "status", HttpStatus.BAD_REQUEST.value(), + "timestamp", System.currentTimeMillis(), + "exception", exception.getClass().getName(), + "message", exception.getMessage() + ); + } } diff --git a/management/src/main/java/eu/nebulous/resource/discovery/registration/model/ArchivedRegistrationRequest.java b/resource-discovery/src/main/java/eu/nebulous/resource/discovery/registration/model/ArchivedRegistrationRequest.java similarity index 100% rename from management/src/main/java/eu/nebulous/resource/discovery/registration/model/ArchivedRegistrationRequest.java rename to resource-discovery/src/main/java/eu/nebulous/resource/discovery/registration/model/ArchivedRegistrationRequest.java diff --git a/management/src/main/java/eu/nebulous/resource/discovery/registration/model/Device.java b/resource-discovery/src/main/java/eu/nebulous/resource/discovery/registration/model/Device.java similarity index 59% rename from management/src/main/java/eu/nebulous/resource/discovery/registration/model/Device.java rename to resource-discovery/src/main/java/eu/nebulous/resource/discovery/registration/model/Device.java index 9f1a10d..cdf04fa 100644 --- a/management/src/main/java/eu/nebulous/resource/discovery/registration/model/Device.java +++ b/resource-discovery/src/main/java/eu/nebulous/resource/discovery/registration/model/Device.java @@ -1,7 +1,10 @@ package eu.nebulous.resource.discovery.registration.model; +import com.fasterxml.jackson.annotation.JsonProperty; +import eu.nebulous.resource.discovery.common.DeviceLocation; import lombok.Data; import lombok.NoArgsConstructor; +import lombok.ToString; import lombok.experimental.SuperBuilder; import java.util.Map; @@ -15,8 +18,13 @@ public class Device { private String name; private String owner; private String ipAddress; + private DeviceLocation location; private String username; + @ToString.Exclude + @JsonProperty(access = JsonProperty.Access.WRITE_ONLY) private char[] password; + @ToString.Exclude + @JsonProperty(access = JsonProperty.Access.WRITE_ONLY) private char[] publicKey; private Map deviceInfo; } diff --git a/management/src/main/java/eu/nebulous/resource/discovery/registration/model/RegistrationRequest.java b/resource-discovery/src/main/java/eu/nebulous/resource/discovery/registration/model/RegistrationRequest.java similarity index 65% rename from management/src/main/java/eu/nebulous/resource/discovery/registration/model/RegistrationRequest.java rename to resource-discovery/src/main/java/eu/nebulous/resource/discovery/registration/model/RegistrationRequest.java index 0286b84..210cf10 100644 --- a/management/src/main/java/eu/nebulous/resource/discovery/registration/model/RegistrationRequest.java +++ b/resource-discovery/src/main/java/eu/nebulous/resource/discovery/registration/model/RegistrationRequest.java @@ -25,8 +25,18 @@ public class RegistrationRequest { private Instant lastUpdateDate; private Instant archiveDate; private RegistrationRequestStatus status; - private List history; + private List history = new ArrayList<>(); private String nodeReference; @Setter(AccessLevel.NONE) private List messages = new ArrayList<>(); + + // Required in order BeanUtils.copyProperties() to also copy this + public void setHistory(List history) { + this.history = new ArrayList<>(history); + } + + // Required in order BeanUtils.copyProperties() to also copy this + public void setMessages(List messages) { + this.messages = new ArrayList<>(messages); + } } diff --git a/management/src/main/java/eu/nebulous/resource/discovery/registration/model/RegistrationRequestException.java b/resource-discovery/src/main/java/eu/nebulous/resource/discovery/registration/model/RegistrationRequestException.java similarity index 100% rename from management/src/main/java/eu/nebulous/resource/discovery/registration/model/RegistrationRequestException.java rename to resource-discovery/src/main/java/eu/nebulous/resource/discovery/registration/model/RegistrationRequestException.java diff --git a/management/src/main/java/eu/nebulous/resource/discovery/registration/model/RegistrationRequestHistoryEntry.java b/resource-discovery/src/main/java/eu/nebulous/resource/discovery/registration/model/RegistrationRequestHistoryEntry.java similarity index 100% rename from management/src/main/java/eu/nebulous/resource/discovery/registration/model/RegistrationRequestHistoryEntry.java rename to resource-discovery/src/main/java/eu/nebulous/resource/discovery/registration/model/RegistrationRequestHistoryEntry.java diff --git a/management/src/main/java/eu/nebulous/resource/discovery/registration/model/RegistrationRequestStatus.java b/resource-discovery/src/main/java/eu/nebulous/resource/discovery/registration/model/RegistrationRequestStatus.java similarity index 100% rename from management/src/main/java/eu/nebulous/resource/discovery/registration/model/RegistrationRequestStatus.java rename to resource-discovery/src/main/java/eu/nebulous/resource/discovery/registration/model/RegistrationRequestStatus.java diff --git a/management/src/main/java/eu/nebulous/resource/discovery/registration/repository/ArchivedRegistrationRequestRepository.java b/resource-discovery/src/main/java/eu/nebulous/resource/discovery/registration/repository/ArchivedRegistrationRequestRepository.java similarity index 100% rename from management/src/main/java/eu/nebulous/resource/discovery/registration/repository/ArchivedRegistrationRequestRepository.java rename to resource-discovery/src/main/java/eu/nebulous/resource/discovery/registration/repository/ArchivedRegistrationRequestRepository.java diff --git a/management/src/main/java/eu/nebulous/resource/discovery/registration/repository/InMemoryRegistrationRequestRepository.java b/resource-discovery/src/main/java/eu/nebulous/resource/discovery/registration/repository/InMemoryRegistrationRequestRepository.java similarity index 100% rename from management/src/main/java/eu/nebulous/resource/discovery/registration/repository/InMemoryRegistrationRequestRepository.java rename to resource-discovery/src/main/java/eu/nebulous/resource/discovery/registration/repository/InMemoryRegistrationRequestRepository.java diff --git a/management/src/main/java/eu/nebulous/resource/discovery/registration/repository/RegistrationRequestRepository.java b/resource-discovery/src/main/java/eu/nebulous/resource/discovery/registration/repository/RegistrationRequestRepository.java similarity index 85% rename from management/src/main/java/eu/nebulous/resource/discovery/registration/repository/RegistrationRequestRepository.java rename to resource-discovery/src/main/java/eu/nebulous/resource/discovery/registration/repository/RegistrationRequestRepository.java index 395d910..27d4618 100644 --- a/management/src/main/java/eu/nebulous/resource/discovery/registration/repository/RegistrationRequestRepository.java +++ b/resource-discovery/src/main/java/eu/nebulous/resource/discovery/registration/repository/RegistrationRequestRepository.java @@ -7,4 +7,5 @@ import java.util.List; public interface RegistrationRequestRepository extends MongoRepository { List findByRequester(String requester); + List findByDeviceIpAddress(String ipAddress); } diff --git a/management/src/main/java/eu/nebulous/resource/discovery/registration/service/RegistrationRequestConversionService.java b/resource-discovery/src/main/java/eu/nebulous/resource/discovery/registration/service/RegistrationRequestConversionService.java similarity index 100% rename from management/src/main/java/eu/nebulous/resource/discovery/registration/service/RegistrationRequestConversionService.java rename to resource-discovery/src/main/java/eu/nebulous/resource/discovery/registration/service/RegistrationRequestConversionService.java diff --git a/management/src/main/java/eu/nebulous/resource/discovery/registration/service/RegistrationRequestService.java b/resource-discovery/src/main/java/eu/nebulous/resource/discovery/registration/service/RegistrationRequestService.java similarity index 74% rename from management/src/main/java/eu/nebulous/resource/discovery/registration/service/RegistrationRequestService.java rename to resource-discovery/src/main/java/eu/nebulous/resource/discovery/registration/service/RegistrationRequestService.java index e1c07bc..e775cb3 100644 --- a/management/src/main/java/eu/nebulous/resource/discovery/registration/service/RegistrationRequestService.java +++ b/resource-discovery/src/main/java/eu/nebulous/resource/discovery/registration/service/RegistrationRequestService.java @@ -1,5 +1,6 @@ package eu.nebulous.resource.discovery.registration.service; +import eu.nebulous.resource.discovery.monitor.service.DeviceManagementService; import eu.nebulous.resource.discovery.registration.model.*; import eu.nebulous.resource.discovery.registration.repository.ArchivedRegistrationRequestRepository; import eu.nebulous.resource.discovery.registration.repository.RegistrationRequestRepository; @@ -7,6 +8,7 @@ import lombok.NonNull; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.BeanUtils; import org.springframework.security.core.Authentication; import org.springframework.security.core.GrantedAuthority; import org.springframework.stereotype.Service; @@ -21,6 +23,7 @@ public class RegistrationRequestService { private final RegistrationRequestRepository registrationRequestRepository; private final ArchivedRegistrationRequestRepository archivedRegistrationRequestRepository; private final RegistrationRequestConversionService registrationRequestConversionService; + private final DeviceManagementService deviceManagementService; /*private final InMemoryRegistrationRequestRepository registrationRequestRepository = new InMemoryRegistrationRequestRepository<>(); @@ -38,10 +41,19 @@ public class RegistrationRequestService { return registrationRequestRepository.findById(id); } + public List getByDeviceIpAddress(@NonNull String ipAddress) { + return registrationRequestRepository.findByDeviceIpAddress(ipAddress); + } + public List getAll() { return Collections.unmodifiableList(registrationRequestRepository.findAll()); } + public boolean isIpAddressInUse(@NonNull String ipAddress, String excludeId) { + List result = registrationRequestRepository.findByDeviceIpAddress(ipAddress); + return result.stream().anyMatch(r -> !r.getId().equals(excludeId)); + } + public @NonNull RegistrationRequest save(@NonNull RegistrationRequest registrationRequest) { RegistrationRequestStatus status = registrationRequest.getStatus(); if (status == null) { @@ -62,6 +74,9 @@ public class RegistrationRequestService { registrationRequest.setRequestDate(Instant.now()); checkRegistrationRequest(registrationRequest); + // check IP address uniqueness + checkIpAddressUniqueness(registrationRequest); + registrationRequestRepository.save(registrationRequest); return registrationRequest; } @@ -71,6 +86,10 @@ public class RegistrationRequestService { } public RegistrationRequest update(@NonNull RegistrationRequest registrationRequest, boolean checkEditDel) { + return update(registrationRequest, checkEditDel, false); + } + + public RegistrationRequest update(@NonNull RegistrationRequest registrationRequest, boolean checkEditDel, boolean skipUniqueIpAddressCheck) { Optional result = getById(registrationRequest.getId()); if (result.isEmpty()) throw new RegistrationRequestException( @@ -79,13 +98,45 @@ public class RegistrationRequestService { if (checkEditDel) canEditOrDelete(result.get()); - registrationRequest.setLastUpdateDate(Instant.now()); - registrationRequestRepository.save(registrationRequest); + // check IP address uniqueness + if (!skipUniqueIpAddressCheck) + checkIpAddressUniqueness(registrationRequest); + + // Copy submitted registration request data onto the retrieved request + BeanUtils.copyProperties(registrationRequest, result.get(), + "id", "device", "requester", "requestDate"); + result.get().setLastUpdateDate(Instant.now()); + + // Check if device password/public key need update... + List ignoreList = new ArrayList<>(); + if (isCharArrayIsBlank(registrationRequest.getDevice().getPassword()) + && isCharArrayIsBlank(registrationRequest.getDevice().getPublicKey())) + { + ignoreList.add("password"); + ignoreList.add("publicKey"); + } + // ...then copy submitted request's device data onto the retrieved request's device + BeanUtils.copyProperties( + registrationRequest.getDevice(), + result.get().getDevice(), + ignoreList.toArray(new String[0])); + + registrationRequestRepository.save(result.get()); return getById(registrationRequest.getId()).orElseThrow(() -> new RegistrationRequestException("Request update failed for Id: "+registrationRequest.getId())); } + private boolean isCharArrayIsBlank(char[] arr) { + if (arr==null) return true; + for (char c : arr) + if (!isWhiteSpaceChar(c)) return false; + return true; + } + private boolean isWhiteSpaceChar(char c) { + return c==' ' || c=='\t' || c=='\r' || c=='\n'; + } + private void checkRegistrationRequest(@NonNull RegistrationRequest registrationRequest) { List errors = new ArrayList<>(); if (StringUtils.isBlank(registrationRequest.getId())) errors.add("Null or blank Id"); @@ -105,6 +156,17 @@ public class RegistrationRequestService { //XXX:TODO } + private void checkIpAddressUniqueness(RegistrationRequest registrationRequest) { + boolean exists1 = this.isIpAddressInUse( + registrationRequest.getDevice().getIpAddress(), registrationRequest.getId()); + boolean exists2 = deviceManagementService.isIpAddressInUse( + registrationRequest.getDevice().getIpAddress()); + if (exists1 || exists2) { + throw new RegistrationRequestException( + "The IP address is already in use by: another-registration-request="+exists1+", registered-device="+exists2); + } + } + private void canEditOrDelete(RegistrationRequest registrationRequest) { RegistrationRequestStatus status = registrationRequest.getStatus(); if (status==RegistrationRequestStatus.ONBOARDING_REQUESTED || status== RegistrationRequestStatus.SUCCESS) @@ -248,16 +310,34 @@ public class RegistrationRequestService { registrationRequestRepository.delete(result.get()); } - public void unarchiveRequest(String id, Authentication authentication) { + public void unarchiveRequest(String id, Map credentials, Authentication authentication) { Optional result = getArchivedById(id); if (result.isEmpty()) throw new RegistrationRequestException( "Archived registration request with Id does not exists in repository: "+id); checkAdmin(result.get().getId(), authentication); + checkCredentials(result.get().getId(), credentials); result.get().setArchiveDate(null); - registrationRequestRepository.save( - registrationRequestConversionService.toRegistrationRequest(result.get())); + RegistrationRequest restoredRequest = registrationRequestConversionService.toRegistrationRequest(result.get()); + Device device = restoredRequest.getDevice(); + device.setUsername(credentials.get("username")); + device.setPassword(credentials.get("password").toCharArray()); + device.setPublicKey(credentials.get("publicKey").toCharArray()); + registrationRequestRepository.save(restoredRequest); archivedRegistrationRequestRepository.deleteById(result.get().getId()); } + + private void checkCredentials(String id, Map 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); + } } diff --git a/resource-discovery/src/main/resources/application.yml b/resource-discovery/src/main/resources/application.yml new file mode 100644 index 0000000..ec64793 --- /dev/null +++ b/resource-discovery/src/main/resources/application.yml @@ -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 \ No newline at end of file diff --git a/resource-discovery/src/main/resources/banner.txt b/resource-discovery/src/main/resources/banner.txt new file mode 100644 index 0000000..1e12e7a --- /dev/null +++ b/resource-discovery/src/main/resources/banner.txt @@ -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}) diff --git a/resource-discovery/src/main/resources/static/freebees_webdesign_6/archived-device-view.html b/resource-discovery/src/main/resources/static/freebees_webdesign_6/archived-device-view.html new file mode 100644 index 0000000..6b4e34e --- /dev/null +++ b/resource-discovery/src/main/resources/static/freebees_webdesign_6/archived-device-view.html @@ -0,0 +1,434 @@ + + + + + + + + NebulOuS Resource Discovery - Management page + + + + + + + + + + + + + + + + +
+
+ +
+
+ + + + +       + + +       + + + +       + +
+
+
+ +
+ +
+ +
+

* * * CAUTION: YOU'RE VIEWING A DEVICE YOU DON'T OWN * * *

+ +

Device ---

+ + + +       + +       + +

 

+ +
+
+ +
+
+
Device details
+
+ + +
+ +
+ +
+
+ +
+ +
+ +
+
+ +
+ +
+ +
+
+ +
+ +
+ +
+
+ +
+ +
+ +
+
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+
+ +
+ +
+ +
+
+ +
+ +
+ +
+
+ +
+ +
+ +
+
+ + +
+ +
+ +
+
+ +
+ +
+ +
+
+ +
+ +
+ +
+
+ +
+ +
+ +
+
+ +
+ +
+ +
+
+ +
+ +
+ +
+
+ + +
+ +
+ +
+
+ + + +
+ +
+ +
+
+
Device metrics
+
+ +
+ +
+
+ +
+ +
+
+
+
+
+ + + +
+
+
+
Fbee 2022 copyright
+ + \ No newline at end of file diff --git a/management/src/main/resources/static/freebees_webdesign_6/archived-view.html b/resource-discovery/src/main/resources/static/freebees_webdesign_6/archived-request-view.html similarity index 91% rename from management/src/main/resources/static/freebees_webdesign_6/archived-view.html rename to resource-discovery/src/main/resources/static/freebees_webdesign_6/archived-request-view.html index c3ae664..16d70ba 100644 --- a/management/src/main/resources/static/freebees_webdesign_6/archived-view.html +++ b/resource-discovery/src/main/resources/static/freebees_webdesign_6/archived-request-view.html @@ -146,6 +146,11 @@ function flattenObject(ob) {
+ + + + +             @@ -295,6 +300,21 @@ function flattenObject(ob) {
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+
diff --git a/resource-discovery/src/main/resources/static/freebees_webdesign_6/archived.html b/resource-discovery/src/main/resources/static/freebees_webdesign_6/archived.html new file mode 100644 index 0000000..a97df0b --- /dev/null +++ b/resource-discovery/src/main/resources/static/freebees_webdesign_6/archived.html @@ -0,0 +1,454 @@ + + + + + + + + NebulOuS Resource Discovery - Management page + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + +       + + +       + + + +       + +
+ +
+
+
+

Archived Registration Requests

+ + + +       +       + +       + + +
+ + +
+ + + + + + + + + + + + + + +
#RequesterDevice nameIP AddressReg. DateStatusActions
+
+ +
+
+ +
+
+
+

Archived Devices

+ + + +       +       + +       + + +
+ + +
+ + + + + + + + + + + + + + +
#OwnerDevice nameIP AddressReg. DateStatusActions
+
+ +
+
+ + + + +
+
+
+ + + +
+
+
+
Fbee 2022 copyright
+ + \ No newline at end of file diff --git a/management/src/main/resources/static/freebees_webdesign_6/css/style.css b/resource-discovery/src/main/resources/static/freebees_webdesign_6/css/style.css similarity index 100% rename from management/src/main/resources/static/freebees_webdesign_6/css/style.css rename to resource-discovery/src/main/resources/static/freebees_webdesign_6/css/style.css diff --git a/management/src/main/resources/static/freebees_webdesign_6/css/style.css.map b/resource-discovery/src/main/resources/static/freebees_webdesign_6/css/style.css.map similarity index 100% rename from management/src/main/resources/static/freebees_webdesign_6/css/style.css.map rename to resource-discovery/src/main/resources/static/freebees_webdesign_6/css/style.css.map diff --git a/management/src/main/resources/static/freebees_webdesign_6/device-view.html b/resource-discovery/src/main/resources/static/freebees_webdesign_6/device-view.html similarity index 50% rename from management/src/main/resources/static/freebees_webdesign_6/device-view.html rename to resource-discovery/src/main/resources/static/freebees_webdesign_6/device-view.html index a72adee..37b9a9c 100644 --- a/management/src/main/resources/static/freebees_webdesign_6/device-view.html +++ b/resource-discovery/src/main/resources/static/freebees_webdesign_6/device-view.html @@ -144,6 +144,51 @@ function updateFormData(data) { } else { $(`[id="device#messages"]`).val( '' ).attr( 'rows', 1 ); } + + // Update device metrics + if (data.metrics) { + // Clear contents of 'Device metrics' form part + var target = $('#device-metrics'); + target.html(''); + + // Add timestamp in 'Device metrics' form part + var timestamp = data.metrics.timestamp; + //console.log(`TIMESTAMP: ${timestamp}`); + target.append(` +
+ +
+ +
+
+ `); + + // 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(` +
+ +
+ +
+
+ `); + } + } } function flattenObject(ob) { @@ -259,6 +304,11 @@ function sendDeviceData(deviceData) {
+ + + + +             @@ -311,125 +361,156 @@ function sendDeviceData(deviceData) {

 

-
-
-
Device details
+
+
+ + +
+
Device details
+
+ + +
+ +
+ +
+
+ +
+ +
+ +
+
+ +
+ +
+ +
+
+ +
+ +
+ +
+
+ +
+ +
+ +
+
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+
+ +
+ +
+ +
+
+ +
+ +
+ +
+
+ +
+ +
+ +
+
+ + +
+ +
+ +
+
+ +
+ +
+ +
+
+ +
+ +
+ +
+
+ +
+ +
+ +
+
+ +
+ +
+ +
+
+ +
+ +
+ +
+
+ + +
+ +
+ +
+
+ + + + +
- -
- -
- +
+
+
Device metrics
-
- -
- -
- -
-
- -
- -
- -
-
- -
- -
- -
-
- -
- -
- -
-
- -
- -
- -
-
- -
- -
- -
-
- -
- -
- -
-
- -
- -
- -
-
- -
- -
- -
-
- -
- -
- -
-
- -
- -
- -
-
- -
- -
- -
-
+
- -
- -
- -
- -
-
Device metrics
-
- - ++++ TODO ++++ - - - - +
diff --git a/management/src/main/resources/static/freebees_webdesign_6/devices.html b/resource-discovery/src/main/resources/static/freebees_webdesign_6/devices.html similarity index 66% rename from management/src/main/resources/static/freebees_webdesign_6/devices.html rename to resource-discovery/src/main/resources/static/freebees_webdesign_6/devices.html index 4fc8cf2..363dffe 100644 --- a/management/src/main/resources/static/freebees_webdesign_6/devices.html +++ b/resource-discovery/src/main/resources/static/freebees_webdesign_6/devices.html @@ -55,16 +55,26 @@ function updateDevicesList(asAdmin) { var owner = item.owner; var devName = (item.name && item.name.trim()!=='') ? item.name.trim() : `(No name - Id ${devId})`; var ipAddress = item.ipAddress; - var load = 'TODO'; - var status = item.status; - var color = getStatusColor(status); + var load = getLoadStr(item); + var status = `${ ((item.statusUpdate && item.statusUpdate.state) ? item.statusUpdate.state : 'na') } +
(${item.status}) `; + var color = getStatusColor(item.status); + var isOffboarded = item.status==='OFFBOARDED' || item.status==='OFFBOARD_ERROR'; - var adminActions = (isAdmin) ? ` - - + + `; + var adminActions = (isAdmin) + ? ` + ` : ''; ii++; @@ -77,10 +87,11 @@ function updateDevicesList(asAdmin) { ${load} ${status} - ${adminActions} + ${userActions} + ${adminActions} ` ) ); @@ -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 = `cpu: ${val}%`; + } + if (item.metrics.metrics.ram) { + var val = Math.round(item.metrics.metrics.ram); + var color = (val>80) ? 'bg-danger text-white' : ''; + ram = `ram: ${val}%`; + } + } + return `${cpu}
${ram}`; +} + function getStatusColor(status) { - if (status.indexOf('ERROR')>0) return 'table-danger'; - if (status.indexOf('REJECT')>0) return 'bg-danger'; - if (status.indexOf('PENDING')>=0) return 'table-warning'; if (status=='NEW_DEVICE') return ''; - if (status=='BUSY') return 'table-success'; - if (status=='IDLE') return 'table-warning'; + if (status.indexOf('ERROR')>0) return 'table-danger'; + if (status=='FAILED') return 'table-danger'; + if (status=='SUSPECT') return 'table-warning'; + if (status.indexOf('BOARDING')>0) return 'table-warning'; + if (status=='ONBOARDED') return 'table-success'; + if (status=='HEALTHY' || status=='BUSY' || status=='IDLE') return 'table-success'; + if (status=='OFFBOARDED' || status=='ON_HOLD') return 'table-secondary'; return 'table-info'; } @@ -120,6 +151,53 @@ function manageDevice(id, action) { }) ; } + +function requestUpdate() { + $.ajax({ + url: '/monitor/request-update', + method: 'GET', + async: 'true' + + }) + .done(function(data, status) { + console.log('requestUpdate: OK'); + }) + .fail(function(xhr, status, error) { + console.error('requestUpdate: ERROR: ', status, error); + }) + .always(function(data, status) { + updateDevicesList(); + }) + ; +} + +function processDevices() { + $.ajax({ url: '/monitor/device/process' }) + .done(function(data, status) { + //console.log('processDevices: OK: ', data); + }) + .fail(function(xhr, status, error) { + console.error('processDevices: ERROR: ', status, error); + }) + .always(function(data, status) { + setTimeout(updateDevicesList, 500); + }) + ; +} + +function archiveDevice(devId) { + $.ajax({ + url: `/monitor/device/${devId}/archive`, + dataType: 'text' + }) + .done(function(data, status) { + console.log('archiveDevice: OK: ', devId, data); + updateDevicesList(true); + }) + .fail(function(xhr, status, error) { + console.error('archiveDevice: ERROR: ', status, error); + }); +} @@ -129,6 +207,12 @@ function manageDevice(id, action) {
+ +
+ + + +             @@ -158,6 +242,13 @@ function manageDevice(id, action) {       + +       +      
-
+

Settings

View the Resource Discovery service settings. Admins can also manage the service, diff --git a/management/src/main/resources/static/freebees_webdesign_6/js/addshadow.js b/resource-discovery/src/main/resources/static/freebees_webdesign_6/js/addshadow.js similarity index 100% rename from management/src/main/resources/static/freebees_webdesign_6/js/addshadow.js rename to resource-discovery/src/main/resources/static/freebees_webdesign_6/js/addshadow.js diff --git a/management/src/main/resources/static/freebees_webdesign_6/request-edit.html b/resource-discovery/src/main/resources/static/freebees_webdesign_6/request-edit.html similarity index 90% rename from management/src/main/resources/static/freebees_webdesign_6/request-edit.html rename to resource-discovery/src/main/resources/static/freebees_webdesign_6/request-edit.html index 5bb7728..05dd881 100644 --- a/management/src/main/resources/static/freebees_webdesign_6/request-edit.html +++ b/resource-discovery/src/main/resources/static/freebees_webdesign_6/request-edit.html @@ -252,8 +252,9 @@ function saveRequestInfo() { // Fix messages var messages = $(`[id="request#messages"]`).val().trim(); - if (messages==='') messages = ''; - root['messages'] = messages.split(/\r\n|\r|\n/); + root['messages'] = (messages!=='') + ? messages.split(/\r\n|\r|\n/) + : []; // Check request Id if (requestId!=='' && requestId!==root.id) { @@ -283,9 +284,11 @@ function sendRequestData(requestData) { refreshRequestInfo(requestId); }) .fail(function(xhr, status, error) { - console.error('sendRequestData: ERROR: ', status, error); + var data = {}; + try { data = JSON.parse(xhr.responseText); } catch (e) { } + console.log('sendRequestData: ERROR: ', status, error, xhr.responseText); $('#page_title').html( - $(`

Error: ${status}: ${error}
`) + $(`
Error: ${status}: ${error ?? ''} ${data ? data.message : ''}
`) ); }) .always(function(data, status) { @@ -304,6 +307,11 @@ function sendRequestData(requestData) {
+ + + + +             @@ -450,6 +458,21 @@ function sendRequestData(requestData) {
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+
@@ -461,14 +484,14 @@ function sendRequestData(requestData) {
- +
- +
diff --git a/management/src/main/resources/static/freebees_webdesign_6/requests.html b/resource-discovery/src/main/resources/static/freebees_webdesign_6/requests.html similarity index 89% rename from management/src/main/resources/static/freebees_webdesign_6/requests.html rename to resource-discovery/src/main/resources/static/freebees_webdesign_6/requests.html index a1e2296..f8203cb 100644 --- a/management/src/main/resources/static/freebees_webdesign_6/requests.html +++ b/resource-discovery/src/main/resources/static/freebees_webdesign_6/requests.html @@ -80,13 +80,9 @@ function updateRequestsList(asAdmin) { ` : ''; adminActions += (isAdmin) ? ` - - - + `: ''; ii++; tbody.append( $(` @@ -148,7 +144,11 @@ function processRequests() { }) .fail(function(xhr, status, error) { console.error('processRequests: ERROR: ', status, error); - }); + }) + .always(function(data, status) { + setTimeout(updateRequestsList, 500); + }) + ; } function authorizeRequest(reqId, authorize) { @@ -179,6 +179,20 @@ function archiveRequest(reqId) { console.error('archiveRequest: ERROR: ', status, error); }); } + +function changeStatus(reqId) { + var newStatus = prompt('Change status to:', 'NEW_REQUEST'); + if (newStatus!=null && newStatus.trim()!=='') { + $.ajax({ + url: '/discovery/request/'+reqId+'/status/'+newStatus.trim().toUpperCase(), + dataType: 'json' + }) + .always(function(data, status) { + console.log('changeStatus: OK: ', data); + updateRequestsList(true); + }) + } +} @@ -188,6 +202,12 @@ function archiveRequest(reqId) {
+
+ + + + +             diff --git a/management/src/main/resources/static/freebees_webdesign_6/sass/_colors.scss b/resource-discovery/src/main/resources/static/freebees_webdesign_6/sass/_colors.scss similarity index 100% rename from management/src/main/resources/static/freebees_webdesign_6/sass/_colors.scss rename to resource-discovery/src/main/resources/static/freebees_webdesign_6/sass/_colors.scss diff --git a/management/src/main/resources/static/freebees_webdesign_6/sass/style.scss b/resource-discovery/src/main/resources/static/freebees_webdesign_6/sass/style.scss similarity index 100% rename from management/src/main/resources/static/freebees_webdesign_6/sass/style.scss rename to resource-discovery/src/main/resources/static/freebees_webdesign_6/sass/style.scss diff --git a/management/src/main/resources/static/index.html b/resource-discovery/src/main/resources/static/index.html similarity index 100% rename from management/src/main/resources/static/index.html rename to resource-discovery/src/main/resources/static/index.html diff --git a/management/src/test/java/eu/nebulous/resource/discovery/registration/ResourceManagementApplicationTests.java b/resource-discovery/src/test/java/eu/nebulous/resource/discovery/registration/ResourceManagementApplicationTests.java similarity index 100% rename from management/src/test/java/eu/nebulous/resource/discovery/registration/ResourceManagementApplicationTests.java rename to resource-discovery/src/test/java/eu/nebulous/resource/discovery/registration/ResourceManagementApplicationTests.java