Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

IPMI network allocations API with pool support #513

Merged
merged 18 commits into from
Apr 6, 2017
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
FROM java:8-jre
FROM openjdk:8-jre
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have you though of using openjdk:8-jre-alpine ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It uses muscl instead of glibc, and didnt want to take a chance of breaking something. Plus, for downstream consumers, they expect to be able to apt-get install stuff for collins to hook into. We could maintain 2 different Dockerfiles that produces a debian and alpine build, but probably outside the scope of this diff

MAINTAINER Gabe Conradi <[email protected]>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

probably should change the MAINTAINER to [email protected]

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is that alias accessible from outside the company? I thought there was a public maintainers mailing list for collins?


# Solr cores should be stored in a volume, so we arent writing stuff to our rootfs
VOLUME /opt/collins/conf/solr/cores/collins/data

COPY . /build/collins
RUN apt-get update && \
apt-get install --no-install-recommends -y openjdk-8-jdk zip unzip ipmitool && \
apt-get install --no-install-recommends -y openjdk-8-jdk openjdk-8-jdk-headless zip unzip ipmitool && \
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

isn't openjdk-8-jdk-headless already installed in the base image?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no, this is the JRE image. We install only the JDK for the duration of the build, then remove it

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nah, thats only JRE. We need JDK for the build

rm -r /var/lib/apt/lists/* && \
cd /build && \
export ACTIVATOR_VERSION=1.3.7 && \
Expand All @@ -19,7 +19,7 @@ RUN apt-get update && \
cd / && rm -rf /build && \
rm -rf /root/.ivy2 && \
rm -rf /root/.sbt && \
apt-get remove -y --purge openjdk-8-jdk && \
apt-get remove -y --purge openjdk-8-jdk openjdk-8-jdk-headless && \
apt-get autoremove --purge -y && \
apt-get clean && \
rm /var/log/dpkg.log
Expand Down
2 changes: 1 addition & 1 deletion app/collins/controllers/AssetApi.scala
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ trait AssetApi {
FindAction(PageParams(page, size, sort, sortField), Permissions.AssetApi.GetAssets, this)

// PUT /api/asset/:tag
def createAsset(tag: String) = CreateAction(Some(tag), None, Permissions.AssetApi.CreateAsset, this)
def createAsset(tag: String) = CreateAction(Some(tag), None, None, Permissions.AssetApi.CreateAsset, this)

// POST /api/asset/:tag
def updateAsset(tag: String) = UpdateRequestRouter {
Expand Down
67 changes: 61 additions & 6 deletions app/collins/controllers/IpmiApi.scala
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,15 @@ import play.api.mvc.Results

import collins.models.Asset
import collins.models.IpmiInfo
import collins.models.shared.AddressPool
import collins.util.IpAddress

trait IpmiApi {
this: Api with SecureController =>

case class IpmiForm(username: Option[String], password: Option[String], address: Option[String], gateway: Option[String], netmask: Option[String]) {
case class IpmiCreateForm(pool: Option[String])

case class IpmiUpdateForm(username: Option[String], password: Option[String], address: Option[String], gateway: Option[String], netmask: Option[String]) {
def merge(asset: Asset, ipmi: Option[IpmiInfo]): IpmiInfo = {
ipmi.map { info =>
val iu: IpmiInfo = username.map(u => info.copy(username = u)).getOrElse(info)
Expand All @@ -35,27 +38,77 @@ trait IpmiApi {
}
}
}
val IPMI_FORM = Form(

// TODO: extend form to include the ipmi network name if desired
val IPMI_UPDATE_FORM = Form(
mapping(
"username" -> optional(text(1)),
"password" -> optional(text(minLength=4, maxLength=20)),
"address" -> optional(text(7)),
"gateway" -> optional(text(7)),
"netmask" -> optional(text(7))
)(IpmiForm.apply)(IpmiForm.unapply)
)(IpmiUpdateForm.apply)(IpmiUpdateForm.unapply)
)

val IPMI_CREATE_FORM = Form(
mapping(
"pool" -> optional(text(1))
)(IpmiCreateForm.apply)(IpmiCreateForm.unapply)
)

def generateIpmi(tag: String) = SecureAction { implicit req =>
Api.withAssetFromTag(tag) { asset =>
IPMI_CREATE_FORM.bindFromRequest.fold(
hasErrors => {
val error = hasErrors.errors.map { _.message }.mkString(", ")
Left(Api.getErrorMessage("Data submission error: %s".format(error)))
},
ipmiForm => {
try {
ipmiForm match {
case IpmiCreateForm(poolOption) => IpmiInfo.findByAsset(asset) match {
case Some(ipmiinfo) =>
Left(Api.getErrorMessage("Asset already has IPMI details, cannot generate new IPMI details", Results.BadRequest))
case None => IpmiInfo.getConfig(poolOption) match {
case None =>
Left(Api.getErrorMessage("Invalid IPMI pool %s specified".format(poolOption.getOrElse("default")), Results.BadRequest))
case Some(AddressPool(poolName, _, _, _)) =>
// make sure asset does not already have IPMI created, because this
// implies we want to create new IPMI details
val info = IpmiInfo.findByAsset(asset)
val newInfo = IpmiInfo.createForAsset(asset, poolOption)
tattler(None).notice("Generated IPMI configuration from %s: IP %s, Netmask %s, Gateway %s".format(
poolName, newInfo.dottedAddress, newInfo.dottedNetmask, newInfo.dottedGateway), asset)
Right(ResponseData(Results.Created, JsObject(Seq("SUCCESS" -> JsBoolean(true)))))
}
}
}
} catch {
case e: SQLException =>
Left(Api.getErrorMessage("Possible duplicate IPMI Address",
Results.Status(StatusValues.CONFLICT)))
case e: Throwable =>
Left(Api.getErrorMessage("Incomplete form submission: %s".format(e.getMessage)))
}
}
)
}.fold(
err => formatResponseData(err),
suc => formatResponseData(suc)
)
}(Permissions.IpmiApi.GenerateIpmi)


def updateIpmi(tag: String) = SecureAction { implicit req =>
Api.withAssetFromTag(tag) { asset =>
val ipmiInfo = IpmiInfo.findByAsset(asset)
IPMI_FORM.bindFromRequest.fold(
IPMI_UPDATE_FORM.bindFromRequest.fold(
hasErrors => {
val error = hasErrors.errors.map { _.message }.mkString(", ")
Left(Api.getErrorMessage("Data submission error: %s".format(error)))
},
ipmiForm => {
try {
val newInfo = ipmiForm.merge(asset, ipmiInfo)
val newInfo = ipmiForm.merge(asset, IpmiInfo.findByAsset(asset))
val (status, success) = newInfo.id match {
case update if update > 0 =>
IpmiInfo.update(newInfo) match {
Expand All @@ -68,6 +121,8 @@ trait IpmiApi {
if (status == Results.Conflict) {
Left(Api.getErrorMessage("Unable to update IPMI information",status))
} else {
tattler(None).notice("Updated IPMI configuration: IP %s, Netmask %s, Gateway %s".format(
newInfo.dottedAddress, newInfo.dottedNetmask, newInfo.dottedGateway), asset)
Right(ResponseData(status, JsObject(Seq("SUCCESS" -> JsBoolean(success)))))
}
} catch {
Expand Down
1 change: 1 addition & 0 deletions app/collins/controllers/Permissions.scala
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ object Permissions {
object IpmiApi extends PermSpec("controllers.IpmiApi") {
def Spec = spec(AdminSpec)
def UpdateIpmi = spec("updateIpmi", Spec)
def GenerateIpmi = spec("generateIpmi", Spec)
}

object IpAddressApi extends PermSpec("controllers.IpAddressApi") {
Expand Down
3 changes: 2 additions & 1 deletion app/collins/controllers/Resources.scala
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,9 @@ trait Resources extends Controller {
}
}(Permissions.Resources.CreateForm)

// TODO(gabe) should we display a dropdown for selecting the IPMI pool?
def createAsset(atype: String) = CreateAction(
None, Some(atype), Permissions.Resources.CreateAsset, this
None, Some(atype), None, Permissions.Resources.CreateAsset, this
)

/**
Expand Down
13 changes: 9 additions & 4 deletions app/collins/controllers/actions/asset/CreateAction.scala
Original file line number Diff line number Diff line change
Expand Up @@ -30,26 +30,29 @@ import collins.validation.StringUtil
case class CreateAction(
_assetTag: Option[String],
_assetType: Option[String],
_ipmiPool: Option[String],
spec: SecuritySpecification,
handler: SecureController
) extends SecureAction(spec, handler) with AssetAction {

case class ActionDataHolder(
assetTag: String,
generateIpmi: Boolean,
ipmiPool: Option[String],
assetType: AssetType,
assetStatus: Option[AssetStatus]
) extends RequestDataHolder

lazy val dataHolder: Either[RequestDataHolder,ActionDataHolder] = Form(tuple(
"generate_ipmi" -> optional(of[Truthy]),
"ipmi_pool" -> optional(text),
"type" -> optional(of[AssetType]),
"status" -> optional(of[AssetStatus]),
"tag" -> optional(text(1))
)).bindFromRequest()(request).fold(
err => Left(RequestDataHolder.error400(fieldError(err))),
tuple => {
val (generate, atype, astatus, tag) = tuple
val (generate, ipmiPool, atype, astatus, tag) = tuple
val assetType = _assetType.flatMap(a => AssetType.findByName(a)).orElse(atype).orElse(AssetType.ServerNode)
val atString = assetType.map(_.name).getOrElse("Unknown")
val assetTag = getString(_assetTag, tag)
Expand All @@ -61,6 +64,7 @@ case class CreateAction(
Right(ActionDataHolder(
assetTag,
generate.map(_.toBoolean).getOrElse(AssetType.isServerNode(assetType.get)),
ipmiPool,
assetType.get,
astatus
))
Expand All @@ -82,9 +86,9 @@ case class CreateAction(
}

override def execute(rd: RequestDataHolder) = Future { rd match {
case ActionDataHolder(assetTag, genIpmi, assetType, assetStatus) =>
case ActionDataHolder(assetTag, genIpmi, ipmiPool, assetType, assetStatus) =>
val lifeCycle = new AssetLifecycle(userOption(), tattler)
lifeCycle.createAsset(assetTag, assetType, genIpmi, assetStatus) match {
lifeCycle.createAsset(assetTag, assetType, genIpmi, ipmiPool, assetStatus) match {
case Left(throwable) =>
handleError(
RequestDataHolder.error500("Could not create asset: %s".format(throwable.getMessage))
Expand Down Expand Up @@ -118,6 +122,7 @@ case class CreateAction(

protected def fieldError(f: Form[_]) = f match {
case e if e.error("generate_ipmi").isDefined => "generate_ipmi requires a boolean value"
case e if e.error("ipmi_pool").isDefined => "ipmi_pool requires a string value"
case e if e.error("type").isDefined => "Invalid asset type specified"
case e if e.error("status").isDefined => "Invalid status specified"
case e if e.error("tag").isDefined => "Asset tag must not be empty"
Expand All @@ -126,7 +131,7 @@ case class CreateAction(

protected def assetTypeString(rd: RequestDataHolder): Option[String] = rd match {
// FIXME ServerNode not a valid create type via UI
case ActionDataHolder(_, _, at, _) => Some(at.name)
case ActionDataHolder(_, _, _, at, _) => Some(at.name)
case s if s.string("assetType").isDefined => s.string("assetType")
case o => None
}
Expand Down
11 changes: 8 additions & 3 deletions app/collins/models/AssetLifecycle.scala
Original file line number Diff line number Diff line change
Expand Up @@ -56,15 +56,20 @@ class AssetLifecycle(user: Option[User], tattler: Tattler) {

private[this] val logger = Logger.logger

def createAsset(tag: String, assetType: AssetType, generateIpmi: Boolean, status: Option[Status]): AssetLifecycle.Status[AssetLifecycle.AssetIpmi] = {
def createAsset(tag: String, assetType: AssetType, generateIpmi: Boolean, ipmiPool: Option[String], status: Option[Status]): AssetLifecycle.Status[AssetLifecycle.AssetIpmi] = {
import IpmiInfo.Enum._
try {
val _status = status.getOrElse(Status.Incomplete.get)
if (generateIpmi && IpmiInfo.getConfig(ipmiPool).isEmpty) {
return Left(new Exception("Invalid IPMI pool %s specified".format(ipmiPool.getOrElse("default"))))
}
val res = Asset.inTransaction {
val asset = Asset.create(Asset(tag, _status, assetType))
val ipmi = generateIpmi match {
case true => Some(IpmiInfo.createForAsset(asset))
case false => None
// we can assume the ipmiPool is valid, because we already checked it
// before the transaction began
case true => Some(IpmiInfo.createForAsset(asset, ipmiPool))
case _ => None
}
Solr.updateAsset(asset)
(asset, ipmi)
Expand Down
18 changes: 9 additions & 9 deletions app/collins/models/IpAddresses.scala
Original file line number Diff line number Diff line change
Expand Up @@ -70,26 +70,26 @@ object IpAddresses extends IpAddressStorage[IpAddresses] with IpAddressKeys[IpAd
}
}

override def getNextAvailableAddress(overrideStart: Option[String] = None)(implicit scope: Option[String]): Tuple3[Long, Long, Long] = {
override def getNextAvailableAddress(scope: Option[String], overrideStart: Option[String] = None): Tuple3[Long, Long, Long] = {
throw new UnsupportedOperationException("getNextAvailableAddress not supported")
}

def getNextAddress(iteration: Int)(implicit scope: Option[String]): Tuple3[Long, Long, Long] = {
val network = getNetwork
val startAt = getStartAddress
def getNextAddress(iteration: Int, scope: Option[String]): Tuple3[Long, Long, Long] = {
val network = getNetwork(scope)
val startAt = getStartAddress(scope)
val calc = IpAddressCalc(network, startAt)
val gateway: Long = getGateway().getOrElse(calc.minAddressAsLong)
val gateway: Long = getGateway(scope).getOrElse(calc.minAddressAsLong)
val netmask: Long = calc.netmaskAsLong
val currentMax: Option[Long] = getCurrentLowestLocalMaxAddress(calc)
val currentMax: Option[Long] = getCurrentLowestLocalMaxAddress(calc, scope)
val address: Long = calc.nextAvailableAsLong(currentMax)
(gateway, address, netmask)
}

def createForAsset(asset: Asset, scope: Option[String]): IpAddresses = inTransaction {
val assetId = asset.id
val cfg = getConfig()(scope)
val cfg = getConfig(scope)
val ipAddresses = createWithRetry(10) { attempt =>
val (gateway, address, netmask) = getNextAddress(attempt)(scope)
val (gateway, address, netmask) = getNextAddress(attempt, scope)
logger.debug("trying to use address %s".format(IpAddress.toString(address)))
val ipAddresses = IpAddresses(assetId, gateway, address, netmask, scope.getOrElse(""))
super.create(ipAddresses)
Expand Down Expand Up @@ -160,7 +160,7 @@ object IpAddresses extends IpAddressStorage[IpAddresses] with IpAddressKeys[IpAd
select(i.pool)).distinct.toSet
})

override protected def getConfig()(implicit scope: Option[String]): Option[AddressPool] = {
override protected def getConfig(scope: Option[String]): Option[AddressPool] = {
AddressConfig.flatMap(cfg => scope.flatMap(cfg.pool(_)).orElse(cfg.defaultPool))
}

Expand Down
13 changes: 8 additions & 5 deletions app/collins/models/IpmiInfo.scala
Original file line number Diff line number Diff line change
Expand Up @@ -73,12 +73,12 @@ object IpmiInfo extends IpAddressStorage[IpmiInfo] with IpAddressKeys[IpmiInfo]
i.gateway is (indexed),
i.netmask is (indexed)))

def createForAsset(asset: Asset): IpmiInfo = inTransaction {
def createForAsset(asset: Asset, scope: Option[String]): IpmiInfo = inTransaction {
val assetId = asset.id
val username = getUsername(asset)
val password = generateEncryptedPassword()
createWithRetry(10) { attempt =>
val (gateway, address, netmask) = getNextAvailableAddress()(None)
val (gateway, address, netmask) = getNextAvailableAddress(scope)
val ipmiInfo = IpmiInfo(
assetId, username, password, gateway, address, netmask)
tableDef.insert(ipmiInfo)
Expand Down Expand Up @@ -139,9 +139,12 @@ object IpmiInfo extends IpAddressStorage[IpmiInfo] with IpAddressKeys[IpmiInfo]
IpmiConfig.genUsername(asset)
}

override protected def getConfig()(implicit scope: Option[String]): Option[AddressPool] = {
IpmiConfig.get.flatMap(_.defaultPool)
}
override def getConfig(scope: Option[String]): Option[AddressPool] = IpmiConfig.get.flatMap(
addressPool => scope match {
case Some(p) => addressPool.pool(p)
case None => addressPool.defaultPool
}
)

// Converts our query parameters to fragments and parameters for a query
private[this] def collectParams(ipmi: Seq[Tuple2[Enum, String]], ipmiRow: IpmiInfo): LogicalBoolean = {
Expand Down
20 changes: 10 additions & 10 deletions app/collins/models/shared/IpAddressable.scala
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ trait IpAddressStorage[T <: IpAddressable] extends Schema with AnormAdapter[T] w
def storageName: String

// abstract
protected def getConfig()(implicit scope: Option[String]): Option[AddressPool]
protected def getConfig(scope: Option[String]): Option[AddressPool]

protected[this] val logger = Logger.logger

Expand Down Expand Up @@ -77,15 +77,15 @@ trait IpAddressStorage[T <: IpAddressable] extends Schema with AnormAdapter[T] w
tableDef.where(a => a.assetId === asset.id).headOption
})

def getNextAvailableAddress(overrideStart: Option[String] = None)(implicit scope: Option[String]): Tuple3[Long, Long, Long] = {
def getNextAvailableAddress(scope: Option[String], overrideStart: Option[String] = None): Tuple3[Long, Long, Long] = {
//this is used by ip allocation without pools (i.e. IPMI)
val network = getNetwork
val startAt = overrideStart.orElse(getStartAddress)
val network = getNetwork(scope)
val startAt = overrideStart.orElse(getStartAddress(scope))
val calc = IpAddressCalc(network, startAt)
val gateway: Long = getGateway().getOrElse(calc.minAddressAsLong)
val gateway: Long = getGateway(scope).getOrElse(calc.minAddressAsLong)
val netmask: Long = calc.netmaskAsLong
// look for the local maximum address (i.e. the last used address in a continuous sequence from startAddress)
val localMax: Option[Long] = getCurrentLowestLocalMaxAddress(calc)
val localMax: Option[Long] = getCurrentLowestLocalMaxAddress(calc, scope)
val address: Long = calc.nextAvailableAsLong(localMax)
(gateway, address, netmask)
}
Expand Down Expand Up @@ -126,7 +126,7 @@ trait IpAddressStorage[T <: IpAddressable] extends Schema with AnormAdapter[T] w
* For a range 0L..20L, used addresses List(5,6,7,8,19,20), the result will be Some(8)
* For a range 0L..20L, used addresses List(17,18,19,20), the result will be None (allocate from beginning)
*/
protected def getCurrentLowestLocalMaxAddress(calc: IpAddressCalc)(implicit scope: Option[String]): Option[Long] = inTransaction {
protected def getCurrentLowestLocalMaxAddress(calc: IpAddressCalc, scope: Option[String]): Option[Long] = inTransaction {
val startAddress = calc.startAddressAsLong
val maxAddress = calc.maxAddressAsLong
val sortedAddresses = from(tableDef)(t =>
Expand All @@ -150,18 +150,18 @@ trait IpAddressStorage[T <: IpAddressable] extends Schema with AnormAdapter[T] w
localMaximaAddresses.headOption
}

protected def getGateway()(implicit scope: Option[String]): Option[Long] = getConfig() match {
protected def getGateway(scope: Option[String]): Option[Long] = getConfig(scope) match {
case None => None
case Some(config) => config.gateway match {
case Some(value) => Option(IpAddress.toLong(value))
case None => None
}
}
protected def getNetwork()(implicit scope: Option[String]): String = getConfig() match {
protected def getNetwork(scope: Option[String]): String = getConfig(scope) match {
case None => throw new RuntimeException("no %s configuration found".format(getClass.getName))
case Some(config) => config.network
}
protected def getStartAddress()(implicit scope: Option[String]): Option[String] = getConfig() match {
protected def getStartAddress(scope: Option[String]): Option[String] = getConfig(scope) match {
case None => None
case Some(c) => c.startAddress
}
Expand Down
Loading