Revision 0f40d6a8
Added by Vincent MEMBRÉ almost 7 years ago
inventory-api/src/main/scala/com/normation/inventory/domain/AgentTypes.scala | ||
---|---|---|
*
|
||
*/
|
||
sealed abstract class AgentType {
|
||
def toString() : String
|
||
def fullname() : String = "CFEngine "+this
|
||
def toString : String
|
||
def fullname : String = "CFEngine "+this
|
||
// Tag used in fusion inventory ( > 2.3 )
|
||
lazy val tagValue = s"cfengine-${this}".toLowerCase
|
||
def toRulesPath() : String
|
||
def toRulesPath : String
|
||
|
||
// the name to look for in the inventory to know the agent version
|
||
def inventorySoftwareName: String
|
||
// and a transformation function from reported software version name to agent version name
|
||
def toAgentVersionName(softwareVersionName: String): String
|
||
}
|
||
|
||
final case object NOVA_AGENT extends AgentType with HashcodeCaching {
|
||
override def toString() = A_NOVA_AGENT
|
||
override def toRulesPath() = "/cfengine-nova"
|
||
override val inventorySoftwareName = "cfengine nova"
|
||
override def toAgentVersionName(softwareVersionName: String) = s"cfe-${softwareVersionName}"
|
||
}
|
||
|
||
final case object COMMUNITY_AGENT extends AgentType with HashcodeCaching {
|
||
override def toString() = A_COMMUNITY_AGENT
|
||
override def toRulesPath() = "/cfengine-community"
|
||
override val inventorySoftwareName = "rudder-agent"
|
||
override def toAgentVersionName(softwareVersionName: String) = softwareVersionName
|
||
}
|
||
object AgentType {
|
||
|
||
final case object DSC_AGENT extends AgentType with HashcodeCaching {
|
||
override def toString() = A_DSC_AGENT
|
||
override def toRulesPath() = "/dsc"
|
||
override val inventorySoftwareName = "Rudder agent"
|
||
override def toAgentVersionName(softwareVersionName: String) = softwareVersionName+" (dsc)"
|
||
}
|
||
final case object CfeEnterprise extends AgentType with HashcodeCaching {
|
||
override def toString = A_NOVA_AGENT
|
||
override def toRulesPath = "/cfengine-nova"
|
||
override val inventorySoftwareName = "cfengine nova"
|
||
override def toAgentVersionName(softwareVersionName: String) = s"cfe-${softwareVersionName}"
|
||
}
|
||
|
||
object AgentType {
|
||
def allValues = NOVA_AGENT :: COMMUNITY_AGENT :: DSC_AGENT :: Nil
|
||
final case object CfeCommunity extends AgentType with HashcodeCaching {
|
||
override def toString = A_COMMUNITY_AGENT
|
||
override def toRulesPath = "/cfengine-community"
|
||
override val inventorySoftwareName = "rudder-agent"
|
||
override def toAgentVersionName(softwareVersionName: String) = softwareVersionName
|
||
}
|
||
|
||
final case object Dsc extends AgentType with HashcodeCaching {
|
||
override def toString = A_DSC_AGENT
|
||
override def toRulesPath = "/dsc"
|
||
override val inventorySoftwareName = "Rudder agent"
|
||
override def toAgentVersionName(softwareVersionName: String) = softwareVersionName+" (dsc)"
|
||
}
|
||
|
||
def allValues = CfeEnterprise :: CfeCommunity :: Dsc :: Nil
|
||
|
||
def fromValue(value : String) : Box[AgentType] = {
|
||
// Check if the value is correct compared to the agent tag name (fusion > 2.3) or its toString value (added by CFEngine)
|
||
... | ... | |
final case class AgentVersion(value: String)
|
||
|
||
final case class AgentInfo(
|
||
name : AgentType
|
||
agentType : AgentType
|
||
//for now, the version must be an option, because we don't add it in the inventory
|
||
//and must try to find it from packages
|
||
, version: Option[AgentVersion]
|
||
, version : Option[AgentVersion]
|
||
, securityToken : SecurityToken
|
||
)
|
||
|
||
object AgentInfoSerialisation {
|
||
import net.liftweb.json.JsonDSL._
|
||
import AgentType._
|
||
|
||
import net.liftweb.json._
|
||
|
||
implicit class ToJson(agent: AgentInfo) {
|
||
|
||
def toJsonString = compactRender(
|
||
("agentType" -> agent.name.toString())
|
||
~ ("version" -> agent.version.map( _.value ))
|
||
)
|
||
def toJsonString =
|
||
compactRender(
|
||
("agentType" -> agent.agentType.toString())
|
||
~ ("version" -> agent.version.map( _.value ))
|
||
~ ("securityToken" ->
|
||
("value" -> agent.securityToken.key)
|
||
~ ("type" -> SecurityToken.kind(agent.securityToken))
|
||
)
|
||
)
|
||
}
|
||
|
||
def parseSecurityToken(agentType : AgentType, tokenJson: JValue, tokenDefault : Option[String]) : Box[SecurityToken]= {
|
||
import net.liftweb.json.compactRender
|
||
|
||
def extractValue(tokenJson : JValue) = {
|
||
tokenJson \ "value" match {
|
||
case JString(s) => Some(s)
|
||
case _ => None
|
||
}
|
||
}
|
||
|
||
agentType match {
|
||
case Dsc => tokenJson \ "type" match {
|
||
case JString(Certificate.kind) => extractValue(tokenJson) match {
|
||
case Some(token) => Full(Certificate(token))
|
||
case None => Failure("No value defined for security token")
|
||
}
|
||
case JString(PublicKey.kind) => Failure("Cannot have a public Key for dsc agent, only a certificate is valid")
|
||
case JNothing => Failure("No value define for security token")
|
||
case invalidJson => Failure(s"Invalid value for security token, ${compactRender(invalidJson)}")
|
||
}
|
||
case _ => tokenJson \ "type" match {
|
||
case JString(Certificate.kind) => extractValue(tokenJson) match {
|
||
case Some(token) => Full(Certificate(token))
|
||
case None => Failure("No value defined for security token")
|
||
}
|
||
case JString(PublicKey.kind) => extractValue(tokenJson) match {
|
||
case Some(token) => Full(PublicKey(token))
|
||
case None => Failure("No value defined for security token")
|
||
}
|
||
case invalidJson =>
|
||
tokenDefault match {
|
||
case Some(default) => Full(PublicKey(default))
|
||
case None => Failure(s"Invalid value for security token, ${compactRender(invalidJson)}, and no public key were stored")
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
/*
|
||
... | ... | |
* but version isn't, and even if we don't parse it correctly, we
|
||
* successfully return an agent (without version).
|
||
*/
|
||
def parseJson(s: String): Box[AgentInfo] = {
|
||
def parseJson(s: String, optToken : Option[String]): Box[AgentInfo] = {
|
||
for {
|
||
json <- try { Full(parse(s)) } catch { case ex: Exception => Failure(s"Can not parse agent info: ${ex.getMessage }", Full(ex), Empty) }
|
||
info <- (json \ "agentType") match {
|
||
case JString(tpe) => AgentType.fromValue(tpe).map { agentType =>
|
||
(json \ "version") match {
|
||
case JString(version) => AgentInfo(agentType, Some(AgentVersion(version)))
|
||
case _ => AgentInfo(agentType, None)
|
||
}
|
||
}
|
||
case _ => Failure(s"Error when trying to parse string as JSON Agent Info (missing required field 'agentType'): ${s}")
|
||
agentType <- (json \ "agentType") match {
|
||
case JString(tpe) => AgentType.fromValue(tpe)
|
||
case JNothing => Failure("No value defined for security token")
|
||
case invalidJson => Failure(s"Error when trying to parse string as JSON Agent Info (missing required field 'agentType'): ${compactRender(invalidJson)}")
|
||
}
|
||
agentVersion = json \ "version" match {
|
||
case JString(version) => Some(AgentVersion(version))
|
||
case _ => None
|
||
}
|
||
token <- json \ "securityToken" match {
|
||
case JObject(json) => parseSecurityToken(agentType, json, optToken)
|
||
case _ => parseSecurityToken(agentType, JNothing, optToken)
|
||
}
|
||
|
||
} yield {
|
||
info
|
||
AgentInfo(agentType, agentVersion, token)
|
||
}
|
||
}
|
||
|
||
... | ... | |
* - try to parse in json: if ok, we have the new version
|
||
* - else, try to parse in old format, put None to version.
|
||
*/
|
||
def parseCompatNonJson(s: String): Box[AgentInfo] = {
|
||
parseJson(s).or(
|
||
def parseCompatNonJson(s: String, optToken : Option[String]): Box[AgentInfo] = {
|
||
parseJson(s, optToken).or(
|
||
for {
|
||
tpe <- AgentType.fromValue(s) ?~! (
|
||
s"Error when mapping '${s}' to an agent info. We are expecting either a json like "+
|
||
agentType <- AgentType.fromValue(s) ?~! (
|
||
s"Error when mapping '${s}' to an agent info. We are expecting either a json like "+
|
||
s"{'agentType': type, 'version': opt_version}, or an agentType with allowed values in ${AgentType.allValues.mkString(", ")}"
|
||
)
|
||
token <- parseSecurityToken(agentType, JNothing, optToken)
|
||
} yield {
|
||
AgentInfo(tpe, None)
|
||
AgentInfo(agentType, None, token)
|
||
}
|
||
)
|
||
}
|
inventory-api/src/main/scala/com/normation/inventory/domain/DataTypes.scala | ||
---|---|---|
|
||
package com.normation.inventory.domain
|
||
|
||
|
||
import com.normation.utils.Utils._
|
||
import com.normation.utils.HashcodeCaching
|
||
import org.bouncycastle.openssl.PEMParser
|
||
... | ... | |
import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo
|
||
import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter
|
||
import net.liftweb.common._
|
||
import org.bouncycastle.cert.X509CertificateHolder
|
||
|
||
/**
|
||
* A file that contains all the simple data types, like Version,
|
||
* MemorySize, Manufacturer, etc.
|
||
*/
|
||
|
||
|
||
/**
|
||
* A simple class to denote a manufacturer
|
||
* TODO : Should be merge with SoftwareEditor
|
||
... | ... | |
*/
|
||
final case class SoftwareEditor(val name:String) extends HashcodeCaching { assert(!isEmpty(name)) }
|
||
|
||
sealed trait SecurityToken {
|
||
def key : String
|
||
}
|
||
|
||
case object SecurityToken {
|
||
def kind(token : SecurityToken) = {
|
||
token match {
|
||
case _: PublicKey => PublicKey.kind
|
||
case _: Certificate => Certificate.kind
|
||
}
|
||
}
|
||
|
||
}
|
||
|
||
object PublicKey {
|
||
val kind = "publicKey"
|
||
}
|
||
object Certificate {
|
||
val kind = "certificate"
|
||
}
|
||
/**
|
||
* A simple class to denote a software cryptographic public key
|
||
*/
|
||
final case class PublicKey(value : String) extends HashcodeCaching { assert(!isEmpty(value))
|
||
final case class PublicKey(value : String) extends SecurityToken with HashcodeCaching { assert(!isEmpty(value))
|
||
|
||
// Value of the key may be stored (with old fusion inventory version) as one line and without rsa header and footer, we should add them if missing and format the key
|
||
val key = {
|
||
... | ... | |
|
||
}
|
||
|
||
final case class Certificate(value : String) extends SecurityToken with HashcodeCaching { assert(!isEmpty(value))
|
||
|
||
// Value of the key may be stored (with old fusion inventory version) as one line and without rsa header and footer, we should add them if missing and format the key
|
||
val key = {
|
||
if (value.startsWith("-----BEGIN CERTIFICATE-----")) {
|
||
value
|
||
} else {
|
||
s"""-----BEGIN CERTIFICATE-----
|
||
|${value.grouped(80).mkString("\n")}
|
||
|-----END CERTIFICATE-----""".stripMargin
|
||
}
|
||
}
|
||
def cert : Box[X509CertificateHolder] = {
|
||
try {
|
||
val reader = new PEMParser(new StringReader(key))
|
||
reader.readObject() match {
|
||
case a : X509CertificateHolder =>
|
||
Full(a)
|
||
case _ => Failure(s"Key '${key}' cannot be parsed as a valid certificate")
|
||
}
|
||
} catch {
|
||
case e:Exception => Failure(s"Key '${key}' cannot be parsed as a valid certificate")
|
||
}
|
||
}
|
||
|
||
}
|
||
|
||
/**
|
||
* A simple class to denote version
|
inventory-api/src/main/scala/com/normation/inventory/domain/NodeInventory.scala | ||
---|---|---|
, totalSpace : Option[MemorySize] = None
|
||
) extends NodeElement with HashcodeCaching
|
||
|
||
|
||
case class Network (
|
||
name : String
|
||
, description : Option[String] = None
|
||
... | ... | |
}
|
||
}
|
||
|
||
|
||
sealed trait OsType {
|
||
def kernelName : String
|
||
def name : String //name is normalized and not destined to be printed - use localization for that
|
||
... | ... | |
case object UnknownBsdType extends BsdType with HashcodeCaching { val name = "UnknownBSD" }
|
||
case object FreeBSD extends BsdType with HashcodeCaching { val name = "FreeBSD" }
|
||
|
||
|
||
/**
|
||
* The different OS type. For now, we know:
|
||
* - Linux
|
||
... | ... | |
, override val kernelVersion : Version = new Version("N/A")
|
||
) extends OsDetails(UnknownOSType, fullName, version, servicePack, kernelVersion) with HashcodeCaching
|
||
|
||
|
||
case class Linux(
|
||
override val os : OsType
|
||
, override val fullName : String
|
||
... | ... | |
, productId : Option[String] = None
|
||
) extends OsDetails(os, fullName, version, servicePack, kernelVersion) with HashcodeCaching
|
||
|
||
|
||
|
||
|
||
case class NodeSummary(
|
||
id : NodeId
|
||
, status:InventoryStatus
|
||
... | ... | |
, archDescription : Option[String] = None
|
||
, lastLoggedUser : Option[String] = None
|
||
, lastLoggedUserTime : Option[DateTime] = None
|
||
, agents : Seq[AgentInfo] = Seq()
|
||
, publicKeys : Seq[PublicKey] = Seq()
|
||
, serverIps : Seq[String] = Seq()
|
||
, agents : Seq[AgentInfo] = Seq()
|
||
, serverIps : Seq[String] = Seq()
|
||
, machineId : Option[(MachineUuid,InventoryStatus)] = None //if we want several ids, we would have to ass an "alternate machine" field
|
||
, softwareIds : Seq[SoftwareUuid] = Seq()
|
||
, accounts : Seq[String] = Seq()
|
inventory-api/src/main/scala/com/normation/inventory/services/provisionning/CheckInventoryDigest.scala | ||
---|---|---|
import org.apache.commons.io.IOUtils
|
||
import javax.xml.bind.DatatypeConverter
|
||
import com.normation.inventory.services.core.ReadOnlyFullInventoryRepository
|
||
import com.normation.inventory.domain._
|
||
|
||
|
||
import com.normation.inventory.domain.{ PublicKey => AgentKey, _ }
|
||
|
||
/**
|
||
* We are using a simple date structure that handle the digest file
|
||
... | ... | |
|
||
trait CheckInventoryDigest {
|
||
|
||
|
||
/**
|
||
* Here, we want to calculate the digest. The good library for that is most likely
|
||
* bouncy castle: https://www.bouncycastle.org/
|
||
*/
|
||
def check(pubKey: PublicKey, digest: InventoryDigest, inventoryStream: InputStream): Box[Boolean] = {
|
||
try {
|
||
val signature = Signature.getInstance("SHA512withRSA", "BC");
|
||
signature.initVerify(pubKey);
|
||
val data = IOUtils.toByteArray(inventoryStream)
|
||
signature.update(data);
|
||
digest match {
|
||
case InventoryDigestV1(_,digest) =>
|
||
val sig = DatatypeConverter.parseHexBinary(digest)
|
||
|
||
Full(signature.verify(sig))
|
||
}
|
||
} catch {
|
||
case e : Exception =>
|
||
Failure(e.getMessage())
|
||
def check(securityToken: SecurityToken, digest: InventoryDigest, inventoryStream: InputStream): Box[Boolean] = {
|
||
securityToken match {
|
||
case rudderKey : com.normation.inventory.domain.PublicKey =>
|
||
rudderKey.publicKey match {
|
||
case Full(pubKey) =>
|
||
try {
|
||
val signature = Signature.getInstance("SHA512withRSA", "BC");
|
||
signature.initVerify(pubKey);
|
||
val data = IOUtils.toByteArray(inventoryStream)
|
||
signature.update(data);
|
||
digest match {
|
||
case InventoryDigestV1(_,digest) =>
|
||
val sig = DatatypeConverter.parseHexBinary(digest)
|
||
|
||
Full(signature.verify(sig))
|
||
}
|
||
} catch {
|
||
case e : Exception =>
|
||
Failure(e.getMessage())
|
||
}
|
||
case eb : EmptyBox =>
|
||
eb
|
||
}
|
||
|
||
// We don't sign with certificate for now
|
||
case _ : Certificate => Full(true)
|
||
}
|
||
}
|
||
|
||
... | ... | |
*/
|
||
trait GetKey {
|
||
|
||
def getKey (receivedInventory : InventoryReport) : Box[(PublicKey, KeyStatus)]
|
||
def getKey (receivedInventory : InventoryReport) : Box[(SecurityToken, KeyStatus)]
|
||
|
||
}
|
||
|
||
... | ... | |
* either an inventory has already been treated before, it will look into ldap repository
|
||
* or if there was no inventory before, it will look for the key in the received inventory
|
||
*/
|
||
def getKey (receivedInventory : InventoryReport) : Box[(PublicKey, KeyStatus)] = {
|
||
def getKey (receivedInventory : InventoryReport) : Box[(SecurityToken, KeyStatus)] = {
|
||
|
||
def extractKey (node : NodeInventory) : Box[(PublicKey)]= {
|
||
def extractKey (node : NodeInventory) : Box[SecurityToken]= {
|
||
for {
|
||
cfengineKey <- Box(node.publicKeys.headOption) ?~! "There is no public key in inventory"
|
||
publicKey <- cfengineKey.publicKey
|
||
agent <- Box(node.agents.headOption) ?~! "There is no public key in inventory"
|
||
} yield {
|
||
publicKey
|
||
agent.securityToken
|
||
}
|
||
}
|
||
|
||
... | ... | |
val status = storedInventory.node.main.keyStatus
|
||
val inventory : NodeInventory = status match {
|
||
case UndefinedKey =>
|
||
storedInventory.node.publicKeys.headOption match {
|
||
storedInventory.node.agents.map(_.securityToken).headOption match {
|
||
case None =>
|
||
// There is no key and status is undefined, use received key
|
||
receivedInventory.node
|
||
case Some(key) =>
|
||
key.publicKey match {
|
||
// Stored key is valid, use it !
|
||
case Full(_) => storedInventory.node
|
||
// Key stored is not valid and status is undefined try received key,
|
||
// There treat the case of the bootstrapped key for rudder root server
|
||
case _ => receivedInventory.node
|
||
case Some(securityToken) =>
|
||
securityToken match {
|
||
case key : AgentKey =>
|
||
key.publicKey match {
|
||
// Stored key is valid, use it !
|
||
case Full(_) => storedInventory.node
|
||
// Key stored is not valid and status is undefined try received key,
|
||
// There treat the case of the bootstrapped key for rudder root server
|
||
case _ => receivedInventory.node
|
||
}
|
||
case cert : Certificate =>
|
||
// We don't sign inventory with cert for now, use sorted one
|
||
storedInventory.node
|
||
}
|
||
}
|
||
// Certified node always use stored inventory key
|
inventory-fusion/src/main/scala/com/normation/inventory/provisioning/fusion/FusionReportUnmarshaller.scala | ||
---|---|---|
package fusion
|
||
|
||
import com.normation.inventory.domain._
|
||
import com.normation.inventory.domain.AgentType._
|
||
import com.normation.inventory.provisioning.fusion._
|
||
import java.io.InputStream
|
||
import org.joda.time.DateTime
|
||
... | ... | |
|
||
// as a temporary solution, we are getting information from packages
|
||
|
||
val versions = {
|
||
def findAgent(software: Seq[Software], agentType: AgentType) = (
|
||
software.find(p => p.name.getOrElse("")
|
||
.toLowerCase.contains(agentType.inventorySoftwareName ))
|
||
.flatMap(s => s.version.map(v => AgentVersion(agentType.toAgentVersionName(v.value))))
|
||
)
|
||
Map[AgentType, Option[AgentVersion]](
|
||
// for nova, we get the cfengine version, which not exactly what we want, but still better than nothing
|
||
(NOVA_AGENT , findAgent(report.applications, NOVA_AGENT))
|
||
// for community, we only want rudder-agent version
|
||
, (COMMUNITY_AGENT , findAgent(report.applications, COMMUNITY_AGENT))
|
||
)
|
||
}
|
||
def findAgent(software: Seq[Software], agentType: AgentType) = (
|
||
software.find(p => p.name.getOrElse("")
|
||
.toLowerCase.contains(agentType.inventorySoftwareName ))
|
||
.flatMap(s => s.version.map(v => AgentVersion(agentType.toAgentVersionName(v.value))))
|
||
)
|
||
|
||
(xml \\ "RUDDER").headOption match {
|
||
case Some(rudder) =>
|
||
// Fetch all the agents configuration
|
||
val agents = ((rudder \\ "AGENT").map { agentXML =>
|
||
val agents = (rudder \\ "AGENT").flatMap { agentXML =>
|
||
val agent = for {
|
||
agentName <- boxFromOption(optText(agentXML \ "AGENT_NAME"), "could not parse agent name (tag AGENT_NAME) from rudder specific inventory")
|
||
agentType <- (AgentType.fromValue(agentName))
|
||
|
||
rootUser <- boxFromOption(optText(agentXML \\ "OWNER") ,"could not parse rudder user (tag OWNER) from rudder specific inventory")
|
||
policyServerId <- boxFromOption(optText(agentXML \\ "POLICY_SERVER_UUID") ,"could not parse policy server id (tag POLICY_SERVER_UUID) from specific inventory")
|
||
|
||
optCert = optText(agentXML \ "AGENT_CERT")
|
||
optKey = optText(agentXML \ "AGENT_KEY").orElse(optText(agentXML \ "CFENGINE_KEY"))
|
||
securityToken : SecurityToken <- agentType match {
|
||
case Dsc => optCert match {
|
||
case Some(cert) => Full(Certificate(cert))
|
||
case None => Failure("could not parse agent certificate (tag AGENT_CERT), which is mandatory for dsc agent")
|
||
}
|
||
case _ =>
|
||
(optCert,optKey) match {
|
||
case (Some(cert),_) => Full(Certificate(cert))
|
||
case (None,Some(key)) => Full(PublicKey(key))
|
||
case (None,None) => Failure("could not parse agent security Token (tag AGENT_KEY/CFENGINE_KEY/AGENT_CERT), which is mandatory for cfengine agent")
|
||
}
|
||
}
|
||
|
||
} yield {
|
||
//cfkey is not mandatory
|
||
val agentKey = optText(agentXML \ "AGENT_KEY").orElse(optText(agentXML \ "CFENGINE_KEY"))
|
||
(agentType, rootUser, policyServerId, agentKey)
|
||
|
||
val version = findAgent(report.applications,agentType)
|
||
|
||
(AgentInfo(agentType,version,securityToken), rootUser, policyServerId)
|
||
}
|
||
agent match {
|
||
case eb: EmptyBox =>
|
||
... | ... | |
e
|
||
case Full(x) => Full(x)
|
||
}
|
||
}).flatten
|
||
}
|
||
|
||
( for {
|
||
agentOK <- if(agents.size < 1) {
|
||
... | ... | |
|
||
policyServerId <- uniqueValueInSeq(agents.map(_._3), "could not parse policy server id (tag POLICY_SERVER_UUID) from specific inventory")
|
||
} yield {
|
||
val keys = agents.map{case (_,_,_,key) => key.map(PublicKey)}.flatten
|
||
|
||
report.copy (
|
||
node = report.node.copy (
|
||
... | ... | |
, policyServerId = NodeId(policyServerId)
|
||
, id = NodeId(uuid)
|
||
)
|
||
, agents = agents.map(t => AgentInfo(t._1, versions.get(t._1).flatten))
|
||
, publicKeys = keys
|
||
, agents = agents.map(_._1)
|
||
)
|
||
)
|
||
} ) match {
|
inventory-fusion/src/main/scala/com/normation/inventory/provisioning/fusion/RudderParsingRules.scala | ||
---|---|---|
import java.security.MessageDigest
|
||
import com.normation.utils.UuidRegex
|
||
import com.normation.inventory.provisioning.fusion.OptText.optText
|
||
import com.normation.inventory.domain.AgentType.Dsc
|
||
|
||
/**
|
||
* Special handling of some tags.
|
||
... | ... | |
}
|
||
}
|
||
|
||
|
||
/**
|
||
* <PROCESSORS>
|
||
*
|
||
... | ... | |
}
|
||
}
|
||
|
||
/**
|
||
* <CFKEY>
|
||
*/
|
||
class RudderPublicKeyParsing(keyNormalizer:PrintedKeyNormalizer) extends FusionReportParsingExtension {
|
||
override def isDefinedAt(x:(Node,InventoryReport)) = { x._1.label == "CFKEY" }
|
||
override def apply(x:(Node,InventoryReport)) : InventoryReport = {
|
||
optText(x._1) match {
|
||
case None => x._2
|
||
case Some(key) =>
|
||
keyNormalizer(key) match {
|
||
case "" => x._2 //we have an empty key !
|
||
case k => x._2.copy( node = x._2.node.copy( publicKeys = (new PublicKey(key) +: x._2.node.publicKeys ) ) )
|
||
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* <USER>
|
||
*/
|
||
... | ... | |
}
|
||
}
|
||
|
||
|
||
/**
|
||
* <AGENTSNAME>
|
||
*/
|
||
... | ... | |
x._2.copy( node = x._2.node.copy( agents = x._2.node.agents ++ processAgentName(x._1) ) )
|
||
}
|
||
def processAgentName(xml:NodeSeq) : Seq[AgentInfo] = {
|
||
(xml \ "AGENTNAME").flatMap(e => optText(e).flatMap( a =>
|
||
AgentType.fromValue(a) match {
|
||
case Full(x) => Full(AgentInfo(x, None))
|
||
case e:EmptyBox =>
|
||
logger.error("Ignore agent type '%s': unknown value. Authorized values are %s".format(a, AgentType.allValues.mkString(", ")))
|
||
|
||
val keys = (xml \ "CFKEY").map { optText }
|
||
val agents = (xml \ "AGENTNAME").map( optText)
|
||
|
||
agents.zipAll(keys, None , None).flatMap{
|
||
case (None,None) => Empty
|
||
case (None,Some(_)) => Empty
|
||
// A key but no agent, skip
|
||
case (Some(agent), None) =>
|
||
// No key error
|
||
logger.error(s"No key for agent ${agent} defined, a key is mandatory")
|
||
Empty
|
||
case (Some(agent),Some(key)) =>
|
||
AgentType.fromValue(agent) match {
|
||
case Full(Dsc) =>
|
||
Failure("Dsc agent cannot be added using AGENTNAME tag")
|
||
case Full(agent) =>
|
||
Full(AgentInfo(agent, None, PublicKey(key)))
|
||
case e:EmptyBox =>
|
||
logger.error("Ignore agent type '%s': unknown value. Authorized values are %s".format(agent, AgentType.allValues.mkString(", ")))
|
||
Empty
|
||
}
|
||
}
|
||
) )
|
||
}
|
||
}
|
||
|
||
... | ... | |
(xml \ "SERVER_ROLE").flatMap(e => optText(e).map(ServerRole(_)))
|
||
}
|
||
}
|
||
|
||
|
inventory-fusion/src/test/resources/fusion-report/dsc-agent.ocs | ||
---|---|---|
<RUDDER>
|
||
<AGENT>
|
||
<AGENT_NAME>windows-dsc</AGENT_NAME>
|
||
<AGENT_KEY>-----BEGIN RSA PUBLIC KEY-----
|
||
MIIBCAKCAQEAp8XambRZMLpI9wCqkeZNGQuG02wjjiQD9NIUrESkcfMYZ5qvLhX1
|
||
bjufiDTbs1M2ySEGGCDHJN991C67kvPvstFtKx+w8m6DBC9DkpwLR0wGgWN0VCqC
|
||
cG8UwJIRz9SrPTQKRLFWI/fl8b92xcZvDPVXv1r0yS75toX3Ja4dBwyOqaiUGLBO
|
||
kOpwXSfrmHJ16IDWwLUZlesfomgCyzfZ8AUuTOX7JbvxLI9neXysoueXqDMIXklS
|
||
013pCqdZW/kCp3/aPwBjoK5EFnR8XxVLCGSXGXz6JG2Nng7ox8bbjm0VurFQErce
|
||
VyodQDN1ecAsViRI1V18Z+PvZll8isonWQIBIw==
|
||
-----END RSA PUBLIC KEY-----</AGENT_KEY>
|
||
<AGENT_CERT>-----BEGIN CERTIFICATE-----
|
||
MIIE0zCCArugAwIBAgIJAJqeHdZEkElqMA0GCSqGSIb3DQEBCwUAMBExDzANBgNV
|
||
BAMMBkFHRU5UMTAeFw0xNzA2MDcxMzA1NTlaFw0yNzA2MDUxMzA1NTlaMBExDzAN
|
||
BgNVBAMMBkFHRU5UMTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAOPd
|
||
mhaXZcezSjgTi6Zn38alftrYwfAyuKoznll4oM2T5DkYaZ2Lyclvv7jar/4ozUsA
|
||
Jmzw6O8QjCiaufVkHCytGJoQmtRPhywgOp0S0vqUVCXsdfh5dhwZ3Qjy107M6Z6E
|
||
6lyjggsgGZM23LBUNjsMyY7FefSHGpLpeWXgX/1CW4XeBaBq6mzo5A/1N3iyKLL4
|
||
yOGcNli11H4snlmqGJ+YDfNfCIeTkx2wdFftOxRkPFZ1TETmZSt5idrNvGU2IOnp
|
||
tXnT1kN/iknW6ZLu0wLRGe/b0Hde6ZqfPZLdrBn2oeMBvkM4eOl5GpFIFQa0uSEh
|
||
tr57vCqC6KSVvejjNr2VwKs11+9Bkdim6h7ndX74tFrR3N7cBczkXYoc+tlP2MHX
|
||
t/DqXxkB3OhmDVfmlzC/CV9M5rE7GSpWRx8O46wYrkf+6J1Z4p0rOjZNDPolEJWY
|
||
KxKzS//34wgqPfqY7iSav7ElpAFcZRFFfIKldpjpM2M5Gul0dXpBMEqHkmlcRDMT
|
||
VBrHYuTyyXhtdApdVZXDocfAma7UOkis5WcVn2+6O/pI88yVXkkgK4lSNOPBMgc0
|
||
6XLI2YgoGB3hnpGTfKbTbj3uGV8AI2/N3x91gGXRu4EtrvYtiiuxmpT1NJLmHVAA
|
||
vkfQwNm+CFqXzNDn+OPOM+cgsc08Qd++UXUmOI5lAgMBAAGjLjAsMAsGA1UdDwQE
|
||
AwIEsDAdBgNVHQ4EFgQU+S14UemkyCDsEaa5Gto8Qt9xIFEwDQYJKoZIhvcNAQEL
|
||
BQADggIBADTLZLpX/ffSNzxgla7MsboBFVW8JzaFtG8JOe+oYxeI9nHCB6M6fJao
|
||
hBhF7nUA0MyjIoEMfFVPF7LUTf3k0XvJNvXMJGNxlye1jsIKTWJo29ESERUc9eRk
|
||
LDIC6fbWV0PVjo1VolxnKZoLznOIRITPxAxfmn48n2/YJ5eGT8MF4cgTTpIP0cbD
|
||
I7UDDB1GRpLfu/K42Ll4CrTAbLABtdcGv4Rn4CtDXu+KrSpOJG675S6UhjT7ozjj
|
||
J/QDV8XcNWW0BF+2w2zzJsmSo8NjExuNXgsVh29uhCPQGYrjO03xBtz1GmWKX8wS
|
||
RGqgVQI0dHHlcfYSLaaA/zkwU9wkvmiwUS7ODMfYSrCTVNBkRvV/oIKmN9mY9I3R
|
||
//wCOPI18kk6fONEHjOng9NJD6LGVlGrnR+k3MFVWqORuvIUFEhD4YynHPZqu1g8
|
||
60ePwvw8pouI4yoRtCQBZEw2rgnS0bGiY3bpou3sTq+eLxQs/3DmvJkhLGzgfeai
|
||
MFMym+kchpMxU69krkiZXDWwkXxo3txXnPevPx12pQKBSQJONfeADDok6GQYEOeH
|
||
GvNfE6prJSpDchFWdRKK3/RcknoKwq0hDyFICOhZkb3Zc4oWknDgJz/huorU/nnb
|
||
QHjxOfVdBMBBXq0oHSUsOzf9rIxMlWQmCQyNDB6qD+bXZnAZp7j7
|
||
-----END CERTIFICATE-----
|
||
</AGENT_CERT>
|
||
<OWNER>agent1\vagrant</OWNER>
|
||
<POLICY_SERVER_HOSTNAME>192.168.42.0</POLICY_SERVER_HOSTNAME>
|
||
<POLICY_SERVER_UUID>root</POLICY_SERVER_UUID></AGENT>
|
inventory-fusion/src/test/scala/com/normation/inventory/provisioning/fusion/TestPostUnmarshaller.scala | ||
---|---|---|
import net.liftweb.common.EmptyBox
|
||
import net.liftweb.common.Full
|
||
import java.io.File
|
||
import com.normation.inventory.domain.COMMUNITY_AGENT
|
||
import com.normation.inventory.domain.NOVA_AGENT
|
||
import com.normation.inventory.domain.AgentType._
|
||
import com.normation.inventory.domain.Windows
|
||
import com.normation.inventory.domain.Windows2012
|
||
import com.normation.inventory.services.provisioning.PreUnmarshall
|
||
... | ... | |
import org.xml.sax.SAXParseException
|
||
import scala.xml.NodeSeq
|
||
|
||
|
||
/**
|
||
* A simple test class to check that the demo data file is up to date
|
||
* with the schema (there may still be a desynchronization if both
|
||
... | ... | |
linux.toOption must not beNone
|
||
}
|
||
|
||
|
||
"With a valid inventory in Windows" in {
|
||
val windows = post.check("fusion-report/WIN-AI8CLNPLOV5-2014-06-20-18-15-49.ocs")
|
||
windows.toOption must not beNone
|
inventory-fusion/src/test/scala/com/normation/inventory/provisioning/fusion/TestReportParsing.scala | ||
---|---|---|
import scala.xml.XML
|
||
import java.io.File
|
||
import com.normation.inventory.domain._
|
||
import com.normation.inventory.domain.AgentType._
|
||
|
||
/**
|
||
* A simple test class to check that the demo data file is up to date
|
||
... | ... | |
"Agent in Inventory" should {
|
||
|
||
"should be empty when there is no agent" in {
|
||
val agents = parser.parse("fusion-report/rudder-tag/minimal-zero-agent.ocs").node.agents.map(_.name).toList
|
||
val agents = parser.parse("fusion-report/rudder-tag/minimal-zero-agent.ocs").node.agents.map(_.agentType).toList
|
||
agents must be empty
|
||
}
|
||
|
||
"should have one agent when using community" in {
|
||
val agents = parser.parse("fusion-report/rudder-tag/minimal-one-agent.ocs").node.agents.map(_.name).toList
|
||
agents == (COMMUNITY_AGENT :: Nil)
|
||
val agents = parser.parse("fusion-report/rudder-tag/minimal-one-agent.ocs").node.agents.map(_.agentType).toList
|
||
agents == (CfeCommunity :: Nil)
|
||
}
|
||
|
||
"should have two agent when using community and nova" in {
|
||
val agents = parser.parse("fusion-report/rudder-tag/minimal-two-agents.ocs").node.agents.map(_.name).toList
|
||
agents == (COMMUNITY_AGENT :: NOVA_AGENT :: Nil)
|
||
val agents = parser.parse("fusion-report/rudder-tag/minimal-two-agents.ocs").node.agents.map(_.agentType).toList
|
||
agents == (CfeCommunity :: CfeEnterprise :: Nil)
|
||
}
|
||
|
||
"should be empty when there is two agents, using two different policy servers" in {
|
||
val agents = parser.parse("fusion-report/rudder-tag/minimal-two-agents-fails.ocs").node.agents.map(_.name).toList
|
||
val agents = parser.parse("fusion-report/rudder-tag/minimal-two-agents-fails.ocs").node.agents.map(_.agentType).toList
|
||
agents must be empty
|
||
}
|
||
|
||
"should have dsc agent agent when using rudder-agent based on dsc" in {
|
||
val agents = parser.parse("fusion-report/dsc-agent.ocs").node.agents.map(_.name).toList
|
||
agents == (DSC_AGENT :: Nil)
|
||
val agents = parser.parse("fusion-report/dsc-agent.ocs").node.agents.map(_.agentType).toList
|
||
agents == (Dsc :: Nil)
|
||
}
|
||
|
||
}
|
inventory-fusion/src/test/scala/com/normation/inventory/provisioning/fusion/TestSignatureService.scala | ||
---|---|---|
*************************************************************************************
|
||
*/
|
||
|
||
|
||
package com.normation.inventory.provisioning.fusion
|
||
|
||
import org.junit.runner._
|
||
... | ... | |
import com.normation.inventory.domain.KeyStatus
|
||
import java.security.Security
|
||
import org.bouncycastle.jce.provider.BouncyCastleProvider
|
||
|
||
import com.normation.inventory.domain.SecurityToken
|
||
|
||
@RunWith(classOf[JUnitRunner])
|
||
class TestSignatureService extends Specification with Loggable {
|
||
... | ... | |
* either an inventory has already been treated before, it will look into ldap repository
|
||
* or if there was no inventory before, it will look for the key in the received inventory
|
||
*/
|
||
def getKey (receivedInventory : InventoryReport) : Box[(PublicKey, KeyStatus)] = {
|
||
def getKey (receivedInventory : InventoryReport) : Box[(SecurityToken, KeyStatus)] = {
|
||
for {
|
||
cfengineKey <- Box(receivedInventory.node.publicKeys.headOption) ?~! "There is no public key in inventory"
|
||
cfengineKey <- Box(receivedInventory.node.agents.headOption) ?~! "There is no public key in inventory"
|
||
keyStatus = receivedInventory.node.main.keyStatus
|
||
publicKey <- cfengineKey.publicKey
|
||
publicKey = cfengineKey.securityToken//.publicKey
|
||
} yield {
|
||
(publicKey,keyStatus)
|
||
(publicKey,keyStatus)
|
||
}
|
||
}
|
||
}
|
||
|
||
val keyNorm = new PrintedKeyNormalizer
|
||
val extension = new RudderPublicKeyParsing(keyNorm)
|
||
val extension = RudderAgentNameParsing
|
||
|
||
val parser = new FusionReportUnmarshaller(
|
||
new StringUuidGeneratorImpl
|
||
... | ... | |
}
|
||
}
|
||
|
||
|
||
"a signed report" should {
|
||
"Be ok if checked with correct signature" in {
|
||
(for {
|
inventory-provisioning-core/src/main/scala/com/normation/inventory/ldap/provisioning/NodeIdFinder.scala | ||
---|---|---|
}
|
||
}
|
||
|
||
|
||
/*
|
||
* Retrieve the id from the cfengine public key
|
||
*/
|
||
... | ... | |
) extends NodeInventoryDNFinder with Loggable {
|
||
|
||
override def tryWith(entity:NodeInventory) : Box[(NodeId,InventoryStatus)] = {
|
||
val keys = entity.publicKeys
|
||
val keys = entity.agents.map(_.securityToken)
|
||
if(keys.size > 0) {
|
||
val keysFilter = OR((keys.map( k => EQ(A_PKEYS,k.key))):_*)
|
||
|
inventory-provisioning-web/src/main/scala/com/normation/inventory/provisioning/endpoint/config/AppConfig.scala | ||
---|---|---|
@Bean
|
||
def removedNodesDit = new InventoryDit(REMOVED_INVENTORIES_DN,SOFTWARE_INVENTORIES_DN,"Removed Servers")
|
||
|
||
|
||
@Bean
|
||
def inventoryDitService = new InventoryDitServiceImpl(pendingNodesDit,acceptedNodesDit, removedNodesDit)
|
||
|
||
... | ... | |
RudderPolicyServerParsing ::
|
||
RudderMachineIdParsing ::
|
||
RudderCpuParsing ::
|
||
new RudderPublicKeyParsing(keyNorm) ::
|
||
RudderRootUserParsing ::
|
||
RudderAgentNameParsing ::
|
||
RudderServerRoleParsing ::
|
||
... | ... | |
NamedNodeInventoryDNFinderAction("use_existing_id", new UseExistingNodeIdFinder(inventoryDitService,roLdapConnectionProvider,acceptedNodesDit.BASE_DN.getParent))
|
||
))
|
||
|
||
|
||
@Bean
|
||
def machineFinder() : MachineDNFinderAction =
|
||
new MachineDNFinderService(Seq(
|
||
... | ... | |
, postCommitPipeline
|
||
)
|
||
|
||
|
||
/*
|
||
* configure the file handler
|
||
*/
|
||
... | ... | |
c
|
||
}
|
||
|
||
|
||
/*
|
||
* The REST end point where OCSi report are
|
||
* uploaded
|
inventory-repository/src/main/scala/com/normation/inventory/ldap/core/InventoryMapper.scala | ||
---|---|---|
import com.normation.utils.Control.sequence
|
||
import com.normation.inventory.domain.NodeTimezone
|
||
|
||
|
||
class DateTimeSerializer extends Serializer[DateTime] {
|
||
private val IntervalClass = classOf[DateTime]
|
||
|
||
... | ... | |
}
|
||
}
|
||
|
||
|
||
////////////////////////////////////////////////////////////////////
|
||
///////////////////////// Machine Elements /////////////////////////
|
||
////////////////////////////////////////////////////////////////////
|
||
... | ... | |
|
||
///////////////////////// Video /////////////////////////
|
||
|
||
|
||
def entryFromVideo(elt:Video,dit:InventoryDit,machineId:MachineUuid) : LDAPEntry = {
|
||
val e = dit.MACHINES.VIDEO.model(machineId,elt.name)
|
||
e.setOpt(elt.description, A_DESCRIPTION, {x:String => x})
|
||
... | ... | |
root.setOpt(machine.manufacturer,A_MANUFACTURER, {x:Manufacturer => x.name})
|
||
root.setOpt(machine.systemSerialNumber,A_SERIAL_NUMBER, {x:String => x})
|
||
|
||
|
||
val tree = LDAPTree(root)
|
||
//now, add machine elements as children
|
||
machine.bios.foreach { x => tree.addChild(entryFromBios(x, dit, machine.id)) }
|
||
... | ... | |
|
||
///////////////////////// Networks /////////////////////////
|
||
|
||
|
||
def entryFromNetwork(elt:Network, dit:InventoryDit, serverId:NodeId) : LDAPEntry = {
|
||
val e = dit.NODES.NETWORK.model(serverId,elt.name)
|
||
e.setOpt(elt.description, A_DESCRIPTION, {x:String => x})
|
||
... | ... | |
|
||
///////////////////////// VM INFO /////////////////////////
|
||
|
||
|
||
def entryFromVMInfo(elt:VirtualMachine, dit:InventoryDit, serverId:NodeId) : LDAPEntry = {
|
||
val e = dit.NODES.VM.model(serverId,elt.uuid.value)
|
||
e.setOpt(elt.description, A_DESCRIPTION, {x:String => x})
|
||
... | ... | |
}
|
||
}
|
||
|
||
|
||
//////////////////Node/ NodeInventory /////////////////////////
|
||
|
||
|
||
|
||
// User defined properties : the regexp that the data should abide by
|
||
// {KEY}VALUE
|
||
private[this] val userDefinedPropertyRegex = """\{([^\}]+)\}(.+)""".r
|
||
... | ... | |
root.setOpt(server.inventoryDate, A_INVENTORY_DATE, { x: DateTime => GeneralizedTime(x).toString })
|
||
root.setOpt(server.receiveDate, A_RECEIVE_DATE, { x: DateTime => GeneralizedTime(x).toString })
|
||
root +=! (A_AGENTS_NAME, server.agents.map(x => x.toJsonString):_*)
|
||
root +=! (A_PKEYS, server.publicKeys.map(x => x.key):_*)
|
||
root +=! (A_SOFTWARE_DN, server.softwareIds.map(x => dit.SOFTWARE.SOFT.dn(x).toString):_*)
|
||
root +=! (A_EV, server.environmentVariables.map(x => Serialization.write(x)):_*)
|
||
root +=! (A_PROCESS, server.processes.map(x => Serialization.write(x)):_*)
|
||
... | ... | |
hostname <- requiredAttr(A_HOSTNAME)
|
||
rootUser <- requiredAttr(A_ROOT_USER)
|
||
policyServerId <- requiredAttr(A_POLICY_SERVER_UUID)
|
||
agentNames <- sequence(entry.valuesFor(A_AGENTS_NAME).toSeq) { x =>
|
||
AgentInfoSerialisation.parseCompatNonJson(x) //compat to be able to migrate easely from Rudder < 4.0
|
||
}
|
||
publicKeys = entry.valuesFor(A_PKEYS).map(Some(_))
|
||
agentNames <- {
|
||
val agents = entry.valuesFor(A_AGENTS_NAME).toSeq.map(Some(_))
|
||
val agentWithKeys = agents.zipAll(publicKeys, None,None).filter(_._1.isDefined)
|
||
sequence(agentWithKeys) {
|
||
case (Some(agent),key) =>
|
||
AgentInfoSerialisation.parseCompatNonJson(agent,key)
|
||
case _ =>
|
||
Failure("Should not happen")
|
||
}
|
||
}
|
||
//now, look for the OS type
|
||
osDetails <- mapOsDetailsFromEntry(entry)
|
||
//now, optionnal things
|
||
... | ... | |
, lastLoggedUser
|
||
, lastLoggedUserTime
|
||
, agentNames
|
||
, publicKeys.toSeq
|
||
, serverIps
|
||
, machineId
|
||
, softwareIds
|
Also available in: Unified diff
Fixes #10879: Adapt inventory processor so it can read agent certificate