modules/mu/clouds/google/server_pool.rb in cloud-mu-2.1.0beta vs modules/mu/clouds/google/server_pool.rb in cloud-mu-3.0.0beta
- old
+ new
@@ -16,42 +16,34 @@
class Cloud
class Google
# A server pool as configured in {MU::Config::BasketofKittens::server_pools}
class ServerPool < MU::Cloud::ServerPool
- @deploy = nil
- @project_id = nil
- @config = nil
- attr_reader :mu_name
- attr_reader :cloud_id
- attr_reader :config
-
- # @param mommacat [MU::MommaCat]: A {MU::Mommacat} object containing the deploy of which this resource is/will be a member.
- # @param kitten_cfg [Hash]: The fully parsed and resolved {MU::Config} resource descriptor as defined in {MU::Config::BasketofKittens::server_pools}
- def initialize(mommacat: nil, kitten_cfg: nil, mu_name: nil, cloud_id: nil)
- @deploy = mommacat
- @config = MU::Config.manxify(kitten_cfg)
- @cloud_id ||= cloud_id
- if !mu_name.nil?
- @mu_name = mu_name
- @config['project'] ||= MU::Cloud::Google.defaultProject(@config['credentials'])
- if !@project_id
- project = MU::Cloud::Google.projectLookup(@config['project'], @deploy, sibling_only: true, raise_on_fail: false)
- @project_id = project.nil? ? @config['project'] : project.cloudobj.cloud_id
- end
- elsif @config['scrub_mu_isms']
- @mu_name = @config['name']
- else
- @mu_name = @deploy.getResourceName(@config['name'])
- end
+ # Initialize this cloud resource object. Calling +super+ will invoke the initializer defined under {MU::Cloud}, which should set the attribtues listed in {MU::Cloud::PUBLIC_ATTRS} as well as applicable dependency shortcuts, like <tt>@vpc</tt>, for us.
+ # @param args [Hash]: Hash of named arguments passed via Ruby's double-splat
+ def initialize(**args)
+ super
+ @mu_name ||= @deploy.getResourceName(@config['name'])
end
# Called automatically by {MU::Deploy#createResources}
def create
- @project_id = MU::Cloud::Google.projectLookup(@config['project'], @deploy).cloudobj.cloud_id
port_objs = []
+ sa = MU::Config::Ref.get(@config['service_account'])
+ if !sa or !sa.kitten or !sa.kitten.cloud_desc
+ raise MuError, "Failed to get service account cloud id from #{@config['service_account'].to_s}"
+ end
+ @service_acct = MU::Cloud::Google.compute(:ServiceAccount).new(
+ email: sa.kitten.cloud_desc.email,
+ scopes: @config['scopes']
+ )
+ if !@config['scrub_mu_isms']
+ MU::Cloud::Google.grantDeploySecretAccess(@service_acct.email, credentials: @config['credentials'])
+ end
+
+
@config['named_ports'].each { |port_cfg|
port_objs << MU::Cloud::Google.compute(:NamedPort).new(
name: port_cfg['name'],
port: port_cfg['port']
)
@@ -75,24 +67,34 @@
az = @config['availability_zone']
if az.nil?
az = MU::Cloud::Google.listAZs(@config['region']).sample
end
+ metadata = { # :items?
+ "startup-script" => @userdata
+ }
+ if @config['metadata']
+ desc[:metadata] = Hash[@config['metadata'].map { |m|
+ [m["key"], m["value"]]
+ }]
+ end
+ deploykey = @config['ssh_user']+":"+@deploy.ssh_public_key
+ if desc[:metadata]["ssh-keys"]
+ desc[:metadata]["ssh-keys"] += "\n"+deploykey
+ else
+ desc[:metadata]["ssh-keys"] = deploykey
+ end
+
instance_props = MU::Cloud::Google.compute(:InstanceProperties).new(
can_ip_forward: !@config['src_dst_check'],
description: @deploy.deploy_id,
-# machine_type: "zones/"+az+"/machineTypes/"+size,
machine_type: size,
+ service_accounts: [@service_acct],
labels: labels,
disks: MU::Cloud::Google::Server.diskConfig(@config, false, false, credentials: @config['credentials']),
network_interfaces: MU::Cloud::Google::Server.interfaceConfig(@config, @vpc),
- metadata: {
- :items => [
- :key => "ssh-keys",
- :value => @config['ssh_user']+":"+@deploy.ssh_public_key
- ]
- },
+ metadata: metadata,
tags: MU::Cloud::Google.compute(:Tags).new(items: [MU::Cloud::Google.nameStr(@mu_name)])
)
template_obj = MU::Cloud::Google.compute(:InstanceTemplate).new(
name: MU::Cloud::Google.nameStr(@mu_name),
@@ -130,13 +132,13 @@
)
# TODO this thing supports based on CPU usage, LB usage, or an arbitrary Cloud
# Monitoring metric. The default is "sustained 60%+ CPU usage". We should
# support all that.
-# http://www.rubydoc.info/github/google/google-api-ruby-client/Google/Apis/ComputeBeta/AutoscalingPolicyCpuUtilization
-# http://www.rubydoc.info/github/google/google-api-ruby-client/Google/Apis/ComputeBeta/AutoscalingPolicyLoadBalancingUtilization
-# http://www.rubydoc.info/github/google/google-api-ruby-client/Google/Apis/ComputeBeta/AutoscalingPolicyCustomMetricUtilization
+# http://www.rubydoc.info/github/google/google-api-ruby-client/Google/Apis/ComputeV1/AutoscalingPolicyCpuUtilization
+# http://www.rubydoc.info/github/google/google-api-ruby-client/Google/Apis/ComputeV1/AutoscalingPolicyLoadBalancingUtilization
+# http://www.rubydoc.info/github/google/google-api-ruby-client/Google/Apis/ComputeV1/AutoscalingPolicyCustomMetricUtilization
policy_obj = MU::Cloud::Google.compute(:AutoscalingPolicy).new(
cooldown_period_sec: @config['default_cooldown'],
max_num_replicas: @config['max_size'],
min_num_replicas: @config['min_size']
)
@@ -164,28 +166,177 @@
def notify
return {}
end
# Locate an existing ServerPool or ServerPools and return an array containing matching Google resource descriptors for those that match.
- # @param cloud_id [String]: The cloud provider's identifier for this resource.
- # @param region [String]: The cloud provider region
- # @param tag_key [String]: A tag key to search.
- # @param tag_value [String]: The value of the tag specified by tag_key to match when searching by tag.
- # @param flags [Hash]: Optional flags
- # @return [Array<Hash<String,OpenStruct>>]: The cloud provider's complete descriptions of matching ServerPools
- def self.find(cloud_id: nil, region: MU.curRegion, tag_key: "Name", tag_value: nil, flags: {}, credentials: nil)
- flags["project"] ||= MU::Cloud::Google.defaultProject(credentials)
- MU.log "XXX ServerPool.find not yet implemented", MU::WARN
- return {}
+ # @return [Hash<String,OpenStruct>]: The cloud provider's complete descriptions of matching ServerPools
+ def self.find(**args)
+ args[:project] ||= args[:habitat]
+ args[:project] ||= MU::Cloud::Google.defaultProject(args[:credentials])
+
+ regions = if args[:region]
+ [args[:region]]
+ else
+ MU::Cloud::Google.listRegions
+ end
+ found = {}
+
+ regions.each { |r|
+ begin
+ resp = MU::Cloud::Google.compute(credentials: args[:credentials]).list_region_instance_group_managers(args[:project], args[:region])
+ if resp and resp.items
+ resp.items.each { |igm|
+ found[igm.name] = igm
+ }
+ end
+ rescue ::Google::Apis::ClientError => e
+ raise e if !e.message.match(/forbidden: /)
+ end
+
+ begin
+# XXX can these guys have name collisions? test this
+ MU::Cloud::Google.listAZs(r).each { |az|
+ resp = MU::Cloud::Google.compute(credentials: args[:credentials]).list_instance_group_managers(args[:project], az)
+ if resp and resp.items
+ resp.items.each { |igm|
+ found[igm.name] = igm
+ }
+ end
+ }
+ rescue ::Google::Apis::ClientError => e
+ raise e if !e.message.match(/forbidden: /)
+ end
+ }
+
+ return found
end
+ # Reverse-map our cloud description into a runnable config hash.
+ # We assume that any values we have in +@config+ are placeholders, and
+ # calculate our own accordingly based on what's live in the cloud.
+ def toKitten(rootparent: nil, billing: nil, habitats: nil)
+ bok = {
+ "cloud" => "Google",
+ "credentials" => @credentials,
+ "cloud_id" => @cloud_id,
+ "region" => @config['region'],
+ "project" => @project_id,
+ }
+ bok['name'] = cloud_desc.name
+
+ scalers = if cloud_desc.zone and cloud_desc.zone.match(/-[a-z]$/)
+ bok['availability_zone'] = cloud_desc.zone.sub(/.*?\/([^\/]+)$/, '\1')
+ MU::Cloud::Google.compute(credentials: @credentials).list_autoscalers(@project_id, bok['availability_zone'])
+ else
+ MU::Cloud::Google.compute(credentials: @credentials).list_region_autoscalers(@project_id, @config['region'], filter: "target eq #{cloud_desc.self_link}")
+ end
+
+ if scalers and scalers.items and scalers.items.size > 0
+ scaler = scalers.items.first
+MU.log bok['name'], MU::WARN, details: scaler.autoscaling_policy
+# scaler.cpu_utilization.utilization_target
+# scaler.cool_down_period_sec
+ bok['min_size'] = scaler.autoscaling_policy.min_num_replicas
+ bok['max_size'] = scaler.autoscaling_policy.max_num_replicas
+ else
+ bok['min_size'] = bok['max_size'] = cloud_desc.target_size
+ end
+if cloud_desc.auto_healing_policies and cloud_desc.auto_healing_policies.size > 0
+MU.log bok['name'], MU::WARN, details: cloud_desc.auto_healing_policies
+end
+
+ template = MU::Cloud::Google.compute(credentials: @credentials).get_instance_template(@project_id, cloud_desc.instance_template.sub(/.*?\/([^\/]+)$/, '\1'))
+
+ iface = template.properties.network_interfaces.first
+ iface.network.match(/(?:^|\/)projects\/(.*?)\/.*?\/networks\/([^\/]+)(?:$|\/)/)
+ vpc_proj = Regexp.last_match[1]
+ vpc_id = Regexp.last_match[2]
+
+ bok['vpc'] = MU::Config::Ref.get(
+ id: vpc_id,
+ cloud: "Google",
+ habitat: MU::Config::Ref.get(
+ id: vpc_proj,
+ cloud: "Google",
+ credentials: @credentials,
+ type: "habitats"
+ ),
+ credentials: @credentials,
+ type: "vpcs",
+ subnet_pref: "any" # "anywhere in this VPC" is what matters
+ )
+
+ bok['basis'] = {
+ "launch_config" => {
+ "name" => bok['name']
+ }
+ }
+
+ template.properties.disks.each { |disk|
+ if disk.initialize_params.source_image and disk.boot
+ bok['basis']['launch_config']['image_id'] ||= disk.initialize_params.source_image.sub(/^https:\/\/www\.googleapis\.com\/compute\/[^\/]+\//, '')
+ elsif disk.type != "SCRATCH"
+ bok['basis']['launch_config']['storage'] ||= []
+ storage_blob = {
+ "size" => disk.initialize_params.disk_size_gb,
+ "device" => "/dev/xvd"+(disk.index+97).chr.downcase
+ }
+ bok['basis']['launch_config']['storage'] << storage_blob
+ else
+ MU.log "Need to sort out scratch disks", MU::WARN, details: disk
+ end
+
+ }
+
+ if template.properties.labels
+ bok['tags'] = template.properties.labels.keys.map { |k| { "key" => k, "value" => template.properties.labels[k] } }
+ end
+ if template.properties.tags and template.properties.tags.items and template.properties.tags.items.size > 0
+ bok['network_tags'] = template.properties.tags.items
+ end
+ bok['src_dst_check'] = !template.properties.can_ip_forward
+ bok['basis']['launch_config']['size'] = template.properties.machine_type.sub(/.*?\/([^\/]+)$/, '\1')
+ bok['project'] = @project_id
+ if template.properties.service_accounts
+ bok['scopes'] = template.properties.service_accounts.map { |sa| sa.scopes }.flatten.uniq
+ end
+ if template.properties.metadata and template.properties.metadata.items
+ bok['metadata'] = template.properties.metadata.items.map { |m| MU.structToHash(m) }
+ end
+
+ # Skip nodes that are just members of GKE clusters
+ if bok['name'].match(/^gke-.*?-[a-f0-9]+-[a-z0-9]+$/) and
+ bok['basis']['launch_config']['image_id'].match(/(:?^|\/)projects\/gke-node-images\//)
+ gke_ish = true
+ bok['network_tags'].each { |tag|
+ gke_ish = false if !tag.match(/^gke-/)
+ }
+ if gke_ish
+ MU.log "ServerPool #{bok['name']} appears to belong to a ContainerCluster, skipping adoption", MU::NOTICE
+ return nil
+ end
+ end
+#MU.log bok['name'], MU::WARN, details: [cloud_desc, template]
+
+ bok
+ end
+
# Cloud-specific configuration properties.
# @param config [MU::Config]: The calling MU::Config object
# @return [Array<Array,Hash>]: List of required fields, and json-schema Hash of cloud-specific configuration parameters for this resource
def self.schema(config)
toplevel_required = []
schema = {
+ "ssh_user" => MU::Cloud::Google::Server.schema(config)[1]["ssh_user"],
+ "metadata" => MU::Cloud::Google::Server.schema(config)[1]["metadata"],
+ "service_account" => MU::Cloud::Google::Server.schema(config)[1]["service_account"],
+ "scopes" => MU::Cloud::Google::Server.schema(config)[1]["scopes"],
+ "network_tags" => MU::Cloud::Google::Server.schema(config)[1]["network_tags"],
+ "availability_zone" => {
+ "type" => "string",
+ "description" => "Target a specific availability zone for this pool, which will create zonal instance managers and scalers instead of regional ones."
+ },
"named_ports" => {
"type" => "array",
"items" => {
"type" => "object",
"required" => ["name", "port"],
@@ -209,10 +360,42 @@
# @param pool [Hash]: The resource to process and validate
# @param configurator [MU::Config]: The overall deployment configurator of which this resource is a member
# @return [Boolean]: True if validation succeeded, False otherwise
def self.validateConfig(pool, configurator)
ok = true
+start = Time.now
+ pool['project'] ||= MU::Cloud::Google.defaultProject(pool['credentials'])
+ if pool['service_account']
+ pool['service_account']['cloud'] = "Google"
+ pool['service_account']['habitat'] ||= pool['project']
+ found = MU::Config::Ref.get(pool['service_account'])
+ if found.id and !found.kitten
+ MU.log "GKE pool #{pool['name']} failed to locate service account #{pool['service_account']} in project #{pool['project']}", MU::ERR
+ ok = false
+ end
+ else
+ user = {
+ "name" => pool['name'],
+ "cloud" => "Google",
+ "project" => pool["project"],
+ "credentials" => pool["credentials"],
+ "type" => "service"
+ }
+ configurator.insertKitten(user, "users", true)
+ pool['dependencies'] ||= []
+ pool['service_account'] = MU::Config::Ref.get(
+ type: "users",
+ cloud: "Google",
+ name: pool["name"],
+ project: pool["project"],
+ credentials: pool["credentials"]
+ )
+ pool['dependencies'] << {
+ "type" => "user",
+ "name" => pool["name"]
+ }
+ end
pool['named_ports'] ||= []
if !pool['named_ports'].include?({"name" => "ssh", "port" => 22})
pool['named_ports'] << {"name" => "ssh", "port" => 22}
end
@@ -222,12 +405,13 @@
launch['size'] = MU::Cloud::Google::Server.validateInstanceType(launch["size"], pool["region"])
ok = false if launch['size'].nil?
if launch['image_id'].nil?
- if MU::Config.google_images.has_key?(pool['platform'])
- launch['image_id'] = configurator.getTail("server_pool"+pool['name']+"Image", value: MU::Config.google_images[pool['platform']], prettyname: "server_pool"+pool['name']+"Image", cloudtype: "Google::Apis::ComputeBeta::Image")
+ img_id = MU::Cloud.getStockImage("Google", platform: pool['platform'])
+ if img_id
+ launch['image_id'] = configurator.getTail("server_pool"+pool['name']+"Image", value: img_id, prettyname: "server_pool"+pool['name']+"Image", cloudtype: "Google::Apis::ComputeV1::Image")
else
MU.log "No image specified for #{pool['name']} and no default available for platform #{pool['platform']}", MU::ERR, details: launch
ok = false
end
end
@@ -268,9 +452,10 @@
# @param ignoremaster [Boolean]: If true, will remove resources not flagged as originating from this Mu server
# @param region [String]: The cloud provider region
# @return [void]
def self.cleanup(noop: false, ignoremaster: false, region: MU.curRegion, credentials: nil, flags: {})
flags["project"] ||= MU::Cloud::Google.defaultProject(credentials)
+ return if !MU::Cloud::Google::Habitat.isLive?(flags["project"], credentials)
if !flags["global"]
["region_autoscaler", "region_instance_group_manager"].each { |type|
MU::Cloud::Google.compute(credentials: credentials).delete(
type,