Compare commits

...

2 Commits

Author SHA1 Message Date
litoral05 af55458ad4 Add router provisioning result endpoint 2026-05-07 17:32:52 +01:00
litoral05 6712a26b2a Improve VPS health response DTO and metrics 2026-05-07 17:12:04 +01:00
12 changed files with 163 additions and 11 deletions
@@ -6,6 +6,7 @@ import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.util.StringUtils; import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter; import org.springframework.web.filter.OncePerRequestFilter;
@@ -58,7 +59,7 @@ public class ApiKeyAuthFilter extends OncePerRequestFilter {
new UsernamePasswordAuthenticationToken( new UsernamePasswordAuthenticationToken(
"api-key-client", "api-key-client",
null, null,
List.of() List.of(new SimpleGrantedAuthority("ROLE_API"))
); );
SecurityContextHolder.getContext().setAuthentication(authentication); SecurityContextHolder.getContext().setAuthentication(authentication);
@@ -5,6 +5,7 @@ import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
@@ -27,11 +28,16 @@ public class SecurityConfig {
return http return http
.csrf(csrf -> csrf.disable()) .csrf(csrf -> csrf.disable())
.cors(cors -> cors.disable())
.formLogin(form -> form.disable()) .formLogin(form -> form.disable())
.httpBasic(basic -> basic.disable()) .httpBasic(basic -> basic.disable())
.logout(logout -> logout.disable())
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)
.authorizeHttpRequests(auth -> auth .authorizeHttpRequests(auth -> auth
.requestMatchers("/actuator/health").permitAll() .requestMatchers("/actuator/health").permitAll()
.anyRequest().authenticated() .anyRequest().permitAll()
) )
.addFilterBefore(apiKeyAuthFilter, UsernamePasswordAuthenticationFilter.class) .addFilterBefore(apiKeyAuthFilter, UsernamePasswordAuthenticationFilter.class)
.build(); .build();
@@ -67,4 +67,12 @@ public class RouterController {
public SyncRoutersFromVpsResponse syncFromVps() { public SyncRoutersFromVpsResponse syncFromVps() {
return routerSyncService.syncFromVps(); return routerSyncService.syncFromVps();
} }
@PostMapping("/{id}/provisioning-result")
public RouterResponse submitProvisioningResult(
@PathVariable UUID id,
@Valid @RequestBody RouterProvisioningResultRequest request
) {
return routerService.submitProvisioningResult(id, request);
}
} }
@@ -0,0 +1,18 @@
package com.litoralregas.vpnprovisioner.router.dto;
import com.litoralregas.vpnprovisioner.router.entity.RouterStatus;
import jakarta.validation.constraints.NotNull;
public record RouterProvisioningResultRequest(
@NotNull
Boolean success,
@NotNull
RouterStatus finalStatus,
String message,
String firmwareVersion,
String validationSummary,
String lastError
) {
}
@@ -19,6 +19,11 @@ public record RouterResponse(
RouterVpnStatus vpnStatus, RouterVpnStatus vpnStatus,
Instant vpnProvisionedAt, Instant vpnProvisionedAt,
Instant createdAt, Instant createdAt,
Instant updatedAt Instant updatedAt,
Instant lastProvisionedAt,
Instant lastValidatedAt,
String validationSummary,
String lastProvisioningError
) { ) {
} }
@@ -42,6 +42,15 @@ public class Router {
@Column(nullable = false) @Column(nullable = false)
private Instant updatedAt; private Instant updatedAt;
private Instant lastProvisionedAt;
private Instant lastValidatedAt;
@Column(columnDefinition = "TEXT")
private String validationSummary;
@Column(columnDefinition = "TEXT")
private String lastProvisioningError;
protected Router() { protected Router() {
} }
@@ -165,4 +174,43 @@ public class Router {
this.vpnProvisionedAt = Instant.now(); this.vpnProvisionedAt = Instant.now();
} }
} }
public void registerProvisioningResult(
boolean success,
RouterStatus finalStatus,
String message,
String firmwareVersion,
String validationSummary,
String lastError
) {
Instant now = Instant.now();
this.status = finalStatus;
this.lastValidatedAt = now;
this.firmwareVersion = firmwareVersion;
this.validationSummary = validationSummary;
if (success) {
this.lastProvisionedAt = now;
this.lastProvisioningError = null;
} else {
this.lastProvisioningError = lastError != null ? lastError : message;
}
}
public Instant getLastProvisionedAt() {
return lastProvisionedAt;
}
public Instant getLastValidatedAt() {
return lastValidatedAt;
}
public String getValidationSummary() {
return validationSummary;
}
public String getLastProvisioningError() {
return lastProvisioningError;
}
} }
@@ -1,6 +1,7 @@
package com.litoralregas.vpnprovisioner.router.service; package com.litoralregas.vpnprovisioner.router.service;
import com.litoralregas.vpnprovisioner.router.dto.CreateRouterRequest; import com.litoralregas.vpnprovisioner.router.dto.CreateRouterRequest;
import com.litoralregas.vpnprovisioner.router.dto.RouterProvisioningResultRequest;
import com.litoralregas.vpnprovisioner.router.dto.RouterResponse; import com.litoralregas.vpnprovisioner.router.dto.RouterResponse;
import com.litoralregas.vpnprovisioner.router.dto.UpdateRouterRequest; import com.litoralregas.vpnprovisioner.router.dto.UpdateRouterRequest;
import com.litoralregas.vpnprovisioner.router.entity.Router; import com.litoralregas.vpnprovisioner.router.entity.Router;
@@ -9,6 +10,7 @@ import com.litoralregas.vpnprovisioner.common.exception.ResourceNotFoundExceptio
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import java.time.Instant;
import java.util.List; import java.util.List;
import java.util.UUID; import java.util.UUID;
@@ -84,7 +86,32 @@ public class RouterService {
router.getVpnStatus(), router.getVpnStatus(),
router.getVpnProvisionedAt(), router.getVpnProvisionedAt(),
router.getCreatedAt(), router.getCreatedAt(),
router.getUpdatedAt() router.getUpdatedAt(),
router.getLastProvisionedAt(),
router.getLastValidatedAt(),
router.getValidationSummary(),
router.getLastProvisioningError()
); );
} }
@Transactional
public RouterResponse submitProvisioningResult(
UUID id,
RouterProvisioningResultRequest request
) {
Router router = routerRepository.findById(id)
.orElseThrow(() -> new ResourceNotFoundException("Router not found: " + id));
router.registerProvisioningResult(
request.success(),
request.finalStatus(),
request.message(),
request.firmwareVersion(),
request.validationSummary(),
request.lastError()
);
return toResponse(router);
}
} }
@@ -86,7 +86,6 @@ public class RouterVpnProvisioningService {
} }
private RouterResponse toResponse(Router router) { private RouterResponse toResponse(Router router) {
return new RouterResponse( return new RouterResponse(
router.getId(), router.getId(),
router.getName(), router.getName(),
@@ -99,7 +98,12 @@ public class RouterVpnProvisioningService {
router.getVpnStatus(), router.getVpnStatus(),
router.getVpnProvisionedAt(), router.getVpnProvisionedAt(),
router.getCreatedAt(), router.getCreatedAt(),
router.getUpdatedAt() router.getUpdatedAt(),
router.getLastProvisionedAt(),
router.getLastValidatedAt(),
router.getValidationSummary(),
router.getLastProvisioningError()
); );
} }
} }
@@ -1,5 +1,6 @@
package com.litoralregas.vpnprovisioner.vps; package com.litoralregas.vpnprovisioner.vps;
import com.litoralregas.vpnprovisioner.vps.dto.VpsHealthResponse;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
@@ -14,8 +15,8 @@ public class VpsController {
} }
@GetMapping(value = "/health", produces = MediaType.APPLICATION_JSON_VALUE) @GetMapping(value = "/health", produces = MediaType.APPLICATION_JSON_VALUE)
public String health() { public VpsHealthResponse health() {
return wireGuardVpsService.getVpsHealthJson(); return wireGuardVpsService.getVpsHealth();
} }
@PostMapping( @PostMapping(
@@ -1,5 +1,8 @@
package com.litoralregas.vpnprovisioner.vps; package com.litoralregas.vpnprovisioner.vps;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.litoralregas.vpnprovisioner.vps.dto.VpsHealthResponse;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import java.util.HashSet; import java.util.HashSet;
@@ -14,9 +17,11 @@ public class WireGuardVpsService {
Pattern.compile("\\b198\\.19\\.\\d{1,3}\\.\\d{1,3}\\b"); Pattern.compile("\\b198\\.19\\.\\d{1,3}\\.\\d{1,3}\\b");
private final SshService sshService; private final SshService sshService;
private final ObjectMapper objectMapper;
public WireGuardVpsService(SshService sshService) { public WireGuardVpsService(SshService sshService, ObjectMapper objectMapper) {
this.sshService = sshService; this.sshService = sshService;
this.objectMapper = objectMapper;
} }
public Set<String> findUsedVpnIps() { public Set<String> findUsedVpnIps() {
@@ -66,7 +71,7 @@ public class WireGuardVpsService {
); );
} }
public String getVpsHealthJson() { public VpsHealthResponse getVpsHealth() {
SshCommandResult result = sshService.executeOnConfiguredVps( SshCommandResult result = sshService.executeOnConfiguredVps(
"sudo /usr/local/sbin/lr-vps-health" "sudo /usr/local/sbin/lr-vps-health"
); );
@@ -77,7 +82,14 @@ public class WireGuardVpsService {
); );
} }
return result.stdout(); try {
return objectMapper.readValue(result.stdout(), VpsHealthResponse.class);
} catch (JsonProcessingException e) {
throw new IllegalStateException(
"Invalid VPS health JSON returned by script",
e
);
}
} }
public String restoreLastWireGuardBackup() { public String restoreLastWireGuardBackup() {
@@ -0,0 +1,17 @@
package com.litoralregas.vpnprovisioner.vps.dto;
public record VpsHealthResponse(
String wireGuardInterface,
boolean wireGuardRunning,
int wireGuardPeerCount,
boolean wireGuardConfigExists,
String udp2rawService,
boolean udp2rawActive,
String latestWireGuardBackup,
String systemUptime,
int diskUsagePercent,
int memoryUsagePercent,
String loadAverage,
String publicIp
) {
}
@@ -0,0 +1,5 @@
ALTER TABLE routers
ADD COLUMN last_provisioned_at TIMESTAMP,
ADD COLUMN last_validated_at TIMESTAMP,
ADD COLUMN validation_summary TEXT,
ADD COLUMN last_provisioning_error TEXT;