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

pt.tecnico.dsi.openstack.neutron.services.Routers.scala Maven / Gradle / Ivy

The newest version!
package pt.tecnico.dsi.openstack.neutron.services

import cats.effect.Concurrent
import cats.syntax.flatMap._
import cats.syntax.functor._
import com.comcast.ip4s.IpAddress
import io.circe.{Decoder, Encoder, Json}
import org.http4s.Status.{Conflict, Successful, NotFound}
import org.http4s.client.Client
import org.http4s.Method.PUT
import org.http4s.{Header, Uri}
import org.log4s.getLogger
import pt.tecnico.dsi.openstack.common.services.CrudService
import pt.tecnico.dsi.openstack.keystone.models.Session
import pt.tecnico.dsi.openstack.neutron.models.Router.{ExternalGatewayInfo, ExternalIp}
import pt.tecnico.dsi.openstack.neutron.models._

final class Routers[F[_] : Concurrent : Client](baseUri: Uri, session: Session)
  extends CrudService[F, Router, Router.Create, Router.Update](baseUri, "router", session.authToken) {
  
  private def computeUpdatedExternalGatewayInfo(existing: Option[ExternalGatewayInfo], create: Option[ExternalGatewayInfo],
    keepExistingElements: Boolean): Option[ExternalGatewayInfo] =
    (existing, create) match {
      case (None, _) =>
        // The existing router does not have an externalGatewayInfo. So whatever the create sets prevails
        create
      case (Some(_), None) =>
        // The existing router has an externalGatewayInfo but the create is requesting for it not to have it.
        if (keepExistingElements) existing else {
          // This does not unset the existing ExternalGatewayInfo because the jsonEncoder drops the nulls
          // https://stackoverflow.com/questions/64754830/encoder-for-update-endpoint-of-a-rest-api
          create
        }
      case (Some(existingInfo), Some(createInfo)) if createInfo.networkId != existingInfo.networkId =>
        // The external IPs are set using a subnetId which must be a child of the networkId.
        // If the network is different the create prevails without having to consider keepExistingElements because the previous elements cannot be preserved.
        create
      case (Some(existingInfo), Some(createInfo)) =>
        var newExternalIps = List.empty[ExternalIp]
        for (newExternalIp @ ExternalIp(subnetId, newIp) <- createInfo.externalFixedIps) {
          existingInfo.externalFixedIps.find(_.subnetId == subnetId) match {
            case None =>
              // The existing router does not have an external IP for this subnetId
              newExternalIps +:= newExternalIp
            case Some(existingExternalIp) =>
              // If the create is setting an explicit IP use that. Otherwise use the existing one.
              newExternalIps +:= newIp.map(_ => newExternalIp).getOrElse(existingExternalIp)
          }
        }
        if (keepExistingElements) {
          // Add all existing external IPs for which the subnetId isn't in the newExternalIps
          newExternalIps ++= existingInfo.externalFixedIps.filter(externalIp => !newExternalIps.exists(_.subnetId == externalIp.subnetId))
        }
        // We need to sort the fixed IPs because equals on a list considers order
        val newInfo = createInfo.copy(externalFixedIps = newExternalIps.sortBy(_.subnetId))
        val existingInfoSorted = existingInfo.copy(externalFixedIps = existingInfo.externalFixedIps.sortBy(_.subnetId))
        Option.when(newInfo != existingInfoSorted)(newInfo)
    }
  
  override def defaultResolveConflict(existing: Router, create: Router.Create, keepExistingElements: Boolean, extraHeaders: Seq[Header.ToRaw]): F[Router] = {
    val updated = Router.Update(
      description = Option(create.description).filter(_ != existing.description),
      adminStateUp = Option(create.adminStateUp).filter(_ != existing.adminStateUp),
      externalGatewayInfo = computeUpdatedExternalGatewayInfo(existing.externalGatewayInfo, create.externalGatewayInfo, keepExistingElements),
      // routes cannot be set when creating the Router, so we don't have to update them, because consistency .
      distributed = if (!create.distributed.contains(existing.distributed)) create.distributed else None,
      ha = if (!create.ha.contains(existing.ha)) create.ha else None,
    )
    if (updated.needsUpdate) update(existing.id, updated, extraHeaders:_*)
    else Concurrent[F].pure(existing)
  }
  override def createOrUpdate(create: Router.Create, keepExistingElements: Boolean = true, extraHeaders: Seq[Header.ToRaw] = Seq.empty)
    (resolveConflict: (Router, Router.Create) => F[Router] = defaultResolveConflict(_, _, keepExistingElements, extraHeaders)): F[Router] = {
    // A router create is not idempotent because Openstack always creates a new router and never returns a Conflict, whatever the parameters.
    // We want it to be idempotent, so we decided to make the name unique **within** a project.
    create.projectId orElse session.scopedProjectId match {
      case None => super.create(create, extraHeaders:_*)
      case Some(projectId) =>
        list("name" -> create.name, "project_id" -> projectId, "limit" -> "2").flatMap {
          case Nil => super.create(create, extraHeaders:_*)
          case List(existing) =>
            getLogger.info(s"createOrUpdate: found unique $name (id: ${existing.id}) with the correct name and projectId.")
            resolveConflict(existing, create)
          case _ =>
            val message =
              s"""Cannot create a $name idempotently because more than one exists with:
                 |name: ${create.name}
                 |project: ${create.projectId}""".stripMargin
            Concurrent[F].raiseError(NeutronError(Conflict.reason, message))
        }
    }
  }
  
  final class Operations(val id: String) { self =>
    import dsl._
    
    private def routesOperation(routes: List[Route[IpAddress]], path: String): F[List[Route[IpAddress]]] = {
      // Double wrapping for double the fun 
      // https://github.com/circe/circe/issues/1536
      implicit val e = Encoder[List[Route[IpAddress]]].mapJson(j => Json.obj("routes" -> j))
      implicit val d = Decoder[List[Route[IpAddress]]].at("routes")
      put(wrappedAt = Some(name), routes, uri / id / path)
    }
    def add(routes: List[Route[IpAddress]]): F[List[Route[IpAddress]]] = routesOperation(routes, "add_extraroutes")
    def remove(routes: List[Route[IpAddress]]): F[List[Route[IpAddress]]] = routesOperation(routes, "remove_extraroutes")
    
    /**
     * If the router interface has already added a None will be returned, otherwise the RouterInterface.
     * Openstack does not provide a way to obtain an already existing router interface. */
    private def addRouterInterface(`type`: String, id: String): F[Option[RouterInterface]] = {
      import dsl._
      val conflicting = """.*?Router already has a port on subnet ([^ ]+)\.""".r
      client.run(PUT.apply(Map(s"${`type`}_id" -> id), uri / self.id / "add_router_interface", authToken)).use {
        case Successful(response) => response.as[RouterInterface].map(ri => Some(ri))
        case response => response.as[NeutronError].flatMap {
          case NeutronError("BadRequest", conflicting(`id`), _) =>
            // Openstack does not provide a way to obtain an already existing router interface.
            // We almost can compute it:
            // search for the existing port (which is created when this method is successful)
            // routerId = self.id
            // networkId = port.networkId
            // projectId = port.projectId
            // subnetId = this should probably be an Option. What happens when `type` = port?
            // portId = port.id
            // tags = port.tags
            Concurrent[F].pure(Option.empty)
          case error => F.raiseError(error)
        }
      }
    }
    
    private def removeRouterInterface(`type`: String, id: String): F[Unit] = {
      // A delete is done with a PUT! 
      val request = PUT.apply(Map(s"${`type`}_id" -> id), uri / self.id / "remove_router_interface", authToken)
      client.run(request).use {
        case Successful(_) | NotFound(_) => F.unit
        case response => defaultOnError(request, response)
      }
    }
    
    def addInterfaceBySubnet(subnetId: String): F[Option[RouterInterface]] =
      addRouterInterface("subnet", subnetId)
    def addInterfaceByPort(portId: String): F[Option[RouterInterface]] =
      addRouterInterface("port", portId)
    
    def removeInterfaceBySubnet(subnetId: String): F[Unit] =
      removeRouterInterface("subnet", subnetId)
    def removeInterfaceByPort(portId: String): F[Unit] =
      removeRouterInterface("port", portId)
    
    def addInterface(subnet: Subnet[_]): F[Option[RouterInterface]] = addInterfaceBySubnet(subnet.id)
    def removeInterface(subnet: Subnet[_]): F[Unit] = removeInterfaceBySubnet(subnet.id)
  }
  
  /** Allows performing operations on the router with `id` */
  def on(id: String): Operations = new Operations(id)
  /** Allows performing operations on `router`. */
  def on(router: Router): Operations = on(router.id)
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy