class MU::Cloud::Google::Server

A server as configured in {MU::Config::BasketofKittens::servers}. In Google Cloud, this amounts to a single Instance in an Unmanaged Instance Group.

Public Class Methods

cleanup(noop: false, deploy_id: MU.deploy_id, ignoremaster: false, region: MU.curRegion, credentials: nil, flags: {}) click to toggle source

Remove all instances associated with the currently loaded deployment. Also cleans up associated volumes, droppings in the MU master's /etc/hosts and ~/.ssh, and in whatever Groomer was used. @param noop [Boolean]: If true, will only print what would be done @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]

# File modules/mu/providers/google/server.rb, line 1293
        def self.cleanup(noop: false, deploy_id: MU.deploy_id, ignoremaster: false, region: MU.curRegion, credentials: nil, flags: {})
          flags["habitat"] ||= MU::Cloud::Google.defaultProject(credentials)
          return if !MU::Cloud.resourceClass("Google", "Habitat").isLive?(flags["habitat"], credentials)

# XXX make damn sure MU.deploy_id is set
          filter = %Q{(labels.mu-id = "#{MU.deploy_id.downcase}")}
          if !ignoremaster and MU.mu_public_ip
            filter += %Q{ AND (labels.mu-master-ip = "#{MU.mu_public_ip.gsub(/\./, "_")}")}
          end

          MU::Cloud::Google.listAZs(region).each { |az|
            disks = []
            resp = MU::Cloud::Google.compute(credentials: credentials).list_instances(
              flags["habitat"],
              az,
              filter: filter
            )
            if !resp.items.nil? and resp.items.size > 0
              resp.items.each { |instance|
                MU.log "Terminating instance #{instance.name}"
                if !instance.disks.nil? and instance.disks.size > 0
                  instance.disks.each { |disk|
                    disks << disk if !disk.auto_delete
                  }
                end
                MU::Cloud::Google.compute(credentials: credentials).delete_instance(
                  flags["habitat"],
                  az,
                  instance.name
                ) if !noop
                if instance.service_accounts
                  instance.service_accounts.each { |sa|
                    MU.log "Removing service account #{sa.email}"
                    begin
                      MU::Cloud::Google.iam(credentials: credentials).delete_project_service_account(
                        "projects/#{flags["habitat"]}/serviceAccounts/#{sa.email}"
                      ) if !noop
                    rescue ::Google::Apis::ClientError => e
                      raise e if !e.message.match(/^notFound: /)
                    end
                  }
                end
# XXX wait-loop on pending?
#                pp deletia
              }
            end

            if disks.size > 0
# XXX make sure we don't miss anything that got created with dumb flags
            end
# XXX honor snapshotting
            MU::Cloud::Google.compute(credentials: credentials).delete(
              "disk",
              flags["habitat"],
              az,
              noop
            ) if !noop
          }
        end
createImage(name: nil, instance_id: nil, storage: {}, exclude_storage: false, project: nil, make_public: false, tags: [], region: nil, family: nil, zone: MU::Cloud::Google.listAZs.sample, credentials: nil) click to toggle source

Create an image out of a running server. Requires either the name of a MU resource in the current deployment, or the cloud provider id of a running instance. @param name [String]: The MU resource name of the server to use as the basis for this image. @param instance_id [String]: The cloud provider resource identifier of the server to use as the basis for this image. @param storage [Hash]: The storage devices to include in this image. @param exclude_storage [Boolean]: Do not include the storage device profile of the running instance when creating this image. @param region [String]: The cloud provider region @param tags [Array<String>]: Extra/override tags to apply to the image. @return [String]: The cloud provider identifier of the new machine image.

# File modules/mu/providers/google/server.rb, line 880
def self.createImage(name: nil, instance_id: nil, storage: {}, exclude_storage: false, project: nil, make_public: false, tags: [], region: nil, family: nil, zone: MU::Cloud::Google.listAZs.sample, credentials: nil)
  project ||= MU::Cloud::Google.defaultProject(credentials)
  instance = MU::Cloud::Server.find(cloud_id: instance_id, region: region)
  if instance.nil?
    raise MuError, "Failed to find instance '#{instance_id}' in createImage"
  end

  labels = Hash[tags.keys.map { |k|
    [k.downcase, tags[k].downcase.gsub(/[^-_a-z0-9]/, '-')] }
  ]
  labels["name"] = name

  bootdisk = nil
  threads = []
  parent_thread_id = Thread.current.object_id
  if !exclude_storage
    instance[instance_id].disks.each { |disk|
      threads << Thread.new {
        Thread.abort_on_exception = false
        MU.dupGlobals(parent_thread_id)
        if disk.boot
          bootdisk = disk.source
        else
          snapobj = MU::Cloud::Google.compute(:Snapshot).new(
            name: name+"-"+disk.device_name,
            description: "Mu image created from #{name} (#{disk.device_name})"
          )
          diskname = disk.source.gsub(/.*?\//, "")
          MU.log "Creating snapshot of #{diskname} in #{zone}", MU::NOTICE, details: snapobj
          snap = MU::Cloud::Google.compute(credentials: credentials).create_disk_snapshot(
            project,
            zone,
            diskname,
            snapobj
          )
          MU::Cloud::Google.compute(credentials: credentials).set_snapshot_labels(
            project,
            snap.name,
            MU::Cloud::Google.compute(:GlobalSetLabelsRequest).new(
              label_fingerprint: snap.label_fingerprint,
              labels: labels.merge({
                "mu-device-name" => disk.device_name,
                "mu-parent-image" => name,
                "mu-orig-zone" => zone
              })
            )
          )
        end
      }
    }
  end
  threads.each do |t|
    t.join
  end

  labels["name"] = instance_id.downcase
  image_desc = {
    :name => name,
    :source_disk => bootdisk,
    :description => "Mu image created from #{name}",
    :labels => labels
  }
  image_desc[:family] = family if family

  MU.log "Creating image of #{name}", MU::NOTICE, details: image_desc
  newimage = MU::Cloud::Google.compute(credentials: credentials).insert_image(
    project,
    MU::Cloud::Google.compute(:Image).new(image_desc)
  )

  if make_public
    MU.log "Making image #{newimage.name} public"
    MU::Cloud::Google.compute(credentials: credentials).set_image_iam_policy(
      project,
      newimage.name,
      MU::Cloud::Google.compute(:GlobalSetPolicyRequest).new(
        bindings: [
          MU::Cloud::Google.compute(:Binding).new(
            members: ["allAuthenticatedUsers"],
            role: "roles/compute.imageUser"
          )
        ],
      )
    )
  end

  newimage.name
end
diskConfig(config, create = true, disk_as_url = true, credentials: nil) click to toggle source

Generator for disk configuration parameters for a Compute instance @param config [Hash]: The MU::Cloud::Server config hash for whom we're configuring disks @param create [Boolean]: Actually create extra (non-root) disks, or just the one declared as the root disk of the image @param disk_as_url [Boolean]: Whether to declare the disk type as a short string or full URL, which can vary depending on the calling resource @return [Array]: The Compute :AttachedDisk objects describing disks that've been created

# File modules/mu/providers/google/server.rb, line 164
        def self.diskConfig(config, create = true, disk_as_url = true, credentials: nil)
          disks = []
          if config['image_id'].nil? and config['basis'].nil?
            raise MuError, "Can't generate disk configuration for server #{config['name']} without an image ID or basis specified"
          end

          img = fetchImage(config['image_id'] || config['basis']['launch_config']['image_id'], credentials: credentials)

#          if img.source_disk and img.source_disk.match(/projects\/([^\/]+)\/zones\/([^\/]+)\/disks\/(.*)/)
#            _junk, proj, az, name = Regexp.last_match
#            disk_desc = MU::Cloud::Google.compute(credentials: credentials).get_disk(proj, az, name)
#            pp disk_desc
#            raise "nah"
#          end

          disktype = "projects/#{config['project']}/zones/#{config['availability_zone']}/diskTypes/pd-standard"


          disktype.gsub!(/.*?\/([^\/])$/, '\1') if !disk_as_url

          imageobj = MU::Cloud::Google.compute(:AttachedDiskInitializeParams).new(
            source_image: img.self_link,
            disk_size_gb: img.disk_size_gb,
            disk_type: disktype,
          )
          disks << MU::Cloud::Google.compute(:AttachedDisk).new(
            auto_delete: true,
            boot: true,
            mode: "READ_WRITE",
            type: "PERSISTENT",
            initialize_params: imageobj
          )
          if config["storage"]
            config["storage"].each { |vol|
              devicename = vol['device'].gsub(/[^\w\-\.]/, "-").sub(/^[^\w]/, "")
              disk_desc = {
                :auto_delete => true,
                :device_name => devicename, # XXX empty string is also legit
                :mode => "READ_WRITE",
                :type => "PERSISTENT" # SCRATCH is equivalent of ephemeral? cheap virtual memory disk? maybe ship a standard set
              }

              if vol['snapshot_id']
                disk_desc[:source_snapshot] = vol['snapshot_id']
# XXX check existence in in validateConfig
              elsif vol['somekindofidforaloosevolume']
                disk_desc[:source] = vol['somekindofidforaloosevolume']
# XXX check existence in in validateConfig
              end
# XXX I don't know how to do this in managed instance groups
#next
next if !create
              diskname = MU::Cloud::Google.nameStr(config['mu_name']+"-"+devicename)
              newdiskobj = MU::Cloud::Google.compute(:Disk).new(
                size_gb: vol['size'],
                description: MU.deploy_id,
                zone: config['availability_zone'],
#                    type: "projects/#{config['project']}/zones/#{config['availability_zone']}/diskTypes/pd-ssd",
                type: "projects/#{config['project']}/zones/#{config['availability_zone']}/diskTypes/pd-standard",
                source_snapshot: vol['snapshot_id'],
# type: projects/project/zones/#{config['availability_zone']}/diskTypes/pd-standard Other values include pd-ssd and local-ssd
                name: diskname
              )
              MU.log "Creating disk #{diskname}", details: newdiskobj

              newdisk = MU::Cloud::Google.compute(credentials: credentials).insert_disk(
                config['project'],
                config['availability_zone'],
                newdiskobj
              )

              disk_desc[:source] = newdisk.self_link

              disks << MU::Cloud::Google.compute(:AttachedDisk).new(disk_desc)
            }
          end

          disks
        end
fetchImage(image_id, credentials: nil) click to toggle source

Retrieve the cloud descriptor for this machine image, which can be a whole or partial URL. Will follow deprecation notices and retrieve the latest version, if applicable. @param image_id [String]: URL to a Google disk image @param credentials [String] @return [Google::Apis::ComputeV1::Image]

# File modules/mu/providers/google/server.rb, line 104
def self.fetchImage(image_id, credentials: nil)
  return @@image_id_map[image_id] if @@image_id_map[image_id]

  img_proj = img_name = nil
  if image_id.match(/\//)
    img_proj = image_id.gsub(/(?:https?:\/\/.*?\.googleapis\.com\/compute\/.*?\/)?.*?\/?(?:projects\/)?([^\/]+)\/.*/, '\1')
    img_name = image_id.gsub(/.*?([^\/]+)$/, '\1')
  else
    img_name = image_id
  end

  begin
    @@image_id_map[image_id] = MU::Cloud::Google.compute(credentials: credentials).get_image_from_family(img_proj, img_name)
    return @@image_id_map[image_id]
  rescue ::Google::Apis::ClientError
    # This is fine- we don't know that what we asked for is really an
    # image family name, instead of just an image.
  end

  begin
    img = MU::Cloud::Google.compute(credentials: credentials).get_image(img_proj, img_name)
    if !img.deprecated.nil? and !img.deprecated.replacement.nil?
      image_id = img.deprecated.replacement
      img_proj = image_id.gsub(/(?:https?:\/\/.*?\.googleapis\.com\/compute\/.*?\/)?.*?\/?(?:projects\/)?([^\/]+)\/.*/, '\1')
      img_name = image_id.gsub(/.*?([^\/]+)$/, '\1')
    end
  rescue ::Google::Apis::ClientError => e
    # SOME people *cough* don't use deprecation or image family names
    # and just spew out images with a version appended to the name, so
    # let's try some crude semantic versioning list.
    if e.message.match(/^notFound: /) and img_name.match(/-[^\-]+$/)
      list = MU::Cloud::Google.compute(credentials: credentials).list_images(img_proj, filter: "name eq #{img_name.sub(/-[^\-]+$/, '')}-.*")
      if list and list.items
        latest = nil
        list.items.each { |candidate|
          created = DateTime.parse(candidate.creation_timestamp)
          if latest.nil? or created > latest
            latest = created
            img = candidate
          end
        }
        if latest
          MU.log "Mapped #{image_id} to #{img.name} with semantic versioning guesswork", MU::WARN
          @@image_id_map[image_id] = img
          return @@image_id_map[image_id]
        end
      end
    end
    raise e # if our little semantic versioning party trick failed
  end while !img.deprecated.nil? and img.deprecated.state == "DEPRECATED" and !img.deprecated.replacement.nil?
  final = MU::Cloud::Google.compute(credentials: credentials).get_image(img_proj, img_name)
  @@image_id_map[image_id] = final
  @@image_id_map[image_id]
end
find(**args) click to toggle source

Locate an existing instance or instances and return an array containing matching AWS resource descriptors for those that match. @return [Array<Hash<String,OpenStruct>>]: The cloud provider's complete descriptions of matching instances

# File modules/mu/providers/google/server.rb, line 656
        def self.find(**args)
          args = MU::Cloud::Google.findLocationArgs(args)

          if !args[:region].nil? and MU::Cloud::Google.listRegions.include?(args[:region])
            regions = [args[:region]]
          else
            regions = MU::Cloud::Google.listRegions
          end

          found = {}
          search_semaphore = Mutex.new
          search_threads = []

          # If we got an instance id, go get it
          parent_thread_id = Thread.current.object_id
          regions.each { |r|
            search_threads << Thread.new(r) { |region|
              Thread.abort_on_exception = false
              MU.dupGlobals(parent_thread_id)
              MU.log "Hunting for instance with cloud id '#{args[:cloud_id]}' in #{region}", MU::DEBUG
              MU::Cloud::Google.listAZs(region).each { |az|
                begin
                  if !args[:cloud_id].nil? and !args[:cloud_id].empty?
                    resp = MU::Cloud::Google.compute(credentials: args[:credentials]).get_instance(
                      args[:project],
                      az,
                      args[:cloud_id]
                    )
                    search_semaphore.synchronize {
                      found[args[:cloud_id]] = resp if !resp.nil?
                    }
                  else
                    resp = MU::Cloud::Google.compute(credentials: args[:credentials]).list_instances(
                      args[:project],
                      az
                    )
                    if resp and resp.items
                      resp.items.each { |instance|
                        search_semaphore.synchronize {
                          found[instance.name] = instance
                        }
                      }
                    end
                  end
                rescue ::OpenSSL::SSL::SSLError => e
                  MU.log "Got #{e.message} looking for instance #{args[:cloud_id]} in project #{args[:project]} (#{az}). Usually this means we've tried to query a non-functional region.", MU::DEBUG
                rescue ::Google::Apis::ClientError => e
                  raise e if !e.message.match(/^(?:notFound|forbidden): /)
                end
              }
            }
          }
          done_threads = []
          begin
            search_threads.reject! { |t| t.nil? }
            search_threads.each { |t|
              joined = t.join(2)
              done_threads << joined if !joined.nil?
            }
          end while found.size < 1 and done_threads.size != search_threads.size
          # Ok, well, let's try looking it up by IP then
#          if instance.nil? and !args[:ip].nil?
#            MU.log "Hunting for instance by IP '#{args[:ip]}'", MU::DEBUG
#          end

#          if !instance.nil?
#            return {instance.name => instance} if !instance.nil?
#          end

          # Fine, let's try it by tag.
#          if !args[:tag_value].nil?
#            MU.log "Searching for instance by tag '#{args[:tag_key]}=#{args[:tag_value]}'", MU::DEBUG
#          end

          return found
        end
genericNAT() click to toggle source

Return a BoK-style config hash describing a NAT instance. We use this to approximate Amazon's NAT gateway functionality with a plain instance. @return [Hash]

# File modules/mu/providers/google/server.rb, line 433
def self.genericNAT
  return {
    "cloud" => "Google",
    "size" => "g1-small",
    "run_list" => [ "mu-nat" ],
    "groomer" => "Ansible",
    "platform" => "centos7",
    "src_dst_check" => false,
    "ssh_user" => "centos",
    "associate_public_ip" => true,
    "static_ip" => { "assign_ip" => true },
    "routes" => [ {
      "gateway" => "#INTERNET",
      "priority" => 50,
      "destination_network" => "0.0.0.0/0"
    } ]
  }
end
imageTimeStamp(image_id, credentials: nil) click to toggle source

Return the date/time a machine image was created. @param image_id [String]: URL to a Google disk image @param credentials [String] @return [DateTime]

# File modules/mu/providers/google/server.rb, line 85
def self.imageTimeStamp(image_id, credentials: nil)
  begin
    img = fetchImage(image_id, credentials: credentials)
    return DateTime.new if img.nil?
    return DateTime.parse(img.creation_timestamp)
  rescue ::Google::Apis::ClientError
  end

  return DateTime.new
end
interfaceConfig(config, vpc) click to toggle source

Generator for disk configuration parameters for a Compute instance @param config [Hash]: The MU::Cloud::Server config hash for whom we're configuring network interfaces @param vpc [MU::Cloud::Google::VPC]: The VPC in which this interface should reside @return [Array]: Configuration objects for network interfaces, suitable for passing to the Compute API

# File modules/mu/providers/google/server.rb, line 248
def self.interfaceConfig(config, vpc)
  subnet_cfg = config['vpc']
  if config['vpc']['subnets'] and
     !subnet_cfg['subnet_name'] and !subnet_cfg['subnet_id']
    # XXX if illegal subnets somehow creep in here, we'll need to be
    # picky by region or somesuch
    subnet_cfg = config['vpc']['subnets'].sample

  end

  subnet = vpc.getSubnet(name: subnet_cfg['subnet_name'], cloud_id: subnet_cfg['subnet_id'])
  if subnet.nil?
    if config['vpc']['name']
      subnet = vpc.getSubnet(name: config['vpc']['name']+subnet_cfg['subnet_name'], cloud_id: subnet_cfg['subnet_id'])
    end
    if subnet.nil?
      raise MuError, "Couldn't find subnet details for #{subnet_cfg['subnet_name'] || subnet_cfg['subnet_id']} while configuring Server #{config['name']} (VPC: #{vpc.mu_name})"
    end
  end

  base_iface_obj = {
    :network => vpc.url,
    :subnetwork => subnet.url
  }

  if config['associate_public_ip']
    base_iface_obj[:access_configs] = [
      MU::Cloud::Google.compute(:AccessConfig).new
    ]
  end
  interfaces = [base_iface_obj]
  # XXX add more if they asked for it (e.g. config['private_ip'])

  interfaces
end
isGlobal?() click to toggle source

Does this resource type exist as a global (cloud-wide) artifact, or is it localized to a region/zone? @return [Boolean]

# File modules/mu/providers/google/server.rb, line 1278
def self.isGlobal?
  false
end
new(**args) click to toggle source

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 @vpc, for us. @param args [Hash]: Hash of named arguments passed via Ruby's double-splat

Calls superclass method
# File modules/mu/providers/google/server.rb, line 34
        def initialize(**args)
          super

          @userdata = if @config['userdata_script']
            @config['userdata_script']
          elsif @deploy and !@config['scrub_mu_isms']
            MU::Cloud.fetchUserdata(
              platform: @config["platform"],
              cloud: "Google",
              credentials: @config['credentials'],
              template_variables: {
                "deployKey" => Base64.urlsafe_encode64(@deploy.public_key),
                "deploySSHKey" => @deploy.ssh_public_key,
                "muID" => MU.deploy_id,
                "muUser" => MU.mu_user,
                "publicIP" => MU.mu_public_ip,
                "skipApplyUpdates" => @config['skipinitialupdates'],
                "windowsAdminName" => @config['windows_admin_username'],
                "adminBucketName" => MU::Cloud::Google.adminBucketName(@credentials),
                "chefVersion" => MU.chefVersion,
                "mommaCatPort" => MU.mommaCatPort,
                "resourceName" => @config["name"],
                "resourceType" => "server",
                "platform" => @config["platform"]
              },
              custom_append: @config['userdata_script']
            )
          end
# XXX writing things into @config at runtime is a bad habit and we should stop
          if !@mu_name.nil?
            @config['mu_name'] = @mu_name # XXX whyyyy
            # describe
            @mu_windows_name = @deploydata['mu_windows_name'] if @mu_windows_name.nil? and @deploydata
          else
            if kitten_cfg.has_key?("basis")
              @mu_name = @deploy.getResourceName(@config['name'], need_unique_string: true)
            else
              @mu_name = @deploy.getResourceName(@config['name'])
            end
            @config['mu_name'] = @mu_name

          end
          @config['instance_secret'] ||= Password.random(50)
          @config['ssh_user'] ||= "muadmin"

        end
quality() click to toggle source

Denote whether this resource implementation is experiment, ready for testing, or ready for production use.

# File modules/mu/providers/google/server.rb, line 1284
def self.quality
  MU::Cloud::RELEASE
end
schema(config) click to toggle source

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

# File modules/mu/providers/google/server.rb, line 1356
def self.schema(config)
  toplevel_required = []
  schema = {
    "roles" => MU::Cloud.resourceClass("Google", "User").schema(config)[1]["roles"],
    "windows_admin_username" => {
      "type" => "string",
      "default" => "muadmin"
    },
    "create_image" => {
      "properties" => {
        "family" => {
          "type" => "string",
          "description" => "Add a GCP image +family+ string to the created image(s)"
        }
      }
    },
    "availability_zone" => {
      "type" => "string",
      "description" => "Target this instance to a specific Availability Zone"
    },
    "ssh_user" => {
      "type" => "string",
      "description" => "Account to use when connecting via ssh. Google Cloud images don't come with predefined remote access users, and some don't work with our usual default of +root+, so we recommend using some other (non-root) username.",
      "default" => "muadmin"
    },
    "network_tags" => {
      "type" => "array",
      "items" => {
        "type" => "string",
        "description" => "Add a network tag to this host, which can be used to selectively apply routes or firewall rules."
      }
    },
    "service_account" => MU::Config::Ref.schema(
      type: "users",
      desc: "An existing service account to use instead of the default one generated by Mu during the deployment process."
    ),
    "metadata" => {
      "type" => "array",
      "items" => {
        "type" => "object",
        "description" => "Custom key-value pairs to be added to the metadata of Google Cloud virtual machines",
        "required" => ["key", "value"],
        "properties" => {
          "key" => {
            "type" => "string"
          },
          "value" => {
            "type" => "string"
          }
        }
      }
    },
    "routes" => {
      "type" => "array",
      "items" => MU::Config::VPC.routeschema
    },
    "scopes" => {
      "type" => "array",
      "items" => {
        "type" => "string",
        "description" => "API scopes to make available to this resource's service account."
      },
      "default" => ["https://www.googleapis.com/auth/compute.readonly", "https://www.googleapis.com/auth/logging.write", "https://www.googleapis.com/auth/cloud-platform", "https://www.googleapis.com/auth/monitoring.write", "https://www.googleapis.com/auth/devstorage.read_only"]
    }
  }
  [toplevel_required, schema]
end
validateConfig(server, configurator) click to toggle source

Cloud-specific pre-processing of {MU::Config::BasketofKittens::servers}, bare and unvalidated. @param server [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

# File modules/mu/providers/google/server.rb, line 1508
        def self.validateConfig(server, configurator)
          ok = true

          server['project'] ||= MU::Cloud::Google.defaultProject(server['credentials'])

          size = validateInstanceType(server["size"], server["region"], project: server['project'], credentials: server['credentials'])

          if size.nil?
            MU.log "Failed to verify instance size #{server["size"]} for Server #{server['name']}", MU::WARN
          else
            server["size"] = size
          end

          # If we're not targeting an availability zone, pick one randomly
          if !server['availability_zone']
            server['availability_zone'] = MU::Cloud::Google.listAZs(server['region']).sample
          end

          if server['service_account']
            server['service_account'] = server['service_account'].to_h
            server['service_account']['cloud'] = "Google"
            server['service_account']['habitat'] ||= server['project']
            found = MU::Config::Ref.get(server['service_account'])
            if found.id and !found.kitten
              MU.log "GKE server #{server['name']} failed to locate service account #{server['service_account']} in project #{server['project']}", MU::ERR
              ok = false
            end
          else
            server = MU::Cloud.resourceClass("Google", "User").genericServiceAccount(server, configurator)
          end

          subnets = nil
          if !server['vpc']
            vpcs = MU::Cloud.resourceClass("Google", "VPC").find(credentials: server['credentials'])
            if vpcs["default"]
              server["vpc"] ||= {}
              server["vpc"]["vpc_id"] = vpcs["default"].self_link
              subnets = vpcs["default"].subnetworks
              MU.log "No VPC specified for Server #{server['name']}, using default VPC for project #{server['project']}", MU::NOTICE
            else
              ok = false
              MU.log "You must specify a target VPC when creating a Server", MU::ERR
            end
          end

          if !server['vpc']['subnet_id'] and server['vpc']['subnet_name'].nil?
            if !subnets
              if server["vpc"]["vpc_id"]
                vpcs = MU::Cloud.resourceClass("Google", "VPC").find(cloud_id: server["vpc"]["vpc_id"])
                subnets = vpcs["default"].subnetworks.sample
              end
            end

            if subnets
              server['vpc']['subnet_id'] = subnets.delete_if { |subnet|
                !subnet.match(/regions\/#{Regexp.quote(server['region'])}\/subnetworks/)
              }.sample
            end
            if server['vpc']['subnet_id'].nil?
              ok = false
              MU.log "Failed to identify a subnet in my region (#{server['region']})", MU::ERR, details: server["vpc"]["vpc_id"]
            end
          end

          if server['vpc']
            server['vpc']['project'] ||= server['project']
          end

          if server['image_id'].nil?
            img_id = MU::Cloud.getStockImage("Google", platform: server['platform'])
            if img_id
              server['image_id'] = configurator.getTail("server"+server['name']+"Image", value: img_id, prettyname: "server"+server['name']+"Image", cloudtype: "Google::Apis::ComputeV1::Image")
            else
              MU.log "No image specified for #{server['name']} and no default available for platform #{server['platform']}", MU::ERR, details: server
              ok = false
            end
          end

          real_image = nil
          begin
            real_image = MU::Cloud::Google::Server.fetchImage(server['image_id'].to_s, credentials: server['credentials'])
          rescue ::Google::Apis::ClientError => e
          end

          if real_image.nil?
            MU.log "Image #{server['image_id']} for server #{server['name']} does not appear to exist", MU::ERR
            ok = false
          else
            server['image_id'] = real_image.self_link
            server['image_id'].match(/projects\/([^\/]+)\/.*?\/([^\/]+)$/)
            img_project = Regexp.last_match[1]
            img_name = Regexp.last_match[2]
            begin
              MU::Cloud::Google.compute(credentials: server['credentials']).get_image(img_project, img_name)
              snaps = MU::Cloud::Google.compute(credentials: server['credentials']).list_snapshots(
                img_project,
                filter: "name eq #{img_name}-.*"
              )
              server['storage'] ||= []
              used_devs = server['storage'].map { |disk| disk['device'].gsub(/.*?\//, "") }
              if snaps and snaps.items
                snaps.items.each { |snap|
                  next if !snap.labels.is_a?(Hash) or !snap.labels["mu-device-name"] or snap.labels["mu-parent-image"] != img_name
                  devname = snap.labels["mu-device-name"]

                  if used_devs.include?(devname)
                    MU.log "Device name #{devname} already declared in server #{server['name']} (snapshot #{snap.name} wants the name)", MU::ERR
                    ok = false
                  end
                  server['storage'] << {
                    "snapshot_id" => snap.self_link,
                    "size" => snap.disk_size_gb,
                    "delete_on_termination" => true, 
                    "device" => devname
                  }
                  used_devs << devname
                }
                if snaps.items.size > 0
#                  MU.log img_name, MU::WARN, details: snaps.items
                end
              end
            rescue ::Google::Apis::ClientError => e
              # it's ok, sometimes we don't have permission to list snapshots
              # in other peoples' projects
#              MU.log img_name, MU::WARN, details: img
              raise e if !e.message.match(/^forbidden: /)
            end
          end

          ok
        end
validateInstanceType(size, region, project: nil, credentials: nil) click to toggle source

Confirm that the given instance size is valid for the given region. If someone accidentally specified an equivalent size from some other cloud provider, return something that makes sense. If nothing makes sense, return nil. @param size [String]: Instance type to check @param region [String]: Region to check against @return [String,nil]

# File modules/mu/providers/google/server.rb, line 1431
def self.validateInstanceType(size, region, project: nil, credentials: nil)
  size = size.dup.to_s
  if @@instance_type_cache[project] and
     @@instance_type_cache[project][region] and
     @@instance_type_cache[project][region][size]
    return @@instance_type_cache[project][region][size]
  end

  if size.match(/\/?custom-(\d+)-(\d+)(?:-ext)?$/)
    cpus = Regexp.last_match[1].to_i
    mem = Regexp.last_match[2].to_i
    ok = true
    if cpus < 1 or cpus > 32 or (cpus % 2 != 0 and cpus != 1)
      MU.log "Custom instance type #{size} illegal: CPU count must be 1 or an even number between 2 and 32", MU::ERR
      ok = false
    end
    if (mem % 256) != 0
      MU.log "Custom instance type #{size} illegal: Memory must be a multiple of 256 (MB)", MU::ERR
      ok = false
    end
    if ok
      return "custom-#{cpus.to_s}-#{mem.to_s}"
    else
      return nil
    end
  end

  project ||= MU::Cloud::Google.defaultProject(credentials)

  @@instance_type_cache[project] ||= {}
  @@instance_type_cache[project][region] ||= {}
  types = MU::Cloud::Google.listInstanceTypes(region, project: project, credentials: credentials)[project][region]
  realsize = size.dup

  if types and (realsize.nil? or !types.has_key?(realsize))
    # See if it's a type we can approximate from one of the other clouds
    foundmatch = false
    MU::Cloud.availableClouds.each { |cloud|
      next if cloud == "Google"
      foreign_types = (MU::Cloud.cloudClass(cloud).listInstanceTypes).values.first
      if foreign_types.size == 1
        foreign_types = foreign_types.values.first
      end
      if foreign_types and foreign_types.size > 0 and foreign_types.has_key?(size)
        vcpu = foreign_types[size]["vcpu"]
        mem = foreign_types[size]["memory"]
        ecu = foreign_types[size]["ecu"]
        types.keys.sort.reverse.each { |type|
          features = types[type]
          next if ecu == "Variable" and ecu != features["ecu"]
          next if features["vcpu"] != vcpu
          if (features["memory"] - mem.to_f).abs < 0.10*mem
            foundmatch = true
            MU.log "You specified #{cloud} instance type '#{realsize}.' Approximating with Google Compute type '#{type}.'", MU::WARN
            realsize = type
            break
          end
        }
      end
      break if foundmatch
    }

    if !foundmatch
      MU.log "Invalid size '#{realsize}' for Google Compute instance in #{region} (checked project #{project}). Supported types:", MU::ERR, details: types.keys.sort.join(", ")
      @@instance_type_cache[project][region][size] = nil
      return nil
    end
  end
  @@instance_type_cache[project][region][size] = realsize
  @@instance_type_cache[project][region][size]
end

Public Instance Methods

active?() click to toggle source

Determine whether the node in question exists at the Cloud provider layer. @return [Boolean]

# File modules/mu/providers/google/server.rb, line 1164
def active?
  true
end
addVolume(dev, size, type: "pd-standard", delete_on_termination: false) click to toggle source

Add a volume to this instance @param dev [String]: Device name to use when attaching to instance @param size [String]: Size (in gb) of the new volume @param type [String]: Cloud storage type of the volume, if applicable @param delete_on_termination [Boolean]: Value of delete_on_termination flag to set

# File modules/mu/providers/google/server.rb, line 1112
        def addVolume(dev, size, type: "pd-standard", delete_on_termination: false)
          devname = dev.gsub(/.*?\/([^\/]+)$/, '\1')
          resname = MU::Cloud::Google.nameStr(@mu_name+"-"+devname)
          MU.log "Creating disk #{resname}"

          description = @deploy ? @deploy.deploy_id : @mu_name+"-"+devname

          newdiskobj = MU::Cloud::Google.compute(:Disk).new(
            size_gb: size,
            description: description,
            zone: @config['availability_zone'],
#            type: "projects/#{config['project']}/zones/#{config['availability_zone']}/diskTypes/pd-ssd",
            type: "projects/#{@project_id}/zones/#{@config['availability_zone']}/diskTypes/#{type}",
# Other values include pd-ssd and local-ssd
            name: resname
          )

          begin
            newdisk = MU::Cloud::Google.compute(credentials: @config['credentials']).insert_disk(
              @project_id,
              @config['availability_zone'],
              newdiskobj
            )
          rescue ::Google::Apis::ClientError => e
            if e.message.match(/^alreadyExists: /)
              MU.log "Disk #{resname} already exists, ignoring request to create", MU::WARN
              return
            else
              raise e
            end
          end

          attachobj = MU::Cloud::Google.compute(:AttachedDisk).new(
            device_name: devname,
            source: newdisk.self_link,
            type: "PERSISTENT",
            auto_delete: delete_on_termination
          )

          MU.log "Attaching disk #{resname} to #{@cloud_id} at #{devname}"
          MU::Cloud::Google.compute(credentials: @config['credentials']).attach_disk(
            @project_id,
            @config['availability_zone'],
            @cloud_id,
            attachobj
          )

        end
canonicalIP() click to toggle source

Return the IP address that we, the Mu server, should be using to access this host via the network. Note that this does not factor in SSH bastion hosts that may be in the path, see getSSHConfig if that's what you need.

# File modules/mu/providers/google/server.rb, line 973
def canonicalIP
  describe(cloud_id: @cloud_id)

  if !cloud_desc
    raise MuError, "Couldn't retrieve cloud descriptor for server #{self}"
  end

  private_ips = []
  public_ips = []

  cloud_desc.network_interfaces.each { |iface|
    private_ips << iface.network_ip
    if iface.access_configs
      iface.access_configs.each { |acfg|
        public_ips << acfg.nat_ip if acfg.nat_ip
      }
    end
  }

  # Our deploydata gets corrupted often with server pools, this will cause us to use the wrong IP to identify a node
  # which will cause us to create certificates, DNS records and other artifacts with incorrect information which will cause our deploy to fail.
  # The cloud_id is always correct so lets use 'cloud_desc' to get the correct IPs
  if MU::Cloud.resourceClass("Google", "VPC").haveRouteToInstance?(cloud_desc, credentials: @config['credentials']) or public_ips.size == 0
    @config['canonical_ip'] = private_ips.first
    return private_ips.first
  else
    @config['canonical_ip'] = public_ips.first
    return public_ips.first
  end
end
create() click to toggle source

Called automatically by {MU::Deploy#createResources}

# File modules/mu/providers/google/server.rb, line 285
def create
  @project_id = MU::Cloud::Google.projectLookup(@config['project'], @deploy).cloud_id

  sa = nil
  retries = 0
  begin
    sa = MU::Config::Ref.get(@config['service_account'])
    if !sa or !sa.kitten or !sa.kitten.cloud_desc
      sleep 10
    end
  end while !sa or !sa.kitten or !sa.kitten.cloud_desc and retries < 5

  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

  begin
    disks = MU::Cloud::Google::Server.diskConfig(@config, credentials: @config['credentials'])
    interfaces = MU::Cloud::Google::Server.interfaceConfig(@config, @vpc)

    if @config['routes']
      @config['routes'].each { |route|
        @vpc.cloudobj.createRouteForInstance(route, self)
      }
    end

    desc = {
      :name => MU::Cloud::Google.nameStr(@mu_name),
      :can_ip_forward => !@config['src_dst_check'],
      :description => @deploy.deploy_id,
      :service_accounts => [@service_acct],
      :network_interfaces => interfaces,
      :machine_type => "zones/"+@config['availability_zone']+"/machineTypes/"+@config['size'],
      :tags => MU::Cloud::Google.compute(:Tags).new(items: [MU::Cloud::Google.nameStr(@mu_name)])
    }
    desc[:disks] = disks if disks.size > 0

    metadata = {}
    if @config['metadata']
      metadata = Hash[@config['metadata'].map { |m|
        [m["key"], m["value"]]
      }]
    end
    metadata["startup-script"] = @userdata if @userdata and !@userdata.empty?

    deploykey = @config["ssh_user"]+":"+@deploy.ssh_public_key
    if metadata["ssh-keys"]
      metadata["ssh-keys"] += "\n"+deploykey
    else
      metadata["ssh-keys"] = deploykey
    end
    desc[:metadata] = MU::Cloud::Google.compute(:Metadata).new(
      :items => metadata.keys.map { |k|
        MU::Cloud::Google.compute(:Metadata)::Item.new(
          key: k,
          value: metadata[k]
        )
      }
    )

    # Tags in GCP means something other than what we think of;
    # labels are the thing you think you mean
    desc[:labels] = {}
    MU::MommaCat.listStandardTags.each_pair { |name, value|
      if !value.nil?
        desc[:labels][name.downcase] = value.downcase.gsub(/[^a-z0-9\-\_]/i, "_")
      end
    }
    desc[:labels]["name"] = @mu_name.downcase

    if @config['network_tags'] and @config['network_tags'].size > 0
      desc[:tags] = MU::Cloud::Google.compute(:Tags).new(
        items: @config['network_tags']
      )
    end

    instanceobj = MU::Cloud::Google.compute(:Instance).new(desc)

    MU.log "Creating instance #{@mu_name} in #{@project_id} #{@config['availability_zone']}", details: instanceobj

    begin
      instance = MU::Cloud::Google.compute(credentials: @config['credentials']).insert_instance(
        @project_id,
        @config['availability_zone'],
        instanceobj
      )
      if instance and instance.name
        @cloud_id = instance.name
      else
        sleep 10
      end
    rescue ::Google::Apis::ClientError => e
      MU.log e.message+" inserting instance into #{@project_id}/#{@config['availability_zone']}", MU::ERR, details: instanceobj
      raise e
    end while @cloud_id.nil?

    if !@config['async_groom']
      sleep 5
      MU::MommaCat.lock(@cloud_id+"-create")
      if !postBoot
        MU.log "#{@config['name']} is already being groomed, skipping", MU::NOTICE
      else
        MU.log "Node creation complete for #{@config['name']}"
      end
      MU::MommaCat.unlock(@cloud_id+"-create")
    end
    done = false

    @deploy.saveNodeSecret(@cloud_id, @config['instance_secret'], "instance_secret")
    @config.delete("instance_secret")

    if cloud_desc.nil? or cloud_desc.status != "RUNNING"
      raiseert MuError, "#{@cloud_id} appears to have gone sideways mid-bootstrap #{cloud_desc.status if cloud_desc.nil?}"
    end

    notify

  rescue StandardError => e
    if !cloud_desc.nil? and !done
      MU.log "Aborted before I could finish setting up #{@config['name']}, cleaning it up. Stack trace will print once cleanup is complete.", MU::WARN if !@deploy.nocleanup
      MU::MommaCat.unlockAll
      if !@deploy.nocleanup
        parent_thread_id = Thread.current.object_id
        Thread.new {
          MU.dupGlobals(parent_thread_id)
          MU::Cloud::Google::Server.cleanup(noop: false, ignoremaster: false, flags: { "skipsnapshots" => true }, region: @config['region'] )
        }
      end
    end
    raise e
  end

  return @config
end
getSSHConfig() click to toggle source

Figure out what's needed to SSH into this server. @return [Array<String>]: nat_ssh_key, nat_ssh_user, nat_ssh_host, canonical_ip, ssh_user, ssh_key_name, alternate_names

# File modules/mu/providers/google/server.rb, line 488
        def getSSHConfig
          describe(cloud_id: @cloud_id)
# XXX add some awesome alternate names from metadata and make sure they end
# up in MU::MommaCat's ssh config wangling
          return nil if @config.nil? or @deploy.nil?

          nat_ssh_key = nat_ssh_user = nat_ssh_host = nil
          if !@config["vpc"].nil? and !MU::Cloud.resourceClass("Google", "VPC").haveRouteToInstance?(cloud_desc, credentials: @config['credentials'])

            if !@nat.nil?
              if @nat.cloud_desc.nil?
                MU.log "NAT was missing cloud descriptor when called in #{@mu_name}'s getSSHConfig", MU::ERR
                return nil
              end
              _foo, _bar, _baz, nat_ssh_host, nat_ssh_user, nat_ssh_key  = @nat.getSSHConfig
              if nat_ssh_user.nil? and !nat_ssh_host.nil?
                MU.log "#{@config["name"]} (#{MU.deploy_id}) is configured to use #{@config['vpc']} NAT #{nat_ssh_host}, but username isn't specified. Guessing root.", MU::ERR, details: caller
                nat_ssh_user = "root"
              end
            end
          end

          if @config['ssh_user'].nil?
            if windows?
              @config['ssh_user'] = @config['windows_admin_user']
              @config['ssh_user'] ||= "Administrator"
            else
              @config['ssh_user'] = "root"
            end
          end

          return [nat_ssh_key, nat_ssh_user, nat_ssh_host, canonicalIP, @config['ssh_user'], @deploy.ssh_key_name]

        end
getWindowsAdminPassword(use_cache: true) click to toggle source

return [String]: A password string.

# File modules/mu/providers/google/server.rb, line 1021
def getWindowsAdminPassword(use_cache: true)
  @config['windows_auth_vault'] ||= {
    "vault" => @mu_name,
    "item" => "windows_credentials",
    "password_field" => "password"
  }

  if use_cache
    begin
      win_admin_password = @groomer.getSecret(
        vault: @config['windows_auth_vault']['vault'],
        item: @config['windows_auth_vault']['item'],
        field: @config["windows_auth_vault"]["password_field"]
      )
      return win_admin_password if win_admin_password
    rescue MU::Groomer::MuNoSuchSecret, MU::Groomer::RunError
    end
  end

  require 'openssl/oaep'
  timeout = 300

  serial_out = nil
  key = OpenSSL::PKey::RSA.generate 2048

  missing_response = Proc.new {
    !serial_out or !serial_out.contents or serial_out.contents.empty? or JSON.parse(serial_out.contents)["userName"] != @config['windows_admin_username']
  }

  did_metadata = false
  MU.retrier(loop_if: missing_response, wait: 10, max: timeout/10) {
    serial_out = MU::Cloud::Google.compute(credentials: @credentials).get_instance_serial_port_output(@project_id, @config['availability_zone'], @cloud_id, port: 4)

    if missing_response.call and
       !cloud_desc(use_cache: false).metadata.items.map { |i| i.key }.include?("windows-keys")
      keybytes = Base64.decode64(key.public_key.export.gsub(/-----(?:BEGIN|END) PUBLIC KEY-----/, ''))
      modulus = keybytes.byteslice(33,256)
      exponent = keybytes.byteslice(291,3)
      keydata = {
        "userName" => @config['windows_admin_username'],
        "modulus" => Base64.strict_encode64(modulus),
        "exponent" => Base64.strict_encode64(exponent),
        "email" => MU.muCfg['mu_admin_email'],
        "expireOn" => (Time.now.utc+timeout).strftime('%Y-%m-%dT%H:%M:%SZ')
      }

      new_items = cloud_desc.metadata.items.map { |item|
        MU::Cloud::Google.compute(:Metadata)::Item.new(
          key: item.key,
          value: item.value
        )
      }
      new_items.reject! { |item| item.key == "windows-keys" }
      new_items << MU::Cloud::Google.compute(:Metadata)::Item.new(
        key: "windows-keys",
        value: JSON.generate(keydata)
      )
      new_metadata = MU::Cloud::Google.compute(:Metadata).new(
        fingerprint: cloud_desc(use_cache: false).metadata.fingerprint,
        items: new_items
      )

      MU::Cloud::Google.compute(credentials: @credentials).set_instance_metadata(@project_id, @config['availability_zone'], @cloud_id, new_metadata)
    end
  }

  return nil if missing_response.call

  pwdata = JSON.parse(serial_out.contents)
  if pwdata['encryptedPassword'] and pwdata['userName'] == @config['windows_admin_username']
    decrypted_pw = key.private_decrypt_oaep(Base64.strict_decode64(pwdata['encryptedPassword']))
    creds = {
      "username" => @config['windows_admin_username'],
      "password" => decrypted_pw,
      "sshd_username" => "sshd_service",
      "sshd_password" => decrypted_pw
    }
    @groomer.saveSecret(vault: @mu_name, item: "windows_credentials", data: creds, permissions: "name:#{@mu_name}")

    return decrypted_pw
  end

  nil
end
groom() click to toggle source

Called automatically by {MU::Deploy#createResources}

# File modules/mu/providers/google/server.rb, line 791
        def groom
          @project_id = MU::Cloud::Google.projectLookup(@config['project'], @deploy).cloud_id

          MU::MommaCat.lock(@cloud_id+"-groom")
          
          node, _config, deploydata = describe(cloud_id: @cloud_id)

          if node.nil? or node.empty?
            raise MuError, "MU::Cloud::Google::Server.groom was called without a mu_name"
          end

          # Make double sure we don't lose a cached mu_windows_name value.
          if windows? or !@config['active_directory'].nil?
            if @mu_windows_name.nil?
              @mu_windows_name = deploydata['mu_windows_name']
            end
          end

#          punchAdminNAT

          @groomer.saveDeployData

          begin
            @groomer.run(purpose: "Full Initial Run", max_retries: 15)
          rescue MU::Groomer::RunError
            MU.log "Proceeding after failed initial Groomer run, but #{node} may not behave as expected!", MU::WARN
          end

          if !@config['create_image'].nil? and !@config['image_created']
            img_cfg = @config['create_image']
            # Scrub things that don't belong on an AMI
            session = getSSHSession
            sudo = purgecmd = ""
            sudo = "sudo" if @config['ssh_user'] != "root"
            if windows?
              purgecmd = "rm -rf /cygdrive/c/mu_installed_chef"
            else
              purgecmd = "rm -rf /opt/mu_installed_chef"
            end
            if img_cfg['image_then_destroy']
              if windows?
                purgecmd = "rm -rf /cygdrive/c/chef/ /home/#{@config['windows_admin_username']}/.ssh/authorized_keys /home/Administrator/.ssh/authorized_keys /cygdrive/c/mu-installer-ran-updates /cygdrive/c/mu_installed_chef"
                # session.exec!("powershell -Command \"& {(Get-WmiObject -Class Win32_Product -Filter \"Name='UniversalForwarder'\").Uninstall()}\"")
              else
                purgecmd = "#{sudo} rm -rf /root/.ssh/authorized_keys /etc/ssh/ssh_host_*key* /etc/chef /etc/opscode/* /.mu-installer-ran-updates /var/chef /opt/mu_installed_chef /opt/chef ; #{sudo} sed -i 's/^HOSTNAME=.*//' /etc/sysconfig/network"
              end
            end
            session.exec!(purgecmd)
            session.close
            stop
            image_id = MU::Cloud::Google::Server.createImage(
                name: MU::Cloud::Google.nameStr(@mu_name),
                instance_id: @cloud_id,
                region: @config['region'],
                storage: @config['storage'],
                project: @project_id,
                exclude_storage: img_cfg['image_exclude_storage'],
                make_public: img_cfg['public'],
                tags: @tags,
                zone: @config['availability_zone'],
                family: img_cfg['family'],
                credentials: @config['credentials']
            )
            @deploy.notify("images", @config['name'], {"image_id" => image_id})
            @config['image_created'] = true
            if img_cfg['image_then_destroy']
              MU.log "Image #{image_id} ready, removing source node #{node}"
              MU::Cloud::Google.compute(credentials: @config['credentials']).delete_instance(
                @project_id,
                @config['availability_zone'],
                @cloud_id
              )
              destroy
            else
              start
            end
          end

          MU::MommaCat.unlock(@cloud_id+"-groom")
        end
listIPs() click to toggle source

Return all of the IP addresses, public and private, from all of our network interfaces. @return [Array<String>]

# File modules/mu/providers/google/server.rb, line 1007
def listIPs
  ips = []
  cloud_desc.network_interfaces.each { |iface|
    ips << iface.network_ip
    if iface.access_configs
      iface.access_configs.each { |acfg|
        ips << acfg.nat_ip if acfg.nat_ip
      }
    end
  }
  ips
end
notify() click to toggle source

Return a description of this resource appropriate for deployment metadata. Arguments reflect the return values of the MU::Cloud::.describe method

# File modules/mu/providers/google/server.rb, line 735
        def notify
          if cloud_desc.nil?
            raise MuError, "Failed to load instance metadata for #{@config['mu_name']}/#{@cloud_id}"
          end

          interfaces = []
          private_ips = []
          public_ips = []

          cloud_desc.network_interfaces.each { |iface|
            private_ips << iface.network_ip
            if iface.access_configs
              iface.access_configs.each { |acfg|
                public_ips << acfg.nat_ip if acfg.nat_ip
              }
            end
            interfaces << {
              "network_interface_id" => iface.name,
              "subnet_id" => iface.subnetwork,
              "vpc_id" => iface.network
            }
          }

          deploydata = {
              "nodename" => @mu_name,
              "run_list" => @config['run_list'],
              "image_created" => @config['image_created'],
#              "iam_role" => @config['iam_role'],
              "cloud_desc_id" => @cloud_id,
              "project_id" => @project_id,
              "private_ip_address" => private_ips.first,
              "public_ip_address" => public_ips.first,
              "private_ip_list" => private_ips,
#              "key_name" => cloud_desc.key_name,
#              "subnet_id" => cloud_desc.subnet_id,
#              "cloud_desc_type" => cloud_desc.instance_type #,
              #                         "network_interfaces" => interfaces,
              #                         "config" => server
          }

          if !@mu_windows_name.nil?
            deploydata["mu_windows_name"] = @mu_windows_name
          end
          if !@config['chef_data'].nil?
            deploydata.merge!(@config['chef_data'])
          end
          deploydata["region"] = @config['region'] if !@config['region'].nil?
          if !@named
            MU::MommaCat.nameKitten(self)
            @named = true
          end

          return deploydata
        end
postBoot(instance_id = nil) click to toggle source

Apply tags, bootstrap our configuration management, and other administravia for a new instance.

# File modules/mu/providers/google/server.rb, line 525
        def postBoot(instance_id = nil)
          if !instance_id.nil?
            @cloud_id = instance_id
          end

          node, _config, deploydata = describe(cloud_id: @cloud_id)
          instance = cloud_desc
          raise MuError, "Couldn't find instance of #{@mu_name} (#{@cloud_id})" if !instance
          return false if !MU::MommaCat.lock(@cloud_id+"-orchestrate", true)
          return false if !MU::MommaCat.lock(@cloud_id+"-groom", true)

#          MU::Cloud::AWS.createStandardTags(@cloud_id, region: @config['region'])
#          MU::Cloud::AWS.createTag(@cloud_id, "Name", node, region: @config['region'])
#
#          if @config['optional_tags']
#            MU::MommaCat.listOptionalTags.each { |key, value|
#              MU::Cloud::AWS.createTag(@cloud_id, key, value, region: @config['region'])
#            }
#          end
#
#          if !@config['tags'].nil?
#            @config['tags'].each { |tag|
#              MU::Cloud::AWS.createTag(@cloud_id, tag['key'], tag['value'], region: @config['region'])
#            }
#          end
#          MU.log "Tagged #{node} (#{@cloud_id}) with MU-ID=#{MU.deploy_id}", MU::DEBUG
#
          # Make double sure we don't lose a cached mu_windows_name value.
          if windows? or !@config['active_directory'].nil?
            if @mu_windows_name.nil?
              @mu_windows_name = deploydata['mu_windows_name']
            end
          end

#          punchAdminNAT
#
#
#          # If we came up via AutoScale, the Alarm module won't have had our
#          # instance ID to associate us with itself. So invoke that here.
#          if !@config['basis'].nil? and @config["alarms"] and !@config["alarms"].empty?
#            @config["alarms"].each { |alarm|
#              alarm_obj = MU::MommaCat.findStray(
#                "AWS",
#                "alarms",
#                region: @config["region"],
#                deploy_id: @deploy.deploy_id,
#                name: alarm['name']
#              ).first
#              alarm["dimensions"] = [{:name => "InstanceId", :value => @cloud_id}]
#
#              if alarm["enable_notifications"]
#                topic_arn = MU::Cloud::AWS::Notification.createTopic(alarm["notification_group"], region: @config["region"])
#                MU::Cloud::AWS::Notification.subscribe(arn: topic_arn, protocol: alarm["notification_type"], endpoint: alarm["notification_endpoint"], region: @config["region"])
#                alarm["alarm_actions"] = [topic_arn]
#                alarm["ok_actions"]  = [topic_arn]
#              end
#
#              alarm_name = alarm_obj ? alarm_obj.cloud_id : "#{node}-#{alarm['name']}".upcase
#
#              MU::Cloud::AWS::Alarm.setAlarm(
#                name: alarm_name,
#                ok_actions: alarm["ok_actions"],
#                alarm_actions: alarm["alarm_actions"],
#                insufficient_data_actions: alarm["no_data_actions"],
#                metric_name: alarm["metric_name"],
#                namespace: alarm["namespace"],
#                statistic: alarm["statistic"],
#                dimensions: alarm["dimensions"],
#                period: alarm["period"],
#                unit: alarm["unit"],
#                evaluation_periods: alarm["evaluation_periods"],
#                threshold: alarm["threshold"],
#                comparison_operator: alarm["comparison_operator"],
#                region: @config["region"]
#              )
#            }
#          end
#
#          # We have issues sometimes where our dns_records are pointing at the wrong node name and IP address.
#          # Make sure that doesn't happen. Happens with server pools only
#          if @config['dns_records'] && !@config['dns_records'].empty?
#            @config['dns_records'].each { |dnsrec|
#              if dnsrec.has_key?("name")
#                if dnsrec['name'].start_with?(MU.deploy_id.downcase) && !dnsrec['name'].start_with?(node.downcase)
#                  MU.log "DNS records for #{node} seem to be wrong, deleting from current config", MU::WARN, details: dnsrec
#                  dnsrec.delete('name')
#                  dnsrec.delete('target')
#                end
#              end
#            }
#          end

          # Unless we're planning on associating a different IP later, set up a
          # DNS entry for this thing and let it sync in the background. We'll
          # come back to it later.
          if @config['static_ip'].nil? && !@named
            MU::MommaCat.nameKitten(self)
            @named = true
          end

          _nat_ssh_key, _nat_ssh_user, nat_ssh_host, _canonical_ip, _ssh_user, _ssh_key_name = getSSHConfig
          if !nat_ssh_host and !MU::Cloud.resourceClass("Google", "VPC").haveRouteToInstance?(cloud_desc, credentials: @config['credentials'])
# XXX check if canonical_ip is in the private ranges
#            raise MuError, "#{node} has no NAT host configured, and I have no other route to it"
          end

          # See if this node already exists in our config management. If it does,
          # we're done.
          if @groomer.haveBootstrapped?
            MU.log "Node #{node} has already been bootstrapped, skipping groomer setup.", MU::NOTICE
            @groomer.saveDeployData
            MU::MommaCat.unlock(@cloud_id+"-orchestrate")
            MU::MommaCat.unlock(@cloud_id+"-groom")
            return true
          end

          @groomer.bootstrap

          # Make sure we got our name written everywhere applicable
          if !@named
            MU::MommaCat.nameKitten(self)
            @named = true
          end

          MU::MommaCat.unlock(@cloud_id+"-groom")
          MU::MommaCat.unlock(@cloud_id+"-orchestrate")
          return true
        end
reboot(_hard = false) click to toggle source

Ask the Google API to restart this node @param _hard [Boolean]: [IGNORED] Force a stop/start. This is the only available way to restart an instance in Google, so this flag is ignored.

# File modules/mu/providers/google/server.rb, line 480
def reboot(_hard = false)
  return if @cloud_id.nil?
  stop
  start
end
start() click to toggle source

Ask the Google API to start this node

# File modules/mu/providers/google/server.rb, line 466
def start
  MU.log "Starting #{@cloud_id}"
  MU::Cloud::Google.compute(credentials: @config['credentials']).start_instance(
    @project_id,
    @config['availability_zone'],
    @cloud_id
  )
  begin
    sleep 5
  end while cloud_desc.status != "RUNNING"
end
stop() click to toggle source

Ask the Google API to stop this node

# File modules/mu/providers/google/server.rb, line 453
def stop
  MU.log "Stopping #{@cloud_id}"
  MU::Cloud::Google.compute(credentials: @config['credentials']).stop_instance(
    @project_id,
    @config['availability_zone'],
    @cloud_id
  )
  begin
    sleep 5
  end while cloud_desc(use_cache: false).status != "TERMINATED" # means STOPPED
end
toKitten(**_args) click to toggle source

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.

# File modules/mu/providers/google/server.rb, line 1171
def toKitten(**_args)
  bok = {
    "cloud" => "Google",
    "credentials" => @config['credentials'],
    "cloud_id" => @cloud_id,
    "project" => @project_id
  }
  if !cloud_desc
    MU.log "toKitten failed to load a cloud_desc from #{@cloud_id}", MU::ERR, details: @config
    return nil
  end
  bok['name'] = cloud_desc.name

  # XXX we can have multiple network interfaces, and often do; need
  # language to account for this
  iface = cloud_desc.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_id: iface.subnetwork.sub(/.*?\/([^\/]+)$/, '\1')
  )

  cloud_desc.disks.each { |disk|
    next if !disk.source
    disk.source.match(/\/projects\/([^\/]+)\/zones\/([^\/]+)\/disks\/(.*)/)
    proj = Regexp.last_match[1]
    az = Regexp.last_match[2]
    name = Regexp.last_match[3]
    begin
      disk_desc = MU::Cloud::Google.compute(credentials: @credentials).get_disk(proj, az, name)
      if disk_desc.source_image and disk.boot
        bok['image_id'] ||= disk_desc.source_image.sub(/^https:\/\/www\.googleapis\.com\/compute\/[^\/]+\//, '')
      else
        bok['storage'] ||= []
        storage_blob = {
          "size" => disk_desc.size_gb,
          "device" => "/dev/xvd"+(disk.index+97).chr.downcase
        }
        bok['storage'] <<  storage_blob
      end
    rescue ::Google::Apis::ClientError => e
      MU.log "Failed to retrieve disk #{name} attached to server #{@cloud_id} in #{proj}/#{az}", MU::WARN, details: e.message
      next
    end
    
  }

  if cloud_desc.labels
    bok['tags'] = cloud_desc.labels.keys.map { |k| { "key" => k, "value" => cloud_desc.labels[k] } }
  end
  if cloud_desc.tags and cloud_desc.tags.items and cloud_desc.tags.items.size > 0
    bok['network_tags'] = cloud_desc.tags.items
  end
  bok['src_dst_check'] = !cloud_desc.can_ip_forward
  bok['size'] = cloud_desc.machine_type.sub(/.*?\/([^\/]+)$/, '\1')
  bok['project'] = @project_id
  if cloud_desc.service_accounts
    bok['scopes'] = cloud_desc.service_accounts.map { |sa| sa.scopes }.flatten.uniq
  end
  if cloud_desc.metadata and cloud_desc.metadata.items
    bok['metadata'] = cloud_desc.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['image_id'].match(/(:?^|\/)projects\/gke-node-images\//)
    found_gke_tag = false
    bok['network_tags'].each { |tag|
      if tag.match(/^gke-/)
        found_gke_tag = true
        break
      end
    }
    if found_gke_tag
      MU.log "Server #{bok['name']} appears to belong to a ContainerCluster, skipping adoption", MU::DEBUG
      return nil
    end
  end

  if bok['metadata']
    bok['metadata'].each { |item|
      if item[:key] == "created-by" and item[:value].match(/\/instanceGroupManagers\//)
        MU.log "Server #{bok['name']} appears to belong to a ServerPool, skipping adoption", MU::DEBUG, details: item[:value]
        return nil
      end
    }
  end


  bok
end