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 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/IpmiApi.scala b/app/collins/controllers/IpmiApi.scala index d544823f7..25e3334a8 100644 --- a/app/collins/controllers/IpmiApi.scala +++ b/app/collins/controllers/IpmiApi.scala @@ -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) @@ -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 { @@ -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 { 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/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 19a716979..e50ea8bd6 100644 --- a/app/collins/models/AssetLifecycle.scala +++ b/app/collins/models/AssetLifecycle.scala @@ -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) 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 a7897272c..386567cf4 100644 --- a/app/collins/models/IpmiInfo.scala +++ b/app/collins/models/IpmiInfo.scala @@ -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) @@ -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 = { diff --git a/app/collins/models/shared/IpAddressable.scala b/app/collins/models/shared/IpAddressable.scala index 99d2d82b7..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 - 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/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/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 { diff --git a/conf/reference/ipmi_reference.conf b/conf/reference/ipmi_reference.conf index 73af5f3fe..12e285e49 100644 --- a/conf/reference/ipmi_reference.conf +++ b/conf/reference/ipmi_reference.conf @@ -1,9 +1,26 @@ ipmi { - - passwordLength = 12 randomUsername = false username = "root" + passwordLength = 16 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/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) 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 + } + } + } +} 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/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-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 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/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/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/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/support/ruby/collins-shell/lib/collins_shell/ipmi.rb b/support/ruby/collins-shell/lib/collins_shell/ipmi.rb index 9ff2f3add..900e86324 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, :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 + 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) 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(