From e3d68a85fd228eb5ddb67ba96952cfebd24acaf6 Mon Sep 17 00:00:00 2001 From: Richard Kosegi Date: Thu, 17 Aug 2023 07:18:09 +0200 Subject: [PATCH] Add support to attach volumes to server Fixes #71 Signed-off-by: Richard Kosegi --- README.md | 9 +++++++ .../hetzner/ConfigurationValidator.java | 26 ++++++++++++++++++- .../jenkins/plugins/hetzner/Helper.java | 5 ++++ .../hetzner/HetznerCloudResourceManager.java | 6 +++++ .../hetzner/HetznerServerTemplate.java | 19 ++++++++++++++ .../HetznerServerTemplate/config.jelly | 9 +++++++ .../help-automountVolumes.html | 18 +++++++++++++ .../HetznerServerTemplate/help-volumeIds.html | 19 ++++++++++++++ 8 files changed, 110 insertions(+), 1 deletion(-) create mode 100644 src/main/resources/cloud/dnation/jenkins/plugins/hetzner/HetznerServerTemplate/help-automountVolumes.html create mode 100644 src/main/resources/cloud/dnation/jenkins/plugins/hetzner/HetznerServerTemplate/help-volumeIds.html diff --git a/README.md b/README.md index a16ed04..afff01a 100644 --- a/README.md +++ b/README.md @@ -142,6 +142,11 @@ These additional attributes can be specified, but are not required: - `Only public networking will be allocated` - public IP address will be allocated to the server - `Configure both private and public networking` +- `Automount volumes` - Auto-mount volumes after attach. + +- `Volume IDs to attach` - Volume IDs which should be attached to the Server at the creation time. Volumes must be in the same Location. + Note that volumes can be mounted into **single server** at the time. + ### Scripted configuration using Groovy ```groovy @@ -197,11 +202,15 @@ jenkins: remoteFs: /var/lib/jenkins location: fsn1 image: name=jenkins + mode: EXCLUSIVE network: subsystem=cd labelStr: java numExecutors: 3 placementGroup: "1000656" connectivity: "public-only" + automountVolumes: true + volumeIds: + - 12345678 connector: root: sshCredentialsId: 'ssh-private-key' diff --git a/src/main/java/cloud/dnation/jenkins/plugins/hetzner/ConfigurationValidator.java b/src/main/java/cloud/dnation/jenkins/plugins/hetzner/ConfigurationValidator.java index fa38ff3..3c8d441 100644 --- a/src/main/java/cloud/dnation/jenkins/plugins/hetzner/ConfigurationValidator.java +++ b/src/main/java/cloud/dnation/jenkins/plugins/hetzner/ConfigurationValidator.java @@ -25,6 +25,7 @@ import cloud.dnation.hetznerclient.GetPlacementGroupByIdResponse; import cloud.dnation.hetznerclient.GetPlacementGroupsResponse; import cloud.dnation.hetznerclient.GetServerTypesResponse; +import cloud.dnation.hetznerclient.GetVolumeByIdResponse; import cloud.dnation.hetznerclient.HetznerApi; import com.google.common.base.Preconditions; import com.google.common.base.Strings; @@ -34,6 +35,8 @@ import lombok.Data; import lombok.extern.slf4j.Slf4j; +import java.util.Arrays; + @Slf4j public class ConfigurationValidator { /** @@ -205,6 +208,27 @@ static ValidationResult verifyLocation(String location, String credentialsId) { }, credentialsId); } + static ValidationResult verifyVolume(String volume, String credentialsId) { + return validateWithClient(api -> { + if (!Helper.isPossiblyLong(volume)) { + return new ValidationResult(false, String.format("not a valid volume ID: %s", volume)); + } + final GetVolumeByIdResponse result = api.getVolumeById(Long.parseLong(volume)).execute().body(); + if (result == null) { + return new ValidationResult(false, String.format("Volume %s not found", volume)); + }else { + return new ValidationResult(true, String.format("%s: %s", + result.getVolume().getName(), result.getVolume().getFormat())); + } + }, credentialsId); + } + + static ValidationResult verifyVolumes(String volumeIds, String credentialId) { + log.info("volumeIds: {}", volumeIds); + return Arrays.stream(volumeIds.split(",")).map(volId -> verifyVolume(volId, credentialId)) + .filter(res -> !res.isSuccess()).findFirst().orElse(ValidationResult.OK); + } + public static FormValidation doCheckPositiveInt(String value, String name) { if (Ints.tryParse(value) == null) { return FormValidation.error(name + " must be positive integer : " + value); @@ -226,7 +250,7 @@ private interface ValidationAction { @Data static class ValidationResult { - static final ValidationResult OK = new ValidationResult(true, null); + static final ValidationResult OK = new ValidationResult(true, "OK"); private final boolean success; private final String message; diff --git a/src/main/java/cloud/dnation/jenkins/plugins/hetzner/Helper.java b/src/main/java/cloud/dnation/jenkins/plugins/hetzner/Helper.java index 17462c4..ea73a30 100644 --- a/src/main/java/cloud/dnation/jenkins/plugins/hetzner/Helper.java +++ b/src/main/java/cloud/dnation/jenkins/plugins/hetzner/Helper.java @@ -42,6 +42,7 @@ import java.time.LocalDateTime; import java.time.ZoneOffset; import java.time.format.DateTimeFormatter; +import java.util.Arrays; import java.util.Base64; import java.util.Collections; import java.util.List; @@ -105,6 +106,10 @@ public static boolean isPossiblyLong(String str) { } } + public static List idList(String str) { + return Arrays.stream(str.split(",")).map(Long::parseLong).collect(Collectors.toList()); + } + public static List getPayload(@Nonnull Response response, @Nonnull Function> mapper) { final T body = response.body(); if (body == null) { diff --git a/src/main/java/cloud/dnation/jenkins/plugins/hetzner/HetznerCloudResourceManager.java b/src/main/java/cloud/dnation/jenkins/plugins/hetzner/HetznerCloudResourceManager.java index 529fed9..16be08f 100644 --- a/src/main/java/cloud/dnation/jenkins/plugins/hetzner/HetznerCloudResourceManager.java +++ b/src/main/java/cloud/dnation/jenkins/plugins/hetzner/HetznerCloudResourceManager.java @@ -251,6 +251,12 @@ public HetznerServerInfo createServer(HetznerServerAgent agent) { } final CreateServerRequest createServerRequest = new CreateServerRequest(); + if (agent.getTemplate().isAutomountVolumes()) { + createServerRequest.setAutomount(true); + } + if (!Strings.isNullOrEmpty(agent.getTemplate().getVolumeIds())) { + createServerRequest.setVolumes(Helper.idList(agent.getTemplate().getVolumeIds())); + } final ConnectivityType ct = agent.getTemplate().getConnectivity().getType(); if (ct == ConnectivityType.BOTH || ct == ConnectivityType.PRIVATE) { if (!Strings.isNullOrEmpty(agent.getTemplate().getNetwork())) { diff --git a/src/main/java/cloud/dnation/jenkins/plugins/hetzner/HetznerServerTemplate.java b/src/main/java/cloud/dnation/jenkins/plugins/hetzner/HetznerServerTemplate.java index a1da9de..76ef4ae 100644 --- a/src/main/java/cloud/dnation/jenkins/plugins/hetzner/HetznerServerTemplate.java +++ b/src/main/java/cloud/dnation/jenkins/plugins/hetzner/HetznerServerTemplate.java @@ -53,6 +53,7 @@ import static cloud.dnation.jenkins.plugins.hetzner.ConfigurationValidator.verifyNetwork; import static cloud.dnation.jenkins.plugins.hetzner.ConfigurationValidator.verifyPlacementGroup; import static cloud.dnation.jenkins.plugins.hetzner.ConfigurationValidator.verifyServerType; +import static cloud.dnation.jenkins.plugins.hetzner.ConfigurationValidator.verifyVolumes; import static cloud.dnation.jenkins.plugins.hetzner.Helper.getStringOrDefault; import static cloud.dnation.jenkins.plugins.hetzner.HetznerConstants.DEFAULT_REMOTE_FS; @@ -130,6 +131,14 @@ public class HetznerServerTemplate extends AbstractDescribableImpl + + + + + + + + diff --git a/src/main/resources/cloud/dnation/jenkins/plugins/hetzner/HetznerServerTemplate/help-automountVolumes.html b/src/main/resources/cloud/dnation/jenkins/plugins/hetzner/HetznerServerTemplate/help-automountVolumes.html new file mode 100644 index 0000000..af0e246 --- /dev/null +++ b/src/main/resources/cloud/dnation/jenkins/plugins/hetzner/HetznerServerTemplate/help-automountVolumes.html @@ -0,0 +1,18 @@ + +
+ Auto-mount Volumes after attach. +
diff --git a/src/main/resources/cloud/dnation/jenkins/plugins/hetzner/HetznerServerTemplate/help-volumeIds.html b/src/main/resources/cloud/dnation/jenkins/plugins/hetzner/HetznerServerTemplate/help-volumeIds.html new file mode 100644 index 0000000..821cbf5 --- /dev/null +++ b/src/main/resources/cloud/dnation/jenkins/plugins/hetzner/HetznerServerTemplate/help-volumeIds.html @@ -0,0 +1,19 @@ + +
+ Volume IDs which should be attached to the Server at the creation time. Volumes must be in the same Location. + Note that volumes can be mounted into single server at the time. +