app/model/Instance.scala (174 lines of code) (raw):

package model import java.net.ConnectException import java.util.Date import com.amazonaws.services.ec2.model.{DescribeInstancesRequest, Instance => AwsEc2Instance} import lib._ import play.api.Logger import play.api.cache.SyncCacheApi import play.api.libs.json.Json import play.api.libs.ws.{WSClient, WSResponse} import scala.jdk.CollectionConverters._ import scala.concurrent.ExecutionContext.Implicits.global import scala.concurrent.Future import scala.concurrent.duration._ import scala.util.{Failure, Success, Try} case class Instance( id: String, publicDns: String, publicIp: String, privateDns: String, privateIp: String, instanceType: String, state: String, availabilityZone: String, cost: Option[BigDecimal], approxMonthlyCost: Option[BigDecimal], costingType: EC2CostingType, uptime: String, launched: Date, tags: Map[String, String], stage: String, app: String, version: Option[String], usefulUrls: Map[String, String] ) { val nameOpt = tags.get("Name") } object EC2Instance { def apply(awsInstance: AwsEc2Instance, version: Option[String], usefulUrls: Seq[(String, String)], awsCost: AWSCost, flag:Boolean = false) = { val tags = awsInstance.getTags.asScala.map(t => t.getKey -> t.getValue).toMap.withDefaultValue("") val costingType = EC2CostingType(awsInstance.getInstanceType, awsInstance.getPlacement.getAvailabilityZone) val cost: Option[BigDecimal] = Try(awsCost(costingType)).toOption Instance( id = awsInstance.getInstanceId, publicDns = awsInstance.getPublicDnsName, publicIp = awsInstance.getPublicIpAddress, privateDns = awsInstance.getPrivateDnsName, privateIp = awsInstance.getPrivateIpAddress, instanceType = awsInstance.getInstanceType, state = awsInstance.getState.getName, availabilityZone = awsInstance.getPlacement.getAvailabilityZone, cost = cost, approxMonthlyCost = cost map (_ * 24 * 30), costingType = costingType, launched = awsInstance.getLaunchTime, uptime = UptimeDisplay.print(awsInstance.getLaunchTime), tags = tags, stage = tags("Stage"), app = tags("App"), version = version, usefulUrls = usefulUrls.toMap ) } } case class ElasticSearchInstance(privateDns: String) extends AppSpecifics { val baseUrl = s"http://$privateDns:9200" val versionUrl = baseUrl def usefulUrls = List( "head" -> (baseUrl + "/_plugin/head/"), "bigdesk" -> (baseUrl + "/_plugin/bigdesk/"), "paramedic" -> (baseUrl + "/_plugin/paramedic/") ) val versionExtractor = { r: WSResponse => val v = (r.json \ "version" \ "number").as[String] val name = (r.json \ "name").as[String] Some(v + " " + name) } } case class StandardWebApp(versionUrl: String) extends AppSpecifics { def usefulUrls = List( "manifest" -> versionUrl ) val versionExtractor = { r: WSResponse => val values = r.body.linesIterator.map(_.split(':').map(_.trim)).collect { case Array(k, v) => k -> v }.toMap values.get("Build") } } trait AppSpecifics { val log = Logger(classOf[AppSpecifics]) def usefulUrls: Seq[(String, String)] def versionUrl: String def versionExtractor: WSResponse => Option[String] def version(implicit wsClient: WSClient) :Future[Option[String]] = { (for { wsWithUrl <- Future.fromTry(Try(wsClient.url(versionUrl))) response <- wsWithUrl.withRequestTimeout(200.milliseconds).get() map (versionExtractor) } yield response) recover { case _: ConnectException => log.error(s"Couldn't retrieve $versionUrl") None case e => log.error(s"Couldn't retrieve $versionUrl", e) None } } } object Instance { implicit val instanceWrites = Json.writes[Instance] val log = Logger(classOf[Instance]) private def uncachedGet(id: String, awsCost: AWSCost)(implicit awsConn: AmazonConnection, wsClient: WSClient): Future[Instance] = { (for { result <- AWS.futureOf(awsConn.ec2.describeInstancesAsync, new DescribeInstancesRequest().withInstanceIds(id)) i <- (result.getReservations.asScala flatMap (_.getInstances.asScala) map (from(_, awsCost))).head } yield i) recover { case e => { log.error(s"Unable to retrieve details for instance: $id") UnknownInstance(id) } } } def get(id: String, awsCost: AWSCost)(implicit awsConn: AmazonConnection, cache: SyncCacheApi, wsClient: WSClient ): Future[Instance] = cache.get[Instance](id) map (Future.successful(_)) getOrElse { uncachedGet(id, awsCost) map { instance: Instance => cache.set(id, instance, Duration(30, SECONDS)) instance } } def from(i: AwsEc2Instance, awsCost: AWSCost)(implicit wsClient: WSClient): Future[Instance] = { val tags = i.getTags.asScala.map(t => t.getKey -> t.getValue).toMap.withDefaultValue("") val dns = i.getPublicDnsName val managementTag = ManagementTag(tags.get("Management")) val managementEndpoint = managementTag map (ManagementEndpoint(dns, _)) val specifics = if (managementTag.flatMap(_.format).exists(_ == "elasticsearch")) new ElasticSearchInstance(i.getPrivateDnsName) else new StandardWebApp(s"${managementEndpoint.get.url}/manifest") log.debug(s"Retrieving version of instance with tags: $tags") specifics.version map { v => EC2Instance(i, v, specifics.usefulUrls, awsCost) } } } object ManagementEndpoint { def apply(dnsName: String, tag: ManagementTag): ManagementEndpoint = { val port: Int = tag.port.orElse(Config.managementPort).getOrElse(9000) val protocol: String = tag.protocol getOrElse "http" val path: String = tag.path getOrElse "/management" def url: String = s"""$protocol://$dnsName:$port$path""" ManagementEndpoint(dnsName, tag, port, protocol, path, url) } } case class ManagementEndpoint(dnsName: String, tag: ManagementTag, port: Int, protocol: String, path: String, url: String) object UnknownInstance { def apply(id: String) = Instance( id = id, app = "???", stage = "???", uptime = "???", state = "???", cost = None, approxMonthlyCost = None, availabilityZone = "???", instanceType = "???", privateIp = "???", privateDns = "???", publicIp = "???", publicDns = "???", costingType = EC2CostingType("", ""), version = None, launched = new Date(), usefulUrls = Map.empty, tags = Map.empty ) }