├── cloud-openstack-common ├── .gitignore ├── pom.xml └── src │ └── main │ └── java │ └── jetbrains │ └── buildServer │ └── clouds │ └── openstack │ └── OpenstackCloudParameters.java ├── cloud-openstack-agent ├── .gitignore ├── src │ ├── test │ │ ├── resources │ │ │ ├── meta_data_truncated.json │ │ │ ├── meta_data_noUserData.json │ │ │ ├── meta_data_userData.json │ │ │ └── log4j.xml │ │ └── java │ │ │ └── jetbrains │ │ │ └── buildServer │ │ │ └── clouds │ │ │ └── openstack │ │ │ ├── util │ │ │ └── Lo4jBeanAppender.java │ │ │ └── OpenstackAgentPropertiesTest.java │ └── main │ │ ├── resources │ │ └── META-INF │ │ │ └── build-agent-plugin-cloud-openstack.xml │ │ └── java │ │ └── jetbrains │ │ └── buildServer │ │ └── clouds │ │ └── openstack │ │ └── OpenstackAgentProperties.java ├── teamcity-plugin.xml └── pom.xml ├── cloud-openstack-server ├── .gitignore ├── src │ ├── test │ │ ├── resources │ │ │ ├── .gitignore │ │ │ ├── test.v2.properties.here │ │ │ ├── test.v3.properties.here │ │ │ ├── test.v2.yml.here │ │ │ ├── test.v3.yml.here │ │ │ ├── test.vMock.yml │ │ │ ├── __files │ │ │ │ ├── v2.1-nova-id-servers.json │ │ │ │ ├── v2.1-nova-id-flavors-detail.json │ │ │ │ ├── v2.1-nova-id-images-detail.json │ │ │ │ ├── v2.0.floatingips.json │ │ │ │ ├── v2.0.networks.json │ │ │ │ ├── v2.1-nova-id-servers-detail-not-defined.json │ │ │ │ ├── v2.1-nova-id-servers-detail-empty.json │ │ │ │ ├── v2.1-nova-id-servers-detail-run.json │ │ │ │ ├── v2.1-nova-id-servers-detail-stopped.json │ │ │ │ ├── v2.1-nova-id-servers-detail-build.json │ │ │ │ ├── v2.1-nova-id-servers-detail-stopping.json │ │ │ │ ├── v2.1-nova-id-servers-detail-restore.json │ │ │ │ └── v3-auth-tokens.json │ │ │ └── log4j.xml │ │ └── java │ │ │ └── jetbrains │ │ │ └── buildServer │ │ │ ├── clouds │ │ │ └── openstack │ │ │ │ ├── OpenstackExceptionTest.java │ │ │ │ ├── IdGeneratorTest.java │ │ │ │ ├── util │ │ │ │ ├── Lo4jBeanAppender.java │ │ │ │ └── TestCloudClientParameters.java │ │ │ │ ├── OpenstackApiTest.java │ │ │ │ ├── OpenstackIdentityTest.java │ │ │ │ ├── AbstractTestOpenstackCloudClient.java │ │ │ │ ├── OpenstackCloudClientTest.java │ │ │ │ └── OpenstackCloudClientMockedTest.java │ │ │ └── serverSide │ │ │ └── TeamCityPropertiesMock.java │ └── main │ │ ├── resources │ │ ├── buildServerResources │ │ │ ├── image-details.jsp │ │ │ └── profile-settings.jsp │ │ └── META-INF │ │ │ └── build-server-plugin-cloud-openstack.xml │ │ └── java │ │ └── jetbrains │ │ └── buildServer │ │ └── clouds │ │ └── openstack │ │ ├── ExecutorServiceFactory.java │ │ ├── OpenstackException.java │ │ ├── OpenstackCloudImageDetailsExtension.java │ │ ├── IdGenerator.java │ │ ├── OpenstackIdentity.java │ │ ├── OpenstackCloudClientFactory.java │ │ ├── OpenstackApi.java │ │ ├── OpenstackCloudClient.java │ │ ├── OpenstackCloudImage.java │ │ └── OpenstackCloudInstance.java └── pom.xml ├── .gitignore ├── LICENSE ├── teamcity-plugin.xml ├── pom.xml └── README.md /cloud-openstack-common/.gitignore: -------------------------------------------------------------------------------- 1 | /.classpath 2 | /.project 3 | /.settings/ 4 | /pom.xml.versionsBackup 5 | -------------------------------------------------------------------------------- /cloud-openstack-agent/.gitignore: -------------------------------------------------------------------------------- 1 | /.settings/ 2 | /.classpath 3 | /.project 4 | /test-output/ 5 | /pom.xml.versionsBackup 6 | -------------------------------------------------------------------------------- /cloud-openstack-server/.gitignore: -------------------------------------------------------------------------------- 1 | /.classpath 2 | /.settings/ 3 | /.project 4 | /test-output/ 5 | /pom.xml.versionsBackup 6 | -------------------------------------------------------------------------------- /cloud-openstack-server/src/test/resources/.gitignore: -------------------------------------------------------------------------------- 1 | /test.v2.properties 2 | /test.v2.yml 3 | /test.v3.properties 4 | /test.v3.yml 5 | -------------------------------------------------------------------------------- /cloud-openstack-server/src/test/resources/test.v2.properties.here: -------------------------------------------------------------------------------- 1 | test.url=https://openstack.company.com/v2.0 2 | test.identity=tenant:user 3 | test.password=foobar 4 | test.region=region1 5 | -------------------------------------------------------------------------------- /cloud-openstack-server/src/test/resources/test.v3.properties.here: -------------------------------------------------------------------------------- 1 | test.url=https://openstack.company.com/v3 2 | test.identity=domain:tenant:user 3 | test.password=foobar 4 | test.region=region1 -------------------------------------------------------------------------------- /cloud-openstack-server/src/test/resources/test.v2.yml.here: -------------------------------------------------------------------------------- 1 | openstack-test-teamcity-plugin: 2 | image: anyImage 3 | flavor: m1.small 4 | network: networkProviderName 5 | security_group: default 6 | key_pair: yourKey 7 | -------------------------------------------------------------------------------- /cloud-openstack-server/src/test/resources/test.v3.yml.here: -------------------------------------------------------------------------------- 1 | openstack-test-teamcity-plugin: 2 | image: anyImage 3 | flavor: m1.small 4 | network: networkProviderName 5 | security_group: default 6 | key_pair: yourKey 7 | -------------------------------------------------------------------------------- /cloud-openstack-agent/src/test/resources/meta_data_truncated.json: -------------------------------------------------------------------------------- 1 | { 2 | "random_seed": "xxxx", 3 | "uuid": "xxxx-yyyyy-zzzz", 4 | "availability_zone": "nova", 5 | "keys": [{ 6 | "data": "ssh-rsa foobar Generated-by-Nova", 7 | "type": "ssh", 8 | "na -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # idea settings 2 | *.iml 3 | .idea/ 4 | target/ 5 | .DS_Store 6 | /**/.project 7 | /**/.classpath 8 | /**/.settings/ 9 | /**/bin/ 10 | /.README.md.html 11 | /pom.xml.versionsBackup 12 | /**/pom.xml.releaseBackup 13 | /**/pom.xml.versionsBackup 14 | /**/release.properties 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /cloud-openstack-agent/teamcity-plugin.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /cloud-openstack-server/src/test/resources/test.vMock.yml: -------------------------------------------------------------------------------- 1 | openstack-test-teamcity-plugin: 2 | image: Ubuntu 3 | flavor: large.c8 4 | network: test_network 5 | security_group: test_security_group 6 | key_pair: test_key_pair 7 | # Floating IP is false, hard to initiate Nova client completly via Mock 8 | auto_floating_ip: false 9 | -------------------------------------------------------------------------------- /cloud-openstack-server/src/test/resources/__files/v2.1-nova-id-servers.json: -------------------------------------------------------------------------------- 1 | { 2 | "server": { 3 | "security_groups": [ 4 | { 5 | "name": "test_security_groups" 6 | } 7 | ], 8 | "OS-DCF:diskConfig": "MANUAL", 9 | "id": "server-id", 10 | "adminPass": "adminfoobar" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /cloud-openstack-server/src/main/resources/buildServerResources/image-details.jsp: -------------------------------------------------------------------------------- 1 | <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> 2 | 3 | 4 | image: flavor: 5 | -------------------------------------------------------------------------------- /cloud-openstack-server/src/main/java/jetbrains/buildServer/clouds/openstack/ExecutorServiceFactory.java: -------------------------------------------------------------------------------- 1 | package jetbrains.buildServer.clouds.openstack; 2 | 3 | import org.jetbrains.annotations.NotNull; 4 | import java.util.concurrent.ScheduledExecutorService; 5 | 6 | 7 | public interface ExecutorServiceFactory { 8 | @NotNull 9 | ScheduledExecutorService createExecutorService(@NotNull String duty); 10 | } 11 | -------------------------------------------------------------------------------- /cloud-openstack-agent/src/test/resources/meta_data_noUserData.json: -------------------------------------------------------------------------------- 1 | { 2 | "random_seed": "xxxx", 3 | "uuid": "xxxx-yyyyy-zzzz", 4 | "availability_zone": "nova", 5 | "keys": [{ 6 | "data": "ssh-rsa foobar Generated-by-Nova", 7 | "type": "ssh", 8 | "name": "keyHOS" 9 | }], 10 | "hostname": "test.novalocal", 11 | "launch_index": 0, 12 | "public_keys": { 13 | "KeyHOS": "ssh-rsa foobar Generated-by-Nova" 14 | }, 15 | "project_id": "vufhfi435T4trvtrvtr", 16 | "name": "openstack_test" 17 | } -------------------------------------------------------------------------------- /cloud-openstack-server/src/test/java/jetbrains/buildServer/clouds/openstack/OpenstackExceptionTest.java: -------------------------------------------------------------------------------- 1 | package jetbrains.buildServer.clouds.openstack; 2 | 3 | import org.testng.annotations.Test; 4 | 5 | public class OpenstackExceptionTest { 6 | 7 | @Test 8 | public void testExceptions() { 9 | new OpenstackException(); 10 | new OpenstackException("Test"); 11 | new OpenstackException("Test", new Exception()); 12 | new OpenstackException(new Exception()); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /cloud-openstack-agent/src/main/resources/META-INF/build-agent-plugin-cloud-openstack.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /cloud-openstack-server/src/test/resources/__files/v2.1-nova-id-flavors-detail.json: -------------------------------------------------------------------------------- 1 | { 2 | "flavors": [ 3 | { 4 | "ram": 32768, 5 | "OS-FLV-DISABLED:disabled": false, 6 | "os-flavor-access:is_public": true, 7 | "rxtx_factor": 1.0, 8 | "disk": 20, 9 | "id": "flavor-id", 10 | "name": "large.c8", 11 | "vcpus": 8, 12 | "swap": "", 13 | "OS-FLV-EXT-DATA:ephemeral": 0 14 | } 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2014 YANDEX 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | -------------------------------------------------------------------------------- /cloud-openstack-server/src/main/resources/META-INF/build-server-plugin-cloud-openstack.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /cloud-openstack-agent/src/test/resources/meta_data_userData.json: -------------------------------------------------------------------------------- 1 | { 2 | "random_seed": "xxxx", 3 | "uuid": "xxxx-yyyyy-zzzz", 4 | "availability_zone": "nova", 5 | "keys": [{ 6 | "data": "ssh-rsa foobar Generated-by-Nova", 7 | "type": "ssh", 8 | "name": "keyHOS" 9 | }], 10 | "hostname": "test.novalocal", 11 | "launch_index": 0, 12 | "meta": { 13 | "agent.cloud.ip": "192.168.42.42" 14 | }, 15 | "public_keys": { 16 | "KeyHOS": "ssh-rsa foobar Generated-by-Nova" 17 | }, 18 | "project_id": "vufhfi435T4trvtrvtr", 19 | "name": "openstack_test" 20 | } -------------------------------------------------------------------------------- /cloud-openstack-server/src/test/resources/__files/v2.1-nova-id-images-detail.json: -------------------------------------------------------------------------------- 1 | { 2 | "images": [ 3 | { 4 | "status": "ACTIVE", 5 | "updated": "2019-10-17T09:37:45Z", 6 | "id": "image-id", 7 | "OS-EXT-IMG-SIZE:size": 21474836480, 8 | "name": "Ubuntu", 9 | "created": "2019-10-17T09:33:05Z", 10 | "minDisk": 0, 11 | "progress": 100, 12 | "minRam": 0, 13 | "metadata": { 14 | "bus": "scsi", 15 | "hw_scsi_model": "virtio-scsi", 16 | "hw_disk_bus": "scsi" 17 | } 18 | } 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /cloud-openstack-server/src/main/java/jetbrains/buildServer/clouds/openstack/OpenstackException.java: -------------------------------------------------------------------------------- 1 | package jetbrains.buildServer.clouds.openstack; 2 | 3 | public class OpenstackException extends Exception { 4 | 5 | private static final long serialVersionUID = -8835657293823900872L; 6 | 7 | public OpenstackException() { 8 | super(); 9 | } 10 | 11 | public OpenstackException(String message) { 12 | super(message); 13 | } 14 | 15 | public OpenstackException(Throwable exception) { 16 | super(exception); 17 | } 18 | 19 | public OpenstackException(String message, Throwable exception) { 20 | super(message, exception); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /cloud-openstack-server/src/test/java/jetbrains/buildServer/clouds/openstack/IdGeneratorTest.java: -------------------------------------------------------------------------------- 1 | package jetbrains.buildServer.clouds.openstack; 2 | 3 | import org.testng.Assert; 4 | import org.testng.annotations.Test; 5 | 6 | public class IdGeneratorTest { 7 | 8 | @Test 9 | public void testGenerate() { 10 | IdGenerator generator = new IdGenerator(); 11 | String lastId = ""; 12 | long d = System.currentTimeMillis(); 13 | for (int i = 0; i < 10; i++) { 14 | String newId = generator.next(); 15 | System.out.println(lastId + "/" + newId); 16 | Assert.assertNotEquals(lastId, newId); 17 | Assert.assertTrue(d <= Long.parseLong(newId)); 18 | lastId = newId; 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /cloud-openstack-server/src/main/java/jetbrains/buildServer/clouds/openstack/OpenstackCloudImageDetailsExtension.java: -------------------------------------------------------------------------------- 1 | package jetbrains.buildServer.clouds.openstack; 2 | 3 | import jetbrains.buildServer.clouds.web.CloudImageDetailsExtensionBase; 4 | import jetbrains.buildServer.web.openapi.PagePlaces; 5 | import jetbrains.buildServer.web.openapi.PluginDescriptor; 6 | import org.jetbrains.annotations.NotNull; 7 | 8 | public class OpenstackCloudImageDetailsExtension extends CloudImageDetailsExtensionBase { 9 | public OpenstackCloudImageDetailsExtension(@NotNull final PagePlaces pagePlaces, @NotNull final PluginDescriptor pluginDescriptor) { 10 | super(OpenstackCloudImage.class, pagePlaces, pluginDescriptor, "image-details.jsp"); 11 | register(); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /teamcity-plugin.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | cloud-openstack 5 | cloud-openstack 6 | @Version@ 7 | Cloud Openstack Integration for TeamCity 8 | Plugin download URL 9 | mavlyutov@yandex-team.ru 10 | 11 | Yandex LLC 12 | http://yandex.ru 13 | https://company.yandex.ru/i/yandex_eng_logo-240.png 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /cloud-openstack-server/src/main/java/jetbrains/buildServer/clouds/openstack/IdGenerator.java: -------------------------------------------------------------------------------- 1 | package jetbrains.buildServer.clouds.openstack; 2 | 3 | import java.util.concurrent.atomic.AtomicLong; 4 | 5 | import org.jetbrains.annotations.NotNull; 6 | 7 | public class IdGenerator { 8 | 9 | private final AtomicLong lastId = new AtomicLong(System.currentTimeMillis()); 10 | 11 | @NotNull 12 | public String next() { 13 | long now = System.currentTimeMillis(); 14 | while (true) { 15 | long lastTime = lastId.get(); 16 | if (lastTime >= now) { 17 | now = lastTime + 1; 18 | } 19 | if (lastId.compareAndSet(lastTime, now)) { 20 | return String.valueOf(now); 21 | } 22 | } 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /cloud-openstack-agent/src/test/resources/log4j.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /cloud-openstack-server/src/test/resources/__files/v2.0.floatingips.json: -------------------------------------------------------------------------------- 1 | { 2 | "floatingips": [ 3 | { 4 | "router_id": null, 5 | "status": "DOWN", 6 | "description": "", 7 | "tags": [ 8 | ], 9 | "updated_at": "2019-10-30T17:32:02Z", 10 | "dns_domain": "", 11 | "floating_network_id": "00000000-0000-0000-0000-000000000000", 12 | "fixed_ip_address": null, 13 | "floating_ip_address": "10.248.7.219", 14 | "revision_number": 38, 15 | "port_id": null, 16 | "id": "00000000-0000-0000-0000-000000000000", 17 | "dns_name": "", 18 | "created_at": "2019-09-11T06:36:16Z", 19 | "tenant_id": "00000000000000000000000000000000", 20 | "project_id": "00000000000000000000000000000000" 21 | } 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /cloud-openstack-common/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | 5 | cloud-openstack 6 | jetbrains.buildServer.clouds 7 | 1.7-SNAPSHOT 8 | 9 | cloud-openstack-common 10 | jar 11 | 12 | 13 | 14 | org.jetbrains.teamcity 15 | tests-support 16 | test 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /cloud-openstack-server/src/test/resources/log4j.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /cloud-openstack-server/src/test/resources/__files/v2.0.networks.json: -------------------------------------------------------------------------------- 1 | { 2 | "networks": [ 3 | { 4 | "status": "ACTIVE", 5 | "router:external": false, 6 | "availability_zone_hints": [ 7 | ], 8 | "availability_zones": [ 9 | "nova" 10 | ], 11 | "ipv4_address_scope": null, 12 | "description": "", 13 | "subnets": [ 14 | "subnet-id" 15 | ], 16 | "tenant_id": "test_tenant_id", 17 | "created_at": "2019-09-11T06:36:03Z", 18 | "tags": [ 19 | ], 20 | "updated_at": "2019-09-11T06:36:03Z", 21 | "dns_domain": "", 22 | "mtu": 8950, 23 | "ipv6_address_scope": null, 24 | "revision_number": 2, 25 | "admin_state_up": true, 26 | "shared": false, 27 | "project_id": "test_project_id", 28 | "id": "network-id", 29 | "name": "test_network" 30 | } 31 | ] 32 | } 33 | -------------------------------------------------------------------------------- /cloud-openstack-agent/src/test/java/jetbrains/buildServer/clouds/openstack/util/Lo4jBeanAppender.java: -------------------------------------------------------------------------------- 1 | package jetbrains.buildServer.clouds.openstack.util; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | 6 | import org.apache.log4j.WriterAppender; 7 | import org.apache.log4j.spi.LoggingEvent; 8 | 9 | public class Lo4jBeanAppender extends WriterAppender { 10 | 11 | /** Logger. Multithread usage => synchronized usage (simply solution for UT) */ 12 | private static List logs = new ArrayList<>(); 13 | 14 | @Override 15 | public void append(LoggingEvent event) { 16 | synchronized (logs) { 17 | logs.add("[" + event.getLevel().toString() + "] " + event.getMessage().toString()); 18 | } 19 | } 20 | 21 | public static void clear() { 22 | synchronized (logs) { 23 | logs.clear(); 24 | } 25 | } 26 | 27 | public static boolean contains(String string) { 28 | synchronized (logs) { 29 | for (String s : logs) { 30 | if (s.contains(string)) { 31 | return true; 32 | } 33 | } 34 | return false; 35 | } 36 | } 37 | 38 | public static boolean isEmpty() { 39 | return logs.isEmpty(); 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /cloud-openstack-server/src/test/java/jetbrains/buildServer/clouds/openstack/util/Lo4jBeanAppender.java: -------------------------------------------------------------------------------- 1 | package jetbrains.buildServer.clouds.openstack.util; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | 6 | import org.apache.log4j.WriterAppender; 7 | import org.apache.log4j.spi.LoggingEvent; 8 | 9 | public class Lo4jBeanAppender extends WriterAppender { 10 | 11 | /** Logger. Multithread usage => synchronized usage (simply solution for UT) */ 12 | private static List logs = new ArrayList<>(); 13 | 14 | @Override 15 | public void append(LoggingEvent event) { 16 | synchronized (logs) { 17 | logs.add("[" + event.getLevel().toString() + "] " + event.getMessage().toString()); 18 | } 19 | } 20 | 21 | public static void clear() { 22 | synchronized (logs) { 23 | logs.clear(); 24 | } 25 | } 26 | 27 | public static boolean contains(String string) { 28 | synchronized (logs) { 29 | for (String s : logs) { 30 | if (s.contains(string)) { 31 | return true; 32 | } 33 | } 34 | return false; 35 | } 36 | } 37 | 38 | public static boolean isEmpty() { 39 | return logs.isEmpty(); 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /cloud-openstack-common/src/main/java/jetbrains/buildServer/clouds/openstack/OpenstackCloudParameters.java: -------------------------------------------------------------------------------- 1 | package jetbrains.buildServer.clouds.openstack; 2 | 3 | import jetbrains.buildServer.agent.Constants; 4 | 5 | public final class OpenstackCloudParameters { 6 | 7 | private OpenstackCloudParameters() { 8 | super(); 9 | } 10 | 11 | public static final String CLOUD_TYPE = "NOVA"; // that should be equal or less than 6 symbols, thanks for brainfuck debugging jetbrains guys! 12 | public static final String CLOUD_DISPLAY_NAME = "Openstack Cloud"; 13 | public static final String PLUGIN_SHORT_NAME = "openstack"; 14 | 15 | public static final String ENDPOINT_URL = "clouds.openstack.endpointUrl"; 16 | public static final String IDENTITY = "clouds.openstack.identity"; 17 | public static final String PASSWORD = Constants.SECURE_PROPERTY_PREFIX + "clouds.openstack.password"; // NOSONAR: No clear password 18 | public static final String REGION = "clouds.openstack.zone"; 19 | public static final String INSTANCE_CAP = "clouds.openstack.instanceCap"; 20 | 21 | public static final String AGENT_METADATA_DISABLE = "clouds.openstack.metadata.disable"; 22 | 23 | public static final String IMAGES_PROFILES = "clouds.openstack.images"; 24 | 25 | public static final String OPENSTACK_INSTANCE_ID = "agent.cloud.uuid"; 26 | public static final String AGENT_CLOUD_TYPE = "agent.cloud.type"; 27 | public static final String AGENT_CLOUD_IP = "agent.cloud.ip"; 28 | } 29 | -------------------------------------------------------------------------------- /cloud-openstack-server/src/test/java/jetbrains/buildServer/clouds/openstack/util/TestCloudClientParameters.java: -------------------------------------------------------------------------------- 1 | package jetbrains.buildServer.clouds.openstack.util; 2 | 3 | import java.util.Collection; 4 | import java.util.Map; 5 | 6 | import jetbrains.buildServer.clouds.CloudClientParameters; 7 | import jetbrains.buildServer.clouds.CloudImageParameters; 8 | import jetbrains.buildServer.util.StringUtil; 9 | 10 | public class TestCloudClientParameters extends CloudClientParameters { 11 | 12 | private Map params; 13 | 14 | public TestCloudClientParameters(Map params) { 15 | this.params = params; 16 | 17 | // Remove empty parameters 18 | params.entrySet().removeIf(e -> StringUtil.isEmpty(e.getValue())); 19 | } 20 | 21 | @Override 22 | public String getProfileId() { 23 | throw new UnsupportedOperationException("NYI"); 24 | } 25 | 26 | @Override 27 | public String getParameter(String name) { 28 | return params.get(name); 29 | } 30 | 31 | @Override 32 | public Collection listParameterNames() { 33 | throw new UnsupportedOperationException("NYI"); 34 | } 35 | 36 | @Override 37 | public Collection getCloudImages() { 38 | throw new UnsupportedOperationException("NYI"); 39 | } 40 | 41 | @Override 42 | public Map getParameters() { 43 | throw new UnsupportedOperationException("NYI"); 44 | } 45 | 46 | @Override 47 | public String getProfileDescription() { 48 | throw new UnsupportedOperationException("NYI"); 49 | } 50 | 51 | } 52 | -------------------------------------------------------------------------------- /cloud-openstack-server/src/test/java/jetbrains/buildServer/clouds/openstack/OpenstackApiTest.java: -------------------------------------------------------------------------------- 1 | package jetbrains.buildServer.clouds.openstack; 2 | 3 | import org.testng.Assert; 4 | import org.testng.annotations.Test; 5 | 6 | public class OpenstackApiTest { 7 | 8 | @Test 9 | public void testGetKeystoneVersion() { 10 | Assert.assertEquals(OpenstackApi.getKeystoneVersion(null), "3"); 11 | Assert.assertEquals(OpenstackApi.getKeystoneVersion(""), "3"); 12 | Assert.assertEquals(OpenstackApi.getKeystoneVersion("http://my.openstack.org/v"), "3"); 13 | Assert.assertEquals(OpenstackApi.getKeystoneVersion("http://my.openstack.org/v3"), "3"); 14 | Assert.assertEquals(OpenstackApi.getKeystoneVersion("http://my.openstack.org/v4"), "4"); 15 | Assert.assertEquals(OpenstackApi.getKeystoneVersion("http://my.openstack.org/v2.0"), "2"); 16 | Assert.assertEquals(OpenstackApi.getKeystoneVersion("http://my.openstack.org/v2/v3"), "3"); 17 | Assert.assertEquals(OpenstackApi.getKeystoneVersion("http://my.openstack.org/v3/v2.0"), "2"); 18 | Assert.assertEquals(OpenstackApi.getKeystoneVersion("https://my.openstack.org/v3"), "3"); 19 | Assert.assertEquals(OpenstackApi.getKeystoneVersion("https://my.openstack.org/v2.0"), "2"); 20 | Assert.assertEquals(OpenstackApi.getKeystoneVersion("https://my.openstack.org/v3/"), "3"); 21 | Assert.assertEquals(OpenstackApi.getKeystoneVersion("https://my.openstack.org/v2.0/"), "2"); 22 | Assert.assertEquals(OpenstackApi.getKeystoneVersion("https://my.openstack.org/V3"), "3"); 23 | Assert.assertEquals(OpenstackApi.getKeystoneVersion("https://my.openstack.org/V2.0"), "2"); 24 | Assert.assertEquals(OpenstackApi.getKeystoneVersion("https://my.openstack.org:42/v3"), "3"); 25 | Assert.assertEquals(OpenstackApi.getKeystoneVersion("https://my.openstack.org:42/v2.0"), "2"); 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /cloud-openstack-server/src/test/java/jetbrains/buildServer/serverSide/TeamCityPropertiesMock.java: -------------------------------------------------------------------------------- 1 | package jetbrains.buildServer.serverSide; 2 | 3 | import java.util.HashMap; 4 | import java.util.Map; 5 | 6 | import jetbrains.buildServer.serverSide.TeamCityProperties.Model; 7 | 8 | public class TeamCityPropertiesMock { 9 | 10 | private static Model initialModel = null; 11 | 12 | private static class ModelMock implements Model { 13 | 14 | private Map properties = new HashMap<>(); 15 | 16 | @Override 17 | public String getPropertyOrNull(String key) { 18 | return properties.get(key); 19 | } 20 | 21 | public void storeValue(String key, String defaultValue) { 22 | properties.put(key, defaultValue); 23 | } 24 | 25 | @Override 26 | public void storeDefaultValue(String key, String defaultValue) { 27 | // not used 28 | } 29 | 30 | @Override 31 | public Map getSystemProperties() { 32 | throw new UnsupportedOperationException("Not used"); 33 | } 34 | 35 | @Override 36 | public Map getUserDefinedProperties() { 37 | throw new UnsupportedOperationException("Not used"); 38 | } 39 | 40 | }; 41 | 42 | public static void addProperty(String key, String value) { 43 | if (initialModel == null) { 44 | initialModel = TeamCityProperties.getModel(); 45 | TeamCityProperties.setModel(new ModelMock()); 46 | } 47 | ((ModelMock) TeamCityProperties.getModel()).storeValue(key, value); 48 | } 49 | 50 | public static void reset() { 51 | if (initialModel == null) { 52 | throw new UnsupportedOperationException("Please use 'addProperty' first"); 53 | } 54 | TeamCityProperties.setModel(initialModel); 55 | initialModel = null; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /cloud-openstack-server/src/test/resources/__files/v2.1-nova-id-servers-detail-not-defined.json: -------------------------------------------------------------------------------- 1 | { 2 | "servers": [ 3 | { 4 | "OS-EXT-STS:task_state": null, 5 | "addresses": { 6 | "test_network": [ 7 | { 8 | "OS-EXT-IPS-MAC:mac_addr": "00:00:00:00:00:00", 9 | "version": 4, 10 | "addr": "192.168.1.42", 11 | "OS-EXT-IPS:type": "fixed" 12 | }, 13 | { 14 | "OS-EXT-IPS-MAC:mac_addr": "00:00:00:00:00:00", 15 | "version": 4, 16 | "addr": "10.248.7.42", 17 | "OS-EXT-IPS:type": "floating" 18 | } 19 | ] 20 | }, 21 | "image": { 22 | "id": "image-id" 23 | }, 24 | "OS-EXT-STS:vm_state": "active", 25 | "OS-SRV-USG:launched_at": null, 26 | "flavor": { 27 | "id": "flavor-id" 28 | }, 29 | "user_id": "user-id", 30 | "OS-DCF:diskConfig": "MANUAL", 31 | "accessIPv4": "10.248.7.42", 32 | "accessIPv6": "", 33 | "progress": 0, 34 | "OS-EXT-STS:power_state": 0, 35 | "OS-EXT-AZ:availability_zone": "", 36 | "config_drive": "", 37 | "status": "ACTIVE", 38 | "updated": "2019-10-31T08:49:21Z", 39 | "hostId": "", 40 | "OS-SRV-USG:terminated_at": null, 41 | "key_name": "test_key_pair", 42 | "name": "openstack-test-teamcity-plugin-42", 43 | "created": "2019-10-31T08:49:21Z", 44 | "tenant_id": "tenant-id", 45 | "os-extended-volumes:volumes_attached": [ 46 | ], 47 | "metadata": { 48 | "agent.cloud.ip": "10.248.7.42", 49 | "teamcity.cloud.agent.remove.policy": "remove" 50 | } 51 | } 52 | ] 53 | } 54 | -------------------------------------------------------------------------------- /cloud-openstack-server/src/test/resources/__files/v2.1-nova-id-servers-detail-empty.json: -------------------------------------------------------------------------------- 1 | { 2 | "servers": [ 3 | { 4 | "OS-EXT-STS:task_state": null, 5 | "addresses": { 6 | "test_network": [ 7 | { 8 | "OS-EXT-IPS-MAC:mac_addr": "00:00:00:00:00:00", 9 | "version": 4, 10 | "addr": "192.168.1.42", 11 | "OS-EXT-IPS:type": "fixed" 12 | }, 13 | { 14 | "OS-EXT-IPS-MAC:mac_addr": "00:00:00:00:00:00", 15 | "version": 4, 16 | "addr": "10.248.7.42", 17 | "OS-EXT-IPS:type": "floating" 18 | } 19 | ] 20 | }, 21 | "image": { 22 | "id": "image-id-other" 23 | }, 24 | "OS-EXT-STS:vm_state": "active", 25 | "OS-SRV-USG:launched_at": null, 26 | "flavor": { 27 | "id": "flavor-id" 28 | }, 29 | "id": "server-id", 30 | "user_id": "user-id", 31 | "OS-DCF:diskConfig": "MANUAL", 32 | "accessIPv4": "10.248.7.42", 33 | "accessIPv6": "", 34 | "progress": 0, 35 | "OS-EXT-STS:power_state": 0, 36 | "OS-EXT-AZ:availability_zone": "", 37 | "config_drive": "", 38 | "status": "ACTIVE", 39 | "updated": "2019-10-31T08:49:21Z", 40 | "hostId": "", 41 | "OS-SRV-USG:terminated_at": null, 42 | "key_name": "test_key_pair", 43 | "name": "another-vm-4242424242", 44 | "created": "2019-10-31T08:49:21Z", 45 | "tenant_id": "tenant-id", 46 | "os-extended-volumes:volumes_attached": [ 47 | ], 48 | "metadata": { 49 | "agent.cloud.ip": "10.248.7.42", 50 | "teamcity.cloud.agent.remove.policy": "remove" 51 | } 52 | } 53 | ] 54 | } 55 | -------------------------------------------------------------------------------- /cloud-openstack-server/src/test/resources/__files/v2.1-nova-id-servers-detail-run.json: -------------------------------------------------------------------------------- 1 | { 2 | "servers": [ 3 | { 4 | "OS-EXT-STS:task_state": null, 5 | "addresses": { 6 | "test_network": [ 7 | { 8 | "OS-EXT-IPS-MAC:mac_addr": "00:00:00:00:00:00", 9 | "version": 4, 10 | "addr": "192.168.1.42", 11 | "OS-EXT-IPS:type": "fixed" 12 | }, 13 | { 14 | "OS-EXT-IPS-MAC:mac_addr": "00:00:00:00:00:00", 15 | "version": 4, 16 | "addr": "10.248.7.42", 17 | "OS-EXT-IPS:type": "floating" 18 | } 19 | ] 20 | }, 21 | "image": { 22 | "id": "image-id" 23 | }, 24 | "OS-EXT-STS:vm_state": "active", 25 | "OS-SRV-USG:launched_at": null, 26 | "flavor": { 27 | "id": "flavor-id" 28 | }, 29 | "id": "server-id", 30 | "user_id": "user-id", 31 | "OS-DCF:diskConfig": "MANUAL", 32 | "accessIPv4": "10.248.7.42", 33 | "accessIPv6": "", 34 | "progress": 0, 35 | "OS-EXT-STS:power_state": 0, 36 | "OS-EXT-AZ:availability_zone": "", 37 | "config_drive": "", 38 | "status": "ACTIVE", 39 | "updated": "2019-10-31T08:49:21Z", 40 | "hostId": "", 41 | "OS-SRV-USG:terminated_at": null, 42 | "key_name": "test_key_pair", 43 | "name": "openstack-test-teamcity-plugin-42", 44 | "created": "2019-10-31T08:49:21Z", 45 | "tenant_id": "tenant-id", 46 | "os-extended-volumes:volumes_attached": [ 47 | ], 48 | "metadata": { 49 | "agent.cloud.ip": "10.248.7.42", 50 | "teamcity.cloud.agent.remove.policy": "remove" 51 | } 52 | } 53 | ] 54 | } 55 | -------------------------------------------------------------------------------- /cloud-openstack-server/src/test/resources/__files/v2.1-nova-id-servers-detail-stopped.json: -------------------------------------------------------------------------------- 1 | { 2 | "servers": [ 3 | { 4 | "OS-EXT-STS:task_state": null, 5 | "addresses": { 6 | "test_network": [ 7 | { 8 | "OS-EXT-IPS-MAC:mac_addr": "00:00:00:00:00:00", 9 | "version": 4, 10 | "addr": "192.168.1.42", 11 | "OS-EXT-IPS:type": "fixed" 12 | }, 13 | { 14 | "OS-EXT-IPS-MAC:mac_addr": "00:00:00:00:00:00", 15 | "version": 4, 16 | "addr": "10.248.7.42", 17 | "OS-EXT-IPS:type": "floating" 18 | } 19 | ] 20 | }, 21 | "image": { 22 | "id": "image-id" 23 | }, 24 | "OS-EXT-STS:vm_state": "stopping", 25 | "OS-SRV-USG:launched_at": null, 26 | "flavor": { 27 | "id": "flavor-id" 28 | }, 29 | "id": "server-id", 30 | "user_id": "user-id", 31 | "OS-DCF:diskConfig": "MANUAL", 32 | "accessIPv4": "10.248.7.42", 33 | "accessIPv6": "", 34 | "progress": 0, 35 | "OS-EXT-STS:power_state": 0, 36 | "OS-EXT-AZ:availability_zone": "", 37 | "config_drive": "", 38 | "status": "SHUTOFF", 39 | "updated": "2019-10-31T08:49:21Z", 40 | "hostId": "", 41 | "OS-SRV-USG:terminated_at": null, 42 | "key_name": "test_key_pair", 43 | "name": "openstack-test-teamcity-plugin-42", 44 | "created": "2019-10-31T08:49:21Z", 45 | "tenant_id": "tenant-id", 46 | "os-extended-volumes:volumes_attached": [ 47 | ], 48 | "metadata": { 49 | "agent.cloud.ip": "10.248.7.42", 50 | "teamcity.cloud.agent.remove.policy": "remove" 51 | } 52 | } 53 | ] 54 | } 55 | -------------------------------------------------------------------------------- /cloud-openstack-server/src/test/resources/__files/v2.1-nova-id-servers-detail-build.json: -------------------------------------------------------------------------------- 1 | { 2 | "servers": [ 3 | { 4 | "OS-EXT-STS:task_state": "scheduling", 5 | "addresses": { 6 | "test_network": [ 7 | { 8 | "OS-EXT-IPS-MAC:mac_addr": "00:00:00:00:00:00", 9 | "version": 4, 10 | "addr": "192.168.1.42", 11 | "OS-EXT-IPS:type": "fixed" 12 | }, 13 | { 14 | "OS-EXT-IPS-MAC:mac_addr": "00:00:00:00:00:00", 15 | "version": 4, 16 | "addr": "10.248.7.42", 17 | "OS-EXT-IPS:type": "floating" 18 | } 19 | ] 20 | }, 21 | "image": { 22 | "id": "image-id" 23 | }, 24 | "OS-EXT-STS:vm_state": "building", 25 | "OS-SRV-USG:launched_at": null, 26 | "flavor": { 27 | "id": "flavor-id" 28 | }, 29 | "id": "server-id", 30 | "user_id": "user-id", 31 | "OS-DCF:diskConfig": "MANUAL", 32 | "accessIPv4": "10.248.7.42", 33 | "accessIPv6": "", 34 | "progress": 0, 35 | "OS-EXT-STS:power_state": 0, 36 | "OS-EXT-AZ:availability_zone": "", 37 | "config_drive": "", 38 | "status": "BUILD", 39 | "updated": "2019-10-31T08:49:21Z", 40 | "hostId": "", 41 | "OS-SRV-USG:terminated_at": null, 42 | "key_name": "test_key_pair", 43 | "name": "openstack-test-teamcity-plugin-42", 44 | "created": "2019-10-31T08:49:21Z", 45 | "tenant_id": "tenant-id", 46 | "os-extended-volumes:volumes_attached": [ 47 | ], 48 | "metadata": { 49 | "agent.cloud.ip": "10.248.7.42", 50 | "teamcity.cloud.agent.remove.policy": "remove" 51 | } 52 | } 53 | ] 54 | } 55 | -------------------------------------------------------------------------------- /cloud-openstack-server/src/test/resources/__files/v2.1-nova-id-servers-detail-stopping.json: -------------------------------------------------------------------------------- 1 | { 2 | "servers": [ 3 | { 4 | "OS-EXT-STS:task_state": "powering-off", 5 | "addresses": { 6 | "test_network": [ 7 | { 8 | "OS-EXT-IPS-MAC:mac_addr": "00:00:00:00:00:00", 9 | "version": 4, 10 | "addr": "192.168.1.42", 11 | "OS-EXT-IPS:type": "fixed" 12 | }, 13 | { 14 | "OS-EXT-IPS-MAC:mac_addr": "00:00:00:00:00:00", 15 | "version": 4, 16 | "addr": "10.248.7.42", 17 | "OS-EXT-IPS:type": "floating" 18 | } 19 | ] 20 | }, 21 | "image": { 22 | "id": "image-id" 23 | }, 24 | "OS-EXT-STS:vm_state": "stopping", 25 | "OS-SRV-USG:launched_at": null, 26 | "flavor": { 27 | "id": "flavor-id" 28 | }, 29 | "id": "server-id", 30 | "user_id": "user-id", 31 | "OS-DCF:diskConfig": "MANUAL", 32 | "accessIPv4": "10.248.7.42", 33 | "accessIPv6": "", 34 | "progress": 0, 35 | "OS-EXT-STS:power_state": 0, 36 | "OS-EXT-AZ:availability_zone": "", 37 | "config_drive": "", 38 | "status": "ACTIVE", 39 | "updated": "2019-10-31T08:49:21Z", 40 | "hostId": "", 41 | "OS-SRV-USG:terminated_at": null, 42 | "key_name": "test_key_pair", 43 | "name": "openstack-test-teamcity-plugin-42", 44 | "created": "2019-10-31T08:49:21Z", 45 | "tenant_id": "tenant-id", 46 | "os-extended-volumes:volumes_attached": [ 47 | ], 48 | "metadata": { 49 | "agent.cloud.ip": "10.248.7.42", 50 | "teamcity.cloud.agent.remove.policy": "remove" 51 | } 52 | } 53 | ] 54 | } 55 | -------------------------------------------------------------------------------- /cloud-openstack-agent/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | 5 | cloud-openstack 6 | jetbrains.buildServer.clouds 7 | 1.7-SNAPSHOT 8 | 9 | cloud-openstack-agent 10 | jar 11 | 12 | 13 | 14 | 15 | jetbrains.buildServer.clouds 16 | cloud-openstack-common 17 | ${project.version} 18 | 19 | 20 | 21 | org.jetbrains.teamcity 22 | agent-api 23 | provided 24 | 25 | 26 | 27 | javax.servlet 28 | servlet-api 29 | 30 | 31 | 32 | 33 | 34 | 35 | org.jetbrains.teamcity.internal 36 | agent 37 | ${teamcity.version} 38 | provided 39 | 40 | 41 | 42 | 43 | org.jetbrains.teamcity 44 | tests-support 45 | test 46 | 47 | 48 | com.github.tomakehurst 49 | wiremock 50 | 2.27.2 51 | test 52 | 53 | 54 | 55 | junit 56 | junit 57 | 58 | 59 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /cloud-openstack-server/src/main/java/jetbrains/buildServer/clouds/openstack/OpenstackIdentity.java: -------------------------------------------------------------------------------- 1 | package jetbrains.buildServer.clouds.openstack; 2 | 3 | public class OpenstackIdentity { 4 | 5 | private String credentials; 6 | private String tenant; 7 | private String tenantDomain; 8 | 9 | /** 10 | * OpenStack v2/v3 keystone JClouds <-> Specs API (cf. https://issues.apache.org/jira/browse/JCLOUDS-1414)
11 | * v2 tenant:user = 'tenant:user' in credentials, empty 'tenant' and 'tenantDomain'
12 | * v3 tenant:user = 'user' in credentials, 'tenant' as tenant
13 | * v3 tenant:domain_user:user = 'domain_user:user' in credentials, as tenant
14 | * v3 domainTenant:tenant:domainUser:user = 'domainUser:user' in creds, others properties in correct field
15 | * 16 | * @param identity String 17 | * @param keyStoneVersion Object 18 | */ 19 | public OpenstackIdentity(String identity, String keyStoneVersion) { 20 | if (identity == null) { 21 | return; 22 | } 23 | String[] array = identity.split(":"); 24 | if (array.length == 0) { 25 | this.credentials = ""; 26 | } else if (array.length == 1) { 27 | this.credentials = array[0]; 28 | } else if (array.length == 2) { 29 | if ("2".equals(keyStoneVersion)) { 30 | this.credentials = identity; 31 | } else { 32 | this.credentials = array[1]; 33 | this.tenant = array[0]; 34 | } 35 | } else if (array.length == 3) { 36 | this.credentials = array[1] + ":" + array[2]; 37 | if (!"2".equals(keyStoneVersion)) { 38 | this.tenant = array[0]; 39 | } 40 | } else { 41 | this.credentials = array[array.length - 2] + ":" + array[array.length - 1]; 42 | if (!"2".equals(keyStoneVersion)) { 43 | this.tenant = array[array.length - 3]; 44 | this.tenantDomain = array[array.length - 4]; 45 | } 46 | } 47 | } 48 | 49 | /** 50 | * For Openstack v2: 'tenant:user'
51 | * For Openstack v3: '[domain_user:]user' 52 | * 53 | * @return Credentials for OPenstack keystone 54 | */ 55 | public String getCredendials() { 56 | return this.credentials; 57 | } 58 | 59 | /** 60 | * For Openstack v2: empty (tenant is part of credentials)
61 | * For Openstack v3: tenant 62 | * 63 | * @return Tenant 64 | */ 65 | public String getTenant() { 66 | return tenant; 67 | } 68 | 69 | /** 70 | * For Openstack v3 only 71 | * 72 | * @return domain_tenant 73 | */ 74 | public String getTenantDomain() { 75 | return tenantDomain; 76 | } 77 | 78 | } 79 | -------------------------------------------------------------------------------- /cloud-openstack-server/src/main/resources/buildServerResources/profile-settings.jsp: -------------------------------------------------------------------------------- 1 | <%@ page import="jetbrains.buildServer.clouds.openstack.OpenstackCloudParameters" %> 2 | <%@ taglib prefix="props" tagdir="/WEB-INF/tags/props" %> 3 | <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | YAML formatted list of agent images. i.e:
47 | my_teamcity_image:
48 |     image: ubuntu_trusty_14.04
49 |     flavor: m1.small
50 |     network: my_openstack_network
51 |     security_group: default
52 |     *key_pair: my_username_keypair
53 |     *auto_floating_ip: boolean (default:false)
54 |     *user_script: my_startup_script.sh
55 |     *availability_zone: my_zone
56 | starred parameters are optional
57 | user_script should be located at teamcity server in directopy TEAMCITY_DATA_PATH/system/pluginData/openstack 58 |
59 | 60 | 61 | -------------------------------------------------------------------------------- /cloud-openstack-server/src/main/java/jetbrains/buildServer/clouds/openstack/OpenstackCloudClientFactory.java: -------------------------------------------------------------------------------- 1 | package jetbrains.buildServer.clouds.openstack; 2 | 3 | import java.util.Collections; 4 | import java.util.Map; 5 | import java.util.concurrent.Executors; 6 | 7 | import org.jetbrains.annotations.NotNull; 8 | import org.jetbrains.annotations.Nullable; 9 | 10 | import com.intellij.openapi.diagnostic.Logger; 11 | 12 | import jetbrains.buildServer.clouds.CloudClientFactory; 13 | import jetbrains.buildServer.clouds.CloudClientParameters; 14 | import jetbrains.buildServer.clouds.CloudRegistrar; 15 | import jetbrains.buildServer.clouds.CloudState; 16 | import jetbrains.buildServer.log.Loggers; 17 | import jetbrains.buildServer.serverSide.AgentDescription; 18 | import jetbrains.buildServer.serverSide.PropertiesProcessor; 19 | import jetbrains.buildServer.serverSide.ServerPaths; 20 | import jetbrains.buildServer.util.NamedDaemonThreadFactory; 21 | import jetbrains.buildServer.web.openapi.PluginDescriptor; 22 | 23 | public class OpenstackCloudClientFactory implements CloudClientFactory { 24 | 25 | @NotNull 26 | private static final Logger LOG = Logger.getInstance(Loggers.CLOUD_CATEGORY_ROOT); 27 | @NotNull 28 | private final String cloudProfileSettings; 29 | @NotNull 30 | private final ServerPaths serverPaths; 31 | 32 | public OpenstackCloudClientFactory(@NotNull final CloudRegistrar cloudRegistrar, @NotNull final PluginDescriptor pluginDescriptor, 33 | @NotNull final ServerPaths serverPaths) { 34 | cloudProfileSettings = pluginDescriptor.getPluginResourcesPath("profile-settings.jsp"); 35 | this.serverPaths = serverPaths; 36 | cloudRegistrar.registerCloudFactory(this); 37 | } 38 | 39 | @NotNull 40 | public String getCloudCode() { 41 | return OpenstackCloudParameters.CLOUD_TYPE; 42 | } 43 | 44 | @NotNull 45 | public String getDisplayName() { 46 | return OpenstackCloudParameters.CLOUD_DISPLAY_NAME; 47 | } 48 | 49 | @Nullable 50 | public String getEditProfileUrl() { 51 | return cloudProfileSettings; 52 | } 53 | 54 | @NotNull 55 | public Map getInitialParameterValues() { 56 | return Collections.emptyMap(); 57 | } 58 | 59 | @NotNull 60 | public PropertiesProcessor getPropertiesProcessor() { 61 | return properties -> Collections.emptyList(); 62 | } 63 | 64 | public boolean canBeAgentOfType(@NotNull final AgentDescription agentDescription) { 65 | final Map configParams = agentDescription.getConfigurationParameters(); 66 | return configParams.containsValue(OpenstackCloudParameters.CLOUD_TYPE); 67 | } 68 | 69 | @NotNull 70 | public OpenstackCloudClient createNewClient(@NotNull final CloudState state, @NotNull final CloudClientParameters params) { 71 | return new OpenstackCloudClient(params, serverPaths, 72 | duty -> Executors.newSingleThreadScheduledExecutor(new NamedDaemonThreadFactory("openstack-" + duty))); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /cloud-openstack-server/src/test/java/jetbrains/buildServer/clouds/openstack/OpenstackIdentityTest.java: -------------------------------------------------------------------------------- 1 | package jetbrains.buildServer.clouds.openstack; 2 | 3 | import org.testng.Assert; 4 | import org.testng.annotations.Test; 5 | 6 | public class OpenstackIdentityTest { 7 | 8 | @Test 9 | public void testOpenstackIdentityNullOrEmpty() { 10 | Assert.assertEquals(new OpenstackIdentity("", null).getCredendials(), ""); 11 | Assert.assertEquals(new OpenstackIdentity("", "").getCredendials(), ""); 12 | Assert.assertEquals(new OpenstackIdentity("", "3").getCredendials(), ""); 13 | } 14 | 15 | @Test 16 | public void testOpenstackIdentityV2() { 17 | Assert.assertEquals(new OpenstackIdentity("", "2").getCredendials(), ""); 18 | Assert.assertEquals(new OpenstackIdentity("user", "2").getCredendials(), "user"); 19 | 20 | Assert.assertEquals(new OpenstackIdentity("tenant:user", "2").getCredendials(), "tenant:user"); 21 | Assert.assertNull(new OpenstackIdentity("tenant:user", "2").getTenant()); 22 | Assert.assertNull(new OpenstackIdentity("tenant:user", "2").getTenantDomain()); 23 | 24 | Assert.assertEquals(new OpenstackIdentity("fake:tenant:user", "2").getCredendials(), "tenant:user"); 25 | Assert.assertNull(new OpenstackIdentity("fake:tenant:user", "2").getTenant()); 26 | Assert.assertNull(new OpenstackIdentity("fake:tenant:user", "2").getTenantDomain()); 27 | } 28 | 29 | @Test 30 | public void testOpenstackIdentityV3() { 31 | Assert.assertEquals(new OpenstackIdentity("", "3").getCredendials(), ""); 32 | Assert.assertEquals(new OpenstackIdentity("user", "3").getCredendials(), "user"); 33 | 34 | Assert.assertEquals(new OpenstackIdentity("tenant:user", "3").getCredendials(), "user"); 35 | Assert.assertEquals(new OpenstackIdentity("tenant:user", "3").getTenant(), "tenant"); 36 | Assert.assertNull(new OpenstackIdentity("tenant:user", "3").getTenantDomain()); 37 | 38 | Assert.assertEquals(new OpenstackIdentity("tenant:domain_user:user", "3").getCredendials(), "domain_user:user"); 39 | Assert.assertEquals(new OpenstackIdentity("tenant:domain_user:user", "3").getTenant(), "tenant"); 40 | Assert.assertNull(new OpenstackIdentity("tenant:domain_user:user", "3").getTenantDomain()); 41 | 42 | Assert.assertEquals(new OpenstackIdentity("domain_tenant:tenant:domain_user:user", "3").getCredendials(), "domain_user:user"); 43 | Assert.assertEquals(new OpenstackIdentity("domain_tenant:tenant:domain_user:user", "3").getTenant(), "tenant"); 44 | Assert.assertEquals(new OpenstackIdentity("domain_tenant:tenant:domain_user:user", "3").getTenantDomain(), "domain_tenant"); 45 | 46 | Assert.assertEquals(new OpenstackIdentity("fake:domain_tenant:tenant:domain_user:user", "3").getCredendials(), "domain_user:user"); 47 | Assert.assertEquals(new OpenstackIdentity("fake:domain_tenant:tenant:domain_user:user", "3").getTenant(), "tenant"); 48 | Assert.assertEquals(new OpenstackIdentity("fake:domain_tenant:tenant:domain_user:user", "3").getTenantDomain(), "domain_tenant"); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /cloud-openstack-server/src/test/resources/__files/v2.1-nova-id-servers-detail-restore.json: -------------------------------------------------------------------------------- 1 | { 2 | "servers": [ 3 | { 4 | "OS-EXT-STS:task_state": null, 5 | "addresses": { 6 | "test_network": [ 7 | { 8 | "OS-EXT-IPS-MAC:mac_addr": "00:00:00:00:00:00", 9 | "version": 4, 10 | "addr": "192.168.1.42", 11 | "OS-EXT-IPS:type": "fixed" 12 | }, 13 | { 14 | "OS-EXT-IPS-MAC:mac_addr": "00:00:00:00:00:00", 15 | "version": 4, 16 | "addr": "10.248.7.42", 17 | "OS-EXT-IPS:type": "floating" 18 | } 19 | ] 20 | }, 21 | "image": { 22 | "id": "image-id" 23 | }, 24 | "OS-EXT-STS:vm_state": "active", 25 | "OS-SRV-USG:launched_at": null, 26 | "flavor": { 27 | "id": "flavor-id" 28 | }, 29 | "id": "server-id", 30 | "user_id": "user-id", 31 | "OS-DCF:diskConfig": "MANUAL", 32 | "accessIPv4": "10.248.7.42", 33 | "accessIPv6": "", 34 | "progress": 0, 35 | "OS-EXT-STS:power_state": 0, 36 | "OS-EXT-AZ:availability_zone": "", 37 | "config_drive": "", 38 | "status": "ACTIVE", 39 | "updated": "2019-10-31T08:49:21Z", 40 | "hostId": "", 41 | "OS-SRV-USG:terminated_at": null, 42 | "key_name": "test_key_pair", 43 | "name": "openstack-test-teamcity-plugin-42", 44 | "created": "2019-10-31T08:49:21Z", 45 | "tenant_id": "tenant-id", 46 | "os-extended-volumes:volumes_attached": [ 47 | ], 48 | "metadata": { 49 | "agent.cloud.ip": "10.248.7.42", 50 | "teamcity.cloud.agent.remove.policy": "remove" 51 | } 52 | }, 53 | { 54 | "OS-EXT-STS:task_state": null, 55 | "addresses": { 56 | "test_network": [ 57 | { 58 | "OS-EXT-IPS-MAC:mac_addr": "00:00:00:00:00:00", 59 | "version": 4, 60 | "addr": "192.168.1.41", 61 | "OS-EXT-IPS:type": "fixed" 62 | }, 63 | { 64 | "OS-EXT-IPS-MAC:mac_addr": "00:00:00:00:00:00", 65 | "version": 4, 66 | "addr": "10.248.7.41", 67 | "OS-EXT-IPS:type": "floating" 68 | } 69 | ] 70 | }, 71 | "image": { 72 | "id": "image-id" 73 | }, 74 | "OS-EXT-STS:vm_state": "active", 75 | "OS-SRV-USG:launched_at": null, 76 | "flavor": { 77 | "id": "flavor-id" 78 | }, 79 | "id": "server-id", 80 | "user_id": "user-id", 81 | "OS-DCF:diskConfig": "MANUAL", 82 | "accessIPv4": "10.248.7.42", 83 | "accessIPv6": "", 84 | "progress": 0, 85 | "OS-EXT-STS:power_state": 0, 86 | "OS-EXT-AZ:availability_zone": "", 87 | "config_drive": "", 88 | "status": "ACTIVE", 89 | "updated": "2019-10-31T08:49:21Z", 90 | "hostId": "", 91 | "OS-SRV-USG:terminated_at": null, 92 | "key_name": "test_key_pair", 93 | "name": "openstack-test-teamcity-plugin-41", 94 | "created": "2019-10-31T08:49:21Z", 95 | "tenant_id": "tenant-id", 96 | "os-extended-volumes:volumes_attached": [ 97 | ], 98 | "metadata": { 99 | "agent.cloud.ip": "10.248.7.42", 100 | "teamcity.cloud.agent.remove.policy": "remove" 101 | } 102 | } 103 | ] 104 | } 105 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | jetbrains.buildServer.clouds 5 | cloud-openstack 6 | 1.7-SNAPSHOT 7 | pom 8 | 9 | 2021.1 10 | 2.3.0 11 | 1.8 12 | 1.8 13 | UTF-8 14 | 15 | 16 | scm:git:https://github.com/yandex-qatools/teamcity-openstack-plugin 17 | HEAD 18 | 19 | 20 | 21 | JetBrains 22 | http://download.jetbrains.com/teamcity-repository 23 | 24 | 25 | 26 | 27 | 28 | 29 | org.jetbrains.teamcity 30 | agent-api 31 | ${teamcity.version} 32 | provided 33 | 34 | 35 | org.jetbrains.teamcity 36 | server-api 37 | ${teamcity.version} 38 | provided 39 | 40 | 41 | org.jetbrains.teamcity 42 | cloud-shared 43 | ${teamcity.version} 44 | provided 45 | 46 | 47 | org.jetbrains.teamcity 48 | cloud-interface 49 | ${teamcity.version} 50 | provided 51 | 52 | 53 | 54 | 55 | org.jetbrains.teamcity 56 | tests-support 57 | ${teamcity.version} 58 | test 59 | 60 | 61 | 62 | junit 63 | junit 64 | 65 | 66 | 67 | 68 | org.testng 69 | testng 70 | 71 | 7.4.0 72 | test 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | org.apache.maven.plugins 81 | maven-release-plugin 82 | 3.0.0-M4 83 | 84 | 85 | 86 | 87 | 88 | cloud-openstack-server 89 | cloud-openstack-agent 90 | cloud-openstack-common 91 | build 92 | 93 | 94 | -------------------------------------------------------------------------------- /cloud-openstack-server/src/main/java/jetbrains/buildServer/clouds/openstack/OpenstackApi.java: -------------------------------------------------------------------------------- 1 | package jetbrains.buildServer.clouds.openstack; 2 | 3 | import java.util.List; 4 | import java.util.Properties; 5 | 6 | import org.jclouds.ContextBuilder; 7 | import org.jclouds.location.reference.LocationConstants; 8 | import org.jclouds.openstack.keystone.config.KeystoneProperties; 9 | import org.jclouds.openstack.neutron.v2.NeutronApi; 10 | import org.jclouds.openstack.neutron.v2.NeutronApiMetadata; 11 | import org.jclouds.openstack.neutron.v2.domain.FloatingIP; 12 | import org.jclouds.openstack.neutron.v2.domain.Network; 13 | import org.jclouds.openstack.nova.v2_0.NovaApi; 14 | import org.jclouds.openstack.nova.v2_0.NovaApiMetadata; 15 | import org.jclouds.openstack.nova.v2_0.domain.Flavor; 16 | import org.jclouds.openstack.nova.v2_0.domain.Image; 17 | import org.jclouds.openstack.nova.v2_0.features.ServerApi; 18 | import org.springframework.util.StringUtils; 19 | 20 | public class OpenstackApi { 21 | 22 | private final String region; 23 | 24 | private final NeutronApi neutronApi; 25 | private final NovaApi novaApi; 26 | 27 | public OpenstackApi(String endpointUrl, String identity, String password, String region) { 28 | 29 | // For http content debug during unit tests, 30 | // - Fill Constants.PROPERTY_LOGGER_WIRE_LOG_SENSITIVE_INFO to true in overrides properties 31 | // - Add '.modules(ImmutableSet.of(new SLF4JLoggingModule()))' in two ContextBuilder 32 | // - Update log level to 'DEBUG' in 'log4j.xml'. 33 | 34 | this.region = region; 35 | 36 | final Properties overrides = new Properties(); 37 | final String keyStoneVersion = getKeystoneVersion(endpointUrl); 38 | final OpenstackIdentity identityObject = new OpenstackIdentity(identity, keyStoneVersion); 39 | overrides.put(KeystoneProperties.KEYSTONE_VERSION, keyStoneVersion); 40 | overrides.put(LocationConstants.PROPERTY_ZONES, region); 41 | 42 | if (!StringUtils.isEmpty(identityObject.getTenant())) { 43 | // Only for keystone v3, for v2 'tenant' is part of Credentials (cf. OpenstackIdentity) 44 | overrides.put(KeystoneProperties.SCOPE, "project:" + identityObject.getTenant()); 45 | } 46 | if (!StringUtils.isEmpty(identityObject.getTenantDomain())) { 47 | overrides.put(KeystoneProperties.PROJECT_DOMAIN_NAME, identityObject.getTenantDomain()); 48 | } 49 | 50 | neutronApi = ContextBuilder.newBuilder(new NeutronApiMetadata()).credentials(identityObject.getCredendials(), password).endpoint(endpointUrl) 51 | .overrides(overrides).buildApi(NeutronApi.class); 52 | 53 | novaApi = ContextBuilder.newBuilder(new NovaApiMetadata()).endpoint(endpointUrl).credentials(identityObject.getCredendials(), password) 54 | .overrides(overrides).buildApi(NovaApi.class); 55 | } 56 | 57 | public String getImageIdByName(String name) { 58 | List images = novaApi.getImageApi(region).listInDetail().concat().toList(); 59 | for (Image image : images) { 60 | if (image.getName().equals(name)) 61 | return image.getId(); 62 | } 63 | return null; 64 | } 65 | 66 | public String getFlavorIdByName(String name) { 67 | List flavors = novaApi.getFlavorApi(region).listInDetail().concat().toList(); 68 | for (Flavor flavor : flavors) { 69 | if (flavor.getName().equals(name)) 70 | return flavor.getId(); 71 | } 72 | return null; 73 | } 74 | 75 | public String getNetworkIdByName(String name) { 76 | List networks = neutronApi.getNetworkApi(region).list().concat().toList(); 77 | for (Network network : networks) { 78 | if (network.getName().equals(name)) 79 | return network.getId(); 80 | } 81 | return null; 82 | } 83 | 84 | public ServerApi getNovaServerApi() { 85 | return novaApi.getServerApi(region); 86 | } 87 | 88 | public void associateFloatingIp(String serverId, String ip) { 89 | novaApi.getFloatingIPApi(region).get().addToServer(ip, serverId); 90 | } 91 | 92 | public String getFloatingIpAvailable() { 93 | for (FloatingIP ip : neutronApi.getFloatingIPApi(region).list().concat().toList()) { 94 | if (StringUtils.isEmpty(ip.getFixedIpAddress())) { 95 | return ip.getFloatingIpAddress(); 96 | } 97 | } 98 | return null; 99 | } 100 | 101 | /** 102 | * Return keystone version (2 or 3) from endpoint URL 103 | * 104 | * @param url endpoint 105 | * @return 2 or 3 106 | */ 107 | protected static String getKeystoneVersion(String url) { 108 | final String def = "3"; 109 | if (StringUtils.isEmpty(url)) { 110 | return def; 111 | } 112 | int index = url.toLowerCase().lastIndexOf("/v") + 2; 113 | if (url.length() > index) { 114 | return url.substring(index, index + 1); 115 | } 116 | return def; 117 | } 118 | 119 | } 120 | -------------------------------------------------------------------------------- /cloud-openstack-server/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | 5 | cloud-openstack 6 | jetbrains.buildServer.clouds 7 | 1.7-SNAPSHOT 8 | 9 | cloud-openstack-server 10 | jar 11 | 12 | 13 | 14 | jetbrains.buildServer.clouds 15 | cloud-openstack-common 16 | ${project.version} 17 | 18 | 19 | 20 | org.jetbrains.teamcity 21 | server-api 22 | 23 | 24 | com.google.guava 25 | guava 26 | 27 | 28 | com.google.code.gson 29 | gson 30 | 31 | 32 | provided 33 | 34 | 35 | 36 | com.google.code.gson 37 | gson 38 | 2.8.7 39 | 40 | 41 | 42 | org.yaml 43 | snakeyaml 44 | 1.29 45 | 46 | 47 | 48 | com.jcabi 49 | jcabi-log 50 | 0.19.0 51 | 52 | 53 | 54 | org.slf4j 55 | slf4j-api 56 | 57 | 58 | 59 | 60 | 61 | 62 | org.apache.jclouds.driver 63 | jclouds-slf4j 64 | ${jclouds.version} 65 | 66 | 67 | org.apache.jclouds.driver 68 | jclouds-sshj 69 | ${jclouds.version} 70 | 71 | 72 | 73 | 74 | org.apache.jclouds.api 75 | openstack-keystone 76 | ${jclouds.version} 77 | 78 | 79 | org.apache.jclouds.api 80 | openstack-nova 81 | ${jclouds.version} 82 | 83 | 84 | org.apache.jclouds.api 85 | openstack-neutron 86 | ${jclouds.version} 87 | 88 | 89 | 90 | org.jetbrains.teamcity 91 | cloud-shared 92 | provided 93 | 94 | 95 | org.jetbrains.teamcity 96 | cloud-interface 97 | provided 98 | 99 | 100 | 101 | 102 | org.jetbrains.teamcity 103 | tests-support 104 | test 105 | 106 | 107 | org.mockito 108 | mockito-core 109 | 3.11.2 110 | test 111 | 112 | 113 | com.github.tomakehurst 114 | wiremock 115 | 2.27.2 116 | test 117 | 118 | 119 | 120 | junit 121 | junit 122 | 123 | 124 | 125 | com.google.guava 126 | guava 127 | 128 | 129 | 130 | 131 | 132 | -------------------------------------------------------------------------------- /cloud-openstack-agent/src/main/java/jetbrains/buildServer/clouds/openstack/OpenstackAgentProperties.java: -------------------------------------------------------------------------------- 1 | package jetbrains.buildServer.clouds.openstack; 2 | 3 | import java.io.IOException; 4 | import java.io.InputStream; 5 | import java.lang.reflect.Type; 6 | import java.net.HttpURLConnection; 7 | import java.net.URL; 8 | import java.nio.charset.StandardCharsets; 9 | import java.util.HashMap; 10 | import java.util.Map; 11 | 12 | import javax.servlet.http.HttpServletResponse; 13 | 14 | import org.apache.commons.io.IOUtils; 15 | import org.jetbrains.annotations.NotNull; 16 | 17 | import com.google.gson.Gson; 18 | import com.google.gson.JsonElement; 19 | import com.google.gson.JsonParser; 20 | import com.google.gson.reflect.TypeToken; 21 | import com.intellij.openapi.diagnostic.Logger; 22 | 23 | import jetbrains.buildServer.agent.AgentLifeCycleAdapter; 24 | import jetbrains.buildServer.agent.AgentLifeCycleListener; 25 | import jetbrains.buildServer.agent.BuildAgent; 26 | import jetbrains.buildServer.agent.BuildAgentConfigurationEx; 27 | import jetbrains.buildServer.log.Loggers; 28 | import jetbrains.buildServer.util.EventDispatcher; 29 | 30 | public class OpenstackAgentProperties extends AgentLifeCycleAdapter { 31 | @NotNull 32 | private static final Logger LOG = Loggers.AGENT; 33 | @NotNull 34 | private final BuildAgentConfigurationEx agentConfiguration; 35 | @NotNull 36 | private String metadataUrl = "http://169.254.169.254/openstack/latest/meta_data.json"; // NOSONAR: Openstack IP doesn't change 37 | 38 | private static final String LOG_SETTINGS = "Setting %s to %s"; 39 | 40 | public OpenstackAgentProperties(@NotNull final BuildAgentConfigurationEx agentConfiguration, 41 | @NotNull EventDispatcher dispatcher) { 42 | this.agentConfiguration = agentConfiguration; 43 | dispatcher.addListener(this); 44 | } 45 | 46 | @Override 47 | public void afterAgentConfigurationLoaded(@NotNull BuildAgent agent) { 48 | try { 49 | if ("true".equals(agentConfiguration.getConfigurationParameters().get(OpenstackCloudParameters.AGENT_METADATA_DISABLE))) { 50 | LOG.info("Openstack metadata usage disabled (agent configuration not overridden)"); 51 | return; 52 | } 53 | 54 | String rawMetadata = readDataFromUrl(metadataUrl); 55 | LOG.info(String.format("Detected Openstack instance. Will write parameters from metadata: %s", metadataUrl)); 56 | 57 | JsonElement metadataElement = new JsonParser().parse(rawMetadata); 58 | 59 | String uuid = metadataElement.getAsJsonObject().get("uuid").getAsString(); 60 | if (uuid != null) { 61 | LOG.info(String.format(LOG_SETTINGS, OpenstackCloudParameters.OPENSTACK_INSTANCE_ID, uuid)); 62 | agentConfiguration.addConfigurationParameter(OpenstackCloudParameters.OPENSTACK_INSTANCE_ID, uuid); 63 | } 64 | 65 | String name = metadataElement.getAsJsonObject().get("name").getAsString(); 66 | if (name != null) { 67 | LOG.info(String.format(LOG_SETTINGS, "name", name)); 68 | agentConfiguration.setName(name); 69 | } 70 | 71 | // user data is optionnal 72 | JsonElement teamCityUserData = metadataElement.getAsJsonObject().get("meta"); 73 | if (teamCityUserData != null) { 74 | Type type = new TypeToken>() { 75 | }.getType(); 76 | HashMap customParameters = new Gson().fromJson(teamCityUserData, type); 77 | for (Map.Entry entry : customParameters.entrySet()) { 78 | LOG.info(String.format(LOG_SETTINGS, entry.getKey(), entry.getValue())); 79 | agentConfiguration.addConfigurationParameter(entry.getKey(), entry.getValue()); 80 | if (OpenstackCloudParameters.AGENT_CLOUD_IP.equals(entry.getKey())) { 81 | agentConfiguration.addAlternativeAgentAddress(entry.getValue()); 82 | } 83 | } 84 | } 85 | 86 | LOG.info(String.format(LOG_SETTINGS, OpenstackCloudParameters.AGENT_CLOUD_TYPE, OpenstackCloudParameters.CLOUD_TYPE)); 87 | agentConfiguration.addConfigurationParameter(OpenstackCloudParameters.AGENT_CLOUD_TYPE, OpenstackCloudParameters.CLOUD_TYPE); 88 | 89 | } catch (IOException e) { 90 | LOG.info(String.format("It seems build-agent launched at non-Openstack instance: %s", e.getMessage())); 91 | } catch (Exception e) { 92 | LOG.error(String.format("Unknow problem on Openstack plugin: %s.", e.getMessage()), e); 93 | } 94 | } 95 | 96 | private static String readDataFromUrl(String sURL) throws IOException { 97 | String data = ""; 98 | URL url = new URL(sURL); 99 | HttpURLConnection ctx = (HttpURLConnection) url.openConnection(); 100 | if (ctx.getResponseCode() != HttpServletResponse.SC_OK) { 101 | throw new IOException(String.format("Http code %s on meta data URL", ctx.getResponseCode())); 102 | } 103 | try (InputStream in = url.openStream()) { 104 | data = IOUtils.toString(in, StandardCharsets.UTF_8.name()); 105 | } 106 | return data.trim(); 107 | } 108 | 109 | /** 110 | * Override Meta Data URL (using only for unit test) 111 | * 112 | * @param url New URL 113 | */ 114 | protected void setUrlMetaData(String url) { 115 | this.metadataUrl = url; 116 | } 117 | 118 | /** 119 | * Get Meta Data URL (using only for unit test) 120 | */ 121 | protected String getMetadataUrl() { 122 | return this.metadataUrl; 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://teamcity.jetbrains.com/app/rest/builds/buildType:TeamCityThirdPartyPlugins_OpenStackCloudSupport_BuildSnapshotIntegration/statusIcon)](https://teamcity.jetbrains.com/viewType.html?buildTypeId=TeamCityThirdPartyPlugins_OpenStackCloudSupport_BuildSnapshotIntegration) [![SonarCloud Status](https://sonarcloud.io/api/project_badges/measure?project=jetbrains.buildServer.clouds:cloud-openstack&metric=alert_status)](https://sonarcloud.io/dashboard?id=jetbrains.buildServer.clouds:cloud-openstack) 2 | 3 | # TeamCity Cloud Openstack plugin 4 | 5 | ## Download 6 | 7 | | Releases | Snapshot (last) | Compatibility | 8 | |---|---|---| 9 | | [Download](https://teamcity.jetbrains.com/viewType.html?buildTypeId=TeamCityThirdPartyPlugins_OpenStackCloudSupport_Release) | [Download](https://teamcity.jetbrains.com/repository/download/TeamCityThirdPartyPlugins_OpenStackCloudSupport_BuildSnapshotIntegration/.lastSuccessful/cloud-openstack.zip?guest=1) | TeamCity 10+ | 10 | 11 | 12 | ## Agent Configuration 13 | 14 | 1. create one or more Openstack machines 15 | 2. install a standard TeamCity build agent on them, you only need to fill TEAMCITY_SERVER_URL 16 | WARNING: you shouldn't start build agent while preparing image 17 | 3. create images from machines with installed agent. 18 | 19 | ## Server Configuration 20 | 21 | Fill cloud config with your openstack-instance parameters. 22 | Configuration example: 23 |
24 | 25 |
26 | 27 | Once you have created a cloud profile in TeamCity with one or several images, TeamCity does a test start for all the new images to discover the environment of the build agents configured on them. 28 | If for a queued build there are no regular non-cloud agents available, TeamCity will find a matching cloud image with a compatible agent and start a new instance for the image. After that, a virtual agent acts as a regular agent. 29 | You can specify idle time on the agent cloud profile, after which the instance should be terminated or stopped, in case you have an EBS-based instance. 30 | 31 | ### Agent images YAML parameters 32 | 33 | | **Property** | **Required** | **Description** | 34 | |---------------------|--------------|-----------------| 35 | | *image* | true | [Image](https://docs.openstack.org/glance/latest/admin/manage-images.html), ex: `ubuntu_16.04` | 36 | | *flavor* | true | [Flavor](https://docs.openstack.org/horizon/latest/admin/manage-flavors.html), ex: `m1.medium` | 37 | | *network* | true | [Network](https://developer.openstack.org/api-ref/network/v2/index.html#general-api-overview), ex: `VLAN` | 38 | | *security_group* | true | [Security group](https://docs.openstack.org/nova/latest/admin/security-groups.html), ex: `default` | 39 | | *key_pair* | false | [Key pair](https://docs.openstack.org/horizon/latest/user/configure-access-and-security-for-instances.html), ex: `my-key` ; required for SSH connection on created instances (like TeamCity Agent Push feature) | 40 | | *auto_floating_ip* | false | Boolean (`false` by default) for [floating ip](https://docs.openstack.org/ocata/user-guide/cli-manage-ip-addresses.html) association ; first from pool used | 41 | | *user_script* | false | Script executed on instance start | 42 | | *availability_zone* | false | Region for server instance (if not the global configured) 43 | 44 | ### OpenStack v2 Identity 45 | 46 | The *Identity* defines the tenant/project and username, like: `tenant:user` 47 | 48 | ### OpenStack v3 Identity 49 | 50 | The *Identity* defines at minimum the *tenant* and *user* informations, but could in addition defines the *domain(s)* of each items. In this case, only [project-scope](https://docs.openstack.org/keystone/queens/api_curl_examples.html#project-scoped) is supported. 51 | 52 | The Identity is a 2-4 blocks string in this order: `[domain_tenant:]tenant:[domain_user:]user` (Warning: Priority given to *domain_user* for a 3 blocks strings). 53 | 54 | #### Samples 55 | 56 | Below some samples from *Identity* field to JSon produced on https://openstack.hostname.com/v3/auth/tokens URL. 57 | 58 | ##### myTenant:foo 59 | 60 | ``` 61 | {"auth":{"identity":{"methods":["password"],"password":{"user":{"name":"foo","domain":{},"password":"***"}}},"scope":{"project":{"name":"myTenant","domain":{}}}}} 62 | ``` 63 | 64 | ##### myTenant:ldap:foo 65 | 66 | NB: *domain_user* is used for both domains. 67 | 68 | ``` 69 | {"auth":{"identity":{"methods":["password"],"password":{"user":{"name":"foo","domain":{"name":"ldap"},"password":"***"}}},"scope":{"project":{"name":"myTenant","domain":{"name":"ldap"}}}}} 70 | ``` 71 | 72 | ##### myTenantDomain:myTenant:ldap:foo 73 | 74 | ``` 75 | {"auth":{"identity":{"methods":["password"],"password":{"user":{"name":"foo","domain":{"name":"ldap"},"password":"***"}}},"scope":{"project":{"name":"myTenant","domain":{"name":"myTenantDomain"}}}}} 76 | ``` 77 | 78 | ### Update status and restore instances delays 79 | 80 | Some properties can be overridden to customize default plugin behavior, in `internal.properties` or TeamCity UI (*Administration > Diagnostics > Internal Properties*). 81 | 82 | ``` 83 | # Delay (in seconds) to execute recurrent instances update status 84 | openstack.status.delay = 10 85 | 86 | # Delay (in seconds) to execute first instances update status, after image profile creation or update 87 | openstack.status.initial = 5 88 | 89 | # Delay (in seconds) to execute restore instances, after image profile creation or update 90 | openstack.restore.delay = 1 91 | 92 | ``` 93 | 94 | ## Usage 95 | 96 | Use Openstack virtual agents as regular build agents 97 | 98 | 99 | ### Metadata disable 100 | 101 | With this plugin, any TeamCity agent on an Openstack virtual machine retrieves its information from `http://169.254.169.254/openstack/latest/meta_data.json` (uuid, name, user datas). 102 | 103 | If you want disable this metadata usage, please add in agent configuration file (`buildAgent.properties`): 104 | 105 | ``` 106 | clouds.openstack.metadata.disable = true 107 | ``` 108 | 109 | This usage is mainly designed for instantiate some TeamCity agent(s) on an Openstack virtual machine as a classic way (name defined in configuration file, ...), without they are in cloud profile. 110 | 111 | ## Build and Tests 112 | 113 | 1. clone current repository to your local computer 114 | 115 | 2. Provides 4 test files in *server* classpath (ex: `cloud-openstack-server/src/test/resources`) with content: 116 | 117 | ``` 118 | # File: test.v3.properties 119 | test.url=https://openstack.company.com/v3 120 | test.identity=domain_tenant:tenant:domain_user:user 121 | test.password=foobar 122 | test.region=region1 123 | ``` 124 | 125 | ``` 126 | # File: test.v3.yml 127 | openstack-test-teamcity-plugin: 128 | image: anyImage 129 | flavor: m1.small 130 | network: networkProviderName 131 | security_group: default 132 | key_pair: yourKey 133 | ``` 134 | 135 | ``` 136 | # File: test.v2.properties 137 | test.url=https://openstack.company.com/v2.0 138 | test.identity=tenant:user 139 | test.password=foobar 140 | test.region=region1 141 | ``` 142 | 143 | ``` 144 | # File: test.v2.yml 145 | openstack-test-teamcity-plugin: 146 | image: anyImage 147 | flavor: m1.small 148 | network: networkProviderName 149 | security_group: default 150 | key_pair: yourKey 151 | ``` 152 | 153 | 3. run `mvn clean package` (if OpenStack test endpoint requires trustStore certificate not in JVM used for test, add `-Djavax.net.ssl.trustStore=/path/to/cacerts`) 154 | 155 | 4. install resulted *cloud-openstack.zip* plugin file to TeamCity server 156 | 157 | ## Release process 158 | 159 | Execute locally: 160 | 161 | ``` 162 | # git clone and provide test files (see previous "Build and Tests" section) 163 | git fetch origin 164 | git reset --hard origin/master 165 | mvn -B clean release:clean release:prepare -Dusername=yourGitHubLogin -Dpassword=yourGitHubPasswordOrToken 166 | ``` 167 | 168 | And TeamCity [Release build](https://teamcity.jetbrains.com/viewType.html?buildTypeId=TeamCityThirdPartyPlugins_OpenStackCloudSupport_Release) will be executed. 169 | -------------------------------------------------------------------------------- /cloud-openstack-agent/src/test/java/jetbrains/buildServer/clouds/openstack/OpenstackAgentPropertiesTest.java: -------------------------------------------------------------------------------- 1 | package jetbrains.buildServer.clouds.openstack; 2 | 3 | import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; 4 | import static com.github.tomakehurst.wiremock.client.WireMock.get; 5 | import static com.github.tomakehurst.wiremock.client.WireMock.stubFor; 6 | import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo; 7 | import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig; 8 | 9 | import java.io.File; 10 | import java.io.IOException; 11 | import java.net.HttpURLConnection; 12 | import java.net.URL; 13 | import java.util.HashMap; 14 | import java.util.Map; 15 | 16 | import org.apache.commons.io.FileUtils; 17 | import org.apache.commons.lang3.StringUtils; 18 | import org.apache.http.HttpStatus; 19 | import org.jmock.Expectations; 20 | import org.jmock.Mockery; 21 | import org.jmock.lib.legacy.ClassImposteriser; 22 | import org.testng.Assert; 23 | import org.testng.annotations.AfterMethod; 24 | import org.testng.annotations.BeforeMethod; 25 | import org.testng.annotations.Test; 26 | 27 | import com.github.tomakehurst.wiremock.WireMockServer; 28 | import com.github.tomakehurst.wiremock.client.WireMock; 29 | 30 | import jetbrains.buildServer.agent.AgentLifeCycleListener; 31 | import jetbrains.buildServer.agent.BuildAgentConfigurationEx; 32 | import jetbrains.buildServer.clouds.openstack.util.Lo4jBeanAppender; 33 | import jetbrains.buildServer.util.EventDispatcher; 34 | 35 | public class OpenstackAgentPropertiesTest { 36 | 37 | private WireMockServer wireMockServer; 38 | 39 | @BeforeMethod 40 | public void setUp() { 41 | // Initialize manually, WireMockRule requires JUnit 42 | wireMockServer = new WireMockServer(wireMockConfig().dynamicPort()); 43 | wireMockServer.start(); 44 | WireMock.configureFor(wireMockServer.port()); 45 | Lo4jBeanAppender.clear(); 46 | } 47 | 48 | @AfterMethod 49 | public void tearDown() { 50 | wireMockServer.stop(); 51 | } 52 | 53 | private OpenstackAgentProperties prepareAgentProperties() throws IOException { 54 | return prepareAgentProperties(null, null); 55 | } 56 | 57 | private OpenstackAgentProperties prepareAgentProperties(String jsonResponseContentFileInTestResources) throws IOException { 58 | return prepareAgentProperties(jsonResponseContentFileInTestResources, null); 59 | } 60 | 61 | private OpenstackAgentProperties prepareAgentProperties(String jsonResponseContentFileInTestResources, Map parameters) 62 | throws IOException { 63 | return prepareAgentProperties(jsonResponseContentFileInTestResources, parameters, 200); 64 | } 65 | 66 | private OpenstackAgentProperties prepareAgentProperties(String jsonResponseContentFileInTestResources, Map parameters, 67 | int httpCodeReturn) throws IOException { 68 | 69 | final Mockery context = new Mockery(); 70 | final BuildAgentConfigurationEx agentConfig = context.mock(BuildAgentConfigurationEx.class); 71 | context.checking(new Expectations() { 72 | { 73 | allowing(agentConfig).addConfigurationParameter(with(any(String.class)), with(any(String.class))); 74 | allowing(agentConfig).setName(with(any(String.class))); 75 | allowing(agentConfig).addAlternativeAgentAddress(with(any(String.class))); 76 | allowing(agentConfig).getConfigurationParameters(); 77 | if (parameters != null && !parameters.isEmpty()) { 78 | will(returnValue(parameters)); 79 | } 80 | } 81 | }); 82 | 83 | context.setImposteriser(ClassImposteriser.INSTANCE); 84 | @SuppressWarnings("unchecked") 85 | EventDispatcher dispatcher = context.mock(EventDispatcher.class); 86 | context.checking(new Expectations() { 87 | { 88 | allowing(dispatcher).addListener(with(any(OpenstackAgentProperties.class))); 89 | } 90 | }); 91 | final OpenstackAgentProperties oap = new OpenstackAgentProperties(agentConfig, dispatcher); 92 | 93 | if (StringUtils.isNoneBlank(jsonResponseContentFileInTestResources)) { 94 | final String endpoint = "/openstack/test/meta_data.json"; 95 | stubFor(get(urlEqualTo(endpoint)).willReturn(aResponse().withStatus(httpCodeReturn).withHeader("Content-Type", "application/json") 96 | .withBody(FileUtils.readFileToString(new File("src/test/resources", jsonResponseContentFileInTestResources))))); 97 | oap.setUrlMetaData("http://localhost:" + wireMockServer.port() + endpoint); 98 | } 99 | return oap; 100 | } 101 | 102 | @Test 103 | public void testRealNetWorkMetaData() throws IOException { 104 | // Leave real Openstack URL 105 | OpenstackAgentProperties props = prepareAgentProperties(); 106 | props.afterAgentConfigurationLoaded(null); 107 | 108 | URL url = new URL(props.getMetadataUrl()); 109 | boolean openstackInstance = false; 110 | try { 111 | HttpURLConnection connection = (HttpURLConnection) url.openConnection(); 112 | connection.setRequestMethod("GET"); 113 | connection.connect(); 114 | int code = connection.getResponseCode(); 115 | if (HttpStatus.SC_OK == code) { 116 | openstackInstance = true; 117 | } 118 | } catch (IOException e) { 119 | // No open stack instance 120 | } 121 | 122 | if (openstackInstance) { 123 | Assert.assertTrue(Lo4jBeanAppender.contains("Detected Openstack instance. Will write parameters from metadata")); 124 | } else { 125 | Assert.assertTrue(Lo4jBeanAppender.contains("It seems build-agent launched at non-Openstack instance")); 126 | } 127 | } 128 | 129 | @Test 130 | public void testNoUserData() throws IOException { 131 | prepareAgentProperties("meta_data_noUserData.json").afterAgentConfigurationLoaded(null); 132 | 133 | Assert.assertTrue(Lo4jBeanAppender.contains("Detected Openstack instance. Will write parameters from metadata")); 134 | Assert.assertTrue(Lo4jBeanAppender.contains("Setting agent.cloud.uuid to xxxx-yyyyy-zzzz")); 135 | Assert.assertTrue(Lo4jBeanAppender.contains("Setting name to openstack_test")); 136 | 137 | Assert.assertFalse(Lo4jBeanAppender.contains("It seems build-agent launched at non-Openstack instance")); 138 | Assert.assertFalse(Lo4jBeanAppender.contains("Network is unreachable")); 139 | Assert.assertFalse(Lo4jBeanAppender.contains("Unknow problem on Openstack plugin")); 140 | } 141 | 142 | @Test 143 | public void testUserData() throws IOException { 144 | prepareAgentProperties("meta_data_userData.json").afterAgentConfigurationLoaded(null); 145 | 146 | Assert.assertTrue(Lo4jBeanAppender.contains("Detected Openstack instance. Will write parameters from metadata")); 147 | Assert.assertTrue(Lo4jBeanAppender.contains("Setting agent.cloud.ip to 192.168.42.42")); 148 | 149 | Assert.assertFalse(Lo4jBeanAppender.contains("It seems build-agent launched at non-Openstack instance")); 150 | Assert.assertFalse(Lo4jBeanAppender.contains("Network is unreachable")); 151 | Assert.assertFalse(Lo4jBeanAppender.contains("Unknow problem on Openstack plugin")); 152 | } 153 | 154 | @Test 155 | public void testJsonTruncated() throws IOException { 156 | Map parameters = new HashMap(); 157 | parameters.put(OpenstackCloudParameters.AGENT_METADATA_DISABLE, "false"); 158 | prepareAgentProperties("meta_data_truncated.json", parameters).afterAgentConfigurationLoaded(null); 159 | Assert.assertTrue(Lo4jBeanAppender.contains("Detected Openstack instance. Will write parameters from metadata")); 160 | Assert.assertTrue(Lo4jBeanAppender.contains("Unknow problem on Openstack plugin")); 161 | } 162 | 163 | @Test 164 | public void testMetaDataUsageDisable() throws IOException { 165 | Map parameters = new HashMap(); 166 | parameters.put(OpenstackCloudParameters.AGENT_METADATA_DISABLE, "true"); 167 | prepareAgentProperties("meta_data_noUserData.json", parameters).afterAgentConfigurationLoaded(null); 168 | Assert.assertTrue(Lo4jBeanAppender.contains("Openstack metadata usage disabled (agent configuration not overridden)")); 169 | Assert.assertFalse(Lo4jBeanAppender.contains("Detected Openstack instance. Will write parameters from metadata")); 170 | Assert.assertFalse(Lo4jBeanAppender.contains("Unknow problem on Openstack plugin")); 171 | } 172 | 173 | @Test 174 | public void testProxyError() throws IOException { 175 | prepareAgentProperties("meta_data_userData.json", null, 407).afterAgentConfigurationLoaded(null); 176 | 177 | Assert.assertTrue(Lo4jBeanAppender.contains("It seems build-agent launched at non-Openstack instance")); 178 | Assert.assertFalse(Lo4jBeanAppender.contains("Network is unreachable")); 179 | Assert.assertFalse(Lo4jBeanAppender.contains("Unknow problem on Openstack plugin")); 180 | } 181 | } 182 | -------------------------------------------------------------------------------- /cloud-openstack-server/src/test/java/jetbrains/buildServer/clouds/openstack/AbstractTestOpenstackCloudClient.java: -------------------------------------------------------------------------------- 1 | package jetbrains.buildServer.clouds.openstack; 2 | 3 | import static org.mockito.Mockito.mock; 4 | import static org.mockito.Mockito.when; 5 | 6 | import java.io.IOException; 7 | import java.io.InputStream; 8 | import java.util.ArrayList; 9 | import java.util.Arrays; 10 | import java.util.Date; 11 | import java.util.HashMap; 12 | import java.util.List; 13 | import java.util.Map; 14 | 15 | import org.apache.commons.io.IOUtils; 16 | import org.jmock.Expectations; 17 | import org.jmock.Mockery; 18 | import org.testng.Assert; 19 | 20 | import com.intellij.openapi.util.text.StringUtil; 21 | 22 | import jetbrains.buildServer.clouds.CanStartNewInstanceResult; 23 | import jetbrains.buildServer.clouds.CloudClientFactory; 24 | import jetbrains.buildServer.clouds.CloudImage; 25 | import jetbrains.buildServer.clouds.CloudInstance; 26 | import jetbrains.buildServer.clouds.CloudInstanceUserData; 27 | import jetbrains.buildServer.clouds.CloudRegistrar; 28 | import jetbrains.buildServer.clouds.InstanceStatus; 29 | import jetbrains.buildServer.clouds.openstack.util.TestCloudClientParameters; 30 | import jetbrains.buildServer.serverSide.AgentDescription; 31 | import jetbrains.buildServer.serverSide.ServerPaths; 32 | import jetbrains.buildServer.web.openapi.PluginDescriptor; 33 | 34 | public class AbstractTestOpenstackCloudClient { 35 | 36 | protected OpenstackCloudClient getClient(String endpointUrl, String identity, String password, String region, String yaml) { 37 | Map params = new HashMap<>(); 38 | params.put(OpenstackCloudParameters.ENDPOINT_URL, endpointUrl); 39 | params.put(OpenstackCloudParameters.IDENTITY, identity); 40 | params.put(OpenstackCloudParameters.PASSWORD, password); 41 | params.put(OpenstackCloudParameters.REGION, region); 42 | params.put(OpenstackCloudParameters.IMAGES_PROFILES, yaml); 43 | params.put(OpenstackCloudParameters.INSTANCE_CAP, "1"); 44 | 45 | final Mockery context = new Mockery(); 46 | 47 | final PluginDescriptor pluginDescriptor = context.mock(PluginDescriptor.class); 48 | context.checking(new Expectations() { 49 | { 50 | oneOf(pluginDescriptor).getPluginResourcesPath("profile-settings.jsp"); 51 | will(returnValue("target/fake")); 52 | } 53 | }); 54 | 55 | final CloudRegistrar cloudRegistrar = context.mock(CloudRegistrar.class); 56 | context.checking(new Expectations() { 57 | { 58 | oneOf(cloudRegistrar).registerCloudFactory(with(aNonNull(CloudClientFactory.class))); 59 | } 60 | }); 61 | 62 | OpenstackCloudClientFactory factory = new OpenstackCloudClientFactory(cloudRegistrar, pluginDescriptor, new ServerPaths("target")); 63 | return factory.createNewClient(null, new TestCloudClientParameters(params)); 64 | } 65 | 66 | protected String getTestYaml(String version) throws IOException { 67 | final String file = "test.v" + version + ".yml"; 68 | 69 | // Old 'commons-io', but provided by TeamCity 'server-api' ... this is just for unit test 70 | InputStream is = this.getClass().getResourceAsStream("/" + file); 71 | if (is == null) { 72 | throw new UnsupportedOperationException( 73 | String.format("You should provide a '%s' file in test resrources containg OpenStack image descriptor", file)); 74 | } 75 | @SuppressWarnings("unchecked") 76 | List list = IOUtils.readLines(is); 77 | return StringUtil.join(list, "\n"); 78 | } 79 | 80 | protected void testSubSimple(String endpointUrl, String identity, String password, String region, String yaml) throws Exception { 81 | String errorMsg = testSubSimple(endpointUrl, identity, password, region, yaml, false, false); 82 | Assert.assertNull(errorMsg); 83 | } 84 | 85 | /** 86 | * Spy some method of CloudImage (nothing by default) 87 | * 88 | * @param image Image to spy 89 | * @return New image 90 | */ 91 | protected CloudImage spyCloudImage(CloudImage image) { 92 | return image; 93 | } 94 | 95 | protected String testSubSimple(String endpointUrl, String identity, String password, String region, String yaml, 96 | boolean errorInstanceWillOccursAtStart, boolean errorInstanceWillOccursAtEnd) throws Exception { 97 | String returnMessage = null; 98 | Date startTime = new Date(System.currentTimeMillis() - 1000); 99 | OpenstackCloudClient client = getClient(endpointUrl, identity, password, region, yaml); 100 | Assert.assertNull(client.getErrorInfo()); 101 | Assert.assertNotNull(client.getImages()); 102 | Assert.assertFalse(client.getImages().isEmpty()); 103 | CloudImage image = spyCloudImage(client.getImages().iterator().next()); 104 | 105 | Assert.assertEquals(client.canStartNewInstanceWithDetails(image), CanStartNewInstanceResult.yes()); 106 | CloudInstance instance = null; 107 | try { 108 | instance = client.startNewInstance(image, 109 | new CloudInstanceUserData("fakeName", "fakeToken", "localhost", (long) 0, "", "", new HashMap<>())); 110 | List statusInit = new ArrayList<>(Arrays.asList(InstanceStatus.SCHEDULED_TO_START, InstanceStatus.STARTING)); 111 | List statusWanted = new ArrayList<>(Arrays.asList(InstanceStatus.RUNNING)); 112 | if (errorInstanceWillOccursAtStart) { 113 | statusWanted = new ArrayList<>(Arrays.asList(InstanceStatus.ERROR, InstanceStatus.UNKNOWN)); 114 | statusInit.add(InstanceStatus.ERROR); 115 | statusInit.add(InstanceStatus.STOPPED); 116 | statusInit.add(InstanceStatus.UNKNOWN); 117 | } 118 | waitInstanceStatus(instance, statusWanted, 5000, statusInit); 119 | if (errorInstanceWillOccursAtStart) { 120 | if (instance.getErrorInfo() != null) { 121 | return instance.getErrorInfo().getMessage(); 122 | } 123 | return "no details"; 124 | } 125 | String instanceId = ((OpenstackCloudInstance) instance).getOpenstackInstanceId(); 126 | Assert.assertTrue(!StringUtil.isEmpty(instanceId)); 127 | Assert.assertNotNull(instance.getImage()); 128 | Assert.assertTrue(!StringUtil.isEmpty(instance.getImageId())); 129 | // No possible Assert for network identity, only v3 return non-null value 130 | instance.getNetworkIdentity(); 131 | Assert.assertNotNull(instance.getStartedTime()); 132 | Assert.assertTrue(instance.getStartedTime().after(startTime), 133 | String.format("Begin: %s / InstanceStart: %s", startTime, instance.getStartedTime())); 134 | 135 | Map parameters = new HashMap<>(); 136 | AgentDescription agentDescription = mock(AgentDescription.class); 137 | when(agentDescription.getConfigurationParameters()).thenReturn(parameters); 138 | Assert.assertFalse(instance.containsAgent(agentDescription)); 139 | parameters.put("testCloudType", OpenstackCloudParameters.CLOUD_TYPE); 140 | Assert.assertFalse(instance.containsAgent(agentDescription)); 141 | parameters.put(OpenstackCloudParameters.OPENSTACK_INSTANCE_ID, instanceId); 142 | Assert.assertTrue(instance.containsAgent(agentDescription)); 143 | } finally { 144 | if (instance != null) { 145 | client.terminateInstance(instance); 146 | List statusTerminate = new ArrayList<>( 147 | Arrays.asList(InstanceStatus.RUNNING, InstanceStatus.SCHEDULED_TO_STOP, InstanceStatus.STOPPING, InstanceStatus.STOPPED)); 148 | List statusWanted = new ArrayList<>(Arrays.asList(InstanceStatus.STOPPED)); 149 | if (errorInstanceWillOccursAtStart || errorInstanceWillOccursAtEnd) { 150 | statusWanted = new ArrayList<>(Arrays.asList(InstanceStatus.ERROR, InstanceStatus.UNKNOWN)); 151 | statusTerminate.add(InstanceStatus.ERROR); 152 | statusTerminate.add(InstanceStatus.UNKNOWN); 153 | } 154 | waitInstanceStatus(instance, statusWanted, 5000, statusTerminate); 155 | if (errorInstanceWillOccursAtEnd && instance.getErrorInfo() != null) { 156 | returnMessage = instance.getErrorInfo().getMessage(); 157 | } 158 | } 159 | client.dispose(); 160 | } 161 | return returnMessage; 162 | } 163 | 164 | protected void waitInstanceStatus(CloudInstance instance, List wanted, long intervalWait, List intermediates) 165 | throws InterruptedException { 166 | while (!wanted.contains(instance.getStatus())) { 167 | boolean currentIsInIntermediates = false; 168 | for (InstanceStatus intermediate : intermediates) { 169 | if (instance.getStatus().equals(intermediate)) { 170 | currentIsInIntermediates = true; 171 | break; 172 | } 173 | } 174 | if (!currentIsInIntermediates) { 175 | Assert.fail(String.format("Status '%s' is not one of intermediates expected", instance.getStatus().getName())); 176 | } 177 | Thread.sleep(intervalWait); // NOSONAR: Sleep wanted 178 | } 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /cloud-openstack-server/src/test/java/jetbrains/buildServer/clouds/openstack/OpenstackCloudClientTest.java: -------------------------------------------------------------------------------- 1 | package jetbrains.buildServer.clouds.openstack; 2 | 3 | import java.io.File; 4 | import java.io.IOException; 5 | import java.io.InputStream; 6 | import java.util.ArrayList; 7 | import java.util.Arrays; 8 | import java.util.Collection; 9 | import java.util.Date; 10 | import java.util.HashMap; 11 | import java.util.Properties; 12 | 13 | import org.apache.commons.io.FileUtils; 14 | import org.testng.Assert; 15 | import org.testng.annotations.Test; 16 | 17 | import com.intellij.openapi.util.text.StringUtil; 18 | 19 | import jetbrains.buildServer.clouds.CloudInstance; 20 | import jetbrains.buildServer.clouds.CloudInstanceUserData; 21 | import jetbrains.buildServer.clouds.InstanceStatus; 22 | 23 | public class OpenstackCloudClientTest extends AbstractTestOpenstackCloudClient { 24 | 25 | final private static String TEST_KEY_URL = "test.url"; 26 | final private static String TEST_KEY_IDENTITY = "test.identity"; 27 | final private static String TEST_KEY_PASSWORD = "test.password"; 28 | final private static String TEST_KEY_REGION = "test.region"; 29 | 30 | final private static String[] TEST_KEYS_LIST = new String[] { TEST_KEY_URL, TEST_KEY_IDENTITY, TEST_KEY_PASSWORD, TEST_KEY_REGION, }; 31 | 32 | static enum OpenStackVersion { 33 | TWO("2"), THREE("3"); 34 | 35 | private final String value; 36 | 37 | private OpenStackVersion(String value) { 38 | this.value = value; 39 | } 40 | }; 41 | 42 | @Test 43 | public void testNoImage() throws Exception { 44 | Properties props = getTestProps(OpenStackVersion.TWO); 45 | OpenstackCloudClient client = getClient(props.getProperty(TEST_KEY_URL), props.getProperty(TEST_KEY_IDENTITY), 46 | props.getProperty(TEST_KEY_PASSWORD), props.getProperty(TEST_KEY_REGION), null); 47 | Assert.assertEquals(client.getErrorInfo().getMessage(), "No images specified"); 48 | } 49 | 50 | @Test 51 | public void testOnlyComments() throws Exception { 52 | Properties props = getTestProps(OpenStackVersion.TWO); 53 | OpenstackCloudClient client = getClient(props.getProperty(TEST_KEY_URL), props.getProperty(TEST_KEY_IDENTITY), 54 | props.getProperty(TEST_KEY_PASSWORD), props.getProperty(TEST_KEY_REGION), "#A comment"); 55 | Assert.assertEquals(client.getErrorInfo().getMessage(), "No images specified (perhaps only comments)"); 56 | } 57 | 58 | @Test 59 | public void testNoParams() throws Exception { 60 | Properties props = getTestProps(OpenStackVersion.TWO); 61 | OpenstackCloudClient client = getClient(props.getProperty(TEST_KEY_URL), props.getProperty(TEST_KEY_IDENTITY), 62 | props.getProperty(TEST_KEY_PASSWORD), props.getProperty(TEST_KEY_REGION), "some-image:"); 63 | Assert.assertEquals(client.getErrorInfo().getMessage(), "No parameters defined for image: some-image"); 64 | } 65 | 66 | @Test 67 | public void testV2() throws Exception { 68 | Properties props = getTestProps(OpenStackVersion.TWO); 69 | testSubSimple(props.getProperty(TEST_KEY_URL), props.getProperty(TEST_KEY_IDENTITY), props.getProperty(TEST_KEY_PASSWORD), 70 | props.getProperty(TEST_KEY_REGION), getTestYaml(OpenStackVersion.TWO.value)); 71 | } 72 | 73 | @Test 74 | public void testV3() throws Exception { 75 | Properties props = getTestProps(OpenStackVersion.THREE); 76 | testSubSimple(props.getProperty(TEST_KEY_URL), props.getProperty(TEST_KEY_IDENTITY), props.getProperty(TEST_KEY_PASSWORD), 77 | props.getProperty(TEST_KEY_REGION), getTestYaml(OpenStackVersion.THREE.value)); 78 | } 79 | 80 | @Test 81 | public void testWithUserScript() throws Exception { 82 | // Test data should not include floating ip (instance more longer to create) 83 | final String scriptName = "fakeUserScript.sh"; 84 | final File fakeUserScript = new File("target/system/pluginData/openstack", scriptName); 85 | FileUtils.writeStringToFile(fakeUserScript, "echo foo bar"); 86 | 87 | Properties props = getTestProps(OpenStackVersion.TWO); 88 | testSubSimple(props.getProperty(TEST_KEY_URL), props.getProperty(TEST_KEY_IDENTITY), props.getProperty(TEST_KEY_PASSWORD), 89 | props.getProperty(TEST_KEY_REGION), getTestYaml(OpenStackVersion.TWO.value) + "\n user_script: " + scriptName); 90 | } 91 | 92 | @Test 93 | public void testWithUserScriptNotExist() throws Exception { 94 | // Test data should not include floating ip (instance more longer to create) 95 | Properties props = getTestProps(OpenStackVersion.TWO); 96 | String errorMsg = testSubSimple(props.getProperty(TEST_KEY_URL), props.getProperty(TEST_KEY_IDENTITY), props.getProperty(TEST_KEY_PASSWORD), 97 | props.getProperty(TEST_KEY_REGION), getTestYaml(OpenStackVersion.TWO.value) + "\n user_script: fakeScriptNotExist-87648348376.sh", 98 | true, false); 99 | Assert.assertTrue(errorMsg.contains("Error in reading user script"), errorMsg); 100 | } 101 | 102 | @Test 103 | public void testWithBadImageName() throws Exception { 104 | Properties props = getTestProps(OpenStackVersion.TWO); 105 | String yaml = getTestYaml(OpenStackVersion.TWO.value); 106 | yaml = yaml.replaceFirst("image: .*\n", "image: imageNotExist4242\n"); 107 | String errorMsg = testSubSimple(props.getProperty(TEST_KEY_URL), props.getProperty(TEST_KEY_IDENTITY), props.getProperty(TEST_KEY_PASSWORD), 108 | props.getProperty(TEST_KEY_REGION), yaml, true, false); 109 | Assert.assertTrue(errorMsg.contains("No image can be found for name"), errorMsg); 110 | } 111 | 112 | @Test 113 | public void testFindImageById() throws Exception { 114 | Properties props = getTestProps(OpenStackVersion.THREE); 115 | OpenstackCloudClient client = getClient(props.getProperty(TEST_KEY_URL), props.getProperty(TEST_KEY_IDENTITY), 116 | props.getProperty(TEST_KEY_PASSWORD), props.getProperty(TEST_KEY_REGION), getTestYaml(OpenStackVersion.THREE.value)); 117 | 118 | for (OpenstackCloudImage image : client.getImages()) { 119 | OpenstackCloudImage newImage = client.findImageById(image.getId()); 120 | Assert.assertNotNull(newImage); 121 | Assert.assertEquals(image.getName(), newImage.getName()); 122 | } 123 | 124 | client.dispose(); 125 | } 126 | 127 | @Test 128 | public void testRestoration() throws Exception { 129 | Properties props = getTestProps(OpenStackVersion.THREE); 130 | String endpointUrl = props.getProperty(TEST_KEY_URL); 131 | String identity = props.getProperty(TEST_KEY_IDENTITY); 132 | String password = props.getProperty(TEST_KEY_PASSWORD); 133 | String region = props.getProperty(TEST_KEY_REGION); 134 | String yaml = getTestYaml(OpenStackVersion.THREE.value); 135 | 136 | // Start first with 1 VM 137 | OpenstackCloudClient client = getClient(endpointUrl, identity, password, region, yaml); 138 | CloudInstance instance = client.startNewInstance(client.getImages().iterator().next(), 139 | new CloudInstanceUserData("fakeName", "fakeToken", "localhost", (long) 0, "", "", new HashMap<>())); 140 | waitInstanceStatus(instance, new ArrayList<>(Arrays.asList(InstanceStatus.RUNNING)), 5000, 141 | new ArrayList<>(Arrays.asList(InstanceStatus.SCHEDULED_TO_START, InstanceStatus.STARTING))); 142 | while (!client.isInitialized()) { 143 | // Wait client initialization 144 | Thread.sleep(1000); // NOSONAR : Wanted for unit test 145 | } 146 | Assert.assertEquals(client.getImages().iterator().next().getInstances().size(), 1); 147 | 148 | // Simulate an update 149 | Date dateProfileUpdate = new Date(); 150 | client.dispose(); 151 | 152 | // Recreate without any VM start 153 | client = getClient(endpointUrl, identity, password, region, yaml); 154 | while (!client.isInitialized()) { 155 | // Wait client initialization 156 | Thread.sleep(1000); // NOSONAR : Wanted for unit test 157 | } 158 | // Waiting async restoration execution 159 | Thread.sleep(3000); // NOSONAR : Wanted for unit test 160 | 161 | // Assert correct restoration and 'creation date' of previous server 162 | Collection instances = client.getImages().iterator().next().getInstances(); 163 | Assert.assertEquals(instances.size(), 1); 164 | OpenstackCloudInstance instanceCreatedPreviously = instances.iterator().next(); 165 | Assert.assertTrue(instanceCreatedPreviously.getStartedTime().before(dateProfileUpdate), 166 | String.format("Date instance: %s / date profile update: %s", instanceCreatedPreviously.getStartedTime(), dateProfileUpdate)); 167 | 168 | // Clean all 169 | instance = client.getImages().iterator().next().getInstances().iterator().next(); 170 | client.terminateInstance(instance); 171 | waitInstanceStatus(instance, new ArrayList<>(Arrays.asList(InstanceStatus.STOPPED)), 5000, new ArrayList<>( 172 | Arrays.asList(InstanceStatus.RUNNING, InstanceStatus.SCHEDULED_TO_STOP, InstanceStatus.STOPPING, InstanceStatus.STOPPED))); 173 | client.dispose(); 174 | } 175 | 176 | private Properties getTestProps(OpenStackVersion version) throws IOException { 177 | final String file = "test.v" + version.value + ".properties"; 178 | 179 | Properties props = new Properties(); 180 | InputStream is = this.getClass().getResourceAsStream("/" + file); 181 | if (is == null) { 182 | throw new UnsupportedOperationException( 183 | String.format("You should provide a '%s' file in test resrources with keys: %s", file, StringUtil.join(TEST_KEYS_LIST, " / "))); 184 | } 185 | props.load(is); 186 | return props; 187 | } 188 | 189 | } 190 | -------------------------------------------------------------------------------- /cloud-openstack-server/src/main/java/jetbrains/buildServer/clouds/openstack/OpenstackCloudClient.java: -------------------------------------------------------------------------------- 1 | package jetbrains.buildServer.clouds.openstack; 2 | 3 | import java.util.ArrayList; 4 | import java.util.Collection; 5 | import java.util.Collections; 6 | import java.util.List; 7 | import java.util.Map; 8 | import java.util.concurrent.ExecutionException; 9 | import java.util.concurrent.Executors; 10 | import java.util.concurrent.ScheduledExecutorService; 11 | import java.util.concurrent.ScheduledFuture; 12 | import java.util.concurrent.TimeUnit; 13 | import java.util.concurrent.TimeoutException; 14 | 15 | import org.jclouds.openstack.nova.v2_0.options.CreateServerOptions; 16 | import org.jetbrains.annotations.NotNull; 17 | import org.jetbrains.annotations.Nullable; 18 | import org.yaml.snakeyaml.Yaml; 19 | 20 | import com.google.common.base.Strings; 21 | import com.intellij.openapi.diagnostic.Logger; 22 | import com.intellij.util.ObjectUtils; 23 | import com.jcabi.log.VerboseRunnable; 24 | 25 | import jetbrains.buildServer.clouds.CanStartNewInstanceResult; 26 | import jetbrains.buildServer.clouds.CloudClientEx; 27 | import jetbrains.buildServer.clouds.CloudClientParameters; 28 | import jetbrains.buildServer.clouds.CloudErrorInfo; 29 | import jetbrains.buildServer.clouds.CloudImage; 30 | import jetbrains.buildServer.clouds.CloudInstance; 31 | import jetbrains.buildServer.clouds.CloudInstanceUserData; 32 | import jetbrains.buildServer.log.Loggers; 33 | import jetbrains.buildServer.serverSide.AgentDescription; 34 | import jetbrains.buildServer.serverSide.BuildServerAdapter; 35 | import jetbrains.buildServer.serverSide.ServerPaths; 36 | import jetbrains.buildServer.util.StringUtil; 37 | 38 | public class OpenstackCloudClient extends BuildServerAdapter implements CloudClientEx { 39 | @NotNull 40 | private static final Logger LOG = Logger.getInstance(Loggers.CLOUD_CATEGORY_ROOT); 41 | @NotNull 42 | private final List cloudImages = new ArrayList<>(); 43 | @NotNull 44 | private final OpenstackApi openstackApi; 45 | @Nullable 46 | private CloudErrorInfo errorInfo = null; 47 | @Nullable 48 | private final Integer instanceCap; 49 | private ScheduledExecutorService executor; 50 | private ScheduledFuture initialized; 51 | 52 | public OpenstackCloudClient(@NotNull final CloudClientParameters params, @NotNull ServerPaths serverPaths, 53 | @NotNull final ExecutorServiceFactory factory) { 54 | 55 | final String endpointUrl = params.getParameter(OpenstackCloudParameters.ENDPOINT_URL).trim(); 56 | final String identity = params.getParameter(OpenstackCloudParameters.IDENTITY).trim(); 57 | final String password = params.getParameter(OpenstackCloudParameters.PASSWORD).trim(); 58 | final String region = params.getParameter(OpenstackCloudParameters.REGION).trim(); 59 | 60 | instanceCap = Integer.parseInt(params.getParameter(OpenstackCloudParameters.INSTANCE_CAP)); 61 | openstackApi = new OpenstackApi(endpointUrl, identity, password, region); 62 | 63 | final String rawYaml = params.getParameter(OpenstackCloudParameters.IMAGES_PROFILES); 64 | LOG.debug(String.format("Using the following cloud parameters: endpointUrl=%s, identity=%s, zone=%s", endpointUrl, identity, region)); 65 | if (rawYaml == null || rawYaml.trim().length() == 0) { 66 | errorInfo = new CloudErrorInfo("No images specified"); 67 | return; 68 | } 69 | LOG.debug(String.format("Using the following YAML data: %s", rawYaml)); 70 | 71 | Yaml yaml = new Yaml(); 72 | final Map> map = yaml.load(rawYaml); 73 | if (map == null || map.isEmpty()) { 74 | errorInfo = new CloudErrorInfo("No images specified (perhaps only comments)"); 75 | return; 76 | } 77 | 78 | LOG.info(String.format("Testing credentials by retrieving servers list status (identity: %s)...", identity)); 79 | openstackApi.getNovaServerApi().listInDetail(); 80 | 81 | final StringBuilder error = new StringBuilder(); 82 | for (Map.Entry> entry : map.entrySet()) { 83 | final String imageName = entry.getKey().trim(); 84 | if (entry.getValue() == null) { 85 | errorInfo = new CloudErrorInfo(String.format("No parameters defined for image: %s", imageName)); 86 | return; 87 | } 88 | final String openstackImageName = StringUtil.trim(entry.getValue().get("image")); 89 | final String flavorName = StringUtil.trim(entry.getValue().get("flavor")); 90 | final String networkName = StringUtil.trim(entry.getValue().get("network")); 91 | final String securityGroupName = StringUtil.trim(entry.getValue().get("security_group")); 92 | final String keyPair = StringUtil.trim(entry.getValue().get("key_pair")); 93 | final String userScriptPath = entry.getValue().get("user_script"); 94 | Boolean autoFloatingIp = (Boolean) (Object) entry.getValue().get("auto_floating_ip"); // Evil, but Yaml parse Boolean only for this 95 | autoFloatingIp = ObjectUtils.chooseNotNull(autoFloatingIp, false); // Can be null if not defined 96 | 97 | String networkId = openstackApi.getNetworkIdByName(networkName); 98 | CreateServerOptions options = new CreateServerOptions().keyPairName(keyPair).securityGroupNames(securityGroupName).networks(networkId); 99 | 100 | final String availabilityZone = entry.getValue().get("availability_zone"); 101 | if (!Strings.isNullOrEmpty(availabilityZone)) { 102 | options.availabilityZone(availabilityZone.trim()); 103 | } 104 | 105 | LOG.debug(String.format( 106 | "Adding cloud image: imageName=%s, openstackImageName=%s, flavorName=%s, networkName=%s, networkId=%s, securityGroupName=%s, keyPair=%s, floatingIp=%s", 107 | imageName, openstackImageName, flavorName, networkName, networkId, securityGroupName, keyPair, autoFloatingIp)); 108 | 109 | LOG.info(String.format("Create image [%s] ...", imageName)); 110 | final OpenstackCloudImage image = new OpenstackCloudImage(openstackApi, imageName /* imageIdGenerator.next() */, imageName, 111 | openstackImageName, flavorName, autoFloatingIp, options, userScriptPath, serverPaths, factory.createExecutorService(imageName)); 112 | 113 | cloudImages.add(image); 114 | 115 | } 116 | 117 | errorInfo = error.length() == 0 ? null : new CloudErrorInfo(error.substring(1)); 118 | 119 | // start asynchronous initialization: 120 | this.executor = Executors.newSingleThreadScheduledExecutor(); 121 | this.initialized = this.executor.schedule(new VerboseRunnable(() -> { 122 | for (OpenstackCloudImage cloudImage : cloudImages) { 123 | cloudImage.initialize(); 124 | } 125 | }, true), 1, TimeUnit.SECONDS); 126 | } 127 | 128 | @Override 129 | public boolean isInitialized() { 130 | // wait for initialization completion: 131 | if (this.initialized != null) { 132 | try { 133 | this.initialized.get((long) cloudImages.size() * 3, TimeUnit.SECONDS); 134 | if (this.executor != null) { 135 | executor.shutdown(); 136 | executor = null; 137 | } 138 | } catch (InterruptedException ex) { 139 | Thread.currentThread().interrupt(); 140 | } catch (ExecutionException | TimeoutException ex) { 141 | LOG.error(String.format("Initialization failure: %s: %s", ex.getClass().getSimpleName(), ex.getMessage())); 142 | } 143 | } 144 | return true; 145 | } 146 | 147 | @Nullable 148 | public OpenstackCloudImage findImageById(@NotNull final String imageId) { 149 | for (final OpenstackCloudImage image : getImages()) { 150 | if (image.getId().equals(imageId)) { 151 | return image; 152 | } 153 | } 154 | return null; 155 | } 156 | 157 | @Nullable 158 | public OpenstackCloudInstance findInstanceByAgent(@NotNull final AgentDescription agentDescription) { 159 | final Map configParams = agentDescription.getConfigurationParameters(); 160 | if (!configParams.containsValue(OpenstackCloudParameters.CLOUD_TYPE)) { 161 | return null; 162 | } 163 | for (OpenstackCloudImage image : getImages()) { 164 | for (OpenstackCloudInstance instance : image.getInstances()) { 165 | if (instance.getOpenstackInstanceId().equals(configParams.get(OpenstackCloudParameters.OPENSTACK_INSTANCE_ID))) { 166 | return instance; 167 | } 168 | } 169 | } 170 | return null; 171 | } 172 | 173 | @NotNull 174 | public Collection getImages() { 175 | return Collections.unmodifiableList(cloudImages); 176 | } 177 | 178 | @Nullable 179 | @Override 180 | public CloudErrorInfo getErrorInfo() { 181 | return errorInfo; 182 | } 183 | 184 | /** 185 | * Deprecated since 2018.1. jetbrains.buildServer.clouds.CloudClient#canStartNewInstanceWithDetails(jetbrains.buildServer.clouds.CloudImage) is 186 | * being used instead. 187 | * 188 | * @deprecated 189 | * @see jetbrains.buildServer.clouds.CloudClient#canStartNewInstance(jetbrains.buildServer.clouds.CloudImage) 190 | */ 191 | @Override 192 | @Deprecated 193 | public boolean canStartNewInstance(@NotNull final CloudImage image) { // TODO: NOSONAR Should work with 2017 and 2020 194 | if (instanceCap == null) { 195 | return true; 196 | } 197 | int i = 0; 198 | for (final OpenstackCloudImage img : getImages()) { 199 | i += img.getInstances().size(); 200 | } 201 | return i < instanceCap; 202 | } 203 | 204 | @Override 205 | public CanStartNewInstanceResult canStartNewInstanceWithDetails(@NotNull final CloudImage image) { 206 | if (canStartNewInstance(image)) { // TODO: NOSONAR Should work with 2017 and 2020 207 | return CanStartNewInstanceResult.yes(); 208 | } 209 | return CanStartNewInstanceResult.no("Instance cap exceeded"); 210 | } 211 | 212 | @NotNull 213 | public CloudInstance startNewInstance(@NotNull final CloudImage image, @NotNull final CloudInstanceUserData data) { 214 | return ((OpenstackCloudImage) image).startNewInstance(data); 215 | } 216 | 217 | @Override 218 | public void restartInstance(@NotNull final CloudInstance instance) { 219 | ((OpenstackCloudInstance) instance).restart(); 220 | } 221 | 222 | public void terminateInstance(@NotNull final CloudInstance instance) { 223 | ((OpenstackCloudInstance) instance).stop(); 224 | } 225 | 226 | @Nullable 227 | public String generateAgentName(@NotNull final AgentDescription agentDescription) { 228 | return null; 229 | } 230 | 231 | @Override 232 | public void dispose() { 233 | for (final OpenstackCloudImage image : getImages()) { 234 | image.dispose(); 235 | } 236 | cloudImages.clear(); 237 | if (executor != null) 238 | executor.shutdown(); 239 | } 240 | 241 | } 242 | -------------------------------------------------------------------------------- /cloud-openstack-server/src/main/java/jetbrains/buildServer/clouds/openstack/OpenstackCloudImage.java: -------------------------------------------------------------------------------- 1 | package jetbrains.buildServer.clouds.openstack; 2 | 3 | import java.util.Collection; 4 | import java.util.Collections; 5 | import java.util.HashMap; 6 | import java.util.Map; 7 | import java.util.concurrent.ConcurrentHashMap; 8 | import java.util.concurrent.ScheduledExecutorService; 9 | import java.util.concurrent.TimeUnit; 10 | 11 | import org.jclouds.openstack.nova.v2_0.domain.Server; 12 | import org.jclouds.openstack.nova.v2_0.features.ServerApi; 13 | import org.jclouds.openstack.nova.v2_0.options.CreateServerOptions; 14 | import org.jclouds.openstack.v2_0.domain.Resource; 15 | import org.jetbrains.annotations.NotNull; 16 | import org.jetbrains.annotations.Nullable; 17 | 18 | import com.intellij.openapi.diagnostic.Logger; 19 | import com.jcabi.log.VerboseRunnable; 20 | 21 | import jetbrains.buildServer.clouds.CloudErrorInfo; 22 | import jetbrains.buildServer.clouds.CloudImage; 23 | import jetbrains.buildServer.clouds.CloudInstanceUserData; 24 | import jetbrains.buildServer.clouds.InstanceStatus; 25 | import jetbrains.buildServer.log.Loggers; 26 | import jetbrains.buildServer.serverSide.ServerPaths; 27 | import jetbrains.buildServer.serverSide.TeamCityProperties; 28 | 29 | public class OpenstackCloudImage implements CloudImage { 30 | 31 | @NotNull 32 | public static final String DELAY_RESTORE_DELAY_KEY = "openstack.restore.delay"; 33 | @NotNull 34 | public static final int DELAY_RESTORE_DELAY_DEFAULT_VALUE = 1; 35 | 36 | @NotNull 37 | public static final String DELAY_STATUS_INITIAL_KEY = "openstack.status.initial"; 38 | @NotNull 39 | public static final int DELAY_STATUS_INITIAL_DEFAULT_VALUE = 5; 40 | 41 | @NotNull 42 | public static final String DELAY_STATUS_DELAY_KEY = "openstack.status.delay"; 43 | @NotNull 44 | public static final int DELAY_STATUS_DELAY_DEFAULT_VALUE = 10; 45 | 46 | @NotNull 47 | private static final Logger LOG = Logger.getInstance(Loggers.CLOUD_CATEGORY_ROOT); 48 | @NotNull 49 | private final OpenstackApi openstackApi; 50 | @NotNull 51 | private final String imageId; 52 | @NotNull 53 | private final String imageName; 54 | @NotNull 55 | private final String openstackImageName; 56 | @NotNull 57 | private final String flavorName; 58 | @NotNull 59 | private final boolean autoFloatingIp; 60 | @NotNull 61 | private final CreateServerOptions options; 62 | @Nullable 63 | private final String userScriptPath; 64 | @NotNull 65 | private final ServerPaths serverPaths; 66 | @NotNull 67 | private final ScheduledExecutorService executor; 68 | 69 | @NotNull 70 | private final Map instances = new ConcurrentHashMap<>(); 71 | @NotNull 72 | private final IdGenerator instanceIdGenerator = new IdGenerator(); 73 | @Nullable 74 | private CloudErrorInfo errorInfo = null; 75 | 76 | public OpenstackCloudImage(@NotNull final OpenstackApi openstackApi, @NotNull final String imageId, @NotNull final String imageName, 77 | @NotNull final String openstackImageName, @NotNull final String flavorId, @NotNull boolean autoFloatingIp, 78 | @NotNull final CreateServerOptions options, @Nullable final String userScriptPath, @NotNull final ServerPaths serverPaths, 79 | @NotNull final ScheduledExecutorService executor) { 80 | this.openstackApi = openstackApi; 81 | this.imageId = imageId; 82 | this.imageName = imageName; 83 | this.openstackImageName = openstackImageName; 84 | this.flavorName = flavorId; 85 | this.autoFloatingIp = autoFloatingIp; 86 | this.options = options; 87 | this.userScriptPath = userScriptPath; 88 | this.serverPaths = serverPaths; 89 | this.executor = executor; 90 | this.executor.scheduleWithFixedDelay(new VerboseRunnable(() -> { 91 | // Update status of instances managed by this image 92 | LOG.debug(String.format("Updating instances status for openstack image: %s", getName())); 93 | Map status = new HashMap<>(); 94 | try { 95 | for (Server server : openstackApi.getNovaServerApi().listInDetail().concat().filter(p -> p.getName().startsWith(getName()))) { 96 | status.put(server.getName(), server.getStatus()); 97 | } 98 | resetAnyPreviousError(); 99 | } catch (Exception e) { 100 | // All current instances will be set in error 101 | processError("Instances status cannot be updated", e); 102 | } 103 | for (OpenstackCloudInstance instance : getInstances()) { 104 | // If any error on global status retrieve, fill UNKNOW, avoiding any occasional (and not wanted) termination 105 | instance.updateStatus(getErrorInfo() != null ? Server.Status.UNKNOWN : status.get(instance.getName())); 106 | if (instance.getStatus() == InstanceStatus.STOPPED || instance.getStatus() == InstanceStatus.ERROR) { 107 | forgetInstance(instance); 108 | } 109 | } 110 | }, true), getTeamCityProperty(DELAY_STATUS_INITIAL_KEY, DELAY_STATUS_INITIAL_DEFAULT_VALUE), 111 | getTeamCityProperty(DELAY_STATUS_DELAY_KEY, DELAY_STATUS_DELAY_DEFAULT_VALUE), TimeUnit.SECONDS); 112 | } 113 | 114 | private void processError(@NotNull String process, @NotNull final Exception e) { 115 | final String message = e.getMessage(); 116 | LOG.error(message, e); 117 | errorInfo = new CloudErrorInfo(process, message, e); 118 | } 119 | 120 | private void resetAnyPreviousError() { 121 | errorInfo = null; 122 | } 123 | 124 | // Initialize the image 125 | void initialize() { 126 | final String openstackImageId = initialGetOpenstackImageId(5); 127 | if (openstackImageId != null && !openstackImageId.isEmpty()) { 128 | this.executor.schedule(new VerboseRunnable(() -> restoreInstances(openstackImageId), true), 129 | getTeamCityProperty(DELAY_RESTORE_DELAY_KEY, DELAY_RESTORE_DELAY_DEFAULT_VALUE), TimeUnit.SECONDS); 130 | } 131 | } 132 | 133 | // Initially obtain openstack image id 134 | private String initialGetOpenstackImageId(int trials) { 135 | for (int i = 0; i < trials; i++) { 136 | String v = openstackApi.getImageIdByName(openstackImageName); 137 | if (v != null && !v.isEmpty()) 138 | return v; 139 | try { 140 | Thread.sleep(500L); 141 | } catch (InterruptedException ex) { 142 | Thread.currentThread().interrupt(); 143 | break; 144 | } 145 | } 146 | return null; 147 | } 148 | 149 | // Restore instances of the image 150 | private void restoreInstances(String openstackImageId) { 151 | try { 152 | LOG.info(String.format("Restore potential instances for openstack image: %s", getName())); 153 | for (Server server : openstackApi.getNovaServerApi().listInDetail().concat().filter(p -> p.getName().startsWith(getName()))) { 154 | // Restore servers of the specified image id (all status, some could be shutdown but not terminated) 155 | Resource simage = server.getImage(); 156 | if (simage != null && openstackImageId.equals(simage.getId())) { 157 | final String instanceId = server.getName().substring(server.getName().lastIndexOf('-') + 1); 158 | if (!instances.containsKey(instanceId)) { 159 | // Add only if not already existing (sample: started at profile creation) 160 | final OpenstackCloudInstance instance = new OpenstackCloudInstance(this, instanceId, serverPaths, executor, server); 161 | instances.put(instanceId, instance); 162 | } 163 | } 164 | } 165 | resetAnyPreviousError(); 166 | } catch (Exception e) { 167 | processError("Current instances (if any) cannot be restored", e); 168 | } 169 | 170 | } 171 | 172 | @NotNull 173 | public ServerApi getNovaServerApi() { 174 | return openstackApi.getNovaServerApi(); 175 | } 176 | 177 | public String getFloatingIpAvailable() { 178 | return openstackApi.getFloatingIpAvailable(); 179 | } 180 | 181 | public void associateFloatingIp(String serverId, String ip) { 182 | openstackApi.associateFloatingIp(serverId, ip); 183 | } 184 | 185 | private void forgetInstance(@NotNull final OpenstackCloudInstance instance) { 186 | instances.remove(instance.getInstanceId()); 187 | } 188 | 189 | @NotNull 190 | public CreateServerOptions getImageOptions() { 191 | return options; 192 | } 193 | 194 | @NotNull 195 | public String getOpenstackImageId() { 196 | return openstackApi.getImageIdByName(openstackImageName); 197 | } 198 | 199 | @NotNull 200 | public String getFlavorId() { 201 | return openstackApi.getFlavorIdByName(flavorName); 202 | } 203 | 204 | @NotNull 205 | public String getId() { 206 | return imageId; 207 | } 208 | 209 | @NotNull 210 | public String getName() { 211 | return imageName; 212 | } 213 | 214 | @NotNull 215 | public String getOpenstackImageName() { 216 | return this.openstackImageName; 217 | } 218 | 219 | @NotNull 220 | public String getOpenstackFalvorName() { 221 | return this.flavorName; 222 | } 223 | 224 | @NotNull 225 | public boolean isAutoFloatingIp() { 226 | return this.autoFloatingIp; 227 | } 228 | 229 | @Nullable 230 | public String getUserScriptPath() { 231 | return this.userScriptPath; 232 | } 233 | 234 | @NotNull 235 | public Collection getInstances() { 236 | return Collections.unmodifiableCollection(instances.values()); 237 | } 238 | 239 | @Nullable 240 | public OpenstackCloudInstance findInstanceById(@NotNull final String instanceId) { 241 | LOG.debug(String.format("findInstanceById(%s)", instanceId)); 242 | return instances.get(instanceId); 243 | } 244 | 245 | @Nullable 246 | @Override 247 | public Integer getAgentPoolId() { 248 | // Image are affected to 'default' agents pool at creation (required for TeamCity v2021.1 / TW-71939) 249 | // Global "Agents Pools" TeamCity feature should be used to affect image(s) to some pool if needed 250 | return 0; 251 | } 252 | 253 | @Nullable 254 | public CloudErrorInfo getErrorInfo() { 255 | return errorInfo; 256 | } 257 | 258 | @NotNull 259 | synchronized String getNextInstanceId() { 260 | return instanceIdGenerator.next(); 261 | } 262 | 263 | @NotNull 264 | public synchronized OpenstackCloudInstance startNewInstance(@NotNull final CloudInstanceUserData data) { 265 | final String instanceId = getNextInstanceId(); 266 | final OpenstackCloudInstance instance = new OpenstackCloudInstance(this, instanceId, serverPaths, executor); 267 | 268 | instances.put(instanceId, instance); 269 | instance.start(data); 270 | 271 | return instance; 272 | } 273 | 274 | void dispose() { 275 | LOG.debug(String.format("Dispose image %s (id=%s)", imageName, imageId)); 276 | instances.clear(); 277 | executor.shutdown(); 278 | } 279 | 280 | private int getTeamCityProperty(String key, int defaultValue) { 281 | return TeamCityProperties.getInteger(key, defaultValue); 282 | } 283 | 284 | } 285 | -------------------------------------------------------------------------------- /cloud-openstack-server/src/test/resources/__files/v3-auth-tokens.json: -------------------------------------------------------------------------------- 1 | { 2 | "token": { 3 | "is_domain": false, 4 | "methods": [ 5 | "password" 6 | ], 7 | "is_admin_project": false, 8 | "project": { 9 | "domain": { 10 | "id": "default", 11 | "name": "Default" 12 | }, 13 | "id": "0", 14 | "name": "tenant" 15 | }, 16 | "catalog": [ 17 | { 18 | "endpoints": [ 19 | { 20 | "region_id": "region1", 21 | "url": "http://localhost:REPLACE-PORT/v2.1/nova-id", 22 | "region": "region1", 23 | "interface": "public", 24 | "id": "nova-id" 25 | } 26 | ], 27 | "type": "compute", 28 | "id": "0", 29 | "name": "nova" 30 | }, 31 | { 32 | "endpoints": [ 33 | { 34 | "region_id": "region1", 35 | "url": "http://localhost:REPLACE-PORT/", 36 | "region": "region1", 37 | "interface": "public", 38 | "id": "00000000000000000000000000000000" 39 | } 40 | ], 41 | "type": "identity", 42 | "id": "00000000000000000000000000000000", 43 | "name": "keystone" 44 | }, 45 | { 46 | "endpoints": [ 47 | { 48 | "region_id": "region1", 49 | "url": "http://localhost:REPLACE-PORT/v1/AUTH_00000000000000000000000000000000", 50 | "region": "region1", 51 | "interface": "public", 52 | "id": "00000000000000000000000000000000" 53 | } 54 | ], 55 | "type": "object-store", 56 | "id": "00000000000000000000000000000000", 57 | "name": "swift" 58 | }, 59 | { 60 | "endpoints": [ 61 | { 62 | "region_id": "region1", 63 | "url": "http://localhost:REPLACE-PORT/v1/00000000000000000000000000000000", 64 | "region": "region1", 65 | "interface": "internal", 66 | "id": "00000000000000000000000000000000" 67 | } 68 | ], 69 | "type": "orchestration", 70 | "id": "00000000000000000000000000000000", 71 | "name": "heat" 72 | }, 73 | { 74 | "endpoints": [ 75 | { 76 | "region_id": "region1", 77 | "url": "http://localhost:REPLACE-PORT", 78 | "region": "region1", 79 | "interface": "public", 80 | "id": "00000000000000000000000000000000" 81 | } 82 | ], 83 | "type": "dns", 84 | "id": "00000000000000000000000000000000", 85 | "name": "designate" 86 | }, 87 | { 88 | "endpoints": [ 89 | { 90 | "region_id": "region1", 91 | "url": "http://localhost:REPLACE-PORT", 92 | "region": "region1", 93 | "interface": "admin", 94 | "id": "00000000000000000000000000000000" 95 | } 96 | ], 97 | "type": "backup", 98 | "id": "00000000000000000000000000000000", 99 | "name": "freezer" 100 | }, 101 | { 102 | "endpoints": [ 103 | { 104 | "region_id": "region1", 105 | "url": "http://localhost:REPLACE-PORT", 106 | "region": "region1", 107 | "interface": "internal", 108 | "id": "00000000000000000000000000000000" 109 | } 110 | ], 111 | "type": "dashboard", 112 | "id": "00000000000000000000000000000000", 113 | "name": "horizon" 114 | }, 115 | { 116 | "endpoints": [ 117 | { 118 | "region_id": "region1", 119 | "url": "http://localhost:REPLACE-PORT/v1/00000000000000000000000000000000", 120 | "region": "region1", 121 | "interface": "admin", 122 | "id": "00000000000000000000000000000000" 123 | } 124 | ], 125 | "type": "volume", 126 | "id": "00000000000000000000000000000000", 127 | "name": "cinder" 128 | }, 129 | { 130 | "endpoints": [ 131 | { 132 | "region_id": "region1", 133 | "url": "http://localhost:REPLACE-PORT", 134 | "region": "region1", 135 | "interface": "internal", 136 | "id": "00000000000000000000000000000000" 137 | } 138 | ], 139 | "type": "lifecycle", 140 | "id": "00000000000000000000000000000000", 141 | "name": "ardana" 142 | }, 143 | { 144 | "endpoints": [ 145 | { 146 | "region_id": "region1", 147 | "url": "http://localhost:REPLACE-PORT/", 148 | "region": "region1", 149 | "interface": "public", 150 | "id": "00000000000000000000000000000000" 151 | } 152 | ], 153 | "type": "network", 154 | "id": "00000000000000000000000000000000", 155 | "name": "neutron" 156 | }, 157 | { 158 | "endpoints": [ 159 | { 160 | "region_id": "region1", 161 | "url": "http://localhost:REPLACE-PORT/v2/00000000000000000000000000000000", 162 | "region": "region1", 163 | "interface": "admin", 164 | "id": "00000000000000000000000000000000" 165 | } 166 | ], 167 | "type": "sharev2", 168 | "id": "00000000000000000000000000000000", 169 | "name": "manilav2" 170 | }, 171 | { 172 | "endpoints": [ 173 | { 174 | "region_id": "region1", 175 | "url": "http://localhost:REPLACE-PORT/", 176 | "region": "region1", 177 | "interface": "public", 178 | "id": "00000000000000000000000000000000" 179 | } 180 | ], 181 | "type": "placement", 182 | "id": "00000000000000000000000000000000", 183 | "name": "placement" 184 | }, 185 | { 186 | "endpoints": [ 187 | { 188 | "region_id": "region1", 189 | "url": "http://localhost:REPLACE-PORT", 190 | "region": "region1", 191 | "interface": "admin", 192 | "id": "00000000000000000000000000000000" 193 | } 194 | ], 195 | "type": "key-manager", 196 | "id": "00000000000000000000000000000000", 197 | "name": "barbican" 198 | }, 199 | { 200 | "endpoints": [ 201 | { 202 | "region_id": "region1", 203 | "url": "http://localhost:REPLACE-PORT/v2/00000000000000000000000000000000", 204 | "region": "region1", 205 | "interface": "public", 206 | "id": "00000000000000000000000000000000" 207 | } 208 | ], 209 | "type": "volumev2", 210 | "id": "00000000000000000000000000000000", 211 | "name": "cinderv2" 212 | }, 213 | { 214 | "endpoints": [ 215 | { 216 | "region_id": "region1", 217 | "url": "http://localhost:REPLACE-PORT/v3/00000000000000000000000000000000", 218 | "region": "region1", 219 | "interface": "internal", 220 | "id": "00000000000000000000000000000000" 221 | } 222 | ], 223 | "type": "volumev3", 224 | "id": "00000000000000000000000000000000", 225 | "name": "cinderv3" 226 | }, 227 | { 228 | "endpoints": [ 229 | { 230 | "region_id": "region1", 231 | "url": "http://localhost:REPLACE-PORT/v2.0", 232 | "region": "region1", 233 | "interface": "internal", 234 | "id": "00000000000000000000000000000000" 235 | } 236 | ], 237 | "type": "monitoring", 238 | "id": "00000000000000000000000000000000", 239 | "name": "monasca" 240 | }, 241 | { 242 | "endpoints": [ 243 | { 244 | "region_id": "region1", 245 | "url": "http://localhost:REPLACE-PORT/", 246 | "region": "region1", 247 | "interface": "admin", 248 | "id": "00000000000000000000000000000000" 249 | } 250 | ], 251 | "type": "metering", 252 | "id": "00000000000000000000000000000000", 253 | "name": "ceilometer" 254 | }, 255 | { 256 | "endpoints": [ 257 | { 258 | "region_id": "region1", 259 | "url": "http://localhost:REPLACE-PORT/v3.0", 260 | "region": "region1", 261 | "interface": "admin", 262 | "id": "00000000000000000000000000000000" 263 | } 264 | ], 265 | "type": "logging", 266 | "id": "00000000000000000000000000000000", 267 | "name": "kronos" 268 | }, 269 | { 270 | "endpoints": [ 271 | { 272 | "region_id": "region1", 273 | "url": "http://localhost:REPLACE-PORT/v1/00000000000000000000000000000000", 274 | "region": "region1", 275 | "interface": "internal", 276 | "id": "00000000000000000000000000000000" 277 | } 278 | ], 279 | "type": "share", 280 | "id": "00000000000000000000000000000000", 281 | "name": "manila" 282 | }, 283 | { 284 | "endpoints": [ 285 | { 286 | "region_id": "region1", 287 | "url": "http://localhost:REPLACE-PORT/api/v1/", 288 | "region": "region1", 289 | "interface": "internal", 290 | "id": "00000000000000000000000000000000" 291 | } 292 | ], 293 | "type": "opsconsole", 294 | "id": "00000000000000000000000000000000", 295 | "name": "opsconsole" 296 | }, 297 | { 298 | "endpoints": [ 299 | { 300 | "region_id": "region1", 301 | "url": "http://localhost:REPLACE-PORT", 302 | "region": "region1", 303 | "interface": "public", 304 | "id": "00000000000000000000000000000000" 305 | } 306 | ], 307 | "type": "image", 308 | "id": "00000000000000000000000000000000", 309 | "name": "glance" 310 | } 311 | ], 312 | "expires_at": "2919-10-31T12:49:19.000000Z", 313 | "user": { 314 | "password_expires_at": null, 315 | "domain": { 316 | "id": "00000000000000000000000000000000", 317 | "name": "LDAP" 318 | }, 319 | "id": "00000000000000000000000000000000", 320 | "name": "foo" 321 | }, 322 | "audit_ids": [ 323 | "00000000000000000000000000000000" 324 | ], 325 | "issued_at": "2919-10-31T08:49:19.000000Z" 326 | } 327 | } 328 | -------------------------------------------------------------------------------- /cloud-openstack-server/src/main/java/jetbrains/buildServer/clouds/openstack/OpenstackCloudInstance.java: -------------------------------------------------------------------------------- 1 | package jetbrains.buildServer.clouds.openstack; 2 | 3 | import java.io.File; 4 | import java.io.IOException; 5 | import java.nio.charset.StandardCharsets; 6 | import java.util.Date; 7 | import java.util.Map; 8 | import java.util.concurrent.ScheduledExecutorService; 9 | import java.util.concurrent.atomic.AtomicReference; 10 | 11 | import org.jclouds.openstack.nova.v2_0.domain.Server; 12 | import org.jclouds.openstack.nova.v2_0.domain.ServerCreated; 13 | import org.jclouds.openstack.nova.v2_0.options.CreateServerOptions; 14 | import org.jetbrains.annotations.NotNull; 15 | import org.jetbrains.annotations.Nullable; 16 | 17 | import com.google.common.base.Strings; 18 | import com.intellij.openapi.diagnostic.Logger; 19 | import com.intellij.openapi.util.text.StringUtil; 20 | 21 | import jetbrains.buildServer.clouds.CloudConstants; 22 | import jetbrains.buildServer.clouds.CloudErrorInfo; 23 | import jetbrains.buildServer.clouds.CloudInstance; 24 | import jetbrains.buildServer.clouds.CloudInstanceUserData; 25 | import jetbrains.buildServer.clouds.InstanceStatus; 26 | import jetbrains.buildServer.log.Loggers; 27 | import jetbrains.buildServer.serverSide.AgentDescription; 28 | import jetbrains.buildServer.serverSide.ServerPaths; 29 | import jetbrains.buildServer.util.ExceptionUtil; 30 | import jetbrains.buildServer.util.FileUtil; 31 | 32 | public class OpenstackCloudInstance implements CloudInstance { 33 | @NotNull 34 | private static final Logger LOG = Logger.getInstance(Loggers.CLOUD_CATEGORY_ROOT); 35 | @NotNull 36 | private final String instanceId; 37 | @NotNull 38 | private final ServerPaths serverPaths; 39 | @NotNull 40 | private final OpenstackCloudImage cloudImage; 41 | @NotNull 42 | private final Date startDate; 43 | @Nullable 44 | private CloudErrorInfo errorInfo; 45 | @Nullable 46 | private ServerCreated serverCreated; 47 | @NotNull 48 | private final ScheduledExecutorService executor; 49 | private String ip; 50 | 51 | private final AtomicReference status = new AtomicReference<>(InstanceStatus.UNKNOWN); 52 | 53 | public OpenstackCloudInstance(@NotNull final OpenstackCloudImage image, @NotNull final String instanceId, @NotNull ServerPaths serverPaths, 54 | @NotNull ScheduledExecutorService executor) { 55 | this.cloudImage = image; 56 | this.instanceId = instanceId; 57 | this.serverPaths = serverPaths; 58 | this.startDate = new Date(); 59 | this.executor = executor; 60 | setStatus(InstanceStatus.SCHEDULED_TO_START); 61 | } 62 | 63 | public OpenstackCloudInstance(@NotNull final OpenstackCloudImage image, @NotNull final String instanceId, @NotNull ServerPaths serverPaths, 64 | @NotNull ScheduledExecutorService executor, @NotNull Server server) { 65 | this.cloudImage = image; 66 | this.instanceId = instanceId; 67 | this.serverPaths = serverPaths; 68 | this.executor = executor; 69 | this.startDate = server.getCreated(); 70 | final String id = server.getId(); 71 | final String name = server.getName(); 72 | this.serverCreated = ServerCreated.builder().id(id).name(name).diskConfig(server.getDiskConfig().orNull()).build(); 73 | LOG.info(String.format("Cloud openstack instance restored: %s", name)); 74 | } 75 | 76 | public synchronized void updateStatus(Server.Status status) { 77 | try { 78 | LOG.debug(String.format("Set status for openstack instance %s: %s (previous was: %s)", getName(), status, getStatus())); 79 | if (serverCreated == null) { 80 | LOG.debug("Will skip status updating cause instance is not created yet"); 81 | return; 82 | } 83 | if (status == null) { 84 | terminate(); 85 | throw new OpenstackException(String.format("Status cannot be found for instance (so terminated): %s", getName())); 86 | } 87 | switch (status) { 88 | case UNKNOWN: 89 | setStatus(InstanceStatus.UNKNOWN); 90 | break; 91 | case BUILD: 92 | case REBUILD: 93 | setStatus(InstanceStatus.STARTING); 94 | break; 95 | case ACTIVE: 96 | // When OpenStack instance is stopping, the status is always 'ACTIVE' => check if termination started 97 | if (InstanceStatus.SCHEDULED_TO_STOP.equals(getStatus()) || InstanceStatus.STOPPING.equals(getStatus())) { 98 | setStatus(InstanceStatus.STOPPING); 99 | } else { 100 | setStatus(InstanceStatus.RUNNING); 101 | } 102 | break; 103 | case ERROR: 104 | terminate(); 105 | break; 106 | case SHUTOFF: 107 | terminate(); 108 | break; 109 | case DELETED: 110 | case SUSPENDED: 111 | case PAUSED: 112 | case SOFT_DELETED: 113 | case UNRECOGNIZED: 114 | case SHELVED: 115 | case SHELVED_OFFLOADED: 116 | default: 117 | setStatus(InstanceStatus.STOPPED); 118 | break; 119 | } 120 | } catch (final Exception e) { 121 | processError(e); 122 | } 123 | } 124 | 125 | @NotNull 126 | public String getOpenstackInstanceId() { 127 | return serverCreated != null ? serverCreated.getId() : ""; 128 | } 129 | 130 | @NotNull 131 | public InstanceStatus getStatus() { 132 | final CloudErrorInfo er = getErrorInfo(); 133 | return er != null ? InstanceStatus.ERROR : status.get(); 134 | } 135 | 136 | public void setStatus(@NotNull InstanceStatus status) { 137 | this.status.set(status); 138 | } 139 | 140 | @NotNull 141 | public String getInstanceId() { 142 | return instanceId; 143 | } 144 | 145 | @NotNull 146 | public String getName() { 147 | return cloudImage.getName() + "-" + instanceId; 148 | } 149 | 150 | @NotNull 151 | public String getImageId() { 152 | return cloudImage.getId(); 153 | } 154 | 155 | @NotNull 156 | public OpenstackCloudImage getImage() { 157 | return cloudImage; 158 | } 159 | 160 | @NotNull 161 | public Date getStartedTime() { 162 | return startDate; 163 | } 164 | 165 | public String getNetworkIdentity() { 166 | return ip; 167 | } 168 | 169 | @Nullable 170 | public CloudErrorInfo getErrorInfo() { 171 | return errorInfo; 172 | } 173 | 174 | public boolean containsAgent(@NotNull final AgentDescription agentDescription) { 175 | final Map configParams = agentDescription.getConfigurationParameters(); 176 | return configParams.containsValue(OpenstackCloudParameters.CLOUD_TYPE) 177 | && getOpenstackInstanceId().equals(configParams.get(OpenstackCloudParameters.OPENSTACK_INSTANCE_ID)); 178 | } 179 | 180 | public void start(@NotNull final CloudInstanceUserData data) { 181 | LOG.info(String.format("Starting cloud openstack instance %s", getName())); 182 | data.setAgentRemovePolicy(CloudConstants.AgentRemovePolicyValue.RemoveAgent); 183 | executor.submit(ExceptionUtil.catchAll("start openstack cloud: " + this, new StartAgentCommand(data))); 184 | } 185 | 186 | public void restart() { 187 | throw new UnsupportedOperationException("Restart openstack instance operation is not supported yet"); 188 | } 189 | 190 | public void stop() { 191 | LOG.info(String.format("Stopping cloud openstack instance %s", getName())); 192 | setStatus(InstanceStatus.SCHEDULED_TO_STOP); 193 | try { 194 | if (serverCreated != null) { 195 | cloudImage.getNovaServerApi().stop(serverCreated.getId()); 196 | } 197 | } catch (final Exception e) { 198 | processError(e); 199 | setStatus(InstanceStatus.ERROR_CANNOT_STOP); 200 | } 201 | } 202 | 203 | private void terminate() { 204 | LOG.info(String.format("Terminating cloud openstack instance %s", getName())); 205 | setStatus(InstanceStatus.STOPPED); 206 | try { 207 | if (serverCreated != null) { 208 | cloudImage.getNovaServerApi().delete(serverCreated.getId()); 209 | } 210 | } catch (final Exception e) { 211 | processError(e); 212 | setStatus(InstanceStatus.ERROR_CANNOT_STOP); 213 | } 214 | } 215 | 216 | private void processError(@NotNull final Exception e) { 217 | final String message = e.getMessage(); 218 | LOG.error(message, e); 219 | errorInfo = new CloudErrorInfo(message, message, e); 220 | setStatus(InstanceStatus.ERROR); 221 | } 222 | 223 | private class StartAgentCommand implements Runnable { 224 | private final CloudInstanceUserData userData; 225 | 226 | public StartAgentCommand(@NotNull final CloudInstanceUserData data) { 227 | this.userData = data; 228 | } 229 | 230 | private byte[] readUserScriptFile(File userScriptFile) throws IOException { 231 | try { 232 | String userScript = FileUtil.readText(userScriptFile); 233 | // this is userScript actually, but CreateServerOptionscalls it userData 234 | return userScript.trim().getBytes(StandardCharsets.UTF_8); 235 | } catch (IOException e) { 236 | throw new IOException(String.format("Error in reading user script: %s", e.getMessage()), e); 237 | } 238 | } 239 | 240 | public void run() { 241 | try { 242 | String floatingIp = null; 243 | if (cloudImage.isAutoFloatingIp()) { 244 | // Floating ip should be in meta-data before instance start 245 | // If multiple instances start in parallel, perhaps same ip could be retrieved 246 | // So an ip reservation mechanism should implemented in this case 247 | LOG.debug("Retrieve floating ip for future instance association"); 248 | floatingIp = cloudImage.getFloatingIpAvailable(); 249 | if (StringUtil.isEmpty(floatingIp)) { 250 | throw new OpenstackException("Floating ip could not be found, cancel instance start"); 251 | } 252 | LOG.debug(String.format("Floating ip: %s", floatingIp)); 253 | userData.addAgentConfigurationParameter(OpenstackCloudParameters.AGENT_CLOUD_IP, floatingIp); 254 | } 255 | 256 | String openstackImageId = cloudImage.getOpenstackImageId(); 257 | if (StringUtil.isEmpty(openstackImageId)) { 258 | throw new OpenstackException(String.format("No image can be found for name: %s", cloudImage.getOpenstackImageName())); 259 | } 260 | String flavorId = cloudImage.getFlavorId(); 261 | CreateServerOptions options = cloudImage.getImageOptions(); 262 | options.metadata(userData.getCustomAgentConfigurationParameters()); 263 | 264 | // TODO: that code should be in OpenstackCloudImage but as we make it possible to change userScript without touching teamcity, that 265 | // hack takes place, sorry 266 | String userScriptPath = cloudImage.getUserScriptPath(); 267 | if (!Strings.isNullOrEmpty(userScriptPath)) { 268 | File pluginData = serverPaths.getPluginDataDirectory(); 269 | File userScriptFile = new File(new File(pluginData, OpenstackCloudParameters.PLUGIN_SHORT_NAME), userScriptPath); 270 | options.userData(readUserScriptFile(userScriptFile)).configDrive(true); 271 | } 272 | 273 | LOG.debug(String.format("Creating openstack instance with flavorId=%s, imageId=%s, options=%s", flavorId, openstackImageId, options)); 274 | serverCreated = cloudImage.getNovaServerApi().create(getName(), openstackImageId, flavorId, options); 275 | 276 | if (cloudImage.isAutoFloatingIp()) { 277 | LOG.debug(String.format("Associating floating ip to serverId %s", serverCreated.getId())); 278 | // Associating floating IP. Require fixed IP so wait until found 279 | final long maxWait = 120000; 280 | final long beginWait = System.currentTimeMillis(); 281 | while (cloudImage.getNovaServerApi().get(serverCreated.getId()).getAddresses().isEmpty()) { 282 | if (System.currentTimeMillis() > (beginWait + maxWait)) { 283 | throw new OpenstackException(String.format("Waiting fixed ip fails, taking more than %s ms", maxWait)); 284 | } 285 | LOG.debug(String.format("(Waiting fixed ip before floating ip association on serverId: %s)", serverCreated.getId())); 286 | Thread.sleep(1000); 287 | } 288 | cloudImage.associateFloatingIp(serverCreated.getId(), floatingIp); 289 | ip = floatingIp; 290 | } 291 | 292 | setStatus(InstanceStatus.STARTING); 293 | } catch (final Exception e) { 294 | processError(e); 295 | } 296 | } 297 | } 298 | } 299 | -------------------------------------------------------------------------------- /cloud-openstack-server/src/test/java/jetbrains/buildServer/clouds/openstack/OpenstackCloudClientMockedTest.java: -------------------------------------------------------------------------------- 1 | package jetbrains.buildServer.clouds.openstack; 2 | 3 | import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; 4 | import static com.github.tomakehurst.wiremock.client.WireMock.delete; 5 | import static com.github.tomakehurst.wiremock.client.WireMock.get; 6 | import static com.github.tomakehurst.wiremock.client.WireMock.post; 7 | import static com.github.tomakehurst.wiremock.client.WireMock.stubFor; 8 | import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig; 9 | 10 | import java.io.File; 11 | import java.io.IOException; 12 | 13 | import org.apache.commons.io.FileUtils; 14 | import org.mockito.Mockito; 15 | import org.testng.Assert; 16 | import org.testng.annotations.AfterMethod; 17 | import org.testng.annotations.BeforeMethod; 18 | import org.testng.annotations.Test; 19 | 20 | import com.github.tomakehurst.wiremock.WireMockServer; 21 | import com.github.tomakehurst.wiremock.client.WireMock; 22 | import com.github.tomakehurst.wiremock.stubbing.Scenario; 23 | 24 | import jetbrains.buildServer.clouds.CloudImage; 25 | import jetbrains.buildServer.clouds.openstack.util.Lo4jBeanAppender; 26 | import jetbrains.buildServer.serverSide.TeamCityProperties; 27 | import jetbrains.buildServer.serverSide.TeamCityProperties.Model; 28 | import jetbrains.buildServer.serverSide.TeamCityPropertiesMock; 29 | 30 | public class OpenstackCloudClientMockedTest extends AbstractTestOpenstackCloudClient { 31 | 32 | private static final String SCENARIO = "server scenario"; 33 | private static final String SCENARIO_STATE_INIT = "init"; 34 | private static final String SCENARIO_STATE_RUN = "run"; 35 | 36 | private WireMockServer wireMockServer; 37 | 38 | @BeforeMethod 39 | public void setUp() throws Exception { 40 | // Initialise manually, WireMockRule requires JUnit 41 | wireMockServer = new WireMockServer(wireMockConfig().dynamicPort()); 42 | wireMockServer.start(); 43 | WireMock.configureFor(wireMockServer.port()); 44 | Lo4jBeanAppender.clear(); 45 | 46 | Model model = TeamCityProperties.getModel(); 47 | System.out.println(model); 48 | 49 | // Accelerate a little the Mocked unit test 50 | TeamCityPropertiesMock.addProperty(OpenstackCloudImage.DELAY_STATUS_INITIAL_KEY, "3"); 51 | TeamCityPropertiesMock.addProperty(OpenstackCloudImage.DELAY_STATUS_DELAY_KEY, "3"); 52 | } 53 | 54 | @AfterMethod 55 | public void tearDown() { 56 | wireMockServer.stop(); 57 | TeamCityPropertiesMock.reset(); 58 | } 59 | 60 | private String getJSonTestAsStringWithReplaces(String fileName, String... replaces) throws IOException { 61 | String content = FileUtils.readFileToString(new File("src/test/resources/__files", fileName)); 62 | final String p = "REPLACE-"; 63 | if (!content.contains(p)) { 64 | throw new IOException(String.format("File '%s' does not contain any '%s' value", fileName, p)); 65 | } 66 | if (replaces == null || replaces.length % 2 != 0) { 67 | throw new IOException("Replacement values are null/empty or not div 2"); 68 | } 69 | for (int i = 0; i < replaces.length; i = i + 2) { 70 | content = content.replaceAll(replaces[i], replaces[i + 1]); 71 | } 72 | return content; 73 | } 74 | 75 | @Override 76 | protected CloudImage spyCloudImage(CloudImage image) { 77 | OpenstackCloudImage spy = Mockito.spy((OpenstackCloudImage) image); 78 | Mockito.when(spy.getNextInstanceId()).thenReturn("42"); 79 | return spy; 80 | } 81 | 82 | private void initVMStart() throws Exception { 83 | stubFor(post("/v3/auth/tokens").willReturn(aResponse().withStatus(201).withHeader("X-Subject-Token", "test-subject-token") 84 | .withBody(getJSonTestAsStringWithReplaces("v3-auth-tokens.json", "REPLACE-PORT", String.valueOf(wireMockServer.port()))))); 85 | stubFor(get("/v2.0/networks").willReturn(aResponse().withBodyFile("v2.0.networks.json"))); 86 | stubFor(get("/v2.0/floatingips").willReturn(aResponse().withBodyFile("v2.0.floatingips.json"))); 87 | stubFor(get("/v2.1/nova-id/images/detail").willReturn(aResponse().withBodyFile("v2.1-nova-id-images-detail.json"))); 88 | stubFor(get("/v2.1/nova-id/flavors/detail").willReturn(aResponse().withBodyFile("v2.1-nova-id-flavors-detail.json"))); 89 | stubFor(post("/v2.1/nova-id/servers").willReturn(aResponse().withStatus(201).withBodyFile("v2.1-nova-id-servers.json"))); 90 | stubFor(get("/v2.1/nova-id/extensions").willReturn(aResponse().withBody("{}"))); 91 | } 92 | 93 | @Test 94 | public void testTokenExpirationDoNotRemoveAgent() throws Exception { 95 | initVMStart(); 96 | 97 | // First call is for cloud profile creation => "empty" (not status for VM created) 98 | stubFor(get("/v2.1/nova-id/servers/detail").inScenario(SCENARIO).whenScenarioStateIs(Scenario.STARTED).willSetStateTo("profilecreation") 99 | .willReturn(aResponse().withBodyFile("v2.1-nova-id-servers-detail-empty.json"))); 100 | // Second call is for VMs restoration => "empty" (not status for VM created) 101 | stubFor(get("/v2.1/nova-id/servers/detail").inScenario(SCENARIO).whenScenarioStateIs("profilecreation").willSetStateTo(SCENARIO_STATE_INIT) 102 | .willReturn(aResponse().withBodyFile("v2.1-nova-id-servers-detail-empty.json"))); 103 | stubFor(get("/v2.1/nova-id/servers/detail").inScenario(SCENARIO).whenScenarioStateIs(SCENARIO_STATE_INIT).willSetStateTo(SCENARIO_STATE_RUN) 104 | .willReturn(aResponse().withBodyFile("v2.1-nova-id-servers-detail-build.json"))); 105 | stubFor(get("/v2.1/nova-id/servers/detail").inScenario(SCENARIO).whenScenarioStateIs(SCENARIO_STATE_RUN).willSetStateTo("tokenExpired1") 106 | .willReturn(aResponse().withBodyFile("v2.1-nova-id-servers-detail-run.json"))); 107 | 108 | // Introduce authentication problem in status request (multiple, to deal with retry-mechanism) 109 | final String msg401 = "{\"error\": {\"code\": 401, \"title\": \"Unauthorized\", \"message\": \"The request you have made requires authentication.\"}}"; 110 | stubFor(get("/v2.1/nova-id/servers/detail").inScenario(SCENARIO).whenScenarioStateIs("tokenExpired1").willSetStateTo("tokenExpired2") 111 | .willReturn(aResponse().withStatus(401).withBody(msg401))); 112 | stubFor(get("/v2.1/nova-id/servers/detail").inScenario(SCENARIO).whenScenarioStateIs("tokenExpired2").willSetStateTo("tokenExpired3") 113 | .willReturn(aResponse().withStatus(401).withBody(msg401))); 114 | stubFor(get("/v2.1/nova-id/servers/detail").inScenario(SCENARIO).whenScenarioStateIs("tokenExpired3").willSetStateTo("tokenExpired4") 115 | .willReturn(aResponse().withStatus(401).withBody(msg401))); 116 | stubFor(get("/v2.1/nova-id/servers/detail").inScenario(SCENARIO).whenScenarioStateIs("tokenExpired4").willSetStateTo("tokenExpired5") 117 | .willReturn(aResponse().withStatus(401).withBody(msg401))); 118 | stubFor(get("/v2.1/nova-id/servers/detail").inScenario(SCENARIO).whenScenarioStateIs("tokenExpired5").willSetStateTo("run2") 119 | .willReturn(aResponse().withStatus(401).withBody(msg401))); 120 | 121 | // Following request is a classic run 122 | stubFor(get("/v2.1/nova-id/servers/detail").inScenario(SCENARIO).whenScenarioStateIs("run2").willSetStateTo("stopping") 123 | .willReturn(aResponse().withBodyFile("v2.1-nova-id-servers-detail-run.json"))); 124 | stubFor(get("/v2.1/nova-id/servers/detail").inScenario(SCENARIO).whenScenarioStateIs("stopping").willSetStateTo("stopped") 125 | .willReturn(aResponse().withBodyFile("v2.1-nova-id-servers-detail-stopping.json"))); 126 | stubFor(get("/v2.1/nova-id/servers/detail").inScenario(SCENARIO).whenScenarioStateIs("stopped") 127 | .willReturn(aResponse().withBodyFile("v2.1-nova-id-servers-detail-stopped.json"))); 128 | 129 | // POST, do not add the content (should be ~"{ \"os-stop\" : null \"}" but only /action is a stop in scenario) 130 | stubFor(post("/v2.1/nova-id/servers/server-id/action").willReturn(aResponse().withStatus(202))); 131 | 132 | stubFor(delete("/v2.1/nova-id/servers/server-id").willReturn(aResponse().withStatus(204))); 133 | 134 | // UNKNOW status could be in state due to 401 error 135 | testSubSimple(wireMockServer.baseUrl() + "/v3", "default:my-tenant:ldap:foo", "bar", "region1", getTestYaml("Mock"), false, true); 136 | } 137 | 138 | @Test 139 | public void testRestore() throws Exception { 140 | initVMStart(); 141 | 142 | // First call with VM already exist ... in other call the status will not exist 143 | stubFor(get("/v2.1/nova-id/servers/detail").inScenario(SCENARIO).whenScenarioStateIs(Scenario.STARTED).willSetStateTo(SCENARIO_STATE_INIT) 144 | .willReturn(aResponse().withBodyFile("v2.1-nova-id-servers-detail-restore.json"))); 145 | stubFor(get("/v2.1/nova-id/servers/detail").inScenario(SCENARIO).whenScenarioStateIs(SCENARIO_STATE_INIT).willSetStateTo(SCENARIO_STATE_RUN) 146 | .willReturn(aResponse().withBodyFile("v2.1-nova-id-servers-detail-build.json"))); 147 | stubFor(get("/v2.1/nova-id/servers/detail").inScenario(SCENARIO).whenScenarioStateIs(SCENARIO_STATE_RUN).willSetStateTo("stopping") 148 | .willReturn(aResponse().withBodyFile("v2.1-nova-id-servers-detail-run.json"))); 149 | stubFor(get("/v2.1/nova-id/servers/detail").inScenario(SCENARIO).whenScenarioStateIs("stopping").willSetStateTo("stopped") 150 | .willReturn(aResponse().withBodyFile("v2.1-nova-id-servers-detail-stopping.json"))); 151 | stubFor(get("/v2.1/nova-id/servers/detail").inScenario(SCENARIO).whenScenarioStateIs("stopped") 152 | .willReturn(aResponse().withBodyFile("v2.1-nova-id-servers-detail-stopped.json"))); 153 | 154 | // POST, do not add the content (should be ~"{ \"os-stop\" : null \"}" but only /action is a stop in scenario) 155 | stubFor(post("/v2.1/nova-id/servers/server-id/action").willReturn(aResponse().withStatus(202))); 156 | 157 | stubFor(delete("/v2.1/nova-id/servers/server-id").willReturn(aResponse().withStatus(204))); 158 | 159 | testSubSimple(wireMockServer.baseUrl() + "/v3", "default:my-tenant:ldap:foo", "bar", "region1", getTestYaml("Mock")); 160 | } 161 | 162 | @Test 163 | public void testNoError() throws Exception { 164 | initVMStart(); 165 | 166 | // First call is for VMs restoration => "empty" (not status for VM created) 167 | stubFor(get("/v2.1/nova-id/servers/detail").inScenario(SCENARIO).whenScenarioStateIs(Scenario.STARTED).willSetStateTo(SCENARIO_STATE_INIT) 168 | .willReturn(aResponse().withBodyFile("v2.1-nova-id-servers-detail-empty.json"))); 169 | stubFor(get("/v2.1/nova-id/servers/detail").inScenario(SCENARIO).whenScenarioStateIs(SCENARIO_STATE_INIT).willSetStateTo(SCENARIO_STATE_RUN) 170 | .willReturn(aResponse().withBodyFile("v2.1-nova-id-servers-detail-build.json"))); 171 | stubFor(get("/v2.1/nova-id/servers/detail").inScenario(SCENARIO).whenScenarioStateIs(SCENARIO_STATE_RUN).willSetStateTo("stopping1") 172 | .willReturn(aResponse().withBodyFile("v2.1-nova-id-servers-detail-run.json"))); 173 | stubFor(get("/v2.1/nova-id/servers/detail").inScenario(SCENARIO).whenScenarioStateIs("stopping1").willSetStateTo("stopping2") 174 | .willReturn(aResponse().withBodyFile("v2.1-nova-id-servers-detail-stopping.json"))); 175 | stubFor(get("/v2.1/nova-id/servers/detail").inScenario(SCENARIO).whenScenarioStateIs("stopping2").willSetStateTo("stopped") 176 | .willReturn(aResponse().withBodyFile("v2.1-nova-id-servers-detail-stopping.json"))); 177 | stubFor(get("/v2.1/nova-id/servers/detail").inScenario(SCENARIO).whenScenarioStateIs("stopped") 178 | .willReturn(aResponse().withBodyFile("v2.1-nova-id-servers-detail-stopped.json"))); 179 | 180 | // POST, do not add the content (should be ~"{ \"os-stop\" : null \"}" but only /action is a stop in scenario) 181 | stubFor(post("/v2.1/nova-id/servers/server-id/action").willReturn(aResponse().withStatus(202))); 182 | 183 | stubFor(delete("/v2.1/nova-id/servers/server-id").willReturn(aResponse().withStatus(204))); 184 | 185 | testSubSimple(wireMockServer.baseUrl() + "/v3", "default:my-tenant:ldap:foo", "bar", "region1", getTestYaml("Mock")); 186 | } 187 | 188 | @Test 189 | public void testErrorStatusEmpty() throws Exception { 190 | initVMStart(); 191 | 192 | // Empty status for created instance 193 | stubFor(get("/v2.1/nova-id/servers/detail").willReturn(aResponse().withBodyFile("v2.1-nova-id-servers-detail-empty.json"))); 194 | 195 | // /action is a stop in testSubSimple scenario (should be here even if not called in real life) 196 | stubFor(post("/v2.1/nova-id/servers/server-id/action").willReturn(aResponse().withStatus(202))); 197 | 198 | // DELETE is required for "terminate" instance when no problem to retrieve all VMs status but current VM status is not found (see #62) 199 | stubFor(delete("/v2.1/nova-id/servers/server-id").willReturn(aResponse().withStatus(204))); 200 | 201 | String err = testSubSimple(wireMockServer.baseUrl() + "/v3", "default:my-tenant:ldap:foo", "bar", "region1", getTestYaml("Mock"), true, 202 | false); 203 | Assert.assertNotNull(err); 204 | Assert.assertTrue(err.contains("Status cannot be found for instance"), err); 205 | } 206 | 207 | @Test 208 | public void testErrorStop() throws Exception { 209 | initVMStart(); 210 | // State 'RUN' indefinitely even if stop engaged 211 | stubFor(get("/v2.1/nova-id/servers/detail").inScenario(SCENARIO).whenScenarioStateIs(Scenario.STARTED).willSetStateTo(SCENARIO_STATE_RUN) 212 | .willReturn(aResponse().withBodyFile("v2.1-nova-id-servers-detail-empty.json"))); 213 | stubFor(get("/v2.1/nova-id/servers/detail").inScenario(SCENARIO).whenScenarioStateIs(SCENARIO_STATE_RUN).willSetStateTo(SCENARIO_STATE_RUN) 214 | .willReturn(aResponse().withBodyFile("v2.1-nova-id-servers-detail-run.json"))); 215 | 216 | // POST, do not add the content 217 | stubFor(post("/v2.1/nova-id/servers/server-id/action").willReturn(aResponse().withStatus(500))); 218 | 219 | // DELETE is required for "dispose" client call at end 220 | stubFor(delete("/v2.1/nova-id/servers/server-id").willReturn(aResponse().withStatus(204))); 221 | 222 | String err = testSubSimple(wireMockServer.baseUrl() + "/v3", "default:my-tenant:ldap:foo", "bar", "region1", getTestYaml("Mock"), false, 223 | true); 224 | Assert.assertNotNull(err); 225 | Assert.assertTrue(err.contains("os-stop"), err); 226 | } 227 | 228 | @Test 229 | public void testErrorTerminate() throws Exception { 230 | initVMStart(); 231 | 232 | // First call is for VMs restoration => "empty" (not status for VM created) 233 | stubFor(get("/v2.1/nova-id/servers/detail").inScenario(SCENARIO).whenScenarioStateIs(Scenario.STARTED).willSetStateTo(SCENARIO_STATE_INIT) 234 | .willReturn(aResponse().withBodyFile("v2.1-nova-id-servers-detail-empty.json"))); 235 | stubFor(get("/v2.1/nova-id/servers/detail").inScenario(SCENARIO).whenScenarioStateIs(SCENARIO_STATE_INIT).willSetStateTo(SCENARIO_STATE_RUN) 236 | .willReturn(aResponse().withBodyFile("v2.1-nova-id-servers-detail-build.json"))); 237 | stubFor(get("/v2.1/nova-id/servers/detail").inScenario(SCENARIO).whenScenarioStateIs(SCENARIO_STATE_RUN).willSetStateTo("stopping1") 238 | .willReturn(aResponse().withBodyFile("v2.1-nova-id-servers-detail-run.json"))); 239 | stubFor(get("/v2.1/nova-id/servers/detail").inScenario(SCENARIO).whenScenarioStateIs("stopping1").willSetStateTo("stopping2") 240 | .willReturn(aResponse().withBodyFile("v2.1-nova-id-servers-detail-stopping.json"))); 241 | stubFor(get("/v2.1/nova-id/servers/detail").inScenario(SCENARIO).whenScenarioStateIs("stopping2").willSetStateTo("stopped") 242 | .willReturn(aResponse().withBodyFile("v2.1-nova-id-servers-detail-stopping.json"))); 243 | stubFor(get("/v2.1/nova-id/servers/detail").inScenario(SCENARIO).whenScenarioStateIs("stopped").willSetStateTo("stopped") 244 | .willReturn(aResponse().withBodyFile("v2.1-nova-id-servers-detail-stopped.json"))); 245 | 246 | // POST, do not add the content (should be ~"{ \"os-stop\" : null \"}" but only /action is a stop in scenario) 247 | stubFor(post("/v2.1/nova-id/servers/server-id/action").willReturn(aResponse().withStatus(202))); 248 | 249 | stubFor(delete("/v2.1/nova-id/servers/server-id").willReturn(aResponse().withStatus(500))); 250 | 251 | String err = testSubSimple(wireMockServer.baseUrl() + "/v3", "default:my-tenant:ldap:foo", "bar", "region1", getTestYaml("Mock"), false, 252 | true); 253 | Assert.assertNotNull(err); 254 | Assert.assertTrue(err.contains("DELETE"), err); 255 | Assert.assertTrue(err.contains("Server Error"), err); 256 | } 257 | 258 | @Test 259 | public void testErrorClientNovaNPEOnUpdateStatus() throws Exception { 260 | initVMStart(); 261 | 262 | // First call is for VMs restoration => "empty" (not status for VM created) 263 | stubFor(get("/v2.1/nova-id/servers/detail").inScenario(SCENARIO).whenScenarioStateIs(Scenario.STARTED).willSetStateTo(SCENARIO_STATE_INIT) 264 | .willReturn(aResponse().withBodyFile("v2.1-nova-id-servers-detail-empty.json"))); 265 | 266 | // OpenStack response with a content without 'id' => will throw NPE on update status 267 | stubFor(get("/v2.1/nova-id/servers/detail").inScenario(SCENARIO).whenScenarioStateIs(SCENARIO_STATE_INIT).willSetStateTo(SCENARIO_STATE_INIT) 268 | .willReturn(aResponse().withBodyFile("v2.1-nova-id-servers-detail-not-defined.json"))); 269 | 270 | // Termination due to NPE + unit 'testSubSimple' termination 271 | stubFor(delete("/v2.1/nova-id/servers/server-id").willReturn(aResponse().withStatus(202))); 272 | 273 | // unit 'testDubSimple' termination 274 | stubFor(post("/v2.1/nova-id/servers/server-id/action").willReturn(aResponse().withStatus(294))); 275 | 276 | testSubSimple(wireMockServer.baseUrl() + "/v3", "default:my-tenant:ldap:foo", "bar", "region1", getTestYaml("Mock"), true, true); 277 | 278 | // Since #62, no termination call because agent not removed if status unknown 279 | // verify(deleteRequestedFor(urlMatching("/v2.1/nova-id/servers/server-id"))); 280 | } 281 | 282 | @Test 283 | public void testErrorClientNovaNPEOnRestore() throws Exception { 284 | initVMStart(); 285 | 286 | // First empty details for cloud profile creation 287 | stubFor(get("/v2.1/nova-id/servers/detail").inScenario(SCENARIO).whenScenarioStateIs(Scenario.STARTED).willSetStateTo(SCENARIO_STATE_INIT) 288 | .willReturn(aResponse().withBodyFile("v2.1-nova-id-servers-detail-empty.json"))); 289 | 290 | // Bad response (no id) on every call (restore, status update, ...) 291 | stubFor(get("/v2.1/nova-id/servers/detail").inScenario(SCENARIO).whenScenarioStateIs(SCENARIO_STATE_INIT) 292 | .willReturn(aResponse().withBodyFile("v2.1-nova-id-servers-detail-not-defined.json"))); 293 | 294 | // Termination due to NPE + unit 'testSubSimple' termination 295 | stubFor(delete("/v2.1/nova-id/servers/server-id").willReturn(aResponse().withStatus(202))); 296 | 297 | // unit 'testDubSimple' termination 298 | stubFor(post("/v2.1/nova-id/servers/server-id/action").willReturn(aResponse().withStatus(204))); 299 | 300 | testSubSimple(wireMockServer.baseUrl() + "/v3", "default:my-tenant:ldap:foo", "bar", "region1", getTestYaml("Mock"), true, true); 301 | 302 | // Since #62, no termination call because agent not removed if status unknown 303 | // verify(deleteRequestedFor(urlMatching("/v2.1/nova-id/servers/server-id"))); 304 | } 305 | 306 | } 307 | --------------------------------------------------------------------------------