diff --git a/clouddriver-oracle-bmcs/src/main/groovy/com/netflix/spinnaker/clouddriver/oraclebmcs/cache/Keys.groovy b/clouddriver-oracle-bmcs/src/main/groovy/com/netflix/spinnaker/clouddriver/oraclebmcs/cache/Keys.groovy index 4c9762f9f42..66afa6d3c46 100644 --- a/clouddriver-oracle-bmcs/src/main/groovy/com/netflix/spinnaker/clouddriver/oraclebmcs/cache/Keys.groovy +++ b/clouddriver-oracle-bmcs/src/main/groovy/com/netflix/spinnaker/clouddriver/oraclebmcs/cache/Keys.groovy @@ -17,6 +17,7 @@ class Keys { static enum Namespace { + SERVER_GROUPS, NETWORKS, SUBNETS, IMAGES, @@ -76,6 +77,19 @@ class Keys { account: parts[4] ] break + case Namespace.SERVER_GROUPS.ns: + def names = Names.parseName(parts[5]) + result << [ + application: names.app, + cluster : parts[2], + account : parts[3], + region : parts[4], + stack : names.stack, + detail : names.detail, + serverGroup: parts[5], + name : parts[5] + ] + break case Namespace.IMAGES.ns: result << [ account: parts[2], @@ -132,4 +146,9 @@ class Keys { "$OracleBMCSCloudProvider.ID:${Namespace.SUBNETS}:${subnetId}:${region}:${account}" } + static String getServerGroupKey(String account, String region, String serverGroupName) { + Names names = Names.parseName(serverGroupName) + "$OracleBMCSCloudProvider.ID:${Namespace.SERVER_GROUPS}:${names.cluster}:${account}:${region}:${serverGroupName}" + } + } diff --git a/clouddriver-oracle-bmcs/src/main/groovy/com/netflix/spinnaker/clouddriver/oraclebmcs/model/OracleBMCSCluster.groovy b/clouddriver-oracle-bmcs/src/main/groovy/com/netflix/spinnaker/clouddriver/oraclebmcs/model/OracleBMCSCluster.groovy new file mode 100644 index 00000000000..7c4f548bc8e --- /dev/null +++ b/clouddriver-oracle-bmcs/src/main/groovy/com/netflix/spinnaker/clouddriver/oraclebmcs/model/OracleBMCSCluster.groovy @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2017 Oracle America, Inc. + * + * The contents of this file are subject to the Apache License Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * If a copy of the Apache License Version 2.0 was not distributed with this file, + * You can obtain one at https://www.apache.org/licenses/LICENSE-2.0.html + */ +package com.netflix.spinnaker.clouddriver.oraclebmcs.model + +import com.fasterxml.jackson.annotation.JsonIgnore +import com.netflix.spinnaker.clouddriver.model.Cluster +import com.netflix.spinnaker.clouddriver.model.LoadBalancer +import com.netflix.spinnaker.clouddriver.oraclebmcs.OracleBMCSCloudProvider +import groovy.transform.Canonical +import groovy.transform.CompileStatic +import groovy.transform.EqualsAndHashCode + +@CompileStatic +@EqualsAndHashCode(includes = ["name", "accountName"]) +class OracleBMCSCluster { + + String name + String accountName + Set serverGroups + + @JsonIgnore + View getView() { + new View() + } + + @Canonical + class View implements Cluster { + + final String type = OracleBMCSCloudProvider.ID + + String name = OracleBMCSCluster.this.name + String accountName = OracleBMCSCluster.this.accountName + Set serverGroups = OracleBMCSCluster.this.serverGroups.collect { it.getView() } as Set + Set loadBalancers = [] as Set + } +} diff --git a/clouddriver-oracle-bmcs/src/main/groovy/com/netflix/spinnaker/clouddriver/oraclebmcs/model/OracleBMCSServerGroup.groovy b/clouddriver-oracle-bmcs/src/main/groovy/com/netflix/spinnaker/clouddriver/oraclebmcs/model/OracleBMCSServerGroup.groovy new file mode 100644 index 00000000000..351de8e1278 --- /dev/null +++ b/clouddriver-oracle-bmcs/src/main/groovy/com/netflix/spinnaker/clouddriver/oraclebmcs/model/OracleBMCSServerGroup.groovy @@ -0,0 +1,128 @@ +/* + * Copyright (c) 2017 Oracle America, Inc. + * + * The contents of this file are subject to the Apache License Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * If a copy of the Apache License Version 2.0 was not distributed with this file, + * You can obtain one at https://www.apache.org/licenses/LICENSE-2.0.html + */ +package com.netflix.spinnaker.clouddriver.oraclebmcs.model + +import com.fasterxml.jackson.annotation.JsonIgnore +import com.fasterxml.jackson.annotation.JsonInclude +import com.netflix.spinnaker.clouddriver.model.HealthState +import com.netflix.spinnaker.clouddriver.model.Instance +import com.netflix.spinnaker.clouddriver.model.ServerGroup +import com.netflix.spinnaker.clouddriver.oraclebmcs.OracleBMCSCloudProvider +import com.netflix.spinnaker.clouddriver.oraclebmcs.security.OracleBMCSNamedAccountCredentials +import groovy.transform.Canonical + +@Canonical +class OracleBMCSServerGroup { + + String name + String region + String zone + Set zones = new HashSet<>() + Set instances = [] + Map launchConfig = [:] + Set securityGroups = [] + Map buildInfo + Boolean disabled = false + Integer targetSize + OracleBMCSNamedAccountCredentials credentials + + @JsonIgnore + View getView() { + new View() + } + + @JsonInclude(JsonInclude.Include.NON_NULL) + @Canonical + class View implements ServerGroup { + + final String type = OracleBMCSCloudProvider.ID + final String cloudProvider = OracleBMCSCloudProvider.ID + + String name = OracleBMCSServerGroup.this.name + String region = OracleBMCSServerGroup.this.region + String zone = OracleBMCSServerGroup.this.zone + Set zones = OracleBMCSServerGroup.this.zones + Set instances = OracleBMCSServerGroup.this.instances + Map launchConfig = OracleBMCSServerGroup.this.launchConfig + Set securityGroups = OracleBMCSServerGroup.this.securityGroups + Map buildInfo = OracleBMCSServerGroup.this.buildInfo + Boolean disabled = OracleBMCSServerGroup.this.disabled + ServerGroup.Capacity capacity = new ServerGroup.Capacity(desired: OracleBMCSServerGroup.this.targetSize, + min: OracleBMCSServerGroup.this.targetSize, max: OracleBMCSServerGroup.this.targetSize) + + @Override + Boolean isDisabled() { // Because groovy isn't smart enough to generate this method :-( + disabled + } + + @Override + Long getCreatedTime() { + launchConfig ? launchConfig.createdTime as Long : null + } + + @Override + Set getLoadBalancers() { + return null + } + + @Override + ServerGroup.Capacity getCapacity() { + capacity + } + + @Override + ServerGroup.ImagesSummary getImagesSummary() { + def bi = OracleBMCSServerGroup.this.buildInfo + return new ServerGroup.ImagesSummary() { + + @Override + List getSummaries() { + return [new ServerGroup.ImageSummary() { + + String serverGroupName = name + String imageName = launchConfig?.instanceTemplate?.name + String imageId = launchConfig?.imageId + + @Override + Map getBuildInfo() { + return bi + } + + @Override + Map getImage() { + return launchConfig?.instanceTemplate + } + }] + } + } + } + + @Override + ServerGroup.ImageSummary getImageSummary() { + imagesSummary?.summaries?.get(0) + } + + @Override + ServerGroup.InstanceCounts getInstanceCounts() { + new ServerGroup.InstanceCounts( + total: instances.size(), + up: filterInstancesByHealthState(instances, HealthState.Up)?.size() ?: 0, + down: filterInstancesByHealthState(instances, HealthState.Down)?.size() ?: 0, + unknown: filterInstancesByHealthState(instances, HealthState.Unknown)?.size() ?: 0, + starting: filterInstancesByHealthState(instances, HealthState.Starting)?.size() ?: 0, + outOfService: filterInstancesByHealthState(instances, HealthState.OutOfService)?.size() ?: 0 + ) + } + + static Collection filterInstancesByHealthState(Set instances, HealthState healthState) { + instances.findAll { Instance it -> it.getHealthState() == healthState } + } + } + +} diff --git a/clouddriver-oracle-bmcs/src/main/groovy/com/netflix/spinnaker/clouddriver/oraclebmcs/provider/OracleBMCSInfrastructureProvider.groovy b/clouddriver-oracle-bmcs/src/main/groovy/com/netflix/spinnaker/clouddriver/oraclebmcs/provider/OracleBMCSInfrastructureProvider.groovy index 0aa320326fd..b580380770f 100644 --- a/clouddriver-oracle-bmcs/src/main/groovy/com/netflix/spinnaker/clouddriver/oraclebmcs/provider/OracleBMCSInfrastructureProvider.groovy +++ b/clouddriver-oracle-bmcs/src/main/groovy/com/netflix/spinnaker/clouddriver/oraclebmcs/provider/OracleBMCSInfrastructureProvider.groovy @@ -22,7 +22,10 @@ class OracleBMCSInfrastructureProvider extends AgentSchedulerAware implements Se final Set defaultCaches = [ Namespace.NETWORKS.ns, Namespace.SUBNETS.ns, - Namespace.IMAGES.ns + Namespace.IMAGES.ns, + Namespace.INSTANCES.ns, + Namespace.SECURITY_GROUPS.ns, + Namespace.SERVER_GROUPS.ns ].asImmutable() final Map urlMappingTemplates = [:] diff --git a/clouddriver-oracle-bmcs/src/main/groovy/com/netflix/spinnaker/clouddriver/oraclebmcs/provider/agent/OracleBMCSServerGroupCachingAgent.groovy b/clouddriver-oracle-bmcs/src/main/groovy/com/netflix/spinnaker/clouddriver/oraclebmcs/provider/agent/OracleBMCSServerGroupCachingAgent.groovy new file mode 100644 index 00000000000..6b3d192be48 --- /dev/null +++ b/clouddriver-oracle-bmcs/src/main/groovy/com/netflix/spinnaker/clouddriver/oraclebmcs/provider/agent/OracleBMCSServerGroupCachingAgent.groovy @@ -0,0 +1,74 @@ +/* + * Copyright (c) 2017 Oracle America, Inc. + * + * The contents of this file are subject to the Apache License Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * If a copy of the Apache License Version 2.0 was not distributed with this file, + * You can obtain one at https://www.apache.org/licenses/LICENSE-2.0.html + */ +package com.netflix.spinnaker.clouddriver.oraclebmcs.provider.agent + +import com.fasterxml.jackson.databind.ObjectMapper +import com.netflix.spinnaker.cats.agent.AgentDataType +import com.netflix.spinnaker.cats.agent.CacheResult +import com.netflix.spinnaker.cats.agent.DefaultCacheResult +import com.netflix.spinnaker.cats.cache.DefaultCacheData +import com.netflix.spinnaker.cats.provider.ProviderCache +import com.netflix.spinnaker.clouddriver.oraclebmcs.cache.Keys +import com.netflix.spinnaker.clouddriver.oraclebmcs.model.OracleBMCSServerGroup +import com.netflix.spinnaker.clouddriver.oraclebmcs.security.OracleBMCSNamedAccountCredentials +import com.netflix.spinnaker.clouddriver.oraclebmcs.service.servergroup.OracleBMCSServerGroupService +import groovy.transform.InheritConstructors +import groovy.util.logging.Slf4j + +/** + * A caching agent for Oracle BMCS server groups. + * + * The groups are persisted cloud-side by the OracleBMCSServerGroupService implementation. In this agent we just read + * all server groups that we can see given our credentials. + * + * This may be a slow operation due to the large number of API calls that the service makes. + * + * Created by hhexo on 27/01/2017. + */ +@Slf4j +@InheritConstructors +class OracleBMCSServerGroupCachingAgent extends AbstractOracleBMCSCachingAgent { + + final Set providedDataTypes = [ + AgentDataType.Authority.AUTHORITATIVE.forType(Keys.Namespace.SERVER_GROUPS.ns) + ] + + private OracleBMCSServerGroupService oracleBMCSServerGroupService + + OracleBMCSServerGroupCachingAgent(String clouddriverUserAgentApplicationName, OracleBMCSNamedAccountCredentials credentials, ObjectMapper objectMapper, OracleBMCSServerGroupService oracleBMCSServerGroupService) { + super(objectMapper, credentials, clouddriverUserAgentApplicationName) + this.oracleBMCSServerGroupService = oracleBMCSServerGroupService + } + + @Override + CacheResult loadData(ProviderCache providerCache) { + List serverGroups = this.oracleBMCSServerGroupService.listAllServerGroups(this.credentials) + return buildCacheResults(serverGroups) + } + + CacheResult buildCacheResults(List serverGroups) { + log.info("Describing items in $agentType") + def data = serverGroups.collect { OracleBMCSServerGroup sg -> + // Don't cache credentials, so save them and restore them later + def creds = sg.credentials + sg.credentials = null + Map attributes = objectMapper.convertValue(sg, ATTRIBUTES) + sg.credentials = creds + new DefaultCacheData( + Keys.getServerGroupKey(this.credentials.name, credentials.region, sg.name), + attributes, + [:] + ) + } + + def cacheData = [(Keys.Namespace.SERVER_GROUPS.ns): data] + log.info("Caching ${data.size()} items in ${agentType}") + return new DefaultCacheResult(cacheData, [:]) + } +} diff --git a/clouddriver-oracle-bmcs/src/main/groovy/com/netflix/spinnaker/clouddriver/oraclebmcs/provider/config/OracleBMCSInfrastructureProviderConfig.groovy b/clouddriver-oracle-bmcs/src/main/groovy/com/netflix/spinnaker/clouddriver/oraclebmcs/provider/config/OracleBMCSInfrastructureProviderConfig.groovy index f76c0222b42..c83cc38d746 100644 --- a/clouddriver-oracle-bmcs/src/main/groovy/com/netflix/spinnaker/clouddriver/oraclebmcs/provider/config/OracleBMCSInfrastructureProviderConfig.groovy +++ b/clouddriver-oracle-bmcs/src/main/groovy/com/netflix/spinnaker/clouddriver/oraclebmcs/provider/config/OracleBMCSInfrastructureProviderConfig.groovy @@ -17,6 +17,7 @@ import com.netflix.spinnaker.clouddriver.oraclebmcs.OracleBMCSConfiguration import com.netflix.spinnaker.clouddriver.oraclebmcs.provider.OracleBMCSInfrastructureProvider import com.netflix.spinnaker.clouddriver.oraclebmcs.provider.agent.* import com.netflix.spinnaker.clouddriver.oraclebmcs.security.OracleBMCSNamedAccountCredentials +import com.netflix.spinnaker.clouddriver.oraclebmcs.service.servergroup.OracleBMCSServerGroupService import com.netflix.spinnaker.clouddriver.security.AccountCredentialsRepository import com.netflix.spinnaker.clouddriver.security.ProviderUtils import org.springframework.beans.factory.config.ConfigurableBeanFactory @@ -35,7 +36,8 @@ class OracleBMCSInfrastructureProviderConfig { OracleBMCSInfrastructureProvider oracleBMCSInfrastructureProvider(String clouddriverUserAgentApplicationName, AccountCredentialsRepository accountCredentialsRepository, ObjectMapper objectMapper, - Registry registry) { + Registry registry, + OracleBMCSServerGroupService oracleBMCSServerGroupService) { def oracleBMCSInfrastructureProvider = new OracleBMCSInfrastructureProvider(Collections.newSetFromMap(new ConcurrentHashMap())) @@ -43,7 +45,8 @@ class OracleBMCSInfrastructureProviderConfig { oracleBMCSInfrastructureProvider, accountCredentialsRepository, objectMapper, - registry + registry, + oracleBMCSServerGroupService ) return oracleBMCSInfrastructureProvider @@ -55,6 +58,7 @@ class OracleBMCSInfrastructureProviderConfig { } class OracleBMCSInfrastructureProviderSynchronizerTypeWrapper implements ProviderSynchronizerTypeWrapper { + @Override Class getSynchronizerType() { return OracleBMCSInfrastructureProviderSynchronizer @@ -70,7 +74,8 @@ class OracleBMCSInfrastructureProviderConfig { OracleBMCSInfrastructureProvider oracleBMCSInfrastructureProvider, AccountCredentialsRepository accountCredentialsRepository, ObjectMapper objectMapper, - Registry registry) { + Registry registry, + OracleBMCSServerGroupService oracleBMCSServerGroupService) { def scheduledAccounts = ProviderUtils.getScheduledAccounts(oracleBMCSInfrastructureProvider) def allAccounts = ProviderUtils.buildThreadSafeSetOfAccounts(accountCredentialsRepository, OracleBMCSNamedAccountCredentials) @@ -98,6 +103,11 @@ class OracleBMCSInfrastructureProviderConfig { credentials, objectMapper) + newlyAddedAgents << new OracleBMCSServerGroupCachingAgent(clouddriverUserAgentApplicationName, + credentials, + objectMapper, + oracleBMCSServerGroupService) + newlyAddedAgents << new OracleBMCSImageCachingAgent(clouddriverUserAgentApplicationName, credentials, objectMapper) diff --git a/clouddriver-oracle-bmcs/src/main/groovy/com/netflix/spinnaker/clouddriver/oraclebmcs/provider/view/OracleBMCSClusterProvider.groovy b/clouddriver-oracle-bmcs/src/main/groovy/com/netflix/spinnaker/clouddriver/oraclebmcs/provider/view/OracleBMCSClusterProvider.groovy new file mode 100644 index 00000000000..d7658ef5eaa --- /dev/null +++ b/clouddriver-oracle-bmcs/src/main/groovy/com/netflix/spinnaker/clouddriver/oraclebmcs/provider/view/OracleBMCSClusterProvider.groovy @@ -0,0 +1,141 @@ +/* + * Copyright (c) 2017 Oracle America, Inc. + * + * The contents of this file are subject to the Apache License Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * If a copy of the Apache License Version 2.0 was not distributed with this file, + * You can obtain one at https://www.apache.org/licenses/LICENSE-2.0.html + */ +package com.netflix.spinnaker.clouddriver.oraclebmcs.provider.view + +import com.fasterxml.jackson.databind.ObjectMapper +import com.netflix.frigga.Names +import com.netflix.spinnaker.cats.cache.Cache +import com.netflix.spinnaker.cats.cache.RelationshipCacheFilter +import com.netflix.spinnaker.clouddriver.model.ClusterProvider +import com.netflix.spinnaker.clouddriver.model.HealthState +import com.netflix.spinnaker.clouddriver.model.ServerGroup +import com.netflix.spinnaker.clouddriver.oraclebmcs.OracleBMCSCloudProvider +import com.netflix.spinnaker.clouddriver.oraclebmcs.cache.Keys +import com.netflix.spinnaker.clouddriver.oraclebmcs.model.OracleBMCSCluster +import com.netflix.spinnaker.clouddriver.oraclebmcs.model.OracleBMCSServerGroup +import com.netflix.spinnaker.clouddriver.security.AccountCredentialsProvider +import groovy.util.logging.Slf4j +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.stereotype.Component + +@Slf4j +@Component +class OracleBMCSClusterProvider implements ClusterProvider { + + final String cloudProviderId = OracleBMCSCloudProvider.ID + + private OracleBMCSInstanceProvider instanceProvider + final ObjectMapper objectMapper + private AccountCredentialsProvider accountCredentialsProvider + private final Cache cacheView + + @Autowired + OracleBMCSClusterProvider(OracleBMCSInstanceProvider instanceProvider, + ObjectMapper objectMapper, + AccountCredentialsProvider accountCredentialsProvider, + Cache cacheView) { + this.instanceProvider = instanceProvider + this.objectMapper = objectMapper + this.accountCredentialsProvider = accountCredentialsProvider + this.cacheView = cacheView + } + + @Override + Map> getClusters() { + Collection identifiers = cacheView.getIdentifiers(Keys.Namespace.SERVER_GROUPS.ns) + Set serverGroups = loadServerGroups(identifiers) + return clustersFromServerGroups(serverGroups).groupBy { it.accountName } + } + + @Override + Map> getClusterSummaries(String application) { + getClusterDetails(application) + } + + @Override + Map> getClusterDetails(String application) { + Collection identifiers = cacheView.getIdentifiers(Keys.Namespace.SERVER_GROUPS.ns).findAll { + application == Keys.parse(it)?.get("application") + } + Set serverGroups = loadServerGroups(identifiers) + return clustersFromServerGroups(serverGroups).groupBy { it.accountName } + } + + @Override + Set getClusters(String application, String account) { + getClusterDetails(application)[account] + } + + @Override + OracleBMCSCluster.View getCluster(String application, String account, String name) { + getClusters(application, account).find { name == it.name } + } + + @Override + ServerGroup getServerGroup(String account, String region, String name) { + def pattern = Keys.getServerGroupKey(account, region, name) + def identifiers = cacheView.filterIdentifiers(Keys.Namespace.SERVER_GROUPS.ns, pattern) + Set serverGroups = loadServerGroups(identifiers) + if (serverGroups.isEmpty()) { + return null + } + return serverGroups.iterator().next().getView() + } + + private OracleBMCSServerGroup restoreCreds(OracleBMCSServerGroup partial, String identifier) { + def account = Keys.parse(identifier)?.get("account") + partial.credentials = accountCredentialsProvider.getCredentials(account) + return partial + } + + private Set loadServerGroups(Collection identifiers) { + def data = cacheView.getAll(Keys.Namespace.SERVER_GROUPS.ns, identifiers, RelationshipCacheFilter.none()) + return data.collect { cacheItem -> + def sg = objectMapper.convertValue(cacheItem.attributes, OracleBMCSServerGroup) + restoreCreds(sg, cacheItem.id) + + sg.instances?.each { + def instance = instanceProvider.getInstance(Keys.parse(cacheItem.id)?.get("account"), "*", it.id) + if (instance) { + it.healthState = instance.healthState + it.health = instance.health + if (sg.disabled) { + it.healthState = HealthState.OutOfService + it.health[0].state = HealthState.OutOfService.name() + } + + } + }?.removeAll { + def instance = instanceProvider.getInstance(Keys.parse(cacheItem.id)?.get("account"), "*", it.id) + return instance == null + } + + return sg + } + } + + private String accountFromServerGroups(List sgs) { + sgs?.iterator()?.next()?.credentials?.name + } + + private Set clustersFromServerGroups(Set serverGroups) { + Map> byClusterName = serverGroups.groupBy { + Names.parseName(it.name).cluster + } + + return byClusterName.collect { k, v -> + new OracleBMCSCluster( + name: k, + accountName: accountFromServerGroups(v), + serverGroups: v as Set + ).getView() + } + } + +} diff --git a/clouddriver-oracle-bmcs/src/main/groovy/com/netflix/spinnaker/clouddriver/oraclebmcs/service/servergroup/DefaultOracleBMCSServerGroupService.groovy b/clouddriver-oracle-bmcs/src/main/groovy/com/netflix/spinnaker/clouddriver/oraclebmcs/service/servergroup/DefaultOracleBMCSServerGroupService.groovy new file mode 100644 index 00000000000..8f896a00121 --- /dev/null +++ b/clouddriver-oracle-bmcs/src/main/groovy/com/netflix/spinnaker/clouddriver/oraclebmcs/service/servergroup/DefaultOracleBMCSServerGroupService.groovy @@ -0,0 +1,218 @@ +/* + * Copyright (c) 2017 Oracle America, Inc. + * + * The contents of this file are subject to the Apache License Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * If a copy of the Apache License Version 2.0 was not distributed with this file, + * You can obtain one at https://www.apache.org/licenses/LICENSE-2.0.html + */ +package com.netflix.spinnaker.clouddriver.oraclebmcs.service.servergroup + +import com.netflix.frigga.Names +import com.netflix.spinnaker.clouddriver.data.task.Task +import com.netflix.spinnaker.clouddriver.model.HealthState +import com.netflix.spinnaker.clouddriver.oraclebmcs.OracleBMCSCloudProvider +import com.netflix.spinnaker.clouddriver.oraclebmcs.model.OracleBMCSInstance +import com.netflix.spinnaker.clouddriver.oraclebmcs.model.OracleBMCSServerGroup +import com.netflix.spinnaker.clouddriver.oraclebmcs.security.OracleBMCSNamedAccountCredentials +import com.oracle.bmc.core.model.LaunchInstanceDetails +import com.oracle.bmc.core.requests.LaunchInstanceRequest +import com.oracle.bmc.core.requests.TerminateInstanceRequest +import com.oracle.bmc.model.BmcException +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.stereotype.Component + +@Component +class DefaultOracleBMCSServerGroupService implements OracleBMCSServerGroupService { + + private static final String DESTROY = "DESTROY_SERVER_GROUP" + private static final String RESIZE = "RESIZE_SERVER_GROUP" + private static final String DISABLE = "DISABLE_SERVER_GROUP" + private static final String ENABLE = "ENABLE_SERVER_GROUP" + + private final OracleBMCSServerGroupPersistence persistence; + + @Autowired + public DefaultOracleBMCSServerGroupService(OracleBMCSServerGroupPersistence persistence) { + this.persistence = persistence + } + + @Override + List listAllServerGroups(OracleBMCSNamedAccountCredentials creds) { + def persistenceCtx = new OracleBMCSPersistenceContext(creds) + List sgNames = persistence.listServerGroupNames(persistenceCtx) + return sgNames.findResults { name -> + persistence.getServerGroupByName(persistenceCtx, name) + } + } + + @Override + List listServerGroupNamesByClusterName(OracleBMCSNamedAccountCredentials creds, String clusterName) { + def persistenceCtx = new OracleBMCSPersistenceContext(creds) + List sgNames = persistence.listServerGroupNames(persistenceCtx) + return sgNames.findAll { clusterName == Names.parseName(it)?.cluster } + } + + @Override + OracleBMCSServerGroup getServerGroup(OracleBMCSNamedAccountCredentials creds, String application, String name) { + def persistenceCtx = new OracleBMCSPersistenceContext(creds) + List sgNames = persistence.listServerGroupNames(persistenceCtx) + List sgNamesInApp = sgNames.findAll { application == Names.parseName(it)?.app } + String foundName = sgNamesInApp.find { name == it } + if (foundName) { + return persistence.getServerGroupByName(persistenceCtx, name) + } + return null; + } + + @Override + void createServerGroup(OracleBMCSServerGroup sg) { + + def instances = [] as Set + for (int i = 0; i < sg.targetSize; i++) { + instances << createInstance(sg, i) + } + sg.instances = instances + persistence.upsertServerGroup(sg) + } + + @Override + boolean destroyServerGroup(Task task, OracleBMCSNamedAccountCredentials creds, String serverGroupName) { + def persistenceCtx = new OracleBMCSPersistenceContext(creds) + def serverGroup = persistence.getServerGroupByName(persistenceCtx, serverGroupName) + if (serverGroup != null) { + task.updateStatus DESTROY, "Found server group: $serverGroup.name" + + for (int i = 0; i < serverGroup.targetSize; i++) { + def instance = serverGroup.instances[i] + task.updateStatus DESTROY, "Terminating instance: $instance.name" + terminateInstance(serverGroup, instance) + } + task.updateStatus DESTROY, "Removing persistent data for $serverGroup.name" + persistence.deleteServerGroup(serverGroup) + return true + } else { + task.updateStatus DESTROY, "Server group not found" + return false + } + } + + @Override + boolean resizeServerGroup(Task task, OracleBMCSNamedAccountCredentials creds, String serverGroupName, Integer targetSize) { + def persistenceCtx = new OracleBMCSPersistenceContext(creds) + def serverGroup = persistence.getServerGroupByName(persistenceCtx, serverGroupName) + if (serverGroup != null) { + task.updateStatus DESTROY, "Found server group: $serverGroup.name" + + if (targetSize > serverGroup.targetSize) { + int numInstancesToCreate = targetSize - serverGroup.targetSize + task.updateStatus RESIZE, "Creating $numInstancesToCreate instances" + + resize(serverGroup, targetSize, serverGroup.targetSize, targetSize, + { int i -> + task.updateStatus RESIZE, "Creating instance: $i" + return createInstance(serverGroup, i) + }, + { OracleBMCSInstance instance -> + serverGroup.instances.add(instance) + }) + + } else if (serverGroup.targetSize > targetSize) { + int numInstancesToTerminate = serverGroup.targetSize - targetSize + task.updateStatus RESIZE, "Terminating $numInstancesToTerminate instances" + + resize(serverGroup, targetSize, targetSize, serverGroup.targetSize, + { int i -> + task.updateStatus RESIZE, "Terminating instance: " + serverGroup.instances[i].name + return terminateInstance(serverGroup, serverGroup.instances[i]) + }, + { OracleBMCSInstance instance -> + serverGroup.instances.remove(instance) + }) + + } else { + task.updateStatus RESIZE, "Already running the desired number of instances" + } + task.updateStatus RESIZE, "Updating persistent data for $serverGroup.name" + persistence.upsertServerGroup(serverGroup) + return true + } else { + task.updateStatus RESIZE, "Server group not found" + return false + } + } + + @Override + void disableServerGroup(Task task, OracleBMCSNamedAccountCredentials creds, String serverGroupName) { + def persistenceCtx = new OracleBMCSPersistenceContext(creds) + def serverGroup = persistence.getServerGroupByName(persistenceCtx, serverGroupName) + if (serverGroup != null) { + task.updateStatus DISABLE, "Found server group: $serverGroup.name" + serverGroup.disabled = true + task.updateStatus DISABLE, "Updating persistent data for $serverGroup.name" + persistence.upsertServerGroup(serverGroup) + } else { + task.updateStatus DISABLE, "Server group not found" + } + } + + @Override + void enableServerGroup(Task task, OracleBMCSNamedAccountCredentials creds, String serverGroupName) { + def persistenceCtx = new OracleBMCSPersistenceContext(creds) + def serverGroup = persistence.getServerGroupByName(persistenceCtx, serverGroupName) + if (serverGroup != null) { + task.updateStatus ENABLE, "Found server group: $serverGroup.name" + serverGroup.disabled = false + task.updateStatus ENABLE, "Updating persistent data for $serverGroup.name" + persistence.upsertServerGroup(serverGroup) + } else { + task.updateStatus ENABLE, "Server group not found" + } + } + + private OracleBMCSInstance createInstance(OracleBMCSServerGroup sg, int i) { + LaunchInstanceRequest rq = LaunchInstanceRequest.builder().launchInstanceDetails(LaunchInstanceDetails.builder() + .availabilityDomain(sg.launchConfig["availabilityDomain"] as String) + .compartmentId(sg.launchConfig["compartmentId"] as String) + .imageId(sg.launchConfig["imageId"] as String) + .shape(sg.launchConfig["shape"] as String) + .subnetId(sg.launchConfig["subnetId"] as String) + .displayName(sg.name + "-$i") + .build()).build() + + def rs = sg.credentials.computeClient.launchInstance(rq) + return new OracleBMCSInstance( + name: rs.instance.displayName, + id: rs.instance.id, + region: rs.instance.region, + zone: rs.instance.availabilityDomain, + healthState: HealthState.Starting, + cloudProvider: OracleBMCSCloudProvider.ID, + launchTime: rs.instance.timeCreated.time) + } + + private OracleBMCSInstance terminateInstance(OracleBMCSServerGroup sg, OracleBMCSInstance instance) { + TerminateInstanceRequest request = TerminateInstanceRequest.builder() + .instanceId(instance.id).build() + try { + sg.credentials.computeClient.terminateInstance(request) + } catch (BmcException e) { + // Ignore missing instances (e.g., terminated manually outside of spinnaker) + if (e.statusCode != 404) { + throw e + } + } + return instance + } + + private void resize(OracleBMCSServerGroup sg, Integer targetSize, int from, int to, Closure operate, Closure update) { + def instances = [] as Set + for (int i = from; i < to; i++) { + instances << operate(i) + } + for (OracleBMCSInstance instance : instances) { + update(instance) + } + sg.targetSize = targetSize + } +} diff --git a/clouddriver-oracle-bmcs/src/main/groovy/com/netflix/spinnaker/clouddriver/oraclebmcs/service/servergroup/OracleBMCSPersistenceContext.groovy b/clouddriver-oracle-bmcs/src/main/groovy/com/netflix/spinnaker/clouddriver/oraclebmcs/service/servergroup/OracleBMCSPersistenceContext.groovy new file mode 100644 index 00000000000..513a648e164 --- /dev/null +++ b/clouddriver-oracle-bmcs/src/main/groovy/com/netflix/spinnaker/clouddriver/oraclebmcs/service/servergroup/OracleBMCSPersistenceContext.groovy @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2017 Oracle America, Inc. + * + * The contents of this file are subject to the Apache License Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * If a copy of the Apache License Version 2.0 was not distributed with this file, + * You can obtain one at https://www.apache.org/licenses/LICENSE-2.0.html + */ +package com.netflix.spinnaker.clouddriver.oraclebmcs.service.servergroup + +import com.netflix.spinnaker.clouddriver.oraclebmcs.security.OracleBMCSNamedAccountCredentials + +/** + * Created by slord on 12/04/2017. + */ +class OracleBMCSPersistenceContext { + + OracleBMCSNamedAccountCredentials creds + String namespace + boolean bucketChecked + + OracleBMCSPersistenceContext(OracleBMCSNamedAccountCredentials creds) { + this.creds = creds + } +} diff --git a/clouddriver-oracle-bmcs/src/main/groovy/com/netflix/spinnaker/clouddriver/oraclebmcs/service/servergroup/OracleBMCSServerGroupPersistence.groovy b/clouddriver-oracle-bmcs/src/main/groovy/com/netflix/spinnaker/clouddriver/oraclebmcs/service/servergroup/OracleBMCSServerGroupPersistence.groovy new file mode 100644 index 00000000000..0be54139e1b --- /dev/null +++ b/clouddriver-oracle-bmcs/src/main/groovy/com/netflix/spinnaker/clouddriver/oraclebmcs/service/servergroup/OracleBMCSServerGroupPersistence.groovy @@ -0,0 +1,258 @@ +/* + * Copyright (c) 2017 Oracle America, Inc. + * + * The contents of this file are subject to the Apache License Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * If a copy of the Apache License Version 2.0 was not distributed with this file, + * You can obtain one at https://www.apache.org/licenses/LICENSE-2.0.html + */ +package com.netflix.spinnaker.clouddriver.oraclebmcs.service.servergroup + +import com.fasterxml.jackson.databind.ObjectMapper +import com.netflix.spinnaker.clouddriver.oraclebmcs.model.OracleBMCSServerGroup +import com.netflix.spinnaker.clouddriver.oraclebmcs.security.OracleBMCSNamedAccountCredentials +import com.oracle.bmc.objectstorage.model.CreateBucketDetails +import com.oracle.bmc.objectstorage.requests.* +import groovy.transform.Synchronized +import groovy.util.logging.Slf4j +import org.springframework.stereotype.Component + +import java.nio.charset.Charset + +/** + * Uses Object Storage as a persistent store for server group data. This is a temporary work around + * because BMCS does not currently support server groups as a native concept. + */ +@Slf4j +@Component +class OracleBMCSServerGroupPersistence { + + private static class OracleBMCSServerGroupPersistenceException extends RuntimeException { + + OracleBMCSServerGroupPersistenceException(String var1, Throwable var2) { + super(var1, var2) + } + + OracleBMCSServerGroupPersistenceException(String var1) { + super(var1) + } + } + + /** + * Types of operation that must be performed in an atomic way. + */ + private enum PersistenceOperation { + + READ, UPSERT, DELETE + } + + /** + * The reserved name for the bucket used to contain all server group data objects. + */ + private final String SERVERGROUP_BUCKET_NAME = "_spinnaker_server_group_data" + + private final Charset UTF_8_CHARSET = Charset.forName("UTF-8") + + /** + * Lists the server group names for the specified account. + * + * We do not consider "list" to conflict with READ, UPSERT or DELETE; this is because we only look at the names and + * to get the actual data a client has to subsequently perform READ operations anyway. + * + * The intended usage by the client of this class is: + * + * - list the server group names; + * - for each name, do a read and get the actual data; + * - if someone has deleted the server group sometimes between the list and the read, the client deals with it. + */ + protected List listServerGroupNames(OracleBMCSPersistenceContext ctx) { + def result = [] + def namespace = getStorageNamespaceFor(ctx) + ensureStorageBucketFor(ctx) + + try { + def rq = ListObjectsRequest.builder().namespaceName(namespace) + .bucketName(SERVERGROUP_BUCKET_NAME) + .build() + def rs = ctx.creds.objectStorageClient.listObjects(rq) + def maybeListSummaries = rs?.getListObjects()?.getObjects() + if (maybeListSummaries != null) { + result = maybeListSummaries.collect { it.getName() } + } + } + catch (Exception e) { + throw new OracleBMCSServerGroupPersistenceException("Failed listing contents of bucket", e) + } + + return result + } + + /** + * Reads the server group data with the provided name in the provided account. + */ + OracleBMCSServerGroup getServerGroupByName(OracleBMCSPersistenceContext ctx, String name) { + return doPersistenceOperation(PersistenceOperation.READ, ctx, null, name) + } + + /** + * Writes the server group data to the persistent store; the account is inferred from the server group data. + */ + void upsertServerGroup(OracleBMCSServerGroup sg) { + if (sg.credentials.compartmentId == sg.launchConfig["compartmentId"] as String) { + doPersistenceOperation(PersistenceOperation.UPSERT, new OracleBMCSPersistenceContext(sg.credentials), sg, sg.name) + } else { + throw new OracleBMCSServerGroupPersistenceException("Different compartments - this is not allowed") + } + } + + /** + * Deletes the server group data from the persistent store; the account is inferred from the server group data. + */ + void deleteServerGroup(OracleBMCSServerGroup sg) { + if (sg.credentials.compartmentId == sg.launchConfig["compartmentId"] as String) { + doPersistenceOperation(PersistenceOperation.DELETE, new OracleBMCSPersistenceContext(sg.credentials), sg, sg.name) + } else { + throw new OracleBMCSServerGroupPersistenceException("Different compartments - this is not allowed") + } + } + + @Synchronized + private String getStorageNamespaceFor(OracleBMCSPersistenceContext ctx) { + if (ctx.namespace) { + return ctx.namespace + } + def rq = GetNamespaceRequest.builder().build() + def rs = ctx.creds.objectStorageClient.getNamespace(rq) + def namespace = rs?.getValue() + if (!namespace) { + throw new OracleBMCSServerGroupPersistenceException("Namespace not found, can't continue") + } + ctx.namespace = namespace + return namespace + } + + @Synchronized + private ensureStorageBucketFor(OracleBMCSPersistenceContext ctx) { + if (ctx.bucketChecked) { + return + } + try { + def rq = HeadBucketRequest.builder().namespaceName(ctx.namespace).bucketName(SERVERGROUP_BUCKET_NAME).build() + def rs = ctx.creds.objectStorageClient.headBucket(rq) + if (rs?.getETag()) { + ctx.bucketChecked = true + return + } + log.info("Bucket not found, will try to create...") + } catch (Exception e) { + log.warn("Exception when getting bucket, will try to create...", e) + } + + try { + def rq = CreateBucketRequest.builder().namespaceName(ctx.namespace).createBucketDetails( + CreateBucketDetails.builder().name(SERVERGROUP_BUCKET_NAME) + .compartmentId(ctx.creds.compartmentId) + .build() + ).build() + def rs = ctx.creds.objectStorageClient.createBucket(rq) + if (rs?.getETag()) { + ctx.bucketChecked = true + return + } + } catch (Exception e) { + throw new OracleBMCSServerGroupPersistenceException("Failed to create bucket", e) + } + throw new OracleBMCSServerGroupPersistenceException("Failed to get or create bucket") + } + + private String serverGroupToJson(OracleBMCSServerGroup sg) { + // Save these to re-assign after ObjectMapper does its work. + def credentials = sg.credentials + sg.credentials = null + def objectMapper = new ObjectMapper(); + def json = objectMapper.writeValueAsString(sg); + sg.credentials = credentials + return json + } + + private OracleBMCSServerGroup jsonToServerGroup(String json, OracleBMCSNamedAccountCredentials creds) { + def objectMapper = new ObjectMapper() + def sg = objectMapper.readValue(json, OracleBMCSServerGroup.class) + sg.credentials = creds + return sg + } + + @Synchronized + private OracleBMCSServerGroup doPersistenceOperation(PersistenceOperation op, + OracleBMCSPersistenceContext ctx, + OracleBMCSServerGroup sg, + String name) { + def namespace = getStorageNamespaceFor(ctx) + ensureStorageBucketFor(ctx) + switch (op) { + case PersistenceOperation.READ: + try { + def rq = GetObjectRequest.builder().namespaceName(namespace) + .bucketName(SERVERGROUP_BUCKET_NAME) + .objectName(name) + .build() + def rs = ctx.creds.objectStorageClient.getObject(rq) + if (!rs?.getETag()) { + log.warn("No object to read") + return null + } + def inputStream = rs.getInputStream() + if (inputStream == null) { + log.warn("Object empty") + return null + } + String json + inputStream.withStream { json = inputStream.getText("UTF-8") } + sg = jsonToServerGroup(json, ctx.creds) + return sg + } catch (Exception e) { + log.error("OSS Read exception", e) + return null + } + break; + case PersistenceOperation.UPSERT: + try { + def json = serverGroupToJson(sg) + def rq = PutObjectRequest.builder().namespaceName(namespace) + .bucketName(SERVERGROUP_BUCKET_NAME) + .objectName(sg.name) + .contentLength(json.getBytes(UTF_8_CHARSET).length) + .putObjectBody(new ByteArrayInputStream(json.getBytes(UTF_8_CHARSET))) + .build() + def rs = ctx.creds.objectStorageClient.putObject(rq) + if (!rs?.getETag()) { + throw new OracleBMCSServerGroupPersistenceException("Upsert failed, ETag was null") + } + return sg + } catch (OracleBMCSServerGroupPersistenceException e) { + throw e + } catch (Exception e) { + throw new OracleBMCSServerGroupPersistenceException("Upsert failed", e) + } + break; + case PersistenceOperation.DELETE: + try { + def rq = DeleteObjectRequest.builder().namespaceName(namespace) + .bucketName(SERVERGROUP_BUCKET_NAME) + .objectName(sg.name) + .build() + def rs = ctx.creds.objectStorageClient.deleteObject(rq) + if (!rs?.getLastModified()) { + throw new OracleBMCSServerGroupPersistenceException("Delete failed, lastModified was null") + } + return sg + } catch (OracleBMCSServerGroupPersistenceException e) { + throw e + } catch (Exception e) { + throw new OracleBMCSServerGroupPersistenceException("Delete failed", e) + } + break; + } + throw new OracleBMCSServerGroupPersistenceException("Unhandled persistence operation") + } +} diff --git a/clouddriver-oracle-bmcs/src/main/groovy/com/netflix/spinnaker/clouddriver/oraclebmcs/service/servergroup/OracleBMCSServerGroupService.groovy b/clouddriver-oracle-bmcs/src/main/groovy/com/netflix/spinnaker/clouddriver/oraclebmcs/service/servergroup/OracleBMCSServerGroupService.groovy new file mode 100644 index 00000000000..0839a099b94 --- /dev/null +++ b/clouddriver-oracle-bmcs/src/main/groovy/com/netflix/spinnaker/clouddriver/oraclebmcs/service/servergroup/OracleBMCSServerGroupService.groovy @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2017 Oracle America, Inc. + * + * The contents of this file are subject to the Apache License Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * If a copy of the Apache License Version 2.0 was not distributed with this file, + * You can obtain one at https://www.apache.org/licenses/LICENSE-2.0.html + */ +package com.netflix.spinnaker.clouddriver.oraclebmcs.service.servergroup + +import com.netflix.spinnaker.clouddriver.data.task.Task +import com.netflix.spinnaker.clouddriver.oraclebmcs.model.OracleBMCSServerGroup +import com.netflix.spinnaker.clouddriver.oraclebmcs.security.OracleBMCSNamedAccountCredentials + +/** + * Created by slord on 16/01/2017. + */ +interface OracleBMCSServerGroupService { + + public List listAllServerGroups(OracleBMCSNamedAccountCredentials creds) + + public List listServerGroupNamesByClusterName(OracleBMCSNamedAccountCredentials creds, String clusterName) + + public OracleBMCSServerGroup getServerGroup(OracleBMCSNamedAccountCredentials creds, String application, String name) + + public void createServerGroup(OracleBMCSServerGroup serverGroup) + + public boolean destroyServerGroup(Task task, OracleBMCSNamedAccountCredentials creds, String serverGroupName) + + public boolean resizeServerGroup(Task task, OracleBMCSNamedAccountCredentials creds, String serverGroupName, Integer targetSize) + + public void disableServerGroup(Task task, OracleBMCSNamedAccountCredentials creds, String serverGroupName) + + public void enableServerGroup(Task task, OracleBMCSNamedAccountCredentials creds, String serverGroupName) + +} diff --git a/clouddriver-oracle-bmcs/src/test/groovy/com/netflix/spinnaker/clouddriver/oraclebmcs/provider/agent/OracleBMCSServerGroupCachingAgentSpec.groovy b/clouddriver-oracle-bmcs/src/test/groovy/com/netflix/spinnaker/clouddriver/oraclebmcs/provider/agent/OracleBMCSServerGroupCachingAgentSpec.groovy new file mode 100644 index 00000000000..bebede1995a --- /dev/null +++ b/clouddriver-oracle-bmcs/src/test/groovy/com/netflix/spinnaker/clouddriver/oraclebmcs/provider/agent/OracleBMCSServerGroupCachingAgentSpec.groovy @@ -0,0 +1,79 @@ +/* + * Copyright (c) 2017 Oracle America, Inc. + * + * The contents of this file are subject to the Apache License Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * If a copy of the Apache License Version 2.0 was not distributed with this file, + * You can obtain one at https://www.apache.org/licenses/LICENSE-2.0.html + */ +package com.netflix.spinnaker.clouddriver.oraclebmcs.provider.agent + +import com.fasterxml.jackson.databind.ObjectMapper +import com.netflix.spinnaker.clouddriver.oraclebmcs.cache.Keys +import com.netflix.spinnaker.clouddriver.oraclebmcs.model.OracleBMCSServerGroup +import com.netflix.spinnaker.clouddriver.oraclebmcs.security.OracleBMCSNamedAccountCredentials +import com.netflix.spinnaker.clouddriver.oraclebmcs.service.servergroup.OracleBMCSServerGroupService +import com.oracle.bmc.Region +import spock.lang.Shared +import spock.lang.Specification + +class OracleBMCSServerGroupCachingAgentSpec extends Specification { + + @Shared + ObjectMapper objectMapper = new ObjectMapper() + + def "agent has correct agentType"() { + setup: + def creds = Mock(OracleBMCSNamedAccountCredentials) + creds.name >> "foo" + creds.compartmentId >> "bar" + creds.region >> Region.US_PHOENIX_1.regionId + def agent = new OracleBMCSServerGroupCachingAgent("", creds, objectMapper, null) + def expectedAgentType = "${creds.name}/${creds.region}/${OracleBMCSServerGroupCachingAgent.class.simpleName}" + + when: + def agentType = agent.getAgentType() + + then: + agentType == expectedAgentType + } + + def "agent handles null items in list server group result"() { + setup: + def creds = Mock(OracleBMCSNamedAccountCredentials) + creds.name >> "foo" + creds.region >> Region.US_PHOENIX_1.regionId + def sgService = Mock(OracleBMCSServerGroupService) + sgService.listAllServerGroups(_) >> [] + def agent = new OracleBMCSServerGroupCachingAgent("", creds, objectMapper, sgService) + + when: + def cacheResult = agent.loadData(null) + + then: + cacheResult != null + cacheResult.cacheResults.containsKey(Keys.Namespace.SERVER_GROUPS.ns) + } + + def "agent creates correct cache result items"() { + setup: + def creds = Mock(OracleBMCSNamedAccountCredentials) + creds.name >> "foo" + creds.region >> Region.US_PHOENIX_1.regionId + def sgService = Mock(OracleBMCSServerGroupService) + sgService.listAllServerGroups(_) >> [new OracleBMCSServerGroup(name: "foo-v001", targetSize: 5)] + def agent = new OracleBMCSServerGroupCachingAgent("", creds, objectMapper, sgService) + + when: + def cacheResult = agent.loadData(null) + + then: + cacheResult != null + cacheResult.cacheResults.containsKey(Keys.Namespace.SERVER_GROUPS.ns) + cacheResult.cacheResults.get(Keys.Namespace.SERVER_GROUPS.ns).size() == 1 + cacheResult.cacheResults.get(Keys.Namespace.SERVER_GROUPS.ns).first().id == Keys.getServerGroupKey(creds.name, creds.region, "foo-v001") + cacheResult.cacheResults.get(Keys.Namespace.SERVER_GROUPS.ns).first().attributes.get("name") == "foo-v001" + cacheResult.cacheResults.get(Keys.Namespace.SERVER_GROUPS.ns).first().attributes.get("targetSize") == 5 + } + +} diff --git a/clouddriver-oracle-bmcs/src/test/groovy/com/netflix/spinnaker/clouddriver/oraclebmcs/provider/view/OracleBMCSClusterProviderSpec.groovy b/clouddriver-oracle-bmcs/src/test/groovy/com/netflix/spinnaker/clouddriver/oraclebmcs/provider/view/OracleBMCSClusterProviderSpec.groovy new file mode 100644 index 00000000000..06a1eae2b87 --- /dev/null +++ b/clouddriver-oracle-bmcs/src/test/groovy/com/netflix/spinnaker/clouddriver/oraclebmcs/provider/view/OracleBMCSClusterProviderSpec.groovy @@ -0,0 +1,80 @@ +/* + * Copyright (c) 2017 Oracle America, Inc. + * + * The contents of this file are subject to the Apache License Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * If a copy of the Apache License Version 2.0 was not distributed with this file, + * You can obtain one at https://www.apache.org/licenses/LICENSE-2.0.html + */ +package com.netflix.spinnaker.clouddriver.oraclebmcs.provider.view + +import com.fasterxml.jackson.databind.ObjectMapper +import com.netflix.spinnaker.cats.cache.Cache +import com.netflix.spinnaker.cats.cache.CacheData +import com.netflix.spinnaker.clouddriver.oraclebmcs.OracleBMCSCloudProvider +import com.netflix.spinnaker.clouddriver.oraclebmcs.cache.Keys +import com.netflix.spinnaker.clouddriver.oraclebmcs.model.OracleBMCSInstance +import com.netflix.spinnaker.clouddriver.oraclebmcs.security.OracleBMCSNamedAccountCredentials +import com.netflix.spinnaker.clouddriver.security.AccountCredentialsProvider +import spock.lang.Specification + +class OracleBMCSClusterProviderSpec extends Specification { + + def "get a server group from the cache"() { + setup: + def cache = Mock(Cache) + def ap = Mock(AccountCredentialsProvider) + ap.getCredentials(_) >> null + def clusterProvider = new OracleBMCSClusterProvider(null, new ObjectMapper(), ap, cache) + def identifiers = Mock(Collection) + def attributes = ["name": "foo-v001", "targetSize": 5] + def mockData = Mock(CacheData) + Collection cacheData = [mockData] + def id = "${OracleBMCSCloudProvider.ID}:${Keys.Namespace.SERVER_GROUPS}:foo-test:account1:us-phoenix-1:foo-test-v001" + + when: + def serverGroup = clusterProvider.getServerGroup("account1", "us-phoenix-1", "foo-test-v001") + + then: + 1 * cache.filterIdentifiers(Keys.Namespace.SERVER_GROUPS.ns, id) >> identifiers + 1 * cache.getAll(Keys.Namespace.SERVER_GROUPS.ns, identifiers, _) >> cacheData + 1 * mockData.attributes >> attributes + 1 * mockData.id >> id + serverGroup.name == attributes["name"] + serverGroup.capacity.desired == attributes["targetSize"] + + noExceptionThrown() + } + + def "get a cluster from the cache"() { + setup: + def cache = Mock(Cache) + def creds = Mock(OracleBMCSNamedAccountCredentials) + creds.name >> "account1" + def accountCredentialsProvider = Mock(AccountCredentialsProvider) + accountCredentialsProvider.getCredentials(_) >> creds + def instanceProvider = Mock(OracleBMCSInstanceProvider) + instanceProvider.getInstance(_, _, _) >> new OracleBMCSInstance() + def clusterProvider = new OracleBMCSClusterProvider(instanceProvider, new ObjectMapper(), accountCredentialsProvider, cache) + def attributes = ["name": "foo-test-v001", "targetSize": 5, "instances": [["name": "blah"]]] + def mockData = Mock(CacheData) + Collection cacheData = [mockData] + def id = "${OracleBMCSCloudProvider.ID}:${Keys.Namespace.SERVER_GROUPS}:foo-test:account1:us-phoenix-1:foo-test-v001" + + when: + def cluster = clusterProvider.getCluster("foo", "account1", "foo-test") + + then: + 1 * cache.getIdentifiers(Keys.Namespace.SERVER_GROUPS.ns) >> [id] + 1 * cache.getAll(Keys.Namespace.SERVER_GROUPS.ns, [id], _) >> cacheData + 1 * mockData.attributes >> attributes + mockData.id >> id + cluster.name == "foo-test" + cluster.serverGroups.size() == 1 + cluster.serverGroups.first().name == attributes["name"] + cluster.serverGroups.first().capacity.desired == attributes["targetSize"] + + noExceptionThrown() + } + +} diff --git a/clouddriver-oracle-bmcs/src/test/groovy/com/netflix/spinnaker/clouddriver/oraclebmcs/service/servergroup/DefaultOracleBMCSServerGroupServiceSpec.groovy b/clouddriver-oracle-bmcs/src/test/groovy/com/netflix/spinnaker/clouddriver/oraclebmcs/service/servergroup/DefaultOracleBMCSServerGroupServiceSpec.groovy new file mode 100644 index 00000000000..aa8057a94df --- /dev/null +++ b/clouddriver-oracle-bmcs/src/test/groovy/com/netflix/spinnaker/clouddriver/oraclebmcs/service/servergroup/DefaultOracleBMCSServerGroupServiceSpec.groovy @@ -0,0 +1,340 @@ +/* + * Copyright (c) 2017 Oracle America, Inc. + * + * The contents of this file are subject to the Apache License Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * If a copy of the Apache License Version 2.0 was not distributed with this file, + * You can obtain one at https://www.apache.org/licenses/LICENSE-2.0.html + */ +package com.netflix.spinnaker.clouddriver.oraclebmcs.service.servergroup + +import com.netflix.spinnaker.clouddriver.data.task.Task +import com.netflix.spinnaker.clouddriver.oraclebmcs.model.OracleBMCSInstance +import com.netflix.spinnaker.clouddriver.oraclebmcs.model.OracleBMCSServerGroup +import com.netflix.spinnaker.clouddriver.oraclebmcs.security.OracleBMCSNamedAccountCredentials +import com.oracle.bmc.Region +import com.oracle.bmc.core.ComputeClient +import com.oracle.bmc.core.model.Instance +import com.oracle.bmc.core.responses.LaunchInstanceResponse +import spock.lang.Specification + +class DefaultOracleBMCSServerGroupServiceSpec extends Specification { + + def "create server group"() { + setup: + def creds = Mock(OracleBMCSNamedAccountCredentials) + creds.getName() >> "foo" + creds.getRegion() >> Region.US_PHOENIX_1.regionId + creds.getComputeClient() >> Mock(ComputeClient) + def persistence = Mock(OracleBMCSServerGroupPersistence) + def sgService = new DefaultOracleBMCSServerGroupService(persistence) + + when: + def sg = new OracleBMCSServerGroup( + name: "sg1", + region: creds.region, + zone: "ad1", + launchConfig: [ + "availabilityDomain": "ad1", + "compartmentId" : "ocid.compartment.123", + "imageId" : "ocid.image.123", + "shape" : "small", + "vpcId" : "ocid.vcn.123", + "subnetId" : "ocid.subnet.123", + "createdTime" : System.currentTimeMillis() + ], + targetSize: 4, + credentials: creds + ) + sgService.createServerGroup(sg) + + then: + 4 * creds.computeClient.launchInstance(_) >> LaunchInstanceResponse.builder().instance( + Instance.builder().timeCreated(new Date()).build() + ).build() + 1 * persistence.upsertServerGroup(_) + } + + def "resize (increase) server group"() { + setup: + def creds = Mock(OracleBMCSNamedAccountCredentials) + creds.getName() >> "foo" + creds.getRegion() >> Region.US_PHOENIX_1.regionId + creds.getComputeClient() >> Mock(ComputeClient) + def task = Mock(Task) + def persistence = Mock(OracleBMCSServerGroupPersistence) + def sgService = new DefaultOracleBMCSServerGroupService(persistence) + def sg = new OracleBMCSServerGroup( + name: "sg1", + region: creds.region, + zone: "ad1", + launchConfig: [ + "availabilityDomain": "ad1", + "compartmentId" : "ocid.compartment.123", + "imageId" : "ocid.image.123", + "shape" : "small", + "vpcId" : "ocid.vcn.123", + "subnetId" : "ocid.subnet.123", + "createdTime" : System.currentTimeMillis() + ], + targetSize: 1, + credentials: creds + ) + + when: + def resized = sgService.resizeServerGroup(task, creds, "sg1", 5) + + then: + 4 * creds.computeClient.launchInstance(_) >> LaunchInstanceResponse.builder().instance( + Instance.builder().timeCreated(new Date()).build() + ).build() + 1 * persistence.getServerGroupByName(_, "sg1") >> sg + 1 * persistence.upsertServerGroup(_) + resized == true + } + + def "resize (decrease) server group"() { + setup: + def creds = Mock(OracleBMCSNamedAccountCredentials) + creds.getName() >> "foo" + creds.getRegion() >> Region.US_PHOENIX_1.regionId + creds.getComputeClient() >> Mock(ComputeClient) + def task = Mock(Task) + def persistence = Mock(OracleBMCSServerGroupPersistence) + def sgService = new DefaultOracleBMCSServerGroupService(persistence) + def sg = new OracleBMCSServerGroup( + name: "sg1", + region: creds.region, + zone: "ad1", + launchConfig: [ + "availabilityDomain": "ad1", + "compartmentId" : "ocid.compartment.123", + "imageId" : "ocid.image.123", + "shape" : "small", + "vpcId" : "ocid.vcn.123", + "subnetId" : "ocid.subnet.123", + "createdTime" : System.currentTimeMillis() + ], + instances: [ + new OracleBMCSInstance(name: "a"), + new OracleBMCSInstance(name: "b"), + new OracleBMCSInstance(name: "c"), + new OracleBMCSInstance(name: "d"), + new OracleBMCSInstance(name: "e") + ], + targetSize: 5, + credentials: creds + ) + + when: + def resized = sgService.resizeServerGroup(task, creds, "sg1", 1) + + then: + 4 * creds.computeClient.terminateInstance(_) + 1 * persistence.getServerGroupByName(_, "sg1") >> sg + 1 * persistence.upsertServerGroup(_) + resized == true + } + + def "enable server group"() { + setup: + def creds = Mock(OracleBMCSNamedAccountCredentials) + creds.getName() >> "foo" + creds.getRegion() >> Region.US_PHOENIX_1.regionId + creds.getComputeClient() >> Mock(ComputeClient) + def task = Mock(Task) + def persistence = Mock(OracleBMCSServerGroupPersistence) + def sgService = new DefaultOracleBMCSServerGroupService(persistence) + def sg = new OracleBMCSServerGroup( + name: "sg1", + region: creds.region, + zone: "ad1", + launchConfig: [ + "availabilityDomain": "ad1", + "compartmentId" : "ocid.compartment.123", + "imageId" : "ocid.image.123", + "shape" : "small", + "vpcId" : "ocid.vcn.123", + "subnetId" : "ocid.subnet.123", + "createdTime" : System.currentTimeMillis() + ], + targetSize: 1, + credentials: creds, + disabled: true + ) + + when: + sgService.enableServerGroup(task, creds, "sg1") + + then: + 1 * persistence.getServerGroupByName(_, "sg1") >> sg + 1 * persistence.upsertServerGroup(_) + sg.disabled == false + } + + def "disable server group"() { + setup: + def creds = Mock(OracleBMCSNamedAccountCredentials) + creds.getName() >> "foo" + creds.getRegion() >> Region.US_PHOENIX_1.regionId + creds.getComputeClient() >> Mock(ComputeClient) + def task = Mock(Task) + def persistence = Mock(OracleBMCSServerGroupPersistence) + def sgService = new DefaultOracleBMCSServerGroupService(persistence) + def sg = new OracleBMCSServerGroup( + name: "sg1", + region: creds.region, + zone: "ad1", + launchConfig: [ + "availabilityDomain": "ad1", + "compartmentId" : "ocid.compartment.123", + "imageId" : "ocid.image.123", + "shape" : "small", + "vpcId" : "ocid.vcn.123", + "subnetId" : "ocid.subnet.123", + "createdTime" : System.currentTimeMillis() + ], + targetSize: 1, + credentials: creds, + disabled: false + ) + + when: + sgService.disableServerGroup(task, creds, "sg1") + + then: + 1 * persistence.getServerGroupByName(_, "sg1") >> sg + 1 * persistence.upsertServerGroup(_) + sg.disabled == true + } + + def "destroy server group"() { + setup: + def creds = Mock(OracleBMCSNamedAccountCredentials) + creds.getName() >> "foo" + creds.getRegion() >> Region.US_PHOENIX_1.regionId + creds.getComputeClient() >> Mock(ComputeClient) + def task = Mock(Task) + def persistence = Mock(OracleBMCSServerGroupPersistence) + def sgService = new DefaultOracleBMCSServerGroupService(persistence) + def sg = new OracleBMCSServerGroup( + name: "sg1", + region: creds.region, + zone: "ad1", + launchConfig: [ + "availabilityDomain": "ad1", + "compartmentId" : "ocid.compartment.123", + "imageId" : "ocid.image.123", + "shape" : "small", + "vpcId" : "ocid.vcn.123", + "subnetId" : "ocid.subnet.123", + "createdTime" : System.currentTimeMillis() + ], + instances: [ + new OracleBMCSInstance(name: "a"), + new OracleBMCSInstance(name: "b"), + new OracleBMCSInstance(name: "c"), + new OracleBMCSInstance(name: "d"), + new OracleBMCSInstance(name: "e") + ], + targetSize: 5, + credentials: creds, + disabled: false + ) + + when: + sgService.destroyServerGroup(task, creds, "sg1") + + then: + 1 * persistence.getServerGroupByName(_, "sg1") >> sg + 5 * creds.computeClient.terminateInstance(_) + 1 * persistence.deleteServerGroup(sg) + } + + def "get server group"() { + setup: + def creds = Mock(OracleBMCSNamedAccountCredentials) + creds.getName() >> "foo" + creds.getRegion() >> Region.US_PHOENIX_1.regionId + creds.getComputeClient() >> Mock(ComputeClient) + def persistence = Mock(OracleBMCSServerGroupPersistence) + def sgService = new DefaultOracleBMCSServerGroupService(persistence) + def sg = new OracleBMCSServerGroup( + name: "foo-v001", + region: creds.region, + zone: "ad1", + launchConfig: [ + "availabilityDomain": "ad1", + "compartmentId" : "ocid.compartment.123", + "imageId" : "ocid.image.123", + "shape" : "small", + "vpcId" : "ocid.vcn.123", + "subnetId" : "ocid.subnet.123", + "createdTime" : System.currentTimeMillis() + ], + targetSize: 5, + credentials: creds, + disabled: false + ) + + when: + sgService.getServerGroup(creds, "foo", "foo-v001") + + then: + 1 * persistence.listServerGroupNames(_) >> ["foo-v001"] + 1 * persistence.getServerGroupByName(_, "foo-v001") >> sg + } + + def "list all server group"() { + setup: + def creds = Mock(OracleBMCSNamedAccountCredentials) + creds.getName() >> "foo" + creds.getRegion() >> Region.US_PHOENIX_1.regionId + creds.getComputeClient() >> Mock(ComputeClient) + def persistence = Mock(OracleBMCSServerGroupPersistence) + def sgService = new DefaultOracleBMCSServerGroupService(persistence) + def sg = new OracleBMCSServerGroup( + name: "foo-v001", + region: creds.region, + zone: "ad1", + launchConfig: [ + "availabilityDomain": "ad1", + "compartmentId" : "ocid.compartment.123", + "imageId" : "ocid.image.123", + "shape" : "small", + "vpcId" : "ocid.vcn.123", + "subnetId" : "ocid.subnet.123", + "createdTime" : System.currentTimeMillis() + ], + targetSize: 5, + credentials: creds, + disabled: false + ) + + when: + def serverGroups = sgService.listAllServerGroups(creds) + + then: + 1 * persistence.listServerGroupNames(_) >> ["foo-v001", "bar-v001", "bbq-v001", "foo-test-v001"] + 4 * persistence.getServerGroupByName(_, _) >> sg + serverGroups.size() == 4 + } + + def "list server group names by cluster"() { + setup: + def creds = Mock(OracleBMCSNamedAccountCredentials) + creds.getName() >> "foo" + creds.getRegion() >> Region.US_PHOENIX_1.regionId + creds.getComputeClient() >> Mock(ComputeClient) + def persistence = Mock(OracleBMCSServerGroupPersistence) + def sgService = new DefaultOracleBMCSServerGroupService(persistence) + + when: + def serverGroups = sgService.listServerGroupNamesByClusterName(creds, "foo-test") + + then: + 1 * persistence.listServerGroupNames(_) >> ["foo-test-v001", "foo-v002", "foo-edge-v001", "foo-test-v002", "bar-v001"] + serverGroups == ["foo-test-v001", "foo-test-v002"] + } + +} diff --git a/clouddriver-oracle-bmcs/src/test/groovy/com/netflix/spinnaker/clouddriver/oraclebmcs/service/servergroup/OracleBMCSServerGroupPersistenceSpec.groovy b/clouddriver-oracle-bmcs/src/test/groovy/com/netflix/spinnaker/clouddriver/oraclebmcs/service/servergroup/OracleBMCSServerGroupPersistenceSpec.groovy new file mode 100644 index 00000000000..93daa1f3690 --- /dev/null +++ b/clouddriver-oracle-bmcs/src/test/groovy/com/netflix/spinnaker/clouddriver/oraclebmcs/service/servergroup/OracleBMCSServerGroupPersistenceSpec.groovy @@ -0,0 +1,100 @@ +/* + * Copyright (c) 2017 Oracle America, Inc. + * + * The contents of this file are subject to the Apache License Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * If a copy of the Apache License Version 2.0 was not distributed with this file, + * You can obtain one at https://www.apache.org/licenses/LICENSE-2.0.html + */ +package com.netflix.spinnaker.clouddriver.oraclebmcs.service.servergroup + +import com.fasterxml.jackson.databind.ObjectMapper +import com.netflix.spinnaker.clouddriver.oraclebmcs.model.OracleBMCSServerGroup +import com.netflix.spinnaker.clouddriver.oraclebmcs.security.OracleBMCSNamedAccountCredentials +import com.oracle.bmc.objectstorage.ObjectStorageClient +import com.oracle.bmc.objectstorage.model.ListObjects +import com.oracle.bmc.objectstorage.model.ObjectSummary +import com.oracle.bmc.objectstorage.responses.* +import spock.lang.Specification + +import java.nio.charset.Charset + +class OracleBMCSServerGroupPersistenceSpec extends Specification { + + def "upsert server group"() { + setup: + def creds = Mock(OracleBMCSNamedAccountCredentials) + creds.getCompartmentId() >> "foo" + creds.getObjectStorageClient() >> Mock(ObjectStorageClient) + OracleBMCSServerGroupPersistence persistence = new OracleBMCSServerGroupPersistence() + def sg = new OracleBMCSServerGroup(name: "foo-v001", credentials: creds, launchConfig: ["compartmentId": "foo"]) + + when: + persistence.upsertServerGroup(sg) + + then: + 1 * creds.getObjectStorageClient().putObject(_) >> PutObjectResponse.builder().eTag("abc").build() + creds.objectStorageClient.getNamespace(_) >> GetNamespaceResponse.builder().value("ns1").build() + creds.objectStorageClient.headBucket(_) >> HeadBucketResponse.builder().eTag("abc").build() + } + + def "delete server group"() { + setup: + def creds = Mock(OracleBMCSNamedAccountCredentials) + creds.getCompartmentId() >> "foo" + creds.getObjectStorageClient() >> Mock(ObjectStorageClient) + OracleBMCSServerGroupPersistence persistence = new OracleBMCSServerGroupPersistence() + def sg = new OracleBMCSServerGroup(name: "foo-v001", credentials: creds, launchConfig: ["compartmentId": "foo"]) + + when: + persistence.deleteServerGroup(sg) + + then: + 1 * creds.getObjectStorageClient().deleteObject(_) >> DeleteObjectResponse.builder().lastModified(new Date()).build() + creds.objectStorageClient.getNamespace(_) >> GetNamespaceResponse.builder().value("ns1").build() + creds.objectStorageClient.headBucket(_) >> HeadBucketResponse.builder().eTag("abc").build() + } + + def "get server group"() { + setup: + def creds = Mock(OracleBMCSNamedAccountCredentials) + creds.getCompartmentId() >> "foo" + creds.getObjectStorageClient() >> Mock(ObjectStorageClient) + OracleBMCSServerGroupPersistence persistence = new OracleBMCSServerGroupPersistence() + def sg = new OracleBMCSServerGroup(name: "foo-v001", launchConfig: ["compartmentId": "foo"]) + def objectMapper = new ObjectMapper(); + def json = objectMapper.writeValueAsString(sg); + def is = new ByteArrayInputStream(json.getBytes(Charset.forName("UTF-8"))) + def OracleBMCSPersistenceContext ctx = new OracleBMCSPersistenceContext(creds) + ctx.namespace = "ns1" + ctx.bucketChecked = true + + when: + def serverGroupRead = persistence.getServerGroupByName(ctx, "foo-v001") + + then: + 1 * creds.getObjectStorageClient().getObject(_) >> GetObjectResponse.builder().eTag("abc").inputStream(is).build() + serverGroupRead != null + serverGroupRead.name == "foo-v001" + } + + def "list server group names"() { + setup: + def creds = Mock(OracleBMCSNamedAccountCredentials) + creds.getCompartmentId() >> "foo" + creds.getObjectStorageClient() >> Mock(ObjectStorageClient) + OracleBMCSServerGroupPersistence persistence = new OracleBMCSServerGroupPersistence() + def OracleBMCSPersistenceContext ctx = new OracleBMCSPersistenceContext(creds) + ctx.namespace = "ns1" + ctx.bucketChecked = true + + when: + def names = persistence.listServerGroupNames(ctx) + + then: + 1 * creds.getObjectStorageClient().listObjects(_) >> ListObjectsResponse.builder() + .listObjects(ListObjects.builder().objects([ObjectSummary.builder().name("foo-v001").build()]).build()).build() + names != null + names == ["foo-v001"] + } +}