class OpenStudioAwsWrapper

Constants

VALID_OPTIONS

Attributes

group_uuid[R]
key_pair_name[R]
private_key_file_name[RW]
proxy[R]
security_groups[RW]
server[R]
worker_keys[R]
workers[R]

Public Class Methods

new(options = {}, group_uuid = nil) click to toggle source
# File lib/openstudio/lib/openstudio_aws_wrapper.rb, line 53
def initialize(options = {}, group_uuid = nil)
  @group_uuid = group_uuid || SecureRandom.uuid.delete('-')

  @security_groups = []
  @key_pair_name = nil
  @private_key_file_name = nil
  @region = options[:region] || 'unknown-region'

  # If the keys exist in the directory then load those, otherwise create new ones.
  @work_dir = File.expand_path options[:save_directory]
  if File.exist?(File.join(@work_dir, 'ec2_worker_key.pem')) && File.exist?(File.join(@work_dir, 'ec2_worker_key.pub'))
    logger.info "Worker keys already exist, loading from #{@work_dir}"
    load_worker_key(File.join(@work_dir, 'ec2_worker_key.pem'))
  else
    logger.info 'Generating new worker keys'
    @worker_keys = SSHKey.generate
  end

  @private_key = nil # Private key data

  # List of instances
  @server = nil
  @workers = []

  # store an instance variable with the proxy for passing to instances for use in scp/ssh
  @proxy = options[:proxy] || nil

  # need to remove the prxoy information here
  @aws = Aws::EC2::Client.new(options[:credentials])
end

Public Instance Methods

calculate_processors(total_procs) click to toggle source

Calculate the number of processors for the server and workers. This is used to scale the docker stack appropriately. @param total_procs [int] Total number of processors that are available

# File lib/openstudio/lib/openstudio_aws_wrapper.rb, line 87
def calculate_processors(total_procs)
  max_requests = ((total_procs + 10) * 1.2).round
  mongo_cores = (total_procs / 64.0).ceil
  web_cores = (total_procs / 32.0).ceil
  max_pool = 16 * web_cores
  rez_mem = 512 * max_pool
  # what is this +2 doing here
  total_procs = total_procs - mongo_cores - web_cores + 2

  [total_procs, max_requests, mongo_cores, web_cores, max_pool, rez_mem]
end
configure_server_and_workers() click to toggle source

blocking method that waits for servers and workers to be fully configured (i.e. execution of user_data has occured on all nodes). Ideally none of these methods would ever need to exist.

@return [Boolean] Will return true unless an exception is raised

# File lib/openstudio/lib/openstudio_aws_wrapper.rb, line 464
def configure_server_and_workers
  logger.info('waiting for server user_data to complete')
  @server.wait_command('[ -e /home/ubuntu/user_data_done ] && echo "true"')
  logger.info('waiting for worker user_data to complete')
  @workers.each { |worker| worker.wait_command('[ -e /home/ubuntu/user_data_done ] && echo "true"') }

  ips = "master|#{@server.data.private_ip_address}|#{@server.data.dns}|#{@server.data.procs}|ubuntu|ubuntu|true\n"
  @workers.each { |worker| ips << "worker|#{worker.data.private_ip_address}|#{worker.data.dns}|#{worker.data.procs}|ubuntu|ubuntu|true\n" }
  file = Tempfile.new('ip_addresses')
  file.write(ips)
  file.close
  @server.upload_file(file.path, 'ip_addresses')
  file.unlink
  logger.info("ips #{ips}")
  @server.shell_command('chmod 664 /home/ubuntu/ip_addresses')

  mongoid = File.read(__dir__ + '/mongoid.yml.template')
  mongoid.gsub!(/SERVER_IP/, @server.data.private_ip_address)
  file = Tempfile.new('mongoid.yml')
  file.write(mongoid)
  file.close
  @server.upload_file(file.path, '/mnt/openstudio/rails-models/mongoid.yml')
  @workers.each { |worker| worker.upload_file(file.path, '/mnt/openstudio/rails-models/mongoid.yml') }
  file.unlink

  @server.shell_command('chmod 664 /mnt/openstudio/rails-models/mongoid.yml')
  @workers.each { |worker| worker.shell_command('chmod 664 /mnt/openstudio/rails-models/mongoid.yml') }

  true
end
configure_swarm_cluster(save_directory) click to toggle source

blocking method that executes required commands for creating and provisioning a docker swarm cluster

# File lib/openstudio/lib/openstudio_aws_wrapper.rb, line 496
def configure_swarm_cluster(save_directory)
  logger.info('waiting for server user_data to complete')
  @server.wait_command('while ! [ -e /home/ubuntu/user_data_done ]; do sleep 5; done && echo "true"')
  logger.info('Running the configuration script for the server.')
  @server.wait_command('echo $(env) &> /home/ubuntu/env.log && echo "true"')
  @server.wait_command('cp /home/ubuntu/server_provision.sh /home/ubuntu/server_provision.sh.bak && echo "true"')
  @server.wait_command('sudo /home/ubuntu/server_provision.sh &> /home/ubuntu/server_provision.log && echo "true"')
  logger.info('Downloading the swarm join command.')
  swarm_file = File.join(save_directory, 'worker_swarm_join.sh')
  @server.download_file('/home/ubuntu/swarmjoin.sh', swarm_file)
  logger.info('waiting for worker user_data to complete')
  @workers.each { |worker| worker.wait_command('while ! [ -e /home/ubuntu/user_data_done ]; do sleep 5; done && echo "true"') }
  logger.info('Running the configuration script for the worker(s).')
  @workers.each { |worker| worker.wait_command('sudo /home/ubuntu/worker_provision.sh &> /home/ubuntu/worker_provision.log && echo "true"') }
  logger.info('Successfully re-sized storage devices for all nodes. Joining server nodes to the swarm.')
  worker_join_cmd = "#{File.read(swarm_file).strip} && echo \"true\""
  @workers.each { |worker| worker.wait_command(worker_join_cmd) }
  logger.info('All worker nodes have been added to the swarm. Setting environment variables and starting the cluster')
  # e.g. 356 CPUs
  # mongo cores = 6
  # web cores = 12
  # total procs = 340 (but should be 336)
  total_procs = @server.procs
  @workers.each { |worker| total_procs += worker.procs }
  total_procs, max_requests, mongo_cores, web_cores, max_pool, rez_mem = calculate_processors(total_procs)
  logger.info('Processors allocations are:')
  logger.info("   total_procs: #{total_procs}")
  logger.info("   max_requests: #{max_requests}")
  logger.info("   mongo_cores: #{mongo_cores}")
  logger.info("   web_cores: #{web_cores}")
  logger.info("   max_pool: #{max_pool}")
  logger.info("   rez_mem: #{rez_mem}")
  @server.shell_command("sed -i -e 's/AWS_MAX_REQUESTS/#{max_requests}/g' /home/ubuntu/docker-compose.yml && echo \"true\"")
  @server.shell_command("sed -i -e 's/AWS_MONGO_CORES/#{mongo_cores}/g' /home/ubuntu/docker-compose.yml && echo \"true\"")
  @server.shell_command("sed -i -e 's/AWS_WEB_CORES/#{web_cores}/g' /home/ubuntu/docker-compose.yml && echo \"true\"")
  @server.shell_command("sed -i -e 's/AWS_MAX_POOL/#{max_pool}/g' /home/ubuntu/docker-compose.yml && echo \"true\"")
  @server.shell_command("sed -i -e 's/AWS_REZ_MEM/#{rez_mem}/g' /home/ubuntu/docker-compose.yml && echo \"true\"")
  @server.shell_command("sed -i -e 's/AWS_OS_SERVER_NUMBER_OF_WORKERS/#{total_procs}/g' /home/ubuntu/docker-compose.yml && echo \"true\"")
  @server.shell_command("echo '' >> /home/ubuntu/.env && echo \"true\"")
  @server.shell_command('docker stack deploy --compose-file docker-compose.yml osserver && echo "true"')
  sleep 10
  logger.info('The OpenStudio Server stack has been started. Waiting for the server to become available.')
  @server.wait_command("while ( nc -zv #{@server.ip} 80 3>&1 1>&2- 2>&3- ) | awk -F \":\" '$3 != \" Connection refused\" {exit 1}'; do sleep 5; done && echo \"true\"")
  logger.info('The OpenStudio Server stack has become available. Scaling the worker nodes.')
  @server.wait_command("docker service scale osserver_worker=#{total_procs} && echo \"true\"")
  logger.info('Waiting up to two minutes for the osserver_worker service to scale.')
  @server.wait_command("timeout 120 bash -c -- 'while [ $( docker service ls -f name=osserver_worker --format=\"{{.Replicas}}\" ) != \"#{total_procs}/#{total_procs}\" ]; do sleep 5; done'; echo \"true\"")
  logger.info('The OpenStudio Server stack is booted and ready for analysis submissions.')
end
create_new_ami_json(version = 1) click to toggle source

method to hit the existing list of available amis and compare to the list of AMIs on Amazon and then generate the new ami list

# File lib/openstudio/lib/openstudio_aws_wrapper.rb, line 615
def create_new_ami_json(version = 1)
  # get the list of existing amis from developer.nrel.gov
  existing_amis = OpenStudioAmis.new(1).list

  # list of available AMIs from AWS
  available_amis = describe_amis

  amis = transform_ami_lists(existing_amis, available_amis)

  if version == 1
    version1 = {}

    # now grab the good keys - they should be sorted newest to older... so go backwards
    amis[:openstudio_server].keys.reverse_each do |key|
      a = amis[:openstudio_server][key]
      # this will override any of the old ami/os version
      version1[a[:openstudio_version].to_sym] = a[:amis]
    end

    # create the default version. First sort, then grab the first hash's values
    version1.sort_by
    default_v = nil
    version1 = Hash[version1.sort_by { |k, _| k.to_s.to_version }.reverse]
    default_v = version1.keys[0]

    version1[:default] = version1[default_v]
    amis = version1
  elsif version == 2
    # don't need to transform anything right now, only flag which ones are stable version so that the uploaded ami JSON has the
    # stable server for OpenStudio PAT to use.
    stable = JSON.parse File.read(File.join(File.dirname(__FILE__), 'ami_stable_version.json')), symbolize_names: true

    # go through and tag the versions of the openstudio instances that are stable,
    stable[:openstudio].each do |k, v|
      if amis[:openstudio][k.to_s] && amis[:openstudio][k.to_s][v.to_sym]
        amis[:openstudio][k.to_s][:stable] = v
      end
    end

    # I'm not sure what the below code is trying to accomplish. Are we even using the default?
    k, v = stable[:openstudio].first
    if k && v
      if amis[:openstudio][k.to_s]
        amis[:openstudio][:default] = v
      end
    end

    # now go through and if the OpenStudio version does not have a stable key, then assign it the most recent
    # stable AMI. This allows for easy testing so a new version of OpenStudio can use an existing AMI.
    stable[:openstudio].each do |stable_openstudio, stable_server|
      amis[:openstudio].each do |k, v|
        next if k == :default

        if k.to_s.to_version > stable_openstudio.to_s.to_version && v[:stable].nil?
          amis[:openstudio][k.to_s][:stable] = stable_server.to_s
        end
      end
    end
  end

  amis
end
create_or_retrieve_default_security_group(tmp_name = 'openstudio-server-sg-v2.2', vpc_id = nil) click to toggle source
# File lib/openstudio/lib/openstudio_aws_wrapper.rb, line 99
def create_or_retrieve_default_security_group(tmp_name = 'openstudio-server-sg-v2.2', vpc_id = nil)
  group = @aws.describe_security_groups(filters: [{ name: 'group-name', values: [tmp_name] }])
  logger.info "Length of the security group is: #{group.data.security_groups.length}"
  if group.data.security_groups.empty?
    logger.info 'security group not found --- will create a new one'
    if vpc_id
      r = @aws.create_security_group(
        group_name: tmp_name, description: "group dynamically created by #{__FILE__}", vpc_id: vpc_id
      )
    else
      r = @aws.create_security_group(group_name: tmp_name, description: "group dynamically created by #{__FILE__}")
    end
    group_id = r[:group_id]
    @aws.authorize_security_group_ingress(
      group_id: group_id,
      ip_permissions: [
        { ip_protocol: 'tcp', from_port: 22, to_port: 22, ip_ranges: [cidr_ip: '0.0.0.0/0'] }, # Eventually make this only the user's IP address seen by the internet
        { ip_protocol: 'tcp', from_port: 80, to_port: 80, ip_ranges: [cidr_ip: '0.0.0.0/0'] },
        { ip_protocol: 'tcp', from_port: 443, to_port: 443, ip_ranges: [cidr_ip: '0.0.0.0/0'] },
        { ip_protocol: 'tcp', from_port: 0, to_port: 65535, user_id_group_pairs: [{ group_name: tmp_name }] }, # allow all machines in the security group talk to each other openly
        { ip_protocol: 'udp', from_port: 0, to_port: 65535, user_id_group_pairs: [{ group_name: tmp_name }] }, # allow all machines in the security group talk to each other openly
        { ip_protocol: 'icmp', from_port: -1, to_port: -1, ip_ranges: [cidr_ip: '0.0.0.0/0'] }
      ]
    )

    # reload group information
    group = @aws.describe_security_groups(filters: [{ name: 'group-name', values: [tmp_name] }])
  else
    logger.info 'Found existing security group'
  end

  @security_groups = [group.data.security_groups.first.group_id]
  logger.info("server_group #{group.data.security_groups.first.group_name}:#{group.data.security_groups.first.group_id}")

  group.data.security_groups.first
end
create_or_retrieve_key_pair(key_pair_name = nil) click to toggle source
# File lib/openstudio/lib/openstudio_aws_wrapper.rb, line 301
def create_or_retrieve_key_pair(key_pair_name = nil)
  tmp_name = key_pair_name || "os-key-pair-#{@group_uuid}"

  # the describe_key_pairs method will raise an expectation if it can't find the key pair, so catch it
  resp = nil
  begin
    resp = @aws.describe_key_pairs(key_names: [tmp_name]).data
    raise 'looks like there are 2 key pairs with the same name' if resp.key_pairs.size >= 2
  rescue StandardError
    logger.info "could not find key pair '#{tmp_name}'"
  end

  if resp.nil? || resp.key_pairs.empty?
    # create the new key_pair
    # check if the key pair name exists
    # create a new key pair everytime
    keypair = @aws.create_key_pair(key_name: tmp_name)

    # save the private key to memory (which can later be persisted via the save_private_key method)
    @private_key = keypair.data.key_material
    @key_pair_name = keypair.data.key_name
  else
    logger.info "found existing keypair #{resp.key_pairs.first}"
    @key_pair_name = resp.key_pairs.first[:key_name]

    # This will not set the private key because it doesn't live on the remote system
  end

  logger.info("create key pair: #{@key_pair_name}")
end
delete_key_pair(key_pair_name = nil) click to toggle source

Delete the key pair from aws

# File lib/openstudio/lib/openstudio_aws_wrapper.rb, line 333
def delete_key_pair(key_pair_name = nil)
  tmp_name = key_pair_name || "os-key-pair-#{@group_uuid}"
  resp = nil
  begin
    logger.info "Trying to delete key pair #{tmp_name}"
    resp = @aws.delete_key_pair(key_name: tmp_name)
  rescue StandardError
    logger.info "could not delete the key pair '#{tmp_name}'"
  end

  resp
end
describe_all_instances() click to toggle source
# File lib/openstudio/lib/openstudio_aws_wrapper.rb, line 158
def describe_all_instances
  resp = @aws.describe_instance_status

  resp
end
describe_amis(image_ids = [], owned_by_me = true) click to toggle source

Describe the list of AMIs adn return the hash. @param [Array] image_ids: List of image ids to find. If empty, then will find all images. @param [Boolean] owned_by_me: Find only the images owned by the current user? @return [Hash]

# File lib/openstudio/lib/openstudio_aws_wrapper.rb, line 245
def describe_amis(image_ids = [], owned_by_me = true)
  resp = nil

  if owned_by_me
    resp = @aws.describe_images(owners: [:self]).data
  else
    resp = @aws.describe_images(image_ids: image_ids).data
  end

  resp = resp.to_hash

  # map the tags to hashes
  resp[:images].each do |image|
    image[:tags_hash] = {}
    image[:tags_hash][:tags] = []

    # If the image is being created then its tags may be empty
    if image[:tags]
      image[:tags].each do |tag|
        if tag[:value]
          image[:tags_hash][tag[:key].to_sym] = tag[:value]
        else
          image[:tags_hash][:tags] << tag[:key]
        end
      end
    end
  end

  resp
end
describe_availability_zones() click to toggle source
# File lib/openstudio/lib/openstudio_aws_wrapper.rb, line 136
def describe_availability_zones
  resp = @aws.describe_availability_zones
  map = []
  resp.data.availability_zones.each do |zn|
    map << zn.to_hash
  end

  { availability_zone_info: map }
end
describe_availability_zones_json() click to toggle source
# File lib/openstudio/lib/openstudio_aws_wrapper.rb, line 146
def describe_availability_zones_json
  describe_availability_zones.to_json
end
describe_instances() click to toggle source

describe the instances by group id (this is the default method)

# File lib/openstudio/lib/openstudio_aws_wrapper.rb, line 165
def describe_instances
  resp = nil
  if group_uuid
    resp = @aws.describe_instances(
      filters: [
        # {name: 'instance-state-code', values: [0.to_s, 16.to_s]}, # running or pending -- any state
        { name: 'tag-key', values: ['GroupUUID'] },
        { name: 'tag-value', values: [group_uuid.to_s] }
      ]
    )
  else
    resp = @aws.describe_instances
  end

  # Any additional filters
  instance_data = nil
  if resp
    instance_data = []
    resp.reservations.each do |r|
      r.instances.each do |i|
        i_h = i.to_hash
        if i_h[:tags].any? { |h| (h[:key] == 'GroupUUID') && (h[:value] == group_uuid.to_s) }
          instance_data << i_h
        end
      end
    end
  end

  instance_data
end
describe_running_instances(group_uuid = nil, openstudio_instance_type = nil) click to toggle source

return all of the running instances, or filter by the group_uuid & instance type

# File lib/openstudio/lib/openstudio_aws_wrapper.rb, line 197
def describe_running_instances(group_uuid = nil, openstudio_instance_type = nil)
  resp = nil
  if group_uuid
    resp = @aws.describe_instances(
      filters: [
        { name: 'instance-state-code', values: [0.to_s, 16.to_s] }, # running or pending
        { name: 'tag-key', values: ['GroupUUID'] },
        { name: 'tag-value', values: [group_uuid.to_s] }
      ]
    )
  else
    resp = @aws.describe_instances
  end

  instance_data = nil
  if resp
    instance_data = []
    resp.reservations.each do |r|
      r.instances.each do |i|
        i_h = i.to_hash
        if group_uuid && openstudio_instance_type
          # {:key=>"Purpose", :value=>"OpenStudioWorker"}
          if i_h[:tags].any? { |h| (h[:key] == 'Purpose') && (h[:value] == "OpenStudio#{openstudio_instance_type.capitalize}") } &&
             i_h[:tags].any? { |h| (h[:key] == 'GroupUUID') && (h[:value] == group_uuid.to_s) }
            instance_data << i_h
          end
        elsif group_uuid
          if i_h[:tags].any? { |h| (h[:key] == 'GroupUUID') && (h[:value] == group_uuid.to_s) }
            instance_data << i_h
          end
        elsif openstudio_instance_type
          if i_h[:tags].any? { |h| (h[:key] == 'Purpose') && (h[:value] == "OpenStudio#{openstudio_instance_type.capitalize}") }
            instance_data << i_h
          end
        else
          instance_data << i_h
        end
      end
    end
  end

  instance_data
end
find_server(server_data_hash) click to toggle source

method to query the amazon api to find the server (if it exists), based on the group id if it is found, then it will set the @server instance variable. The security groups are assigned from the server node information on AWS if the security groups have not been initialized yet.

Note that the information around keys and security groups is pulled from the instance information. @param server_data_hash [Hash] Server data @option server_data_hash [String] :group_id Group ID of the analysis @option server_data_hash [String] :server.private_key_file_name Name of the private key to communicate to the server

# File lib/openstudio/lib/openstudio_aws_wrapper.rb, line 554
def find_server(server_data_hash)
  @group_uuid = server_data_hash[:group_id] || @group_uuid
  load_private_key(server_data_hash[:server][:private_key_file_name])

  logger.info "Finding the server for GroupUUID of #{group_uuid}"
  raise 'no GroupUUID defined either in member variable or method argument' if @group_uuid.nil?

  # This should really just be a single call to describe running instances
  @server = nil
  resp = describe_running_instances(group_uuid, :server)
  if resp
    raise "more than one server running with group uuid of #{group_uuid} found, expecting only one" if resp.size > 1

    resp = resp.first
    if !@server
      if resp
        logger.info "Server found and loading data into object [instance id is #{resp[:instance_id]}]"

        sg = resp[:security_groups].map { |s| s[:group_id] }
        # Set the security groups of the object if these groups haven't been assigned yet.
        @security_groups = sg if @security_groups.empty?
        logger.info "The security groups in aws wrapper are #{@security_groups}"

        # set the key name from AWS if it isn't yet assigned
        logger.info 'Setting the keyname in the aws wrapper'
        @key_pair_name ||= resp[:key_name]

        @server = OpenStudioAwsInstance.new(@aws, :server, @key_pair_name, sg, @group_uuid, @private_key, @private_key_file_name, @proxy)

        @server.load_instance_data(resp)
      end
    else
      logger.info "Server instance is already defined with instance #{resp[:instance_id]}"
    end
  else
    logger.info 'could not find a running server instance'
  end

  # Find the worker instances.
  if @workers.empty?
    resp = describe_running_instances(group_uuid, :worker)
    if resp
      resp.each do |r|
        @workers << OpenStudioAwsInstance.new(@aws, :worker, r[:key_name], r[:security_groups].map { |s| s[:group_id] }, @group_uuid, @private_key, @private_key_file_name, @proxy)
        @workers.last.load_instance_data(r)
      end
    end
  else
    logger.info 'Worker nodes are already defined'
  end

  # set the private key from the hash
  load_private_key server_data_hash[:server][:private_key_file_name]
  load_worker_key server_data_hash[:server][:worker_private_key_file_name]

  # Really don't need to return anything because this sets the class instance variable
  @server
end
get_next_version(base, list_of_svs) click to toggle source

take the base version and increment the patch until. TODO: DEPRECATE

# File lib/openstudio/lib/openstudio_aws_wrapper.rb, line 699
def get_next_version(base, list_of_svs)
  b = base.to_version

  # first go through the array and test that there isn't any other versions > that in the array
  list_of_svs.each do |v|
    b = v.to_version if v.to_version.satisfies("#{b.major}.#{b.minor}.*") && v.to_version.patch > b.patch
  end

  # get the max version in the list_of_svs
  b.patch += 1 while list_of_svs.include?(b.to_s)

  # return the value back as a string
  b.to_s
end
launch_server(image_id, instance_type, launch_options = {}) click to toggle source
# File lib/openstudio/lib/openstudio_aws_wrapper.rb, line 398
def launch_server(image_id, instance_type, launch_options = {})
  defaults = {
    user_id: 'unknown_user',
    tags: [],
    ebs_volume_size: nil,
    user_data_file: 'server_script.sh.template'
  }
  launch_options = defaults.merge(launch_options)

  # replace the server_script.sh.template with the keys to add

  user_data = File.read(File.join(__dir__, launch_options[:user_data_file]))
  user_data.gsub!(/SERVER_HOSTNAME/, 'openstudio.server')
  user_data.gsub!(/WORKER_PRIVATE_KEY_TEMPLATE/, worker_keys.private_key.gsub("\n", '\\n'))
  user_data.gsub!(/WORKER_PUBLIC_KEY_TEMPLATE/, worker_keys.ssh_public_key)

  @server = OpenStudioAwsInstance.new(@aws, :server, @key_pair_name, @security_groups, @group_uuid, @private_key,
                                      @private_key_file_name, @proxy)

  # TODO: create the EBS volumes instead of the ephemeral storage - needed especially for the m3 instances (SSD)

  raise 'image_id is nil' unless image_id
  raise 'instance type is nil' unless instance_type

  @server.launch_instance(image_id, instance_type, user_data, launch_options[:user_id], launch_options)
end
launch_workers(image_id, instance_type, num, launch_options = {}) click to toggle source
# File lib/openstudio/lib/openstudio_aws_wrapper.rb, line 425
def launch_workers(image_id, instance_type, num, launch_options = {})
  defaults = {
    user_id: 'unknown_user',
    tags: [],
    ebs_volume_size: nil,
    availability_zone: @server.data.availability_zone,
    user_data_file: 'worker_script.sh.template'
  }
  launch_options = defaults.merge(launch_options)

  user_data = File.read(File.join(__dir__, launch_options[:user_data_file]))
  user_data.gsub!(/SERVER_IP/, @server.data.private_ip_address)
  user_data.gsub!(/SERVER_HOSTNAME/, 'openstudio.server')
  user_data.gsub!(/WORKER_PUBLIC_KEY_TEMPLATE/, worker_keys.ssh_public_key)
  logger.info("worker user_data #{user_data.inspect}")

  # thread the launching of the workers
  num.times do
    @workers << OpenStudioAwsInstance.new(@aws, :worker, @key_pair_name, @security_groups, @group_uuid,
                                          @private_key, @private_key_file_name, @proxy)
  end

  threads = []
  @workers.each do |worker|
    threads << Thread.new do
      # create the EBS volumes instead of the ephemeral storage - needed especially for the m3 instances (SSD)
      worker.launch_instance(image_id, instance_type, user_data, launch_options[:user_id], launch_options)
    end
  end
  threads.each(&:join)

  # TODO: do we need to have a flag if the worker node is successful?
  # TODO: do we need to check the current list of running workers?
end
load_private_key(filename) click to toggle source
# File lib/openstudio/lib/openstudio_aws_wrapper.rb, line 346
def load_private_key(filename)
  unless File.exist? filename
    # check if the file basename exists in your user directory
    filename = File.expand_path("~/.ssh/#{File.basename(filename)}")
    if File.exist? filename
      logger.info "Found key of same name in user's home ssh folder #{filename}"
      # using the key in your home directory
    else
      raise "Could not find private key #{filename}" unless File.exist? filename
    end
  end

  @private_key_file_name = File.expand_path filename
  @private_key = File.read(filename)
end
load_worker_key(private_key_filename) click to toggle source

Load the worker key for communicating between the server and worker instances on AWS. The public key will be automatically created when loading the private key

@param private_key_filename [String] Fully qualified path to the worker private key

# File lib/openstudio/lib/openstudio_aws_wrapper.rb, line 366
def load_worker_key(private_key_filename)
  logger.info "Loading worker keys from #{private_key_filename}"
  @worker_keys_filename = private_key_filename
  @worker_keys = SSHKey.new(File.read(@worker_keys_filename))
end
save_private_key(directory = '.', filename = 'ec2_server_key.pem') click to toggle source

Save the private key to disk

# File lib/openstudio/lib/openstudio_aws_wrapper.rb, line 373
def save_private_key(directory = '.', filename = 'ec2_server_key.pem')
  if @private_key
    @private_key_file_name = File.expand_path "#{directory}/#{filename}"
    logger.info "Saving server private key in #{@private_key_file_name}"
    File.open(@private_key_file_name, 'w') { |f| f << @private_key }
    logger.info 'Setting permissions of server private key to 0600'
    File.chmod(0o600, @private_key_file_name)
  else
    raise "No private key found in which to persist with filename #{filename}"
  end
end
save_worker_keys(directory = '.') click to toggle source

save off the worker public/private keys that were created

# File lib/openstudio/lib/openstudio_aws_wrapper.rb, line 386
def save_worker_keys(directory = '.')
  @worker_keys_filename = "#{directory}/ec2_worker_key.pem"
  logger.info "Saving worker private key in #{@worker_keys_filename}"
  File.open(@worker_keys_filename, 'w') { |f| f << @worker_keys.private_key }
  logger.info 'Setting permissions of worker private key to 0600'
  File.chmod(0o600, @worker_keys_filename)

  wk = "#{directory}/ec2_worker_key.pub"
  logger.info "Saving worker public key in #{wk}"
  File.open(wk, 'w') { |f| f << @worker_keys.public_key }
end
stop_instances(ids) click to toggle source

Stop specific instances based on the instance_ids @param [Array] ids: Array of ids to stop

# File lib/openstudio/lib/openstudio_aws_wrapper.rb, line 278
def stop_instances(ids)
  resp = @aws.stop_instances(
    instance_ids: ids,
    force: true
  )

  resp
end
terminate_instances(ids) click to toggle source
# File lib/openstudio/lib/openstudio_aws_wrapper.rb, line 287
def terminate_instances(ids)
  resp = nil
  begin
    resp = @aws.terminate_instances(
      instance_ids: ids
    )
  rescue Aws::EC2::Errors::InvalidInstanceIDNotFound
    # Log that the instances couldn't be found?
    resp = { error: 'instances could not be found' }
  end

  resp
end
to_os_hash() click to toggle source

save off the instance configuration and instance information into a JSON file for later use

# File lib/openstudio/lib/openstudio_aws_wrapper.rb, line 679
def to_os_hash
  h = @server.to_os_hash

  h[:server][:worker_private_key_file_name] = @worker_keys_filename
  h[:workers] = @workers.map do |worker|
    {
      id: worker.data.id,
      ip: "http://#{worker.data.ip}",
      dns: worker.data.dns,
      procs: worker.data.procs,
      private_key_file_name: worker.private_key_file_name,
      private_ip_address: worker.private_ip_address
    }
  end

  h
end
total_instances_count() click to toggle source
# File lib/openstudio/lib/openstudio_aws_wrapper.rb, line 150
def total_instances_count
  resp = @aws.describe_instance_status

  availability_zone = !resp.instance_statuses.empty? ? resp.instance_statuses.first.availability_zone : 'no_instances'

  { total_instances: resp.instance_statuses.length, region: @region, availability_zone: availability_zone }
end

Protected Instance Methods

transform_ami_lists(existing, available) click to toggle source

transform the available amis into an easier to read format

# File lib/openstudio/lib/openstudio_aws_wrapper.rb, line 717
def transform_ami_lists(existing, available)
  # initialize ami hash
  amis = { openstudio_server: {}, openstudio: {} }
  list_of_svs = []

  available[:images].each do |ami|
    sv = ami[:tags_hash][:openstudio_server_version]

    if sv.nil? || sv == ''
      logger.info 'found nil Server Version, ignoring'
      next
    end
    list_of_svs << sv

    amis[:openstudio_server][sv.to_sym] = {} unless amis[:openstudio_server][sv.to_sym]
    a = amis[:openstudio_server][sv.to_sym]

    # initialize ami hash
    a[:amis] = {} unless a[:amis]

    # fill in data (this will override data currently)
    a[:openstudio_version] = ami[:tags_hash][:openstudio_version] if ami[:tags_hash][:openstudio_version]
    a[:openstudio_version_sha] = ami[:tags_hash][:openstudio_version_sha] if ami[:tags_hash][:openstudio_version_sha]
    a[:user_uuid] = ami[:tags_hash][:user_uuid] if ami[:tags_hash][:user_uuid]
    a[:created_on] = ami[:tags_hash][:created_on] if ami[:tags_hash][:created_on]
    a[:openstudio_server_version] = sv.to_s
    if ami[:tags_hash][:tested]
      a[:tested] = ami[:tags_hash][:tested].downcase == 'true'
    else
      a[:tested] = false
    end

    if ami[:tags_hash][:openstudio_version].to_version >= '1.6.0'
      if ami[:name] =~ /Server/
        a[:amis][:server] = ami[:image_id]
      elsif ami[:name] =~ /Worker/
        a[:amis][:worker] = ami[:image_id]
      end
    elsif ami[:tags_hash][:openstudio_version].to_version >= '1.5.0'
      if ami[:name] =~ /Server/
        a[:amis][:server] = ami[:image_id]
      elsif ami[:name] =~ /Worker/
        a[:amis][:worker] = ami[:image_id]
        a[:amis][:cc2worker] = ami[:image_id]
      end
    else
      if ami[:name] =~ /Worker|Cluster/
        if ami[:virtualization_type] == 'paravirtual'
          a[:amis][:worker] = ami[:image_id]
        elsif ami[:virtualization_type] == 'hvm'
          a[:amis][:cc2worker] = ami[:image_id]
        else
          raise "unknown virtualization_type in #{ami[:name]}"
        end
      elsif ami[:name] =~ /Server/
        a[:amis][:server] = ami[:image_id]
      end
    end
  end

  # flip these around for openstudio server section
  amis[:openstudio_server].keys.each do |key|
    a = amis[:openstudio_server][key]
    ov = a[:openstudio_version]

    amis[:openstudio][ov] ||= {}
    osv = key
    amis[:openstudio][ov][osv] ||= {}
    amis[:openstudio][ov][osv][:amis] ||= {}
    amis[:openstudio][ov][osv][:amis][:server] = a[:amis][:server]
    amis[:openstudio][ov][osv][:amis][:worker] = a[:amis][:worker]
    amis[:openstudio][ov][osv][:amis][:cc2worker] = a[:amis][:cc2worker]
  end

  # sort the openstudio server version
  amis[:openstudio_server] = Hash[amis[:openstudio_server].sort_by { |_k, v| v[:openstudio_server_version].to_version }.reverse]

  # now sort the openstudio section & determine the defaults
  amis[:openstudio].keys.each do |key|
    amis[:openstudio][key] = Hash[amis[:openstudio][key].sort_by { |k, _| k.to_s.to_version }.reverse]
    amis[:openstudio][key][:default] = amis[:openstudio][key].keys[0]
  end
  amis[:openstudio] = Hash[amis[:openstudio].sort_by { |k, _| k.to_s.to_version }.reverse]

  amis
end