All Downloads are FREE. Search and download functionalities are using the official Maven repository.

tasks.elastic.ec2.ec2allocation.scala Maven / Gradle / Ivy

The newest version!
/*
 * The MIT License
 *
 * Copyright (c) 2015 ECOLE POLYTECHNIQUE FEDERALE DE LAUSANNE, Switzerland,
 * Group Fellay
 * Modified work, Copyright (c) 2016 Istvan Bartha


 *
 * Permission is hereby granted, free of charge, to any person obtaining
 * a copy of this software and associated documentation files (the "Software"),
 * to deal in the Software without restriction, including without limitation
 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
 * and/or sell copies of the Software, and to permit persons to whom the Software
 * is furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

package tasks.elastic.ec2

import akka.actor.{ActorSystem, Props, ActorRef}
import scala.util._

import tasks.elastic._
import tasks.shared._
import tasks.util._
import tasks.util.config._

import com.amazonaws.services.ec2.model.LaunchSpecification;
import com.amazonaws.services.ec2.model.RequestSpotInstancesRequest;
import com.amazonaws.services.ec2.model.GroupIdentifier
import com.amazonaws.services.ec2.model.Tag;
import com.amazonaws.services.ec2.model.IamInstanceProfileSpecification
import com.amazonaws.services.ec2.model.SpotInstanceType
import com.amazonaws.services.ec2.model.BlockDeviceMapping
import com.amazonaws.services.ec2.model.CancelSpotInstanceRequestsRequest
import com.amazonaws.services.ec2.model.SpotPlacement
import com.amazonaws.services.ec2.model.CreateTagsRequest

import scala.jdk.CollectionConverters._
import com.amazonaws.services.ec2.AmazonEC2ClientBuilder
import com.amazonaws.services.ec2.AmazonEC2

class EC2Shutdown(ec2: AmazonEC2) extends ShutdownNode {

  def shutdownRunningNode(nodeName: RunningJobId): Unit =
    EC2Operations.terminateInstance(ec2, nodeName.value)

  def shutdownPendingNode(nodeName: PendingJobId): Unit = {
    val request = new CancelSpotInstanceRequestsRequest(
      List(nodeName.value).asJava
    )
    ec2.cancelSpotInstanceRequests(request)
  }

}

class EC2CreateNode(
    masterAddress: SimpleSocketAddress,
    codeAddress: CodeAddress,
    ec2: AmazonEC2,
    elasticSupport: ElasticSupportFqcn
)(implicit config: TasksConfig)
    extends CreateNode {

  private def gzipBase64(str: String): String = {

    val out = new java.io.ByteArrayOutputStream();
    val gzip = new java.util.zip.GZIPOutputStream(out);
    gzip.write(str.getBytes());
    gzip.close();
    val bytes = out.toByteArray
    java.util.Base64.getEncoder.encodeToString(bytes)
  }

  def requestOneNewJobFromJobScheduler(
      requestSize: ResourceRequest
  ): Try[(PendingJobId, ResourceAvailable)] =
    Try {
      val (requestid, instancetype) = requestSpotInstance(requestSize)
      val jobid = PendingJobId(requestid)
      val size = instancetype._2
      (jobid, size)
    }

  override def initializeNode(node: Node): Unit = {

    ec2.createTags(
      new CreateTagsRequest(
        List(node.name.value).asJava,
        config.instanceTags.map(t => new Tag(t._1, t._2)).asJava
      )
    )

  }

  override def convertRunningToPending(
      p: RunningJobId
  ): Option[PendingJobId] = {
    val describeResult = ec2.describeSpotInstanceRequests();
    val spotInstanceRequests = describeResult.getSpotInstanceRequests();

    spotInstanceRequests.asScala
      .filter(_.getInstanceId == p.value)
      .headOption
      .map { x =>
        PendingJobId(x.getSpotInstanceRequestId)
      }

  }

  private def requestSpotInstance(requestSize: ResourceRequest) = {
    // size is ignored, instance specification is set in configuration
    val selectedInstanceType = EC2Operations
      .workerInstanceType(requestSize)
      .getOrElse(
        throw new RuntimeException("No instance type could fullfill request")
      )

    // Initializes a Spot Instance Request
    val requestRequest = new RequestSpotInstancesRequest();

    if (config.spotPrice > 2.5)
      throw new RuntimeException("Spotprice too high:" + config.spotPrice)

    requestRequest.setSpotPrice(config.spotPrice.toString);
    requestRequest.setInstanceCount(1);
    requestRequest.setType(SpotInstanceType.OneTime)

    val launchSpecification = new LaunchSpecification();
    launchSpecification.setImageId(config.amiID);
    launchSpecification.setInstanceType(selectedInstanceType._1);
    launchSpecification.setKeyName(config.keyName)

    val blockDeviceMappingSDB = new BlockDeviceMapping();
    blockDeviceMappingSDB.setDeviceName("/dev/sdb");
    blockDeviceMappingSDB.setVirtualName("ephemeral0");
    val blockDeviceMappingSDC = new BlockDeviceMapping();
    blockDeviceMappingSDC.setDeviceName("/dev/sdc");
    blockDeviceMappingSDC.setVirtualName("ephemeral1");

    launchSpecification.setBlockDeviceMappings(
      List(blockDeviceMappingSDB, blockDeviceMappingSDC).asJava
    )

    config.iamRole.foreach { iamRole =>
      val iamprofile = new IamInstanceProfileSpecification()
      iamprofile.setName(iamRole)
      launchSpecification.setIamInstanceProfile(iamprofile)
    }

    config.placementGroup.foreach { string =>
      val placement = new SpotPlacement();
      placement.setGroupName(string);
      launchSpecification.setPlacement(placement);
    }

    val userdata = "#!/usr/bin/env bash\n" + Deployment.script(
      memory = selectedInstanceType._2.memory,
      cpu = selectedInstanceType._2.cpu,
      scratch = selectedInstanceType._2.scratch,
      gpus = selectedInstanceType._2.gpu,
      elasticSupport = elasticSupport,
      masterAddress = masterAddress,
      download = Uri(
        scheme = "http",
        hostname = codeAddress.address.getHostName,
        port = codeAddress.address.getPort,
        path = "/"
      ),
      followerHostname = None,
      background = true,
      image = None
    )

    launchSpecification.setUserData(gzipBase64(userdata))

    val securitygroups =
      (config.securityGroup +: config.securityGroups).distinct
        .filter(_.size > 0)

    launchSpecification.setAllSecurityGroups(securitygroups.map { x =>
      val g = new GroupIdentifier
      g.setGroupId(x)
      g
    }.asJava)

    val subnetId = config.subnetId

    launchSpecification.setSubnetId(subnetId)

    // Add the launch specification.
    requestRequest.setLaunchSpecification(launchSpecification)

    // Call the RequestSpotInstance API.
    val requestResult = ec2.requestSpotInstances(requestRequest)

    (
      requestResult.getSpotInstanceRequests.asScala
        .map(_.getSpotInstanceRequestId)
        .head,
      selectedInstanceType
    )

  }

}

class EC2Reaper(terminateSelf: Boolean)(implicit val config: TasksConfig)
    extends Reaper {

  val ec2 =
    if (config.awsRegion.isEmpty) AmazonEC2ClientBuilder.defaultClient
    else AmazonEC2ClientBuilder.standard.withRegion(config.awsRegion).build

  def allSoulsReaped(): Unit = {
    log.debug("All souls reaped. Calling system.shutdown.")
    if (terminateSelf) {
      val nodename = EC2Operations.readMetadata("instance-id").head
      EC2Operations.terminateInstance(ec2, nodename)
    }
    context.system.terminate()
  }
}

class EC2CreateNodeFactory(implicit
    config: TasksConfig,
    ec2: AmazonEC2,
    elasticSupport: ElasticSupportFqcn
) extends CreateNodeFactory {
  def apply(master: SimpleSocketAddress, codeAddress: CodeAddress) =
    new EC2CreateNode(master, codeAddress, ec2, elasticSupport)
}

object EC2GetNodeName extends GetNodeName {
  def getNodeName = EC2Operations.readMetadata("instance-id").head
}

object EC2ReaperFactory extends ReaperFactory {
  def apply(implicit system: ActorSystem, config: TasksConfig): ActorRef =
    system.actorOf(
      Props(new EC2Reaper(config.terminateMaster)),
      name = "reaper"
    )
}

class EC2ElasticSupport extends ElasticSupportFromConfig {

  implicit val fqcn: ElasticSupportFqcn = ElasticSupportFqcn(
    "tasks.elastic.ec2.EC2ElasticSupport"
  )

  def apply(implicit config: TasksConfig) = cats.effect.Resource.pure {
    implicit val ec2 =
      if (config.awsRegion.isEmpty) AmazonEC2ClientBuilder.defaultClient
      else AmazonEC2ClientBuilder.standard.withRegion(config.awsRegion).build
    SimpleElasticSupport(
      fqcn = fqcn,
      hostConfig = Some(new EC2MasterSlave),
      reaperFactory = Some(EC2ReaperFactory),
      shutdown = new EC2Shutdown(ec2),
      createNodeFactory = new EC2CreateNodeFactory,
      getNodeName = EC2GetNodeName
    )
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy