diff --git a/EC2_MONITORING.md b/EC2_MONITORING.md
new file mode 100644
index 000000000..a0c5e612e
--- /dev/null
+++ b/EC2_MONITORING.md
@@ -0,0 +1,89 @@
+# EC2 Provisioning Monitoring
+
+This feature provides real-time monitoring for AWS EC2 node provisioning issues by integrating with Snowhouse database using JDBC.
+
+## Overview
+
+The monitoring system captures detailed information about EC2 provisioning attempts and sends them to a Snowhouse database for real-time analysis and alerting. This helps identify provisioning issues faster than relying on application logs and CloudTrail.
+
+## Data Captured
+
+For each provisioning attempt, the following information is recorded:
+
+- **region**: AWS region where provisioning was attempted
+- **availability_zone**: Specific AZ within the region
+- **request_id**: Unique identifier for the provisioning request
+- **requested_instance_type**: EC2 instance type requested (e.g., m5.large)
+- **requested_max_count**: Maximum number of instances requested
+- **requested_min_count**: Minimum number of instances requested
+- **provisioned_instances_count**: Actual number of instances provisioned
+- **controller_name**: Name of the Jenkins controller
+- **timestamp**: When the event occurred
+- **phase**: Event phase (REQUEST, SUCCESS, FAILURE, REQUEST_FALLBACK, SUCCESS_FALLBACK)
+- **error_message**: Error details if provisioning failed
+- **jenkins_url**: URL of the Jenkins instance
+
+## Database Schema
+
+The monitoring system creates the following table in Snowhouse:
+
+```sql
+CREATE TABLE IF NOT EXISTS EC2_PROVISIONING_EVENTS (
+ ID NUMBER AUTOINCREMENT,
+ CREATE_TIME TIMESTAMP_NTZ,
+ REGION VARCHAR(50),
+ AVAILABILITY_ZONE VARCHAR(50),
+ REQUEST_ID VARCHAR(100),
+ REQUESTED_INSTANCE_TYPE VARCHAR(50),
+ REQUESTED_MAX_COUNT NUMBER,
+ REQUESTED_MIN_COUNT NUMBER,
+ PROVISIONED_INSTANCES_COUNT NUMBER,
+ CONTROLLER_NAME VARCHAR(200),
+ PHASE VARCHAR(50),
+ ERROR_MESSAGE VARCHAR(2000),
+ JENKINS_URL VARCHAR(500),
+ EVENT_DATA VARIANT,
+ PRIMARY KEY (ID)
+);
+```
+
+## Configuration
+
+1. **Install Database Plugin**: The monitoring requires the Jenkins Database plugin to be installed.
+
+2. **Configure Snowhouse Database**: In Jenkins system configuration, add a new Snowflake database connection with:
+ - Account Name: Your Snowflake account
+ - Database: Target database name
+ - Warehouse: Snowflake warehouse to use
+ - Credentials: Username/password credentials for Snowflake
+ - Timeouts: Network, query, and login timeouts
+
+3. **Set as Global Database**: Configure the Snowflake connection as the global database in Jenkins.
+
+## Event Flow
+
+1. **Provisioning Request**: When Jenkins attempts to provision EC2 instances, a "REQUEST" event is recorded
+2. **Success/Failure**: Based on the AWS API response, either "SUCCESS" or "FAILURE" events are recorded
+3. **Fallback Scenarios**: For spot instances that fall back to on-demand, additional "REQUEST_FALLBACK" and "SUCCESS_FALLBACK" events are recorded
+4. **Batch Processing**: Events are queued and sent to Snowhouse in batches every 30 seconds
+
+## Monitoring Points
+
+The system monitors both:
+
+- **On-demand instances**: Direct EC2 instance provisioning
+- **Spot instances**: Spot instance requests and their fallback scenarios
+
+## Benefits
+
+- **Real-time alerting**: Immediate visibility into provisioning issues
+- **Trend analysis**: Historical data for capacity planning
+- **Failure investigation**: Detailed error information for troubleshooting
+- **Performance monitoring**: Track provisioning success rates and response times
+
+## Implementation Details
+
+- **Non-blocking**: Event recording doesn't impact provisioning performance
+- **Fault-tolerant**: Monitoring failures don't affect EC2 provisioning
+- **Scalable**: Batch processing handles high-volume environments
+- **Configurable**: Database connection is configurable through Jenkins UI
\ No newline at end of file
diff --git a/README.md b/README.md
index c8149ec0e..003af1a92 100644
--- a/README.md
+++ b/README.md
@@ -364,7 +364,7 @@ def slaveTemplateUsEast1Parameters = [
]
def EC2CloudParameters = [
- name: 'MyCompany',
+ cloudName: 'MyCompany',
credentialsId: 'jenkins-aws-key',
instanceCapStr: '2',
privateKey: '''-----BEGIN RSA PRIVATE KEY-----
@@ -465,7 +465,7 @@ SlaveTemplate slaveTemplateUsEast1 = new SlaveTemplate(
// https://javadoc.jenkins.io/plugin/ec2/hudson/plugins/ec2/EC2Cloud.html
EC2Cloud ec2Cloud = new EC2Cloud(
- EC2CloudParameters.name,
+ EC2CloudParameters.cloudName,
EC2CloudParameters.useInstanceProfileForCredentials,
EC2CloudParameters.credentialsId,
EC2CloudParameters.region,
diff --git a/jenkins_build.sh b/jenkins_build.sh
new file mode 100755
index 000000000..a706da48b
--- /dev/null
+++ b/jenkins_build.sh
@@ -0,0 +1,32 @@
+#!/usr/bin/env bash
+
+# auth setup
+eval $(sf artifact maven auth)
+
+ENV="sandbox"
+
+if [ -n "${JENKINS_ENVIRONMENT}" ]; then
+ ENV="${JENKINS_ENVIRONMENT}"
+fi
+
+# Generate version depending on the env
+generate_version() {
+ local git_sha=$(git rev-parse --short=7 HEAD)
+
+ local timestamp=$(date +"%Y%m%d-%H%M%S")
+
+ local version="${timestamp}.${git_sha}"
+
+ # build num not set during sandbox dev generally
+ if [ -n "${BUILD_NUMBER}" ]; then
+ version="${version}.${BUILD_NUMBER}"
+ fi
+
+ echo "${version}"
+}
+
+DYNAMIC_VERSION=$(generate_version)
+echo "Generated version: ${DYNAMIC_VERSION}"
+
+# build & deploy
+mvn clean deploy -P "${ENV}" -Dchangelist="${DYNAMIC_VERSION}"
diff --git a/pom.xml b/pom.xml
index 8aada6c44..73994ea88 100644
--- a/pom.xml
+++ b/pom.xml
@@ -72,13 +72,26 @@ THE SOFTWARE.
https://github.com/${gitHubRepo}
+
+
+ snowflake-jenkins-plugins
+ Snowflake Jenkins Plugins Repository
+ ${deploy.repo.url}
+
+
+ snowflake-jenkins-plugins
+ Snowflake Jenkins Plugins Repository
+ ${deploy.repo.url}
+
+
+
- 999999-SNAPSHOT
+
+ 20250819-nthirumoorthy-aws-monitoring-info
2.479
${jenkins.baseline}.3
jenkinsci/${project.artifactId}-plugin
- 1885
false
@@ -158,6 +171,20 @@ THE SOFTWARE.
org.jenkins-ci.plugins.workflow
workflow-step-api
+
+ net.snowflake
+ snowflake-jdbc
+ 3.10.1
+
+
+ org.jenkins-ci.plugins
+ database
+ 274.vea_2e859b_2661
+
+
+ io.jenkins.plugins
+ json-api
+
io.jenkins
configuration-as-code
@@ -208,16 +235,56 @@ THE SOFTWARE.
+
+
+
+ true
+
+
+ false
+
+ central
+ Maven Central
+ https://artifactory.ci1.us-west-2.aws-dev.app.snowflake.com/artifactory/development-maven-virtual
+
+
+
+ true
+
+
+ true
+
repo.jenkins-ci.org
- https://repo.jenkins-ci.org/public/
+ Jenkins CI
+ https://artifactory.ci1.us-west-2.aws-dev.app.snowflake.com/artifactory/development-jenkins-ci-virtual
+
+
+ true
+
+
+ false
+
+ central
+ Maven Central Plugins
+ https://artifactory.ci1.us-west-2.aws-dev.app.snowflake.com/artifactory/development-maven-virtual
+
+
+
+
+ true
+
+
+ true
+
repo.jenkins-ci.org
- https://repo.jenkins-ci.org/public/
+ Jenkins CI Plugins
+ https://artifactory.ci1.us-west-2.aws-dev.app.snowflake.com/artifactory/development-jenkins-ci-virtual
@@ -233,4 +300,25 @@ THE SOFTWARE.
+
+
+
+ prod
+
+ https://artifactory.ci1.us-west-2.aws-dev.app.snowflake.com/artifactory/internal-production-maven-jenkins_plugins-local/
+
+
+
+ dev
+
+ https://artifactory.ci1.us-west-2.aws-dev.app.snowflake.com/artifactory/internal-development-maven-jenkins_plugins-local/
+
+
+
+ sandbox
+
+ https://artifactory.ci1.us-west-2.aws-dev.app.snowflake.com/artifactory/internal-sandbox-maven-jenkins_plugins-local/
+
+
+
diff --git a/pom.xml.bak b/pom.xml.bak
new file mode 100644
index 000000000..ad1c3161f
--- /dev/null
+++ b/pom.xml.bak
@@ -0,0 +1,324 @@
+
+
+
+ 4.0.0
+
+ org.jenkins-ci.plugins
+ plugin
+ 5.18
+
+
+
+ ec2
+ ${changelist}
+ hpi
+ Amazon EC2 plugin
+ This is a Jenkins plugin to support ephemeral Jenkins agents on Amazon EC2 or other EC2-compatible clouds
+ https://github.com/jenkinsci/${project.artifactId}-plugin
+
+
+
+ The MIT License
+ https://opensource.org/licenses/MIT
+ repo
+
+
+
+
+
+
+ thoulen
+ F Manfred Furuholen
+ fabrizio.manfredi@gmail.com
+
+
+ julienduchesne
+ Julien Duchesne
+ julienduchesne@live.com
+
+
+ raihaan
+ Raihaan Shouhell
+ raihaanhimself@gmail.com
+
+
+
+
+ scm:git:https://github.com/${gitHubRepo}.git
+ scm:git:git@github.com:${gitHubRepo}.git
+ ${scmTag}
+ https://github.com/${gitHubRepo}
+
+
+
+
+ snowflake-jenkins-plugins
+ Snowflake Jenkins Plugins Repository
+ ${deploy.repo.url}
+
+
+ snowflake-jenkins-plugins
+ Snowflake Jenkins Plugins Repository
+ ${deploy.repo.url}
+
+
+
+
+
+ 999999-SNAPSHOT
+
+ 2.479
+ ${jenkins.baseline}.3
+ jenkinsci/${project.artifactId}-plugin
+ false
+
+
+
+
+
+ io.jenkins.tools.bom
+ bom-${jenkins.baseline}.x
+ 5054.v620b_5d2b_d5e6
+ pom
+ import
+
+
+
+
+
+
+ com.hierynomus
+ smbj
+ 0.14.0
+
+
+ org.bouncycastle
+ bcprov-jdk18on
+
+
+ org.slf4j
+ slf4j-api
+
+
+
+
+ io.jenkins.plugins.aws-java-sdk2
+ aws-java-sdk2-core
+
+
+ io.jenkins.plugins.aws-java-sdk2
+ aws-java-sdk2-ec2
+
+
+ io.jenkins.plugins.mina-sshd-api
+ mina-sshd-api-core
+
+
+ io.jenkins.plugins.mina-sshd-api
+ mina-sshd-api-scp
+
+
+ org.jenkins-ci.plugins
+ apache-httpcomponents-client-4-api
+
+
+ org.jenkins-ci.plugins
+ aws-credentials
+
+
+ org.jenkins-ci.plugins
+ bouncycastle-api
+
+
+ org.jenkins-ci.plugins
+ command-launcher
+
+
+ org.jenkins-ci.plugins
+ credentials
+
+
+ org.jenkins-ci.plugins
+ node-iterator-api
+
+
+ org.jenkins-ci.plugins
+ ssh-credentials
+
+
+ org.jenkins-ci.plugins.workflow
+ workflow-step-api
+
+
+ net.snowflake
+ snowflake-jdbc
+ 3.10.1
+
+
+ org.jenkins-ci.plugins
+ database
+ 274.vea_2e859b_2661
+
+
+ io.jenkins.plugins
+ json-api
+
+
+ io.jenkins
+ configuration-as-code
+ test
+
+
+ io.jenkins.configuration-as-code
+ test-harness
+ test
+
+
+ org.jenkins-ci.plugins.workflow
+ workflow-api
+ test
+
+
+ org.jenkins-ci.plugins.workflow
+ workflow-cps
+ test
+
+
+ org.jenkins-ci.plugins.workflow
+ workflow-durable-task-step
+ test
+
+
+ org.jenkins-ci.plugins.workflow
+ workflow-job
+ test
+
+
+ org.junit-pioneer
+ junit-pioneer
+ 2.3.0
+ test
+
+
+ org.mockito
+ mockito-junit-jupiter
+ test
+
+
+ org.testcontainers
+ junit-jupiter
+ 1.21.3
+ test
+
+
+
+
+
+
+
+ true
+
+
+ false
+
+ central
+ Maven Central
+ https://artifactory.ci1.us-west-2.aws-dev.app.snowflake.com/artifactory/development-maven-virtual
+
+
+
+
+ true
+
+
+ true
+
+ repo.jenkins-ci.org
+ Jenkins CI
+ https://artifactory.ci1.us-west-2.aws-dev.app.snowflake.com/artifactory/development-jenkins-ci-virtual
+
+
+
+
+
+
+
+ true
+
+
+ false
+
+ central
+ Maven Central Plugins
+ https://artifactory.ci1.us-west-2.aws-dev.app.snowflake.com/artifactory/development-maven-virtual
+
+
+
+
+ true
+
+
+ true
+
+ repo.jenkins-ci.org
+ Jenkins CI Plugins
+ https://artifactory.ci1.us-west-2.aws-dev.app.snowflake.com/artifactory/development-jenkins-ci-virtual
+
+
+
+
+
+
+ org.jenkins-ci.tools
+ maven-hpi-plugin
+ true
+
+ 1.45
+
+
+
+
+
+
+
+ prod
+
+ https://artifactory.ci1.us-west-2.aws-dev.app.snowflake.com/artifactory/internal-production-maven-jenkins_plugins-local/
+
+
+
+ dev
+
+ https://artifactory.ci1.us-west-2.aws-dev.app.snowflake.com/artifactory/internal-development-maven-jenkins_plugins-local/
+
+
+
+ sandbox
+
+ https://artifactory.ci1.us-west-2.aws-dev.app.snowflake.com/artifactory/internal-sandbox-maven-jenkins_plugins-local/
+
+
+
+
diff --git a/set-version.sh b/set-version.sh
new file mode 100755
index 000000000..e48c87105
--- /dev/null
+++ b/set-version.sh
@@ -0,0 +1,23 @@
+#!/bin/bash
+
+# Get current branch name
+BRANCH=$(git branch --show-current)
+
+# Get current date in YYYYMMDD format
+DATE=$(date +%Y%m%d)
+
+# Set version based on branch
+if [[ "$BRANCH" == "main" || "$BRANCH" == "master" ]]; then
+ VERSION="$DATE"
+else
+ # Replace any special characters in branch name with hyphens
+ CLEAN_BRANCH=$(echo "$BRANCH" | sed 's/[^a-zA-Z0-9]/-/g')
+ VERSION="$DATE-$CLEAN_BRANCH"
+fi
+
+echo "Setting version to: $VERSION"
+
+# Update the changelist property in pom.xml
+sed -i.bak "s/.*<\/changelist>/$VERSION<\/changelist>/" pom.xml
+
+echo "Version updated in pom.xml"
\ No newline at end of file
diff --git a/settings.xml b/settings.xml
new file mode 100644
index 000000000..3e0766ea1
--- /dev/null
+++ b/settings.xml
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+ artifactory-central
+ Artifactory Central Mirror
+ https://artifactory.ci1.us-west-2.aws-dev.app.snowflake.com/artifactory/development-maven-virtual
+ central
+
+
+ artifactory-jenkins
+ Artifactory Jenkins Mirror
+ https://artifactory.ci1.us-west-2.aws-dev.app.snowflake.com/artifactory/development-jenkins-ci-virtual
+ repo.jenkins-ci.org
+
+
+
+ artifactory-everything
+ Block External Repositories
+ https://artifactory.ci1.us-west-2.aws-dev.app.snowflake.com/artifactory/development-maven-virtual
+ external:*,!repo.jenkins-ci.org,!central
+
+
+
+
diff --git a/src/main/java/hudson/plugins/ec2/AmazonEC2Cloud.java b/src/main/java/hudson/plugins/ec2/AmazonEC2Cloud.java
index 31083accd..8872b2d0d 100644
--- a/src/main/java/hudson/plugins/ec2/AmazonEC2Cloud.java
+++ b/src/main/java/hudson/plugins/ec2/AmazonEC2Cloud.java
@@ -31,7 +31,7 @@
@Deprecated
public class AmazonEC2Cloud extends EC2Cloud {
public AmazonEC2Cloud(
- String name,
+ String cloudName,
boolean useInstanceProfileForCredentials,
String credentialsId,
String region,
@@ -42,7 +42,7 @@ public AmazonEC2Cloud(
String roleArn,
String roleSessionName) {
super(
- name,
+ cloudName, // now matches the constructor parameter name
useInstanceProfileForCredentials,
credentialsId,
region,
@@ -55,7 +55,7 @@ public AmazonEC2Cloud(
}
public AmazonEC2Cloud(
- String name,
+ String cloudName,
boolean useInstanceProfileForCredentials,
String credentialsId,
String region,
@@ -65,7 +65,7 @@ public AmazonEC2Cloud(
String roleArn,
String roleSessionName) {
super(
- name,
+ cloudName, // now matches the constructor parameter name
useInstanceProfileForCredentials,
credentialsId,
region,
diff --git a/src/main/java/hudson/plugins/ec2/EC2Cloud.java b/src/main/java/hudson/plugins/ec2/EC2Cloud.java
index f7d179c6f..fb83f71ad 100644
--- a/src/main/java/hudson/plugins/ec2/EC2Cloud.java
+++ b/src/main/java/hudson/plugins/ec2/EC2Cloud.java
@@ -217,7 +217,7 @@ public class EC2Cloud extends Cloud {
@DataBoundConstructor
public EC2Cloud(
- String name,
+ String cloudName,
boolean useInstanceProfileForCredentials,
String credentialsId,
String region,
@@ -227,7 +227,7 @@ public EC2Cloud(
List extends SlaveTemplate> templates,
String roleArn,
String roleSessionName) {
- super(name);
+ super(cloudName != null && !cloudName.trim().isEmpty() ? cloudName : "ec2-cloud-" + System.currentTimeMillis());
this.useInstanceProfileForCredentials = useInstanceProfileForCredentials;
this.roleArn = roleArn;
this.roleSessionName = roleSessionName;
@@ -250,6 +250,8 @@ public EC2Cloud(
readResolve(); // set parents
}
+
+
@Deprecated
public EC2Cloud(
String name,
@@ -262,7 +264,7 @@ public EC2Cloud(
String roleArn,
String roleSessionName) {
this(
- name,
+ name, // pass name as cloudName for backward compatibility
useInstanceProfileForCredentials,
credentialsId,
region,
@@ -285,11 +287,11 @@ protected EC2Cloud(
String roleArn,
String roleSessionName) {
this(
- id,
+ id, // pass id as cloudName for backward compatibility
useInstanceProfileForCredentials,
credentialsId,
- privateKey,
null,
+ privateKey,
null,
instanceCapStr,
templates,
@@ -315,13 +317,22 @@ public EC2PrivateKey resolvePrivateKey() {
}
/**
- * @deprecated Use public field "name" instead.
+ * Getter for cloudName to support Configuration as Code (CasC).
+ * This is the preferred field name for EC2 cloud configurations.
*/
- @Deprecated
public String getCloudName() {
return name;
}
+ /**
+ * Setter for cloudName to support Configuration as Code (CasC).
+ * This is the preferred field name for EC2 cloud configurations.
+ */
+ @DataBoundSetter
+ public void setCloudName(String cloudName) {
+ this.name = cloudName;
+ }
+
public String getRegion() {
if (region == null) {
region = DEFAULT_EC2_HOST; // Backward compatibility
diff --git a/src/main/java/hudson/plugins/ec2/SlaveTemplate.java b/src/main/java/hudson/plugins/ec2/SlaveTemplate.java
index dbd962f65..4925616b6 100644
--- a/src/main/java/hudson/plugins/ec2/SlaveTemplate.java
+++ b/src/main/java/hudson/plugins/ec2/SlaveTemplate.java
@@ -42,6 +42,8 @@
import hudson.plugins.ec2.util.KeyPair;
import hudson.plugins.ec2.util.MinimumInstanceChecker;
import hudson.plugins.ec2.util.MinimumNumberOfInstancesTimeRangeConfig;
+import hudson.plugins.ec2.monitoring.EC2ProvisioningMonitor;
+import hudson.plugins.ec2.monitoring.ProvisioningEvent;
import hudson.security.Permission;
import hudson.slaves.NodeProperty;
import hudson.slaves.NodePropertyDescriptor;
@@ -128,6 +130,7 @@
import software.amazon.awssdk.services.ec2.model.ResourceType;
import software.amazon.awssdk.services.ec2.model.RunInstancesMonitoringEnabled;
import software.amazon.awssdk.services.ec2.model.RunInstancesRequest;
+import software.amazon.awssdk.services.ec2.model.RunInstancesResponse;
import software.amazon.awssdk.services.ec2.model.SecurityGroup;
import software.amazon.awssdk.services.ec2.model.ShutdownBehavior;
import software.amazon.awssdk.services.ec2.model.SpotInstanceRequest;
@@ -1940,7 +1943,7 @@ HashMap> makeRunInstancesRequestAndFilters(
.imageId(image.imageId())
.minCount(1)
.maxCount(number)
- .instanceType(type)
+ .instanceType(InstanceType.fromValue(type))
.ebsOptimized(ebsOptimized)
.monitoring(RunInstancesMonitoringEnabled.builder()
.enabled(monitoring)
@@ -2188,27 +2191,55 @@ private List provisionOndemand(
instanceMarketOptionsRequestBuilder.spotOptions(spotOptions);
}
riRequestBuilder.instanceMarketOptions(instanceMarketOptionsRequestBuilder.build());
+ RunInstancesRequest request = riRequestBuilder.build();
try {
- newInstances = new ArrayList<>(
- ec2.runInstances(riRequestBuilder.build()).instances());
+ // Record provisioning attempt
+ recordProvisioningEvent(request, "REQUEST", null, 0);
+
+ RunInstancesResponse response = ec2.runInstances(request);
+ newInstances = new ArrayList<>(response.instances());
+
+ // Record successful provisioning
+ recordProvisioningEvent(request, "SUCCESS", null, newInstances.size());
} catch (Ec2Exception e) {
+ // Record failed provisioning
+ recordProvisioningEvent(request, "FAILURE", e.getMessage(), 0);
+
if (fallbackSpotToOndemand
&& "InsufficientInstanceCapacity"
.equals(e.awsErrorDetails().errorCode())) {
logProvisionInfo(
"There is no spot capacity available matching your request, falling back to on-demand instance.");
riRequestBuilder.instanceMarketOptions(instanceMarketOptionsRequestBuilder.build());
- newInstances = new ArrayList<>(
- ec2.runInstances(riRequestBuilder.build()).instances());
+
+ RunInstancesRequest fallbackRequest = riRequestBuilder.build();
+ // Record fallback attempt
+ recordProvisioningEvent(fallbackRequest, "REQUEST_FALLBACK", null, 0);
+
+ RunInstancesResponse fallbackResponse = ec2.runInstances(fallbackRequest);
+ newInstances = new ArrayList<>(fallbackResponse.instances());
+
+ // Record successful fallback provisioning
+ recordProvisioningEvent(fallbackRequest, "SUCCESS_FALLBACK", null, newInstances.size());
} else {
throw e;
}
}
} else {
+ RunInstancesRequest request = riRequestBuilder.build();
try {
- newInstances = new ArrayList<>(
- ec2.runInstances(riRequestBuilder.build()).instances());
+ // Record provisioning attempt
+ recordProvisioningEvent(request, "REQUEST", null, 0);
+
+ RunInstancesResponse response = ec2.runInstances(request);
+ newInstances = new ArrayList<>(response.instances());
+
+ // Record successful provisioning
+ recordProvisioningEvent(request, "SUCCESS", null, newInstances.size());
} catch (Ec2Exception e) {
+ // Record failed provisioning
+ recordProvisioningEvent(request, "FAILURE", e.getMessage(), 0);
+
logProvisionInfo("Jenkins attempted to reserve "
+ riRequest.maxCount()
+ " instances and received this EC2 exception: " + e.getMessage());
@@ -2463,7 +2494,7 @@ private List provisionSpot(Image image, int number, EnumSet provisionSpot(Image image, int number, EnumSet provisionSpot(Image image, int number, EnumSet envVars = hudson.EnvVars.masterEnvVars;
+ if (envVars != null) {
+ controllerName = envVars.get("JENKINS_BASE_HOSTNAME_SHORT");
+ if (controllerName != null && !controllerName.trim().isEmpty()) {
+ return controllerName.trim();
+ }
+ }
+ } catch (Exception e) {
+ // Ignore if EnvVars not available
+ }
+
+ // Try other environment variables
+ controllerName = System.getenv("HOSTNAME");
+ if (controllerName != null && !controllerName.trim().isEmpty()) {
+ return controllerName.trim();
+ }
+
+ controllerName = System.getenv("COMPUTERNAME");
+ if (controllerName != null && !controllerName.trim().isEmpty()) {
+ return controllerName.trim();
+ }
+
+ // Try system properties
+ try {
+ controllerName = System.getProperty("jenkins.hostname");
+ if (controllerName != null && !controllerName.trim().isEmpty()) {
+ return controllerName.trim();
+ }
+ } catch (Exception e) {
+ // Ignore security exceptions
+ }
+
+ // Try to extract from Jenkins URL
+ try {
+ String jenkinsUrl = Jenkins.get().getRootUrl();
+ if (jenkinsUrl != null) {
+ java.net.URL url = new java.net.URL(jenkinsUrl);
+ String host = url.getHost();
+ if (host != null && !host.trim().isEmpty()) {
+ // Remove domain suffix if present
+ int dotIndex = host.indexOf('.');
+ if (dotIndex > 0) {
+ host = host.substring(0, dotIndex);
+ }
+ return host.trim();
+ }
+ }
+ } catch (Exception e) {
+ // Ignore URL parsing exceptions
+ }
+
+ // Try Java system hostname
+ try {
+ controllerName = java.net.InetAddress.getLocalHost().getHostName();
+ if (controllerName != null && !controllerName.trim().isEmpty()) {
+ // Remove domain suffix if present
+ int dotIndex = controllerName.indexOf('.');
+ if (dotIndex > 0) {
+ controllerName = controllerName.substring(0, dotIndex);
+ }
+ return controllerName.trim();
+ }
+ } catch (Exception e) {
+ // Ignore network exceptions
+ }
+
+ // Final fallback - also log what env vars we do have for debugging
+ LOGGER.log(Level.WARNING, "Could not determine controller name, using fallback: jenkins-controller");
+ LOGGER.log(Level.FINE, "Available env vars: HOSTNAME=" + System.getenv("HOSTNAME") +
+ ", COMPUTERNAME=" + System.getenv("COMPUTERNAME"));
+ return "jenkins-controller";
+ }
}
diff --git a/src/main/java/hudson/plugins/ec2/monitoring/EC2ProvisioningMonitor.java b/src/main/java/hudson/plugins/ec2/monitoring/EC2ProvisioningMonitor.java
new file mode 100644
index 000000000..49f4c01fb
--- /dev/null
+++ b/src/main/java/hudson/plugins/ec2/monitoring/EC2ProvisioningMonitor.java
@@ -0,0 +1,161 @@
+package hudson.plugins.ec2.monitoring;
+
+import hudson.Extension;
+import hudson.triggers.SafeTimerTask;
+import jenkins.model.Jenkins;
+import jenkins.util.Timer;
+import org.jenkinsci.plugins.database.Database;
+import org.jenkinsci.plugins.database.GlobalDatabaseConfiguration;
+import org.json.JSONObject;
+import net.snowflake.client.jdbc.SnowflakeConnection;
+
+import java.io.ByteArrayInputStream;
+import java.sql.Connection;
+import java.sql.PreparedStatement;
+import java.time.Instant;
+import java.util.HashMap;
+import java.util.concurrent.ConcurrentLinkedQueue;
+import java.util.concurrent.TimeUnit;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * Monitor for EC2 provisioning events that sends data to Snowhouse database.
+ * Implements event queuing and batch processing similar to the Snowflake Jenkins connector.
+ */
+@Extension
+public class EC2ProvisioningMonitor {
+ private static final Logger LOG = Logger.getLogger(EC2ProvisioningMonitor.class.getName());
+
+ private static final Level LOG_LEVEL = Level.FINE;
+ private static final ConcurrentLinkedQueue eventQueue = new ConcurrentLinkedQueue<>();
+
+ static {
+ // Schedule periodic sending of events to Snowhouse every 30 seconds
+ Timer.get().scheduleAtFixedRate(new SafeTimerTask() {
+ @Override
+ protected void doRun() throws Exception {
+ if (eventQueue.size() > 0) {
+ EC2ProvisioningMonitor.sendQueue();
+ }
+ }
+ }, TimeUnit.MINUTES.toMillis(1), TimeUnit.SECONDS.toMillis(30), TimeUnit.MILLISECONDS);
+ }
+
+ /**
+ * Add a provisioning event to the queue for batch processing.
+ */
+ public static void recordProvisioningEvent(ProvisioningEvent event) {
+ HashMap eventMap = new HashMap<>();
+ eventMap.put("timestamp", event.getTimestamp());
+ eventMap.put("region", event.getRegion());
+ eventMap.put("availability_zone", event.getAvailabilityZone());
+ eventMap.put("request_id", event.getRequestId());
+ eventMap.put("requested_instance_type", event.getRequestedInstanceType());
+ eventMap.put("requested_max_count", event.getRequestedMaxCount());
+ eventMap.put("requested_min_count", event.getRequestedMinCount());
+ eventMap.put("provisioned_instances_count", event.getProvisionedInstancesCount());
+ eventMap.put("controller_name", event.getControllerName());
+ eventMap.put("cloud_name", event.getCloudName());
+ eventMap.put("phase", event.getPhase());
+ eventMap.put("error_message", event.getErrorMessage());
+ eventMap.put("jenkins_url", event.getJenkinsUrl());
+
+ JSONObject jsonMap = new JSONObject(eventMap);
+ enQueue(jsonMap.toString());
+ }
+
+ /**
+ * Add an event to the queue.
+ */
+ private static boolean enQueue(String queueItem) {
+ boolean retVal = eventQueue.add(queueItem);
+ LOG.log(LOG_LEVEL, "EC2 Provisioning event queue size: " + eventQueue.size());
+ return retVal;
+ }
+
+ /**
+ * Send all queued events to Snowhouse database.
+ */
+ static synchronized void sendQueue() throws Exception {
+ if (eventQueue.size() == 0) {
+ return;
+ }
+
+ Database db = GlobalDatabaseConfiguration.get().getDatabase();
+ if (db == null) {
+ LOG.log(Level.WARNING, "EC2ProvisioningMonitor failed - no database configured. " +
+ "Discarding " + eventQueue.size() + " events");
+ // Drop the existing queue to prevent it from growing forever
+ eventQueue.clear();
+ return;
+ }
+
+ Connection con = null;
+ PreparedStatement copyStatement = null;
+ try {
+ long startTime = System.currentTimeMillis();
+ ConcurrentLinkedQueue pushQueue = new ConcurrentLinkedQueue<>(eventQueue);
+ eventQueue.clear();
+
+ LOG.log(Level.INFO, pushQueue.size() + " EC2 provisioning events found in queue");
+ LOG.log(LOG_LEVEL, "Fetching database connection");
+
+ con = db.getDataSource().getConnection();
+ LOG.log(LOG_LEVEL, "Database connection fetched");
+ con.createStatement().execute("USE SCHEMA PUBLIC;");
+
+ String fileName = Jenkins.get().getRootUrl().replaceAll(
+ "https?://", "").replaceAll("/.*", "").replaceAll(":.*", "") +
+ "_ec2_provisioning.json";
+
+ String eventsString = String.join("\n", pushQueue.toArray(new String[0]));
+ LOG.log(Level.FINER, "Events being sent: " + eventsString);
+
+ // Upload events to Snowflake automatic table stage
+ con.unwrap(SnowflakeConnection.class).uploadStream("@%EC2_PROVISIONING_EVENTS",
+ "ec2_provisioning",
+ new ByteArrayInputStream(eventsString.getBytes()),
+ fileName, true);
+
+ // Copy data from automatic table stage to table (using existing schema)
+ String copySql = "COPY INTO EC2_PROVISIONING_EVENTS " +
+ "(CREATE_TIME, REGION, AVAILABILITY_ZONE, REQUEST_ID, REQUESTED_INSTANCE_TYPE, " +
+ "REQUESTED_MAX_COUNT, REQUESTED_MIN_COUNT, PROVISIONED_INSTANCES_COUNT, " +
+ "CONTROLLER_NAME, PHASE, ERROR_MESSAGE, JENKINS_URL, EVENT_DATA) " +
+ "from (select $1:timestamp, $1:region, $1:availability_zone, $1:request_id, " +
+ "$1:requested_instance_type, $1:requested_max_count, $1:requested_min_count, " +
+ "$1:provisioned_instances_count, $1:controller_name, $1:phase, $1:error_message, " +
+ "$1:jenkins_url, $1 from @%EC2_PROVISIONING_EVENTS/ec2_provisioning/" + fileName + ".gz) " +
+ "file_format=(type='json' strip_outer_array=true) " +
+ "on_error='continue' FORCE=TRUE purge=true;";
+
+ LOG.log(LOG_LEVEL, "Executing SQL: " + copySql);
+
+ copyStatement = con.prepareStatement(copySql);
+ copyStatement.execute();
+ long endTime = System.currentTimeMillis();
+ LOG.log(Level.INFO, copyStatement.getUpdateCount() + " EC2 provisioning events inserted in " +
+ (endTime - startTime) + " ms");
+ } catch (Exception e) {
+ LOG.log(Level.SEVERE, "Failed to send EC2 provisioning events to Snowhouse", e);
+ } finally {
+ if (copyStatement != null) {
+ try {
+ copyStatement.close();
+ LOG.log(LOG_LEVEL, "Statement closed");
+ } catch (Exception e) {
+ LOG.log(Level.WARNING, "Failed to close statement", e);
+ }
+ }
+ if (con != null) {
+ try {
+ con.close();
+ LOG.log(LOG_LEVEL, "Connection closed");
+ } catch (Exception e) {
+ LOG.log(Level.WARNING, "Failed to close connection", e);
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/hudson/plugins/ec2/monitoring/ProvisioningEvent.java b/src/main/java/hudson/plugins/ec2/monitoring/ProvisioningEvent.java
new file mode 100644
index 000000000..f4f108598
--- /dev/null
+++ b/src/main/java/hudson/plugins/ec2/monitoring/ProvisioningEvent.java
@@ -0,0 +1,57 @@
+package hudson.plugins.ec2.monitoring;
+
+import java.time.Instant;
+
+/**
+ * Data model for AWS EC2 provisioning events to be sent to Snowhouse database.
+ * Contains all the required information for monitoring provisioning issues.
+ */
+public class ProvisioningEvent {
+ private final String region;
+ private final String availabilityZone;
+ private final String requestId;
+ private final String requestedInstanceType;
+ private final int requestedMaxCount;
+ private final int requestedMinCount;
+ private final int provisionedInstancesCount;
+ private final String controllerName;
+ private final String cloudName;
+ private final Instant timestamp;
+ private final String phase; // "REQUEST", "SUCCESS", "FAILURE"
+ private final String errorMessage; // null if successful
+ private final String jenkinsUrl;
+
+ public ProvisioningEvent(String region, String availabilityZone, String requestId,
+ String requestedInstanceType, int requestedMaxCount, int requestedMinCount,
+ int provisionedInstancesCount, String controllerName, String cloudName,
+ String phase, String errorMessage, String jenkinsUrl) {
+ this.region = region;
+ this.availabilityZone = availabilityZone;
+ this.requestId = requestId;
+ this.requestedInstanceType = requestedInstanceType;
+ this.requestedMaxCount = requestedMaxCount;
+ this.requestedMinCount = requestedMinCount;
+ this.provisionedInstancesCount = provisionedInstancesCount;
+ this.controllerName = controllerName;
+ this.cloudName = cloudName;
+ this.phase = phase;
+ this.errorMessage = errorMessage;
+ this.jenkinsUrl = jenkinsUrl;
+ this.timestamp = Instant.now();
+ }
+
+ // Getters
+ public String getRegion() { return region; }
+ public String getAvailabilityZone() { return availabilityZone; }
+ public String getRequestId() { return requestId; }
+ public String getRequestedInstanceType() { return requestedInstanceType; }
+ public int getRequestedMaxCount() { return requestedMaxCount; }
+ public int getRequestedMinCount() { return requestedMinCount; }
+ public int getProvisionedInstancesCount() { return provisionedInstancesCount; }
+ public String getControllerName() { return controllerName; }
+ public String getCloudName() { return cloudName; }
+ public Instant getTimestamp() { return timestamp; }
+ public String getPhase() { return phase; }
+ public String getErrorMessage() { return errorMessage; }
+ public String getJenkinsUrl() { return jenkinsUrl; }
+}
\ No newline at end of file
diff --git a/src/main/java/hudson/plugins/ec2/monitoring/SnowflakeDatabase.java b/src/main/java/hudson/plugins/ec2/monitoring/SnowflakeDatabase.java
new file mode 100644
index 000000000..6d6494b87
--- /dev/null
+++ b/src/main/java/hudson/plugins/ec2/monitoring/SnowflakeDatabase.java
@@ -0,0 +1,172 @@
+package hudson.plugins.ec2.monitoring;
+
+import hudson.Extension;
+import hudson.util.FormValidation;
+import hudson.util.ListBoxModel;
+import hudson.util.Secret;
+import org.jenkinsci.plugins.database.BasicDataSource2;
+import org.jenkinsci.plugins.database.DatabaseDescriptor;
+import org.jenkinsci.plugins.database.Database;
+import org.kohsuke.stapler.DataBoundConstructor;
+import org.kohsuke.stapler.QueryParameter;
+import org.kohsuke.stapler.interceptor.RequirePOST;
+import com.snowflake.client.jdbc.SnowflakeDriver;
+
+import javax.sql.DataSource;
+import java.lang.reflect.InvocationTargetException;
+import java.sql.*;
+import java.util.*;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import jenkins.model.Jenkins;
+
+import com.cloudbees.plugins.credentials.CredentialsMatchers;
+import com.cloudbees.plugins.credentials.CredentialsProvider;
+import com.cloudbees.plugins.credentials.common.StandardListBoxModel;
+import com.cloudbees.plugins.credentials.common.StandardUsernamePasswordCredentials;
+import com.cloudbees.plugins.credentials.domains.DomainRequirement;
+import hudson.security.ACL;
+
+/**
+ * Snowflake database configuration for EC2 provisioning monitoring.
+ * Based on the snowflake-jenkins-connector implementation.
+ */
+public class SnowflakeDatabase extends Database {
+
+ private static final Logger LOG = Logger.getLogger(SnowflakeDatabase.class.getName());
+ private transient DataSource source;
+
+ public final String accountname;
+ public final String database;
+ public final String networktimeout;
+ public final String querytimeout;
+ public final String logintimeout;
+ public final String warehouse;
+ public final String credentialsId;
+
+ @DataBoundConstructor
+ public SnowflakeDatabase(String accountname,
+ String database,
+ String warehouse,
+ String credentialsId,
+ String networktimeout,
+ String querytimeout,
+ String logintimeout) {
+
+ this.accountname = accountname;
+ this.database = database;
+ this.warehouse = warehouse;
+ this.credentialsId = credentialsId;
+ this.networktimeout = networktimeout;
+ this.querytimeout = querytimeout;
+ this.logintimeout = logintimeout;
+ }
+
+ protected Class extends Driver> getDriverClass() {
+ return SnowflakeDriver.class;
+ }
+
+ @Override
+ public synchronized DataSource getDataSource() throws SQLException {
+ List credentialList = CredentialsProvider.lookupCredentials(
+ StandardUsernamePasswordCredentials.class, Jenkins.getInstanceOrNull(), ACL.SYSTEM,
+ Collections.emptyList());
+
+ StandardUsernamePasswordCredentials credentials = (StandardUsernamePasswordCredentials)CredentialsMatchers.firstOrNull(credentialList,
+ CredentialsMatchers.allOf(CredentialsMatchers.withId(credentialsId)));
+
+ if (source==null) {
+ BasicDataSource2 fac = new BasicDataSource2();
+ fac.setDriverClass(getDriverClass());
+ fac.setUrl(getJdbcUrl());
+ fac.setUsername(credentials.getUsername());
+ fac.setPassword(Secret.toString(credentials.getPassword()));
+ fac.setValidationQuery("SELECT 1");
+
+ source = fac.createDataSource();
+ }
+ return source;
+ }
+
+ protected String getJdbcUrl() {
+ String url = "jdbc:snowflake://"+this.accountname+
+ ".snowflakecomputing.com/?db="+this.database+
+ "&networkTimeout="+this.networktimeout+
+ "&queryTimeout="+this.querytimeout+
+ "&warehouse="+this.warehouse+
+ "&loginTimeout="+this.logintimeout;
+ LOG.log(Level.FINE, "JDBC URL {0}", url);
+ return url;
+ }
+
+ public String fetchJdbcUrl() {
+ return getJdbcUrl();
+ }
+
+ @Extension
+ public static class DescriptorImpl extends DatabaseDescriptor {
+ @Override
+ public String getDisplayName() {
+ return "Snowflake EC2 Monitoring";
+ }
+
+ public FormValidation doValidateSnowflake(
+ @QueryParameter String accountname,
+ @QueryParameter String database,
+ @QueryParameter String warehouse,
+ @QueryParameter String credentialsId,
+ @QueryParameter String networktimeout,
+ @QueryParameter String querytimeout,
+ @QueryParameter String logintimeout)
+ throws NoSuchMethodException, InvocationTargetException, IllegalAccessException, InstantiationException {
+
+ DataSource ds;
+ Connection con = null;
+ Statement s = null;
+ try {
+ Database db = clazz.getConstructor(String.class,
+ String.class,
+ String.class,
+ String.class,
+ String.class,
+ String.class,
+ String.class).newInstance(accountname,
+ database,
+ warehouse,
+ credentialsId,
+ networktimeout,
+ querytimeout,
+ logintimeout);
+ ds = db.getDataSource();
+ con = ds.getConnection();
+ s = con.createStatement();
+ s.execute("SELECT 1");
+ return FormValidation.ok("OK");
+ } catch (SQLException e) {
+ return FormValidation.error(e, "Failed to connect to " + getDisplayName());
+ } finally {
+ try {
+ if (s != null)
+ s.close();
+ if (con != null)
+ con.close();
+ } catch (Exception e) {
+ }
+ }
+ }
+
+ @RequirePOST
+ public ListBoxModel doFillCredentialsIdItems() {
+ Jenkins.get().checkPermission(Jenkins.ADMINISTER);
+ return new StandardListBoxModel()
+ .withEmptySelection()
+ .withMatching(
+ CredentialsMatchers.always(),
+ CredentialsProvider.lookupCredentials(StandardUsernamePasswordCredentials.class,
+ Jenkins.get(),
+ ACL.SYSTEM,
+ Collections.emptyList()));
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/hudson/plugins/ec2/util/InstanceTypeCompat.java b/src/main/java/hudson/plugins/ec2/util/InstanceTypeCompat.java
index 15fbc4da2..de587508c 100644
--- a/src/main/java/hudson/plugins/ec2/util/InstanceTypeCompat.java
+++ b/src/main/java/hudson/plugins/ec2/util/InstanceTypeCompat.java
@@ -834,6 +834,79 @@ public final class InstanceTypeCompat {
map.put("R8gMetal24xl", "r8g.metal-24xl");
map.put("R8gMetal48xl", "r8g.metal-48xl");
map.put("Mac2M1ultraMetal", "mac2-m1ultra.metal");
+ map.put("M8gMedium", "m8g.medium");
+ map.put("M8gLarge", "m8g.large");
+ map.put("M8gXlarge", "m8g.xlarge");
+ map.put("M8g2xlarge", "m8g.2xlarge");
+ map.put("M8g4xlarge", "m8g.4xlarge");
+ map.put("M8g8xlarge", "m8g.8xlarge");
+ map.put("M8g12xlarge", "m8g.12xlarge");
+ map.put("M8g16xlarge", "m8g.16xlarge");
+ map.put("M8g24xlarge", "m8g.24xlarge");
+ map.put("M8g48xlarge", "m8g.48xlarge");
+ map.put("M8gMetal24xl", "m8g.metal-24xl");
+ map.put("M8gMetal48xl", "m8g.metal-48xl");
+ map.put("M8gdMedium", "m8gd.medium");
+ map.put("M8gdLarge", "m8gd.large");
+ map.put("M8gdXlarge", "m8gd.xlarge");
+ map.put("M8gd2xlarge", "m8gd.2xlarge");
+ map.put("M8gd4xlarge", "m8gd.4xlarge");
+ map.put("M8gd8xlarge", "m8gd.8xlarge");
+ map.put("M8gd12xlarge", "m8gd.12xlarge");
+ map.put("M8gd16xlarge", "m8gd.16xlarge");
+ map.put("M8gd24xlarge", "m8gd.24xlarge");
+ map.put("M8gd48xlarge", "m8gd.48xlarge");
+ map.put("M8gdMetal24xl", "m8gd.metal-24xl");
+ map.put("M8gdMetal48xl", "m8gd.metal-48xl");
+ // R8gd instances (Graviton 4 memory-optimized with local storage)
+ map.put("R8gdMedium", "r8gd.medium");
+ map.put("R8gdLarge", "r8gd.large");
+ map.put("R8gdXlarge", "r8gd.xlarge");
+ map.put("R8gd2xlarge", "r8gd.2xlarge");
+ map.put("R8gd4xlarge", "r8gd.4xlarge");
+ map.put("R8gd8xlarge", "r8gd.8xlarge");
+ map.put("R8gd12xlarge", "r8gd.12xlarge");
+ map.put("R8gd16xlarge", "r8gd.16xlarge");
+ map.put("R8gd24xlarge", "r8gd.24xlarge");
+ map.put("R8gd48xlarge", "r8gd.48xlarge");
+ map.put("R8gdMetal24xl", "r8gd.metal-24xl");
+ map.put("R8gdMetal48xl", "r8gd.metal-48xl");
+ map.put("C8gMedium", "c8g.medium");
+ map.put("C8gLarge", "c8g.large");
+ map.put("C8gXlarge", "c8g.xlarge");
+ map.put("C8g2xlarge", "c8g.2xlarge");
+ map.put("C8g4xlarge", "c8g.4xlarge");
+ map.put("C8g8xlarge", "c8g.8xlarge");
+ map.put("C8g12xlarge", "c8g.12xlarge");
+ map.put("C8g16xlarge", "c8g.16xlarge");
+ map.put("C8g24xlarge", "c8g.24xlarge");
+ map.put("C8g48xlarge", "c8g.48xlarge");
+ map.put("C8gMetal24xl", "c8g.metal-24xl");
+ map.put("C8gMetal48xl", "c8g.metal-48xl");
+ map.put("C8gdMedium", "c8gd.medium");
+ map.put("C8gdLarge", "c8gd.large");
+ map.put("C8gdXlarge", "c8gd.xlarge");
+ map.put("C8gd2xlarge", "c8gd.2xlarge");
+ map.put("C8gd4xlarge", "c8gd.4xlarge");
+ map.put("C8gd8xlarge", "c8gd.8xlarge");
+ map.put("C8gd12xlarge", "c8gd.12xlarge");
+ map.put("C8gd16xlarge", "c8gd.16xlarge");
+ map.put("C8gd24xlarge", "c8gd.24xlarge");
+ map.put("C8gd48xlarge", "c8gd.48xlarge");
+ map.put("C8gdMetal24xl", "c8gd.metal-24xl");
+ map.put("C8gdMetal48xl", "c8gd.metal-48xl");
+ map.put("C8gnMedium", "c8gn.medium");
+ map.put("C8gnLarge", "c8gn.large");
+ map.put("C8gnXlarge", "c8gn.xlarge");
+ map.put("C8gn2xlarge", "c8gn.2xlarge");
+ map.put("C8gn4xlarge", "c8gn.4xlarge");
+ map.put("C8gn8xlarge", "c8gn.8xlarge");
+ map.put("C8gn12xlarge", "c8gn.12xlarge");
+ map.put("C8gn16xlarge", "c8gn.16xlarge");
+ map.put("C8gn24xlarge", "c8gn.24xlarge");
+ map.put("C8gn48xlarge", "c8gn.48xlarge");
+ map.put("C8gnMetal24xl", "c8gn.metal-24xl");
+ map.put("C8gnMetal48xl", "c8gn.metal-48xl");
AWS_SDK_JAVA_V1 = Collections.unmodifiableMap(map);
}
diff --git a/src/main/resources/hudson/plugins/ec2/monitoring/SnowflakeDatabase/config.jelly b/src/main/resources/hudson/plugins/ec2/monitoring/SnowflakeDatabase/config.jelly
new file mode 100644
index 000000000..3a74999be
--- /dev/null
+++ b/src/main/resources/hudson/plugins/ec2/monitoring/SnowflakeDatabase/config.jelly
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/test/java/hudson/plugins/ec2/EC2CloudTest.java b/src/test/java/hudson/plugins/ec2/EC2CloudTest.java
index 685e8a2b0..15f7092b3 100644
--- a/src/test/java/hudson/plugins/ec2/EC2CloudTest.java
+++ b/src/test/java/hudson/plugins/ec2/EC2CloudTest.java
@@ -86,7 +86,7 @@ void testConfigRoundtrip() throws Exception {
r.assertEqualBeans(
cloud,
r.jenkins.clouds.get(EC2Cloud.class),
- "name,region,useInstanceProfileForCredentials,privateKey,instanceCap,roleArn,roleSessionName");
+ "cloudName,region,useInstanceProfileForCredentials,privateKey,instanceCap,roleArn,roleSessionName");
}
@Test
@@ -125,7 +125,9 @@ void testSshKeysCredentialsIdRemainsUnchangedAfterUpdatingOtherFields() throws E
EC2Cloud actual = r.jenkins.clouds.get(EC2Cloud.class);
assertEquals("updatedSessionName", actual.getRoleSessionName());
r.assertEqualBeans(
- cloud, actual, "name,region,useInstanceProfileForCredentials,sshKeysCredentialsId,instanceCap,roleArn");
+ cloud,
+ actual,
+ "cloudName,region,useInstanceProfileForCredentials,sshKeysCredentialsId,instanceCap,roleArn");
}
@Test
@@ -206,6 +208,34 @@ void testCustomSshCredentialTypes() throws IOException {
assertThat(actual.resolvePrivateKey(), notNullValue());
}
+ @Test
+ public void testCloudNameForCasC() {
+ EC2Cloud cloud = new EC2Cloud("test-cloud", false, null, "us-east-1", null, null, null, Collections.emptyList(), null, null);
+
+ // Test that getCloudName returns the name
+ assertEquals("test-cloud", cloud.getCloudName());
+ assertEquals("test-cloud", cloud.name);
+
+ // Test that setCloudName updates the name field
+ cloud.setCloudName("my-ec2-cloud");
+ assertEquals("my-ec2-cloud", cloud.name);
+ assertEquals("my-ec2-cloud", cloud.getCloudName());
+
+ // Test that cloudName is the primary field for CasC configurations
+ cloud.setCloudName("production-ec2");
+ assertEquals("production-ec2", cloud.getCloudName());
+ }
+
+ @Test
+ public void testCloudNameConstructorParameter() {
+ // Test that cloudName constructor parameter works directly
+ EC2Cloud cloud = new EC2Cloud("my-casc-cloud", false, null, "us-east-1", null, null, null, Collections.emptyList(), null, null);
+
+ // Constructor parameter sets the name directly
+ assertEquals("my-casc-cloud", cloud.name);
+ assertEquals("my-casc-cloud", cloud.getCloudName());
+ }
+
private HtmlForm getConfigForm() throws IOException, SAXException {
return r.createWebClient().goTo(cloud.getUrl() + "configure").getFormByName("config");
}
diff --git a/src/test/java/hudson/plugins/ec2/monitoring/EC2ProvisioningMonitorTest.java b/src/test/java/hudson/plugins/ec2/monitoring/EC2ProvisioningMonitorTest.java
new file mode 100644
index 000000000..7934f40f3
--- /dev/null
+++ b/src/test/java/hudson/plugins/ec2/monitoring/EC2ProvisioningMonitorTest.java
@@ -0,0 +1,74 @@
+package hudson.plugins.ec2.monitoring;
+
+import org.junit.Test;
+import org.junit.Rule;
+import org.jvnet.hudson.test.JenkinsRule;
+import jenkins.model.Jenkins;
+
+import static org.junit.Assert.*;
+
+/**
+ * Test for EC2ProvisioningMonitor functionality.
+ */
+public class EC2ProvisioningMonitorTest {
+
+ @Rule
+ public JenkinsRule jenkins = new JenkinsRule();
+
+ @Test
+ public void testProvisioningEventCreation() {
+ String region = "us-west-2";
+ String az = "us-west-2a";
+ String requestId = "test-request-123";
+ String instanceType = "m5.large";
+ int maxCount = 5;
+ int minCount = 1;
+ int provisionedCount = 3;
+ String controllerName = "test-controller";
+ String phase = "SUCCESS";
+ String errorMessage = null;
+ String jenkinsUrl = "https://jenkins.example.com/";
+
+ ProvisioningEvent event = new ProvisioningEvent(
+ region, az, requestId, instanceType, maxCount, minCount,
+ provisionedCount, controllerName, "test-cloud", phase, errorMessage, jenkinsUrl
+ );
+
+ assertEquals(region, event.getRegion());
+ assertEquals(az, event.getAvailabilityZone());
+ assertEquals(requestId, event.getRequestId());
+ assertEquals(instanceType, event.getRequestedInstanceType());
+ assertEquals(maxCount, event.getRequestedMaxCount());
+ assertEquals(minCount, event.getRequestedMinCount());
+ assertEquals(provisionedCount, event.getProvisionedInstancesCount());
+ assertEquals(controllerName, event.getControllerName());
+ assertEquals(phase, event.getPhase());
+ assertEquals(errorMessage, event.getErrorMessage());
+ assertEquals(jenkinsUrl, event.getJenkinsUrl());
+ assertNotNull(event.getTimestamp());
+ }
+
+ @Test
+ public void testProvisioningEventRecording() {
+ // Test that recording an event doesn't throw exceptions
+ // This test will work even without a database configured
+ ProvisioningEvent event = new ProvisioningEvent(
+ "us-west-2", "us-west-2a", "test-request-123", "m5.large",
+ 5, 1, 3, "test-controller", "test-cloud", "SUCCESS", null,
+ "https://jenkins.example.com/"
+ );
+
+ // This should not throw an exception even without database configuration
+ assertDoesNotThrow(() -> {
+ EC2ProvisioningMonitor.recordProvisioningEvent(event);
+ });
+ }
+
+ private void assertDoesNotThrow(Runnable runnable) {
+ try {
+ runnable.run();
+ } catch (Exception e) {
+ fail("Expected no exception, but got: " + e.getMessage());
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/test/resources/hudson/plugins/ec2/Ami.yml b/src/test/resources/hudson/plugins/ec2/Ami.yml
index 4f31e9fd5..165d8b1a6 100644
--- a/src/test/resources/hudson/plugins/ec2/Ami.yml
+++ b/src/test/resources/hudson/plugins/ec2/Ami.yml
@@ -4,7 +4,7 @@ configuration-as-code:
jenkins:
clouds:
- amazonEC2:
- name: "test"
+ cloudName: "test"
privateKey: "${PRIVATE_KEY}"
templates:
- ami: ami-0123456789abcdefg
diff --git a/src/test/resources/hudson/plugins/ec2/BackwardsCompatibleConnectionStrategy.yml b/src/test/resources/hudson/plugins/ec2/BackwardsCompatibleConnectionStrategy.yml
index cefc4c3b1..0abb97154 100644
--- a/src/test/resources/hudson/plugins/ec2/BackwardsCompatibleConnectionStrategy.yml
+++ b/src/test/resources/hudson/plugins/ec2/BackwardsCompatibleConnectionStrategy.yml
@@ -4,7 +4,7 @@ configuration-as-code:
jenkins:
clouds:
- amazonEC2:
- name: "us-east-1"
+ cloudName: "us-east-1"
privateKey: "${PRIVATE_KEY}"
templates:
- associatePublicIp: false
diff --git a/src/test/resources/hudson/plugins/ec2/EC2CloudEmpty.yml b/src/test/resources/hudson/plugins/ec2/EC2CloudEmpty.yml
index dad5e0bea..114db2f8c 100644
--- a/src/test/resources/hudson/plugins/ec2/EC2CloudEmpty.yml
+++ b/src/test/resources/hudson/plugins/ec2/EC2CloudEmpty.yml
@@ -2,5 +2,5 @@
jenkins:
clouds:
- amazonEC2:
- name: "empty"
+ cloudName: "empty"
privateKey: "${PRIVATE_KEY}"
diff --git a/src/test/resources/hudson/plugins/ec2/Mac.yml b/src/test/resources/hudson/plugins/ec2/Mac.yml
index 10d31122b..839a86046 100644
--- a/src/test/resources/hudson/plugins/ec2/Mac.yml
+++ b/src/test/resources/hudson/plugins/ec2/Mac.yml
@@ -2,7 +2,7 @@
jenkins:
clouds:
- amazonEC2:
- name: "staging"
+ cloudName: "staging"
useInstanceProfileForCredentials: true
privateKey: "${PRIVATE_KEY}"
templates:
diff --git a/src/test/resources/hudson/plugins/ec2/MacData.yml b/src/test/resources/hudson/plugins/ec2/MacData.yml
index dc96e0419..811e708ff 100644
--- a/src/test/resources/hudson/plugins/ec2/MacData.yml
+++ b/src/test/resources/hudson/plugins/ec2/MacData.yml
@@ -2,7 +2,7 @@
jenkins:
clouds:
- amazonEC2:
- name: "production"
+ cloudName: "production"
useInstanceProfileForCredentials: true
sshKeysCredentialsId: "random credentials id"
templates:
diff --git a/src/test/resources/hudson/plugins/ec2/MacDataExport.yml b/src/test/resources/hudson/plugins/ec2/MacDataExport.yml
index e499c0b72..26d28e4ed 100644
--- a/src/test/resources/hudson/plugins/ec2/MacDataExport.yml
+++ b/src/test/resources/hudson/plugins/ec2/MacDataExport.yml
@@ -1,5 +1,5 @@
- amazonEC2:
- name: "production"
+ cloudName: "production"
region: "us-east-1"
sshKeysCredentialsId: "random credentials id"
templates:
diff --git a/src/test/resources/hudson/plugins/ec2/Unix-withAvoidUsingOrphanedNodes.yml b/src/test/resources/hudson/plugins/ec2/Unix-withAvoidUsingOrphanedNodes.yml
index aa56da176..db60b7f50 100644
--- a/src/test/resources/hudson/plugins/ec2/Unix-withAvoidUsingOrphanedNodes.yml
+++ b/src/test/resources/hudson/plugins/ec2/Unix-withAvoidUsingOrphanedNodes.yml
@@ -2,7 +2,7 @@
jenkins:
clouds:
- amazonEC2:
- name: "avoidUsingOrphanedNodesTest"
+ cloudName: "avoidUsingOrphanedNodesTest"
useInstanceProfileForCredentials: true
sshKeysCredentialsId: "random credentials id"
templates:
diff --git a/src/test/resources/hudson/plugins/ec2/Unix-withEnclaveEnabled.yml b/src/test/resources/hudson/plugins/ec2/Unix-withEnclaveEnabled.yml
index 7fb3effa8..3e11923eb 100644
--- a/src/test/resources/hudson/plugins/ec2/Unix-withEnclaveEnabled.yml
+++ b/src/test/resources/hudson/plugins/ec2/Unix-withEnclaveEnabled.yml
@@ -2,7 +2,7 @@
jenkins:
clouds:
- amazonEC2:
- name: "production"
+ cloudName: "production"
useInstanceProfileForCredentials: true
sshKeysCredentialsId: "random credentials id"
templates:
diff --git a/src/test/resources/hudson/plugins/ec2/Unix-withMinimumInstancesTimeRange.yml b/src/test/resources/hudson/plugins/ec2/Unix-withMinimumInstancesTimeRange.yml
index 690500c76..4f07db386 100644
--- a/src/test/resources/hudson/plugins/ec2/Unix-withMinimumInstancesTimeRange.yml
+++ b/src/test/resources/hudson/plugins/ec2/Unix-withMinimumInstancesTimeRange.yml
@@ -2,7 +2,7 @@
jenkins:
clouds:
- amazonEC2:
- name: "timed"
+ cloudName: "timed"
useInstanceProfileForCredentials: true
sshKeysCredentialsId: "random credentials id"
templates:
diff --git a/src/test/resources/hudson/plugins/ec2/Unix.yml b/src/test/resources/hudson/plugins/ec2/Unix.yml
index 0681b4063..643bb8ef1 100644
--- a/src/test/resources/hudson/plugins/ec2/Unix.yml
+++ b/src/test/resources/hudson/plugins/ec2/Unix.yml
@@ -2,7 +2,7 @@
jenkins:
clouds:
- amazonEC2:
- name: "staging"
+ cloudName: "staging"
useInstanceProfileForCredentials: true
privateKey: "${PRIVATE_KEY}"
templates:
diff --git a/src/test/resources/hudson/plugins/ec2/UnixData-withAltEndpointAndJavaPath.yml b/src/test/resources/hudson/plugins/ec2/UnixData-withAltEndpointAndJavaPath.yml
index fb53b85d4..3f11d4288 100644
--- a/src/test/resources/hudson/plugins/ec2/UnixData-withAltEndpointAndJavaPath.yml
+++ b/src/test/resources/hudson/plugins/ec2/UnixData-withAltEndpointAndJavaPath.yml
@@ -3,7 +3,7 @@ jenkins:
clouds:
- amazonEC2:
altEC2Endpoint: "https.//ec2.us-east-1.amazonaws.com"
- name: "production"
+ cloudName: "production"
region: "eu-central-1"
useInstanceProfileForCredentials: true
sshKeysCredentialsId: "random credentials id"
diff --git a/src/test/resources/hudson/plugins/ec2/UnixData.yml b/src/test/resources/hudson/plugins/ec2/UnixData.yml
index 4a4bfb34b..4e04ce205 100644
--- a/src/test/resources/hudson/plugins/ec2/UnixData.yml
+++ b/src/test/resources/hudson/plugins/ec2/UnixData.yml
@@ -2,7 +2,7 @@
jenkins:
clouds:
- amazonEC2:
- name: "production"
+ cloudName: "production"
useInstanceProfileForCredentials: true
sshKeysCredentialsId: "random credentials id"
templates:
diff --git a/src/test/resources/hudson/plugins/ec2/UnixDataExport-withAltEndpointAndJavaPath.yml b/src/test/resources/hudson/plugins/ec2/UnixDataExport-withAltEndpointAndJavaPath.yml
index d2f493244..2a6507a0a 100644
--- a/src/test/resources/hudson/plugins/ec2/UnixDataExport-withAltEndpointAndJavaPath.yml
+++ b/src/test/resources/hudson/plugins/ec2/UnixDataExport-withAltEndpointAndJavaPath.yml
@@ -1,6 +1,6 @@
- amazonEC2:
altEC2Endpoint: "https.//ec2.us-east-1.amazonaws.com"
- name: "production"
+ cloudName: "production"
region: "eu-central-1"
sshKeysCredentialsId: "random credentials id"
templates:
diff --git a/src/test/resources/hudson/plugins/ec2/UnixDataExport.yml b/src/test/resources/hudson/plugins/ec2/UnixDataExport.yml
index 0fd486701..062d3c7d5 100644
--- a/src/test/resources/hudson/plugins/ec2/UnixDataExport.yml
+++ b/src/test/resources/hudson/plugins/ec2/UnixDataExport.yml
@@ -1,5 +1,5 @@
- amazonEC2:
- name: "production"
+ cloudName: "production"
region: "us-east-1"
sshKeysCredentialsId: "random credentials id"
templates:
diff --git a/src/test/resources/hudson/plugins/ec2/WindowsData.yml b/src/test/resources/hudson/plugins/ec2/WindowsData.yml
index fff116a61..f271efb06 100644
--- a/src/test/resources/hudson/plugins/ec2/WindowsData.yml
+++ b/src/test/resources/hudson/plugins/ec2/WindowsData.yml
@@ -2,7 +2,7 @@
jenkins:
clouds:
- amazonEC2:
- name: "development"
+ cloudName: "development"
useInstanceProfileForCredentials: true
privateKey: "${PRIVATE_KEY}"
templates:
diff --git a/src/test/resources/hudson/plugins/ec2/WindowsSSHData-withAltEndpointAndJavaPath.yml b/src/test/resources/hudson/plugins/ec2/WindowsSSHData-withAltEndpointAndJavaPath.yml
index 8f3aef987..19750bd9e 100644
--- a/src/test/resources/hudson/plugins/ec2/WindowsSSHData-withAltEndpointAndJavaPath.yml
+++ b/src/test/resources/hudson/plugins/ec2/WindowsSSHData-withAltEndpointAndJavaPath.yml
@@ -3,7 +3,7 @@ jenkins:
clouds:
- amazonEC2:
altEC2Endpoint: "https.//ec2.us-east-1.amazonaws.com"
- name: "production"
+ cloudName: "production"
region: "eu-central-1"
useInstanceProfileForCredentials: true
sshKeysCredentialsId: "random credentials id"
diff --git a/src/test/resources/hudson/plugins/ec2/WindowsSSHData.yml b/src/test/resources/hudson/plugins/ec2/WindowsSSHData.yml
index 442361dcb..ee806eae3 100644
--- a/src/test/resources/hudson/plugins/ec2/WindowsSSHData.yml
+++ b/src/test/resources/hudson/plugins/ec2/WindowsSSHData.yml
@@ -2,7 +2,7 @@
jenkins:
clouds:
- amazonEC2:
- name: "production"
+ cloudName: "production"
useInstanceProfileForCredentials: true
sshKeysCredentialsId: "random credentials id"
templates:
diff --git a/src/test/resources/hudson/plugins/ec2/WindowsSSHDataExport-withAltEndpointAndJavaPath.yml b/src/test/resources/hudson/plugins/ec2/WindowsSSHDataExport-withAltEndpointAndJavaPath.yml
index 32528d4b7..b8e97ac87 100644
--- a/src/test/resources/hudson/plugins/ec2/WindowsSSHDataExport-withAltEndpointAndJavaPath.yml
+++ b/src/test/resources/hudson/plugins/ec2/WindowsSSHDataExport-withAltEndpointAndJavaPath.yml
@@ -1,6 +1,6 @@
- amazonEC2:
altEC2Endpoint: "https.//ec2.us-east-1.amazonaws.com"
- name: "production"
+ cloudName: "production"
region: "eu-central-1"
sshKeysCredentialsId: "random credentials id"
templates:
diff --git a/src/test/resources/hudson/plugins/ec2/WindowsSSHDataExport.yml b/src/test/resources/hudson/plugins/ec2/WindowsSSHDataExport.yml
index ca31f3633..9c23d0537 100644
--- a/src/test/resources/hudson/plugins/ec2/WindowsSSHDataExport.yml
+++ b/src/test/resources/hudson/plugins/ec2/WindowsSSHDataExport.yml
@@ -1,5 +1,5 @@
- amazonEC2:
- name: "production"
+ cloudName: "production"
region: "us-east-1"
sshKeysCredentialsId: "random credentials id"
templates: