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

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

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

import cats.effect.Concurrent
import cats.syntax.flatMap._
import com.comcast.ip4s.IpAddress
import org.http4s.Status.Conflict
import org.http4s.client.Client
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.{AllocationPool, NeutronError, Subnet}

final class Subnets[F[_]: Concurrent: Client](baseUri: Uri, session: Session)
  extends CrudService[F, Subnet[IpAddress], Subnet.Create[IpAddress], Subnet.Update[IpAddress]](baseUri, "subnet", session.authToken)
    with BulkCreate[F, Subnet[IpAddress], Subnet.Create[IpAddress]] {
  
  private def computeUpdatedCollection[T: Ordering](existing: List[T], create: List[T], keepExistingElements: Boolean): Option[List[T]] = {
    val newElements = if (keepExistingElements) {
      existing ++ create.filter(route => !existing.contains(route))
    } else {
      create
    }
    Option(newElements.sorted).filter(_ != existing.sorted)
  }
  
  override def defaultResolveConflict(existing: Subnet[IpAddress], create: Subnet.Create[IpAddress], keepExistingElements: Boolean,
    extraHeaders: Seq[Header.ToRaw]): F[Subnet[IpAddress]] = {
    val updated = Subnet.Update(
      description = Option(create.description).filter(_ != existing.description),
      gatewayIp = if (create.gateway != existing.gateway) create.gateway else None,
      allocationPools = create.allocationPools.map(_.sorted).filter(_ != existing.allocationPools.sorted),
      hostRoutes = computeUpdatedCollection(existing.hostRoutes, create.hostRoutes, keepExistingElements),
      dnsNameservers = computeUpdatedCollection(existing.nameservers, create.nameservers, keepExistingElements),
      enableDhcp = Option(create.enableDhcp).filter(_ != existing.enableDhcp),
      segmentId = if (create.segmentId != existing.segmentId) create.segmentId else None,
      serviceTypes = computeUpdatedCollection(existing.serviceTypes, create.serviceTypes, keepExistingElements),
    )
    
    def errorOut(message: String): IllegalArgumentException = new IllegalArgumentException(
      s"Existing subnet (id: ${existing.id}, name: ${existing.name}, projectId: ${existing.projectId}, networkId: ${existing.networkId}) $message")
    
    val Subnet.Create(_, _, _, cidr, gatewayIp, allocationPools, _, _, _, subnetpoolId, _, _, _, _, version, _, _, _) = create
    val existingIpVersion = existing.cidr.address.version
    
    // We cannot do anything with subnetpoolId, useDefaultSubnetpool, prefixlen, ipv6AddressMode, ipv6RaMode
    (for {
      _ <- Either.cond(version.getOrElse(existingIpVersion) == existingIpVersion, (), errorOut(s"is for $existingIpVersion cannot change it to $version."))
      // TODO: we could search for the default subnetpool, and check its ID against existing.subnetpoolId
      _ <- Either.cond(subnetpoolId == existing.subnetpoolId, (), errorOut(s"has subnetpoolId: ${existing.subnetpoolId} cannot change it to $subnetpoolId."))
      // We cannot update the CIDR directly, that's why we are reimplementing part of its logic
      // If subnetpoolId is set then the cidr, gateway, and allocationPools were set from that subnet pool, so trying to set them would mess things up
      // The same will happen if the user has explicitly set the allocation pools.
      result <- cidr.filter(_ != existing.cidr && existing.subnetpoolId.isEmpty && allocationPools.isEmpty)
        .map(AllocationPool.fromCidrAndGateway(_, gatewayIp)) match {
        case None => Right(updated)
        case Some(None) => Left(errorOut(s"cannot change gatewayIp to ${gatewayIp.get} because it either: is outside of ${cidr.get}; or it collides with the " +
          s"network/broadcast address."))
        case Some(Some((gateway, pools))) =>
          Right(updated.copy(
            gatewayIp = if (existing.gateway.contains(gateway)) None else Some(gateway),
            allocationPools = Option.when(pools != existing.allocationPools)(pools),
          ))
      }
    } yield result) match {
      case Left(error) => Concurrent[F].raiseError(error)
      case Right(updated) if updated.needsUpdate => update(existing.id, updated, extraHeaders:_*)
      case _ => Concurrent[F].pure(existing)
    }
  }
  override def createOrUpdate(create: Subnet.Create[IpAddress], keepExistingElements: Boolean = true, extraHeaders: Seq[Header.ToRaw] = Seq.empty)
    (resolveConflict: (Subnet[IpAddress], Subnet.Create[IpAddress]) => F[Subnet[IpAddress]] = defaultResolveConflict(_, _, keepExistingElements,
      extraHeaders)): F[Subnet[IpAddress]] = {
    // We want the create to be idempotent, so we decided to make the name unique **within** a (project, network).
    create.projectId orElse session.scopedProjectId match {
      case None => super.create(create, extraHeaders:_*)
      case Some(projectId) =>
        list("name" -> create.name, "project_id" -> projectId, "network_id" -> create.networkId, "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, networkId, and projectId.")
            resolveConflict(existing, create)
          case _ =>
            val message =
              s"""Cannot create a $name idempotently because more than one exists with:
                 |name: ${create.name}
                 |projectId: ${create.projectId}
                 |networkId: ${create.networkId}""".stripMargin
            Concurrent[F].raiseError(NeutronError(Conflict.reason, message))
        }
    }
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy