From 2c7472d1507becc20c19d33e2cbf4f1ae124bcda Mon Sep 17 00:00:00 2001 From: Gabe Conradi Date: Fri, 3 Mar 2017 16:50:39 -0500 Subject: [PATCH 01/17] allocate new IPMI details for POST /api/asset/:tag/ipmi --- app/collins/controllers/IpmiApi.scala | 45 +++++++++++++------ app/collins/models/IpmiInfo.scala | 1 + app/collins/models/shared/IpAddressable.scala | 1 + 3 files changed, 33 insertions(+), 14 deletions(-) diff --git a/app/collins/controllers/IpmiApi.scala b/app/collins/controllers/IpmiApi.scala index d544823f7..7e665ef90 100644 --- a/app/collins/controllers/IpmiApi.scala +++ b/app/collins/controllers/IpmiApi.scala @@ -35,6 +35,8 @@ trait IpmiApi { } } } + + // TODO: extend form to include the ipmi network name if desired val IPMI_FORM = Form( mapping( "username" -> optional(text(1)), @@ -47,7 +49,6 @@ trait IpmiApi { def updateIpmi(tag: String) = SecureAction { implicit req => Api.withAssetFromTag(tag) { asset => - val ipmiInfo = IpmiInfo.findByAsset(asset) IPMI_FORM.bindFromRequest.fold( hasErrors => { val error = hasErrors.errors.map { _.message }.mkString(", ") @@ -55,20 +56,36 @@ trait IpmiApi { }, ipmiForm => { try { - val newInfo = ipmiForm.merge(asset, ipmiInfo) - val (status, success) = newInfo.id match { - case update if update > 0 => - IpmiInfo.update(newInfo) match { - case 0 => (Results.Conflict, false) - case _ => (Results.Ok, true) + ipmiForm match { + // if form is empty, generate new info for the asset + case IpmiForm(None, None, None, None, None) => IpmiInfo.findByAsset(asset) match { + case Some(ipmiinfo) => + Left(Api.getErrorMessage("Asset already has IPMI details, cannot generate new IPMI details", Results.BadRequest)) + case None => + //TODO: ensure this is POST to create new details, not PUT! + //TODO(gabe) 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) + Right(ResponseData(Results.Created, JsObject(Seq("SUCCESS" -> JsBoolean(true))))) } - case _ => - (Results.Created, IpmiInfo.create(newInfo).id > 0) - } - if (status == Results.Conflict) { - Left(Api.getErrorMessage("Unable to update IPMI information",status)) - } else { - Right(ResponseData(status, JsObject(Seq("SUCCESS" -> JsBoolean(success))))) + case _ => { + val newInfo = ipmiForm.merge(asset, IpmiInfo.findByAsset(asset)) + val (status, success) = newInfo.id match { + case update if update > 0 => + IpmiInfo.update(newInfo) match { + case 0 => (Results.Conflict, false) + case _ => (Results.Ok, true) + } + case _ => + (Results.Created, IpmiInfo.create(newInfo).id > 0) + } + if (status == Results.Conflict) { + Left(Api.getErrorMessage("Unable to update IPMI information",status)) + } else { + Right(ResponseData(status, JsObject(Seq("SUCCESS" -> JsBoolean(success))))) + } + } } } catch { case e: SQLException => diff --git a/app/collins/models/IpmiInfo.scala b/app/collins/models/IpmiInfo.scala index a7897272c..d1df7ca97 100644 --- a/app/collins/models/IpmiInfo.scala +++ b/app/collins/models/IpmiInfo.scala @@ -73,6 +73,7 @@ object IpmiInfo extends IpAddressStorage[IpmiInfo] with IpAddressKeys[IpmiInfo] i.gateway is (indexed), i.netmask is (indexed))) + // TODO: make this take the IPMI network name def createForAsset(asset: Asset): IpmiInfo = inTransaction { val assetId = asset.id val username = getUsername(asset) diff --git a/app/collins/models/shared/IpAddressable.scala b/app/collins/models/shared/IpAddressable.scala index 99d2d82b7..55d679116 100644 --- a/app/collins/models/shared/IpAddressable.scala +++ b/app/collins/models/shared/IpAddressable.scala @@ -79,6 +79,7 @@ trait IpAddressStorage[T <: IpAddressable] extends Schema with AnormAdapter[T] w def getNextAvailableAddress(overrideStart: Option[String] = None)(implicit scope: Option[String]): Tuple3[Long, Long, Long] = { //this is used by ip allocation without pools (i.e. IPMI) + // TODO: fixme to take a network scope val network = getNetwork val startAt = overrideStart.orElse(getStartAddress) val calc = IpAddressCalc(network, startAt) From c227388e9760b7c608103f963557686a046f9eac Mon Sep 17 00:00:00 2001 From: Gabe Conradi Date: Fri, 3 Mar 2017 17:39:58 -0500 Subject: [PATCH 02/17] have separate handler to generate new IPMI details --- app/collins/controllers/IpmiApi.scala | 87 +++++++++++++------ app/collins/controllers/Permissions.scala | 1 + app/collins/models/AssetLifecycle.scala | 2 +- app/collins/models/IpmiInfo.scala | 5 +- app/collins/models/shared/IpAddressable.scala | 3 +- conf/routes | 1 + 6 files changed, 66 insertions(+), 33 deletions(-) diff --git a/app/collins/controllers/IpmiApi.scala b/app/collins/controllers/IpmiApi.scala index 7e665ef90..ce55c7915 100644 --- a/app/collins/controllers/IpmiApi.scala +++ b/app/collins/controllers/IpmiApi.scala @@ -18,7 +18,9 @@ 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) @@ -37,19 +39,25 @@ trait IpmiApi { } // TODO: extend form to include the ipmi network name if desired - val IPMI_FORM = Form( + 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) ) - def updateIpmi(tag: String) = SecureAction { implicit req => + 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_FORM.bindFromRequest.fold( + IPMI_CREATE_FORM.bindFromRequest.fold( hasErrors => { val error = hasErrors.errors.map { _.message }.mkString(", ") Left(Api.getErrorMessage("Data submission error: %s".format(error))) @@ -57,34 +65,17 @@ trait IpmiApi { ipmiForm => { try { ipmiForm match { - // if form is empty, generate new info for the asset - case IpmiForm(None, None, None, None, None) => IpmiInfo.findByAsset(asset) 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 => - //TODO: ensure this is POST to create new details, not PUT! - //TODO(gabe) make sure asset does not already have IPMI created, because this + // 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) + val newInfo = IpmiInfo.createForAsset(asset, poolOption) // TODO use poolOption here + tattler(None).notice("Generated IPMI configuration: IP %s, Netmask %s, Gateway %s".format( + newInfo.dottedAddress, newInfo.dottedNetmask, newInfo.dottedGateway), asset) Right(ResponseData(Results.Created, JsObject(Seq("SUCCESS" -> JsBoolean(true))))) - } - case _ => { - val newInfo = ipmiForm.merge(asset, IpmiInfo.findByAsset(asset)) - val (status, success) = newInfo.id match { - case update if update > 0 => - IpmiInfo.update(newInfo) match { - case 0 => (Results.Conflict, false) - case _ => (Results.Ok, true) - } - case _ => - (Results.Created, IpmiInfo.create(newInfo).id > 0) - } - if (status == Results.Conflict) { - Left(Api.getErrorMessage("Unable to update IPMI information",status)) - } else { - Right(ResponseData(status, JsObject(Seq("SUCCESS" -> JsBoolean(success))))) - } } } } catch { @@ -100,6 +91,48 @@ trait IpmiApi { err => formatResponseData(err), suc => formatResponseData(suc) ) + }(Permissions.IpmiApi.GenerateIpmi) + + + def updateIpmi(tag: String) = SecureAction { implicit req => + Api.withAssetFromTag(tag) { asset => + 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.findByAsset(asset)) + val (status, success) = newInfo.id match { + case update if update > 0 => + IpmiInfo.update(newInfo) match { + case 0 => (Results.Conflict, false) + case _ => (Results.Ok, true) + } + case _ => + (Results.Created, IpmiInfo.create(newInfo).id > 0) + } + 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 { + 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.UpdateIpmi) } diff --git a/app/collins/controllers/Permissions.scala b/app/collins/controllers/Permissions.scala index 7c9099712..04a65b224 100644 --- a/app/collins/controllers/Permissions.scala +++ b/app/collins/controllers/Permissions.scala @@ -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") { diff --git a/app/collins/models/AssetLifecycle.scala b/app/collins/models/AssetLifecycle.scala index 19a716979..b7ea60653 100644 --- a/app/collins/models/AssetLifecycle.scala +++ b/app/collins/models/AssetLifecycle.scala @@ -63,7 +63,7 @@ class AssetLifecycle(user: Option[User], tattler: Tattler) { val res = Asset.inTransaction { val asset = Asset.create(Asset(tag, _status, assetType)) val ipmi = generateIpmi match { - case true => Some(IpmiInfo.createForAsset(asset)) + case true => Some(IpmiInfo.createForAsset(asset, None)) // assume default network scope case false => None } Solr.updateAsset(asset) diff --git a/app/collins/models/IpmiInfo.scala b/app/collins/models/IpmiInfo.scala index d1df7ca97..3f1bf1db3 100644 --- a/app/collins/models/IpmiInfo.scala +++ b/app/collins/models/IpmiInfo.scala @@ -73,13 +73,12 @@ object IpmiInfo extends IpAddressStorage[IpmiInfo] with IpAddressKeys[IpmiInfo] i.gateway is (indexed), i.netmask is (indexed))) - // TODO: make this take the IPMI network name - 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) diff --git a/app/collins/models/shared/IpAddressable.scala b/app/collins/models/shared/IpAddressable.scala index 55d679116..9476d4d29 100644 --- a/app/collins/models/shared/IpAddressable.scala +++ b/app/collins/models/shared/IpAddressable.scala @@ -79,8 +79,7 @@ trait IpAddressStorage[T <: IpAddressable] extends Schema with AnormAdapter[T] w def getNextAvailableAddress(overrideStart: Option[String] = None)(implicit scope: Option[String]): Tuple3[Long, Long, Long] = { //this is used by ip allocation without pools (i.e. IPMI) - // TODO: fixme to take a network scope - val network = getNetwork + val network = getNetwork()(scope) val startAt = overrideStart.orElse(getStartAddress) val calc = IpAddressCalc(network, startAt) val gateway: Long = getGateway().getOrElse(calc.minAddressAsLong) diff --git a/conf/routes b/conf/routes index 83b2a2f68..6c16e32f9 100644 --- a/conf/routes +++ b/conf/routes @@ -67,6 +67,7 @@ GET /api/log/:id collins.app.Api.getLogData(id: Int) # Asset management API POST /api/asset/:tag/ipmi collins.app.Api.updateIpmi(tag: String) +PUT /api/asset/:tag/ipmi collins.app.Api.generateIpmi(tag: String) POST /api/asset/:tag/power collins.app.Api.powerManagement(tag: String) GET /api/asset/:tag/power collins.app.Api.powerStatus(tag: String) POST /api/provision/:tag collins.app.Api.provisionAsset(tag: String) From 2251a887f8a0515663b6b472a69c319b6e10b86a Mon Sep 17 00:00:00 2001 From: Gabe Conradi Date: Mon, 6 Mar 2017 11:24:06 -0500 Subject: [PATCH 03/17] broken but getting there, need to pull proper scope in ipmiinfo --- app/collins/controllers/IpmiApi.scala | 21 +++++++++++------- app/collins/models/IpAddresses.scala | 18 +++++++-------- app/collins/models/IpmiInfo.scala | 11 +++++++--- app/collins/models/shared/IpAddressable.scala | 20 ++++++++--------- conf/reference/ipmi_reference.conf | 22 ++++++++++++++----- 5 files changed, 57 insertions(+), 35 deletions(-) diff --git a/app/collins/controllers/IpmiApi.scala b/app/collins/controllers/IpmiApi.scala index ce55c7915..2750576b1 100644 --- a/app/collins/controllers/IpmiApi.scala +++ b/app/collins/controllers/IpmiApi.scala @@ -13,6 +13,7 @@ import play.api.mvc.Results import collins.models.Asset import collins.models.IpmiInfo +import collins.models.shared.AddressPool import collins.util.IpAddress trait IpmiApi { @@ -68,14 +69,18 @@ trait IpmiApi { 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 => - // 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) // TODO use poolOption here - tattler(None).notice("Generated IPMI configuration: IP %s, Netmask %s, Gateway %s".format( - newInfo.dottedAddress, newInfo.dottedNetmask, newInfo.dottedGateway), asset) - Right(ResponseData(Results.Created, JsObject(Seq("SUCCESS" -> JsBoolean(true))))) + case None => IpmiInfo.getConfig(poolOption) match { + case None => + Left(Api.getErrorMessage("Invalid IPMI pool %s specified".format(poolOption), 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 { diff --git a/app/collins/models/IpAddresses.scala b/app/collins/models/IpAddresses.scala index 3159fcd00..f085701f5 100644 --- a/app/collins/models/IpAddresses.scala +++ b/app/collins/models/IpAddresses.scala @@ -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) @@ -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)) } diff --git a/app/collins/models/IpmiInfo.scala b/app/collins/models/IpmiInfo.scala index 3f1bf1db3..565b7d462 100644 --- a/app/collins/models/IpmiInfo.scala +++ b/app/collins/models/IpmiInfo.scala @@ -78,7 +78,7 @@ object IpmiInfo extends IpAddressStorage[IpmiInfo] with IpAddressKeys[IpmiInfo] val username = getUsername(asset) val password = generateEncryptedPassword() createWithRetry(10) { attempt => - val (gateway, address, netmask) = getNextAvailableAddress()(scope) + val (gateway, address, netmask) = getNextAvailableAddress(scope) val ipmiInfo = IpmiInfo( assetId, username, password, gateway, address, netmask) tableDef.insert(ipmiInfo) @@ -139,8 +139,13 @@ 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] = { + // TODO: need to figure out how to return the proper network here. !!!!!!!!!!!!!!!!!!!!!!! + // get should be an IpAddressConfig? + logger.error("!!!! %s !!!!".format(scope)) + IpmiConfig.pools.get(poolName(scope.getOrElse(_.defaultPool))) + //get.flatMap(scope.getOrElse(_.defaultPool)) + //get(_.defaultPool) //.flatMap(scope.getOrElse(_.defaultPool)) } // Converts our query parameters to fragments and parameters for a query diff --git a/app/collins/models/shared/IpAddressable.scala b/app/collins/models/shared/IpAddressable.scala index 9476d4d29..0347856a1 100644 --- a/app/collins/models/shared/IpAddressable.scala +++ b/app/collins/models/shared/IpAddressable.scala @@ -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 @@ -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()(scope) - 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) } @@ -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 => @@ -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 } diff --git a/conf/reference/ipmi_reference.conf b/conf/reference/ipmi_reference.conf index 73af5f3fe..af6891e00 100644 --- a/conf/reference/ipmi_reference.conf +++ b/conf/reference/ipmi_reference.conf @@ -1,9 +1,21 @@ ipmi { - - passwordLength = 12 randomUsername = false username = "root" - network = "10.0.0.0/16" - startAddress = "10.0.0.3" - + passwordLength = 16 + defaultPoolName = "OOB-POD01" + pools { + OOB-POD01 { + network="10.102.208.0/21" + startAddress="10.102.208.2" + } + OOB-POD02 { + network="10.102.224.0/20" + startAddress="10.102.224.2" + endAddress="10.102.238.254" + } + OOB-POD03 { + network="10.102.32.0/19" + startAddress="10.102.32.2" + } + } } From aa8dbf9b053c3b81c55d9284fcba38fa0a783262 Mon Sep 17 00:00:00 2001 From: Gabe Conradi Date: Mon, 6 Mar 2017 12:07:36 -0500 Subject: [PATCH 04/17] make allocation work properly for IPMI networks --- app/collins/controllers/IpmiApi.scala | 2 +- app/collins/models/IpmiInfo.scala | 14 ++++++-------- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/app/collins/controllers/IpmiApi.scala b/app/collins/controllers/IpmiApi.scala index 2750576b1..25e3334a8 100644 --- a/app/collins/controllers/IpmiApi.scala +++ b/app/collins/controllers/IpmiApi.scala @@ -71,7 +71,7 @@ trait IpmiApi { 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), Results.BadRequest)) + 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 diff --git a/app/collins/models/IpmiInfo.scala b/app/collins/models/IpmiInfo.scala index 565b7d462..386567cf4 100644 --- a/app/collins/models/IpmiInfo.scala +++ b/app/collins/models/IpmiInfo.scala @@ -139,14 +139,12 @@ object IpmiInfo extends IpAddressStorage[IpmiInfo] with IpAddressKeys[IpmiInfo] IpmiConfig.genUsername(asset) } - override def getConfig(scope: Option[String]): Option[AddressPool] = { - // TODO: need to figure out how to return the proper network here. !!!!!!!!!!!!!!!!!!!!!!! - // get should be an IpAddressConfig? - logger.error("!!!! %s !!!!".format(scope)) - IpmiConfig.pools.get(poolName(scope.getOrElse(_.defaultPool))) - //get.flatMap(scope.getOrElse(_.defaultPool)) - //get(_.defaultPool) //.flatMap(scope.getOrElse(_.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 = { From e776ef7891354df8481c162dc53f1ebde937a4f6 Mon Sep 17 00:00:00 2001 From: Gabe Conradi Date: Mon, 6 Mar 2017 13:37:06 -0500 Subject: [PATCH 05/17] add ability to allocate IPMI details to collins client --- support/ruby/collins-client/VERSION | 2 +- .../collins-client/lib/collins/api/management.rb | 14 ++++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/support/ruby/collins-client/VERSION b/support/ruby/collins-client/VERSION index 6bf5ecb60..3bfb9a48b 100644 --- a/support/ruby/collins-client/VERSION +++ b/support/ruby/collins-client/VERSION @@ -1 +1 @@ -0.2.19 +0.2.20 diff --git a/support/ruby/collins-client/lib/collins/api/management.rb b/support/ruby/collins-client/lib/collins/api/management.rb index 8dbb71a6d..bc1d101c7 100644 --- a/support/ruby/collins-client/lib/collins/api/management.rb +++ b/support/ruby/collins-client/lib/collins/api/management.rb @@ -79,6 +79,20 @@ def ipmi_update asset_or_tag, options = {} end end + # allocate IPMI address/creds on an asset in a given pool, and handles + # generating username/password, like a normal asset creation with IPMI. + # omit pool option for default IPMI pool + def ipmi_allocate asset_or_tag, options = {} + asset = get_asset_or_tag asset_or_tag + parameters = { :pool => get_option(:pool, options, nil) } + parameters = select_non_empty_parameters parameters + logger.debug("Allocating IPMI details on asset #{asset.tag} with parameters #{parameters.inspect}") + http_put("/api/asset/#{asset.tag}/ipmi", parameters, asset.location) do |response| + parse_response response, :expects => [201], :as => :status + end + end + + end # module Management end; end From 7eca9644d750b73ad4855875f9abe211d931667d Mon Sep 17 00:00:00 2001 From: Gabe Conradi Date: Mon, 6 Mar 2017 14:36:32 -0500 Subject: [PATCH 06/17] update collins-shell with ipmi generate --- support/ruby/collins-shell/VERSION | 2 +- .../ruby/collins-shell/lib/collins_shell/ipmi.rb | 16 ++++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/support/ruby/collins-shell/VERSION b/support/ruby/collins-shell/VERSION index 521eb3d6e..ac1661553 100644 --- a/support/ruby/collins-shell/VERSION +++ b/support/ruby/collins-shell/VERSION @@ -1 +1 @@ -0.2.23 +0.2.24 diff --git a/support/ruby/collins-shell/lib/collins_shell/ipmi.rb b/support/ruby/collins-shell/lib/collins_shell/ipmi.rb index 9ff2f3add..a8e818c68 100644 --- a/support/ruby/collins-shell/lib/collins_shell/ipmi.rb +++ b/support/ruby/collins-shell/lib/collins_shell/ipmi.rb @@ -26,6 +26,22 @@ def self.print_ipmi ipmi puts([ipmi.address,ipmi.gateway,ipmi.netmask,ipmi.username,ipmi.password].join(',')) end + desc 'generate', 'generate new IPMI address and creds' + use_collins_options + use_tag_option(true) + method_option :pool, :type => :string, :optional => true, :desc => 'IPMI pool' + def create + call_collins get_collins_client, "generate ipmi" do |client| + ipmi = client.ipmi_allocate options.tag, pool => options.pool + if ipmi then + asset = client.get options.tag + CollinsShell::Ipmi.print_ipmi asset.ipmi + else + say_error "generate IPMI address" + end + end + end + desc 'create', 'create a new IPMI address for the specified asset' use_collins_options use_tag_option(true) From d8cd16c25665d8fd42dcdffdafc0e659d010eba4 Mon Sep 17 00:00:00 2001 From: Gabe Conradi Date: Mon, 6 Mar 2017 14:42:52 -0500 Subject: [PATCH 07/17] fix collins shell --- support/ruby/collins-shell/collins_shell.gemspec | 2 +- support/ruby/collins-shell/lib/collins_shell/ipmi.rb | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/support/ruby/collins-shell/collins_shell.gemspec b/support/ruby/collins-shell/collins_shell.gemspec index de4b16e4c..5ccc13366 100644 --- a/support/ruby/collins-shell/collins_shell.gemspec +++ b/support/ruby/collins-shell/collins_shell.gemspec @@ -56,7 +56,7 @@ Gem::Specification.new do |s| s.require_paths = ['lib'] s.summary = 'Shell for Collins API' - s.add_runtime_dependency('collins_client', '~> 0.2.11') + s.add_runtime_dependency('collins_client', '~> 0.2.20') s.add_runtime_dependency('highline','~> 1.6.15') s.add_runtime_dependency('mustache','~> 0.99.4') s.add_runtime_dependency('pry','~> 0.9.9.6') diff --git a/support/ruby/collins-shell/lib/collins_shell/ipmi.rb b/support/ruby/collins-shell/lib/collins_shell/ipmi.rb index a8e818c68..4489917c5 100644 --- a/support/ruby/collins-shell/lib/collins_shell/ipmi.rb +++ b/support/ruby/collins-shell/lib/collins_shell/ipmi.rb @@ -29,8 +29,8 @@ def self.print_ipmi ipmi desc 'generate', 'generate new IPMI address and creds' use_collins_options use_tag_option(true) - method_option :pool, :type => :string, :optional => true, :desc => 'IPMI pool' - def create + method_option :pool, :type => :string, :required => false, :desc => 'IPMI pool' + def generate call_collins get_collins_client, "generate ipmi" do |client| ipmi = client.ipmi_allocate options.tag, pool => options.pool if ipmi then From 0fe279859ef96191fedd5ac28de8baba4e40ec9e Mon Sep 17 00:00:00 2001 From: Gabe Conradi Date: Mon, 6 Mar 2017 14:49:14 -0500 Subject: [PATCH 08/17] oops --- support/ruby/collins-shell/lib/collins_shell/ipmi.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/support/ruby/collins-shell/lib/collins_shell/ipmi.rb b/support/ruby/collins-shell/lib/collins_shell/ipmi.rb index 4489917c5..900e86324 100644 --- a/support/ruby/collins-shell/lib/collins_shell/ipmi.rb +++ b/support/ruby/collins-shell/lib/collins_shell/ipmi.rb @@ -32,7 +32,7 @@ def self.print_ipmi ipmi method_option :pool, :type => :string, :required => false, :desc => 'IPMI pool' def generate call_collins get_collins_client, "generate ipmi" do |client| - ipmi = client.ipmi_allocate options.tag, pool => options.pool + ipmi = client.ipmi_allocate options.tag, :pool => options.pool if ipmi then asset = client.get options.tag CollinsShell::Ipmi.print_ipmi asset.ipmi From 5e36c6a283c4058b10c9af41a42762576feae742 Mon Sep 17 00:00:00 2001 From: Gabe Conradi Date: Mon, 6 Mar 2017 15:08:41 -0500 Subject: [PATCH 09/17] fix tests --- support/ruby/collins-shell/Gemfile.lock | 66 ++++++++++++++----------- test/collins/models/IpmiInfoSpec.scala | 14 +++--- test/collins/util/SolrSpec.scala | 2 +- 3 files changed, 46 insertions(+), 36 deletions(-) diff --git a/support/ruby/collins-shell/Gemfile.lock b/support/ruby/collins-shell/Gemfile.lock index 31f65c573..071089bd4 100644 --- a/support/ruby/collins-shell/Gemfile.lock +++ b/support/ruby/collins-shell/Gemfile.lock @@ -1,32 +1,46 @@ +PATH + remote: . + specs: + collins_shell (0.2.24) + collins_client (~> 0.2.11) + highline (~> 1.6.15) + mustache (~> 0.99.4) + pry (~> 0.9.9.6) + terminal-table (~> 1.4.5) + thor (~> 0.16.0) + GEM remote: http://rubygems.org/ specs: - coderay (1.0.8) - collins_client (0.2.10) - httparty (~> 0.8.3) + capistrano (2.15.9) + highline + net-scp (>= 1.0.0) + net-sftp (>= 2.0.0) + net-ssh (>= 2.0.14) + net-ssh-gateway (>= 1.1.0) + coderay (1.0.9) + collins_client (0.2.19) + httparty (~> 0.11.0) diff-lcs (1.1.3) - git (1.2.5) - highline (1.6.15) - httparty (0.8.3) + highline (1.6.21) + httparty (0.11.0) multi_json (~> 1.0) - multi_xml - jeweler (1.8.4) - bundler (~> 1.0) - git (>= 1.2.5) - rake - rdoc - json (1.7.5) + multi_xml (>= 0.5.2) method_source (0.7.1) - multi_json (1.5.0) - multi_xml (0.5.1) - mustache (0.99.4) + multi_json (1.12.1) + multi_xml (0.6.0) + mustache (0.99.8) + net-scp (1.2.1) + net-ssh (>= 2.6.5) + net-sftp (2.1.2) + net-ssh (>= 2.6.5) + net-ssh (4.1.0) + net-ssh-gateway (2.0.0) + net-ssh (>= 4.0.0) pry (0.9.9.6) coderay (~> 1.0.5) method_source (~> 0.7.1) slop (>= 2.4.4, < 3) - rake (10.0.3) - rdoc (3.12) - json (~> 1.4) redcarpet (2.2.2) rspec (2.10.0) rspec-core (~> 2.10.0) @@ -36,7 +50,6 @@ GEM rspec-expectations (2.10.0) diff-lcs (~> 1.1.3) rspec-mocks (2.10.1) - rubygems-update (1.8.24) slop (2.4.4) terminal-table (1.4.5) thor (0.16.0) @@ -46,14 +59,11 @@ PLATFORMS ruby DEPENDENCIES - collins_client (>= 0.2.10) - highline (~> 1.6.15) - jeweler (~> 1.8.3) - mustache (~> 0.99.4) - pry (~> 0.9.9.6) + capistrano (~> 2.15.5) + collins_shell! redcarpet rspec (~> 2.10.0) - rubygems-update (~> 1.8.24) - terminal-table (~> 1.4.5) - thor (~> 0.16.0) yard (~> 0.8) + +BUNDLED WITH + 1.13.6 diff --git a/test/collins/models/IpmiInfoSpec.scala b/test/collins/models/IpmiInfoSpec.scala index d9f54c44b..8568b2d7f 100644 --- a/test/collins/models/IpmiInfoSpec.scala +++ b/test/collins/models/IpmiInfoSpec.scala @@ -42,13 +42,13 @@ class IpmiInfoSpec extends mutable.Specification { "Support find methods" in new WithApplication { "nextAvailableAddress" in { val startAt = Some("172.16.32.20") - val l = IpmiInfo.getNextAvailableAddress(startAt)(None)._2 + val l = IpmiInfo.getNextAvailableAddress(startAt, None)._2 val s = IpAddress.toString(l) s mustEqual "172.16.32.20" } "nextAvailableAddress, rollover" in { val startAt = Some("172.16.33.1") - val l = IpmiInfo.getNextAvailableAddress(startAt)(None)._2 + val l = IpmiInfo.getNextAvailableAddress(startAt, None)._2 val s = IpAddress.toString(l) s mustEqual "172.16.33.1" } @@ -56,18 +56,18 @@ class IpmiInfoSpec extends mutable.Specification { val a1 = newIpmiAsset("ipmiAssetTag1") val a2 = newIpmiAsset("ipmiAssetTag2") val a3 = newIpmiAsset("ipmiAssetTag3") - IpmiInfo.createForAsset(a1).dottedAddress mustEqual "172.16.32.20" - IpmiInfo.createForAsset(a2).dottedAddress mustEqual "172.16.32.21" - IpmiInfo.createForAsset(a3).dottedAddress mustEqual "172.16.32.22" + IpmiInfo.createForAsset(a1, None).dottedAddress mustEqual "172.16.32.20" + IpmiInfo.createForAsset(a2, None).dottedAddress mustEqual "172.16.32.21" + IpmiInfo.createForAsset(a3, None).dottedAddress mustEqual "172.16.32.22" IpmiInfo.deleteByAsset(a3) mustEqual 1 - IpmiInfo.createForAsset(a3).dottedAddress mustEqual "172.16.32.22" + IpmiInfo.createForAsset(a3, None).dottedAddress mustEqual "172.16.32.22" } "createForAsset with reuse in range" in { val asset = ipmiAsset("ipmiAssetTag3") val ipmiInfo = IpmiInfo.findByAsset(asset).get IpmiInfo.update(ipmiInfo.copy(address = IpAddress.toLong("172.16.32.254"))) mustEqual 1 val a4 = newIpmiAsset("ipmiAssetTag4") - val ipmi4 = IpmiInfo.createForAsset(a4) + val ipmi4 = IpmiInfo.createForAsset(a4, None) ipmi4.dottedAddress mustEqual "172.16.32.22" ipmi4.dottedGateway mustEqual "172.16.32.1" } diff --git a/test/collins/util/SolrSpec.scala b/test/collins/util/SolrSpec.scala index 54d84b259..39dba7534 100644 --- a/test/collins/util/SolrSpec.scala +++ b/test/collins/util/SolrSpec.scala @@ -102,7 +102,7 @@ class SolrSpec extends mutable.Specification { val asset = generateAsset(assetTag, assetType, status, meta, state) val indexTime = new Date val addresses = IpAddresses.createForAsset(asset, 2, Some("DEV")) - val ipmi = IpmiInfo.createForAsset(asset) + val ipmi = IpmiInfo.createForAsset(asset, None) //alldoc keys are not added to the KEYS field val allDoc = Map( From d57c31cbc2f07f99e3095bec08390c5815e12cee Mon Sep 17 00:00:00 2001 From: Gabe Conradi Date: Mon, 6 Mar 2017 15:42:59 -0500 Subject: [PATCH 10/17] use 172.16.16.0/20 for default OOB range --- conf/reference/ipmi_reference.conf | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/conf/reference/ipmi_reference.conf b/conf/reference/ipmi_reference.conf index af6891e00..7b9db677b 100644 --- a/conf/reference/ipmi_reference.conf +++ b/conf/reference/ipmi_reference.conf @@ -5,8 +5,8 @@ ipmi { defaultPoolName = "OOB-POD01" pools { OOB-POD01 { - network="10.102.208.0/21" - startAddress="10.102.208.2" + network="172.16.16.0/20" + startAddress="172.16.16.2" } OOB-POD02 { network="10.102.224.0/20" From d0f8557d0f669b017fd802e64c3ebe2dbb45d48b Mon Sep 17 00:00:00 2001 From: Gabe Conradi Date: Mon, 6 Mar 2017 16:48:56 -0500 Subject: [PATCH 11/17] support parameterizing create with ipmi_pool on asset create --- app/collins/controllers/AssetApi.scala | 2 +- app/collins/controllers/Resources.scala | 3 ++- .../controllers/actions/asset/CreateAction.scala | 13 +++++++++---- app/collins/models/AssetLifecycle.scala | 7 +++++-- conf/reference/ipmi_reference.conf | 3 ++- .../ruby/collins-client/lib/collins/api/asset.rb | 1 + .../ruby/collins-shell/lib/collins_shell/asset.rb | 7 ++++++- test/collins/controllers/AssetApiSpec.scala | 2 +- 8 files changed, 27 insertions(+), 11 deletions(-) diff --git a/app/collins/controllers/AssetApi.scala b/app/collins/controllers/AssetApi.scala index 46b5edf2c..1f95a0fcf 100644 --- a/app/collins/controllers/AssetApi.scala +++ b/app/collins/controllers/AssetApi.scala @@ -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 { diff --git a/app/collins/controllers/Resources.scala b/app/collins/controllers/Resources.scala index 89d820fb4..c5acf44d8 100644 --- a/app/collins/controllers/Resources.scala +++ b/app/collins/controllers/Resources.scala @@ -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 ) /** diff --git a/app/collins/controllers/actions/asset/CreateAction.scala b/app/collins/controllers/actions/asset/CreateAction.scala index c75843101..2e12587fc 100644 --- a/app/collins/controllers/actions/asset/CreateAction.scala +++ b/app/collins/controllers/actions/asset/CreateAction.scala @@ -30,6 +30,7 @@ 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 { @@ -37,19 +38,21 @@ case class CreateAction( 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) @@ -61,6 +64,7 @@ case class CreateAction( Right(ActionDataHolder( assetTag, generate.map(_.toBoolean).getOrElse(AssetType.isServerNode(assetType.get)), + ipmiPool, assetType.get, astatus )) @@ -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)) @@ -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" @@ -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 } diff --git a/app/collins/models/AssetLifecycle.scala b/app/collins/models/AssetLifecycle.scala index b7ea60653..e5b09be05 100644 --- a/app/collins/models/AssetLifecycle.scala +++ b/app/collins/models/AssetLifecycle.scala @@ -56,14 +56,17 @@ 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) val res = Asset.inTransaction { val asset = Asset.create(Asset(tag, _status, assetType)) val ipmi = generateIpmi match { - case true => Some(IpmiInfo.createForAsset(asset, None)) // assume default network scope + case true => IpmiInfo.getConfig(ipmiPool) match { + case None => throw new IllegalArgumentException("Invalid IPMI pool %s specified".format(ipmiPool.getOrElse("default"))) + case _ => Some(IpmiInfo.createForAsset(asset, ipmiPool)) + } case false => None } Solr.updateAsset(asset) diff --git a/conf/reference/ipmi_reference.conf b/conf/reference/ipmi_reference.conf index 7b9db677b..a09117534 100644 --- a/conf/reference/ipmi_reference.conf +++ b/conf/reference/ipmi_reference.conf @@ -6,7 +6,8 @@ ipmi { pools { OOB-POD01 { network="172.16.16.0/20" - startAddress="172.16.16.2" + gateway="172.16.16.1" + startAddress="172.16.16.3" } OOB-POD02 { network="10.102.224.0/20" diff --git a/support/ruby/collins-client/lib/collins/api/asset.rb b/support/ruby/collins-client/lib/collins/api/asset.rb index 621f75e7a..5af07af52 100644 --- a/support/ruby/collins-client/lib/collins/api/asset.rb +++ b/support/ruby/collins-client/lib/collins/api/asset.rb @@ -8,6 +8,7 @@ def create! asset_or_tag, options = {} asset = get_asset_or_tag asset_or_tag parameters = { :generate_ipmi => get_option(:generate_ipmi, options, false), + :ipmi_pool => get_option(:ipmi_pool, options, nil), :status => get_option(:status, options, asset.status), :type => get_option(:type, options, asset.type) } diff --git a/support/ruby/collins-shell/lib/collins_shell/asset.rb b/support/ruby/collins-shell/lib/collins_shell/asset.rb index 87ab530bb..96a940c9f 100644 --- a/support/ruby/collins-shell/lib/collins_shell/asset.rb +++ b/support/ruby/collins-shell/lib/collins_shell/asset.rb @@ -20,9 +20,14 @@ def self.banner task, namespace = true, subcommand = false method_option :ipmi, :type => :boolean, :desc => 'Generate IPMI data' method_option :status, :type => :string, :desc => 'Status of asset' method_option :type, :type => :string, :desc => 'Asset type' + method_option :ipmi_pool, :type => :string, :desc => 'IPMI pool' def create call_collins get_collins_client, "create asset" do |client| - asset = client.create! options.tag, :generate_ipmi => options.ipmi, :status => options.status, :type => options.type + asset = client.create!(options.tag, + :generate_ipmi => options.ipmi, + :status => options.status, + :type => options.type, + :ipmi_pool => options.ipmi_pool) print_find_results asset, nil end end diff --git a/test/collins/controllers/AssetApiSpec.scala b/test/collins/controllers/AssetApiSpec.scala index 552588b0a..24da3ecb6 100644 --- a/test/collins/controllers/AssetApiSpec.scala +++ b/test/collins/controllers/AssetApiSpec.scala @@ -54,7 +54,7 @@ class AssetApiSpec extends mutable.Specification with ControllerSpec with Resour result must haveStatus(201) result must haveJsonData.which { s => s must /("data") */ ("ASSET") / ("STATUS" -> "Incomplete") - s must /("data") */ ("IPMI") / ("IPMI_GATEWAY" -> "172.16.32.1") + s must /("data") */ ("IPMI") / ("IPMI_GATEWAY" -> "172.16.16.1") s must /("data") */ ("IPMI") / ("IPMI_NETMASK" -> "255.255.240.0") } } From 657c24280a2edc82cf4dac78d2176b92b8dd6a30 Mon Sep 17 00:00:00 2001 From: Gabe Conradi Date: Mon, 6 Mar 2017 17:42:15 -0500 Subject: [PATCH 12/17] java is deprecated, move to openjdk base image for collins --- Dockerfile | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Dockerfile b/Dockerfile index e02800b07..cbffe9125 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM java:8-jre +FROM openjdk:8-jre MAINTAINER Gabe Conradi # Solr cores should be stored in a volume, so we arent writing stuff to our rootfs @@ -6,7 +6,7 @@ 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 && \ rm -r /var/lib/apt/lists/* && \ cd /build && \ export ACTIVATOR_VERSION=1.3.7 && \ @@ -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 From ef901dc17e946f24c2d8bb0f7356a857e90634df Mon Sep 17 00:00:00 2001 From: Gabe Conradi Date: Mon, 6 Mar 2017 17:42:36 -0500 Subject: [PATCH 13/17] update default docker config to use verbose ipmi pool config --- conf/docker/production.conf | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/conf/docker/production.conf b/conf/docker/production.conf index 2b6d5bbf7..43981019a 100644 --- a/conf/docker/production.conf +++ b/conf/docker/production.conf @@ -222,8 +222,13 @@ ipmi { randomUsername = false username = "root" passwordLength = 16 - network="172.16.32.0/20" - startAddress="172.16.32.20" + defaultPoolName = "IPMI-01" + pools { + "IPMI-01" { + network="172.16.32.0/20" + startAddress="172.16.32.20" + } + } } lshw { From 7b616df31695eb0081d434d4858f01fdde8c4a1e Mon Sep 17 00:00:00 2001 From: Gabe Conradi Date: Tue, 7 Mar 2017 12:37:32 -0500 Subject: [PATCH 14/17] just exception, nothing crazy --- app/collins/models/AssetLifecycle.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/collins/models/AssetLifecycle.scala b/app/collins/models/AssetLifecycle.scala index e5b09be05..83942bc2a 100644 --- a/app/collins/models/AssetLifecycle.scala +++ b/app/collins/models/AssetLifecycle.scala @@ -64,7 +64,7 @@ class AssetLifecycle(user: Option[User], tattler: Tattler) { val asset = Asset.create(Asset(tag, _status, assetType)) val ipmi = generateIpmi match { case true => IpmiInfo.getConfig(ipmiPool) match { - case None => throw new IllegalArgumentException("Invalid IPMI pool %s specified".format(ipmiPool.getOrElse("default"))) + case None => throw new Exception("Invalid IPMI pool %s specified".format(ipmiPool.getOrElse("default"))) case _ => Some(IpmiInfo.createForAsset(asset, ipmiPool)) } case false => None From e474d8a0fe27e8a0e8f2594cf2710a91996855ba Mon Sep 17 00:00:00 2001 From: Gabe Conradi Date: Tue, 7 Mar 2017 12:50:23 -0500 Subject: [PATCH 15/17] fix tests to work properly --- conf/dev_base.conf | 22 ++++++++++++ conf/reference/ipmi_reference.conf | 38 ++++++++++++--------- test/collins/controllers/AssetApiSpec.scala | 2 +- 3 files changed, 44 insertions(+), 18 deletions(-) diff --git a/conf/dev_base.conf b/conf/dev_base.conf index 7f5259a71..fdace6711 100644 --- a/conf/dev_base.conf +++ b/conf/dev_base.conf @@ -243,7 +243,29 @@ ipmi { username = "root" passwordLength = 16 network="172.16.32.0/20" + gateway="172.16.32.1" startAddress="172.16.32.20" + + # to use with multiple OOB allocations, instead of the inline + # declaration of network/gateway/startAddress + # + # defaultPoolName = "OOB-POD01" + # pools { + # OOB-POD01 { + # network="172.16.16.0/20" + # gateway="172.16.16.1" + # startAddress="172.16.16.3" + # } + # OOB-POD02 { + # network="10.102.224.0/20" + # startAddress="10.102.224.2" + # endAddress="10.102.238.254" + # } + # OOB-POD03 { + # network="10.102.32.0/19" + # startAddress="10.102.32.2" + # } + # } } lshw { diff --git a/conf/reference/ipmi_reference.conf b/conf/reference/ipmi_reference.conf index a09117534..12e285e49 100644 --- a/conf/reference/ipmi_reference.conf +++ b/conf/reference/ipmi_reference.conf @@ -2,21 +2,25 @@ ipmi { randomUsername = false username = "root" passwordLength = 16 - defaultPoolName = "OOB-POD01" - pools { - OOB-POD01 { - network="172.16.16.0/20" - gateway="172.16.16.1" - startAddress="172.16.16.3" - } - OOB-POD02 { - network="10.102.224.0/20" - startAddress="10.102.224.2" - endAddress="10.102.238.254" - } - OOB-POD03 { - network="10.102.32.0/19" - startAddress="10.102.32.2" - } - } + network = "10.0.0.0/16" + startAddress = "10.0.0.3" + + # use with multiple OOB allocations + # defaultPoolName = "OOB-POD01" + # pools { + # OOB-POD01 { + # network="172.16.16.0/20" + # gateway="172.16.16.1" + # startAddress="172.16.16.3" + # } + # OOB-POD02 { + # network="10.102.224.0/20" + # startAddress="10.102.224.2" + # endAddress="10.102.238.254" + # } + # OOB-POD03 { + # network="10.102.32.0/19" + # startAddress="10.102.32.2" + # } + # } } diff --git a/test/collins/controllers/AssetApiSpec.scala b/test/collins/controllers/AssetApiSpec.scala index 24da3ecb6..552588b0a 100644 --- a/test/collins/controllers/AssetApiSpec.scala +++ b/test/collins/controllers/AssetApiSpec.scala @@ -54,7 +54,7 @@ class AssetApiSpec extends mutable.Specification with ControllerSpec with Resour result must haveStatus(201) result must haveJsonData.which { s => s must /("data") */ ("ASSET") / ("STATUS" -> "Incomplete") - s must /("data") */ ("IPMI") / ("IPMI_GATEWAY" -> "172.16.16.1") + s must /("data") */ ("IPMI") / ("IPMI_GATEWAY" -> "172.16.32.1") s must /("data") */ ("IPMI") / ("IPMI_NETMASK" -> "255.255.240.0") } } From 961c5aefc48c268058c23d61c2d9b718035b7642 Mon Sep 17 00:00:00 2001 From: Gabe Conradi Date: Tue, 7 Mar 2017 13:28:59 -0500 Subject: [PATCH 16/17] make test use a totally separate base config from dev --- conf/test.conf | 4 +- conf/test_base.conf | 369 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 371 insertions(+), 2 deletions(-) create mode 100644 conf/test_base.conf diff --git a/conf/test.conf b/conf/test.conf index f6b531d46..22c747967 100644 --- a/conf/test.conf +++ b/conf/test.conf @@ -1,7 +1,7 @@ -# THIS IS NOT A PRODUCTION CONFIGURATION - DEVELOPMENT ONLY +# THIS IS NOT A PRODUCTION CONFIGURATION - UNIT TEST ONLY # ~~~~~ -include "dev_base.conf" +include "test_base.conf" solr.enabled=false diff --git a/conf/test_base.conf b/conf/test_base.conf new file mode 100644 index 000000000..fdace6711 --- /dev/null +++ b/conf/test_base.conf @@ -0,0 +1,369 @@ +include "validations.conf" + +# Secret key +# ~~~~~ +# The secret key is used to secure cryptographics functions. +# If you deploy your application to several instances be sure to use the same key! +application.secret="AbFgHx0eJx8lalkja812389uasdlkajsdlka98012398uasdlkasdklajsd81298" +parsers.text.maxLength=1048576 +# evolutionplugin should be disabled in a production environment +#evolutionplugin=disabled + +# Do not change the crypto key after you have started using it, you will be unable to decrypt data +crypto.key="lkkajsdlkajsdlkajsdlkajsdlkajsdlkajsdlkajsdlkajsdlkajsdlkajsdla" + +features { + encryptedTags = [ SYSTEM_PASSWORD, LOCATION ] + noLogPurges = [] + useWhitelistOnRepurpose = true + keepSomeMetaOnRepurpose = [ attr1, attr2 ] + deleteSomeMetaOnRepurpose = [ attr4, attr3 ] + syslogAsset = tumblrtag1 + # default values below + #searchResultColumns = [ TAG, HOSTNAME, PRIMARY_ROLE, STATUS, CREATED, UPDATED ] +} + +views { + + enabled = true + + frames = { + giphy { + enabled = true + title = "Giphy" + style = "width: 100%;height: 1200px;" + script = """ + function isEnabled(asset) { + return true; + } + + function getUrl(asset) { + return "http://giphy.com/search/tumblr-feat"; + } + """ + } + } +} + +graph { + enabled = true + FibrGraphs { + customMetrics { + mysqlHosts { + selector = "PRIMARY_ROLE = \"DATABASE\"" + metrics = [ + MYSQL/COMMANDS, + MYSQL/SLOW, + MYSQL/THREADS, + MYSQL/NET + ] + } + + memcacheHosts { + selector = "POOL = MEMCACHE*" + metrics = [ + MEMCACHE/COMMANDS, + MEMCACHE/EVICTS, + MEMCACHE/ITEMS, + MEMCACHE/HITRATIO + ] + } + } + } +} + +multicollins { + enabled=true + thisInstance = "localhost" +} + +nodeclassifier.sortKeys = [SL_RACK_POSITION, SL_RACK] #ordered from least significant to most + +powerconfiguration.unitsRequired=2 + +callbacks.registry { + + nowProvisioned { + on = "asset_update" + + when { + previous.state = "isProvisioning" + current.state = "isProvisioned" + } + + action { + type = exec + command = "echo " + } + } + + provisionEvent { + on = "asset_update" + when { + #current.state = "isProvisioning" + current.state = "isNew" + } + action { + type = exec + command = [ + printf, "asset updated: %s %s\\n", "","" + ] + } + } + onCreate { + on = "asset_create" + + action { + type = exec + command = [ + printf, + "onCreate - %s - %s\\n", + "", + "" + ] + } + } + + hardwareProblem { + on = "asset_update" + when { + previous.state = "!isMaintenance" + current.state = "isMaintenance" + current.states = [ + IPMI_PROBLEM, + HARDWARE_PROBLEM, + HARDWARE_UPGRADE + ] + } + action { + type = exec + command = [ + printf, + "hardwareProblem - %s\\n", + "" + ] + } + } +} + +softlayer.allowedCancelStatus=[Unallocated, Allocated, Maintenance] +softlayer.cancelRequestTimeout=10 seconds +softlayer.activationRequestTimeout=10 seconds + +tagdecorators { + templates { + search = "{value}" + } + decorators { + STATS_LINKS { + decorator="{i.label}" + valueParser="collins.util.views.DelimiterParser" + delimiter=" " + between=" - " + 0.label="Thrift" + 1.label="HTTP" + } + TUMBLR_SHA.decorator="{value}" + CONFIG_SHA.decorator="{value}" + POWER_PORT.decorator=${tagdecorators.templates.search} + RACK_POSITION { + decorator = ${tagdecorators.templates.search} + valueParser = "collins.util.views.DelimiterParser" + delimiter = "-" + between = "-" + } + SL_ROOM.decorator=${tagdecorators.templates.search} + SL_RACK.decorator=${tagdecorators.templates.search} + SL_RACK_POSITION.decorator=${tagdecorators.templates.search} + IP_ADDRESS { + decorator="{value}" + between=", " + } + } +} + +# Provisioner Plugin +provisioner.enabled=true +provisioner.profiles="test/resources/profiles.yaml" +provisioner.rate="1/10 seconds" +provisioner.checkCommand="/usr/bin/true" +provisioner.checkCommandTimeout=5 seconds +provisioner.command="/usr/bin/true" +provisioner.commandTimeout=5 seconds + +# Power Management Plugin +powermanagement { + enabled = true + command_template = "ipmitool -H -U -P -I lan -L OPERATOR" + + commands { + powerOff = ${powermanagement.command_template}" chassis power off" + powerOn = ${powermanagement.command_template}" chassis power on" + powerSoft = ${powermanagement.command_template}" chassis power soft" + powerState = ${powermanagement.command_template}" chassis power status" + rebootHard = ${powermanagement.command_template}" chassis power cycle" + rebootSoft = ${powermanagement.command_template}" chassis power reset" + identify = ${powermanagement.command_template}" chassis identify " + verify = "ping -c 3 " + } +} + +intake.params = [] + +# IP Address Allocation +# Ip Address Allocation Pools +ipAddresses { + pools { + AAA { + network="172.16.4.0/28" + startAddress="172.16.4.4" + } + ADMIN-OPS { + network="172.16.56.0/24" + startAddress="172.16.56.5" + } + DEV { + network="172.16.5.0/24" + startAddress="172.16.5.4" + } + WEB-EDIT { + network="172.16.64.0/24" + startAddress="172.16.64.5" + } + WEB-SECURE { + network="172.16.73.0/28" + startAddress="172.16.73.5" + } + + } +} + +ipmi { + randomUsername = false + username = "root" + passwordLength = 16 + network="172.16.32.0/20" + gateway="172.16.32.1" + startAddress="172.16.32.20" + + # to use with multiple OOB allocations, instead of the inline + # declaration of network/gateway/startAddress + # + # defaultPoolName = "OOB-POD01" + # pools { + # OOB-POD01 { + # network="172.16.16.0/20" + # gateway="172.16.16.1" + # startAddress="172.16.16.3" + # } + # OOB-POD02 { + # network="10.102.224.0/20" + # startAddress="10.102.224.2" + # endAddress="10.102.238.254" + # } + # OOB-POD03 { + # network="10.102.32.0/19" + # startAddress="10.102.32.2" + # } + # } +} + +lshw { + flashProducts = ["fusionio", "tachION", "flashmax"] + flashSize="1400000000000" + + # For assets whose NIC capacity cannot be determined + # Omit this default to instead raise an exception when capacity missing + lshw.defaultNicCapacity=10000000000 +} + +include "authentication.conf" + +# Set logging properties in logger.xml or dev_logger.xml + +querylog { + enabled = false + prefix = "QUERY: " + includeResults = false + frontendLogging = true +} + +solr { + enabled = true + repopulateOnStartup = false + useEmbeddedServer = true + externalUrl="http://localhost:8983/solr" + embeddedSolrHome = "conf/solr/" +} + +# Thread pool Configuration +# ~~~~~ + +# https://www.playframework.com/documentation/2.1.x/ThreadPools +# iteratee thread pool for play framework +iteratee-threadpool-size = 12 + +# internal thread pool for play framework threads +internal-threadpool-size = 12 + +# play default thread pool +# Accessible under play-akka.actor.default-dispatcher +play { + akka { + actor { + default-dispatcher = { + fork-join-executor { + parallelism-factor = 1.0 + parallelism-max = 24 + } + } + } + } +} + +# Akka System Configuration +# ~~~~~ + +# Akka system used internally by Collins +akka { + actor { + default-dispatcher = { + fork-join-executor { + parallelism-factor = 1.0 + parallelism-max = 24 + } + } + + deployment = { + /background-processor = { + dispatcher = default-dispatcher + router = round-robin + nr-of-instances = 128 + } + + /solr_asset_updater = { + dispatcher = default-dispatcher + router = round-robin + nr-of-instances = 1 + } + + /solr_asset_log_updater = { + dispatcher = default-dispatcher + router = round-robin + nr-of-instances = 1 + } + + /change_queue_processor = { + dispatcher = default-dispatcher + router = round-robin + nr-of-instances = 1 + } + + /firehose_processor = { + dispatcher = default-dispatcher + router = round-robin + nr-of-instances = 1 + } + } + } +} From baeeccd25a085bcefcc8030e0b5548b4d4addeb7 Mon Sep 17 00:00:00 2001 From: Gabe Conradi Date: Tue, 7 Mar 2017 14:18:38 -0500 Subject: [PATCH 17/17] fix properly throwing exception --- app/collins/models/AssetLifecycle.scala | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/app/collins/models/AssetLifecycle.scala b/app/collins/models/AssetLifecycle.scala index 83942bc2a..e50ea8bd6 100644 --- a/app/collins/models/AssetLifecycle.scala +++ b/app/collins/models/AssetLifecycle.scala @@ -60,14 +60,16 @@ class AssetLifecycle(user: Option[User], tattler: Tattler) { 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 => IpmiInfo.getConfig(ipmiPool) match { - case None => throw new Exception("Invalid IPMI pool %s specified".format(ipmiPool.getOrElse("default"))) - case _ => Some(IpmiInfo.createForAsset(asset, ipmiPool)) - } - 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)