diff --git a/webapp/sources/rudder/rudder-core/src/main/scala/com/normation/rudder/apidata/JsonQueryObjects.scala b/webapp/sources/rudder/rudder-core/src/main/scala/com/normation/rudder/apidata/JsonQueryObjects.scala index e0cee065d91..c7aa3f64a58 100644 --- a/webapp/sources/rudder/rudder-core/src/main/scala/com/normation/rudder/apidata/JsonQueryObjects.scala +++ b/webapp/sources/rudder/rudder-core/src/main/scala/com/normation/rudder/apidata/JsonQueryObjects.scala @@ -37,6 +37,7 @@ package com.normation.rudder.apidata +import cats.syntax.traverse.* import com.normation.GitVersion import com.normation.GitVersion.ParseRev import com.normation.GitVersion.Revision @@ -59,6 +60,7 @@ import com.normation.rudder.domain.properties.PropertyProvider import com.normation.rudder.domain.queries.NodeReturnType import com.normation.rudder.domain.queries.Query import com.normation.rudder.domain.queries.QueryReturnType +import com.normation.rudder.domain.reports.CompliancePrecision import com.normation.rudder.rule.category.RuleCategory import com.normation.rudder.rule.category.RuleCategoryId import com.normation.rudder.services.queries.CmdbQueryParser @@ -622,4 +624,14 @@ class ZioJsonExtractor(queryParser: CmdbQueryParser with JsonQueryLexer) { } } + def extractCompliancePrecisionFromParams(params: Map[String, List[String]]): PureResult[Option[CompliancePrecision]] = { + for { + precision <- + params.parseString("precision", s => s.toIntOption.toRight(s"percent precison must be an integer, was: '${s}'")) + res <- precision.traverse(CompliancePrecision.fromPrecision(_).toPureResult) + } yield { + res + } + } + } diff --git a/webapp/sources/rudder/rudder-core/src/main/scala/com/normation/rudder/repository/NodeGroupRepository.scala b/webapp/sources/rudder/rudder-core/src/main/scala/com/normation/rudder/repository/NodeGroupRepository.scala index fc9bb83e57e..a38cd8f66ee 100644 --- a/webapp/sources/rudder/rudder-core/src/main/scala/com/normation/rudder/repository/NodeGroupRepository.scala +++ b/webapp/sources/rudder/rudder-core/src/main/scala/com/normation/rudder/repository/NodeGroupRepository.scala @@ -244,11 +244,6 @@ trait RoNodeGroupRepository { // TODO: add QC def getAll(): IOResult[Seq[NodeGroup]] - /** - * Get all node groups by ids - */ - def getAllByIds(ids: Seq[NodeGroupId]): IOResult[Seq[NodeGroup]] - /** * Get all the node group id and the set of ndoes within * Goal is to be more efficient @@ -277,6 +272,14 @@ trait RoNodeGroupRepository { qc: QueryContext ): IOResult[SortedMap[List[NodeGroupCategoryId], CategoryAndNodeGroup]] + /** + * Get all node groups grouped by their direct parent category. + * Group ids can be filtered, by default return all groups with an empty filter. + */ + def getGroupsByCategoryByIds(ids: Seq[NodeGroupId] = Seq.empty, includeSystem: Boolean = false)(implicit + qc: QueryContext + ): IOResult[Map[NodeGroupCategory, Seq[NodeGroup]]] + /** * Retrieve all groups that have at least one of the given * node ID in there member list. diff --git a/webapp/sources/rudder/rudder-core/src/main/scala/com/normation/rudder/repository/ldap/LDAPNodeGroupRepository.scala b/webapp/sources/rudder/rudder-core/src/main/scala/com/normation/rudder/repository/ldap/LDAPNodeGroupRepository.scala index d068d54e307..6d897e4d58b 100644 --- a/webapp/sources/rudder/rudder-core/src/main/scala/com/normation/rudder/repository/ldap/LDAPNodeGroupRepository.scala +++ b/webapp/sources/rudder/rudder-core/src/main/scala/com/normation/rudder/repository/ldap/LDAPNodeGroupRepository.scala @@ -208,6 +208,52 @@ class RoLDAPNodeGroupRepository( } } + def getGroupsByCategoryByIds(ids: Seq[NodeGroupId], includeSystem: Boolean = false)(implicit + qc: QueryContext + ): IOResult[Map[NodeGroupCategory, Seq[NodeGroup]]] = { + groupLibMutex.readLock { + for { + con <- ldap + entries <- if (ids.isEmpty) con.searchSub(rudderDit.GROUP.dn, IS(OC_RUDDER_NODE_GROUP)) + else con.searchSub(rudderDit.GROUP.dn, OR(ids.map(id => EQ(A_NODE_GROUP_UUID, id.serialize))*)) + groups <- ZIO.foreach(entries)(groupEntry => { + for { + g <- mapper + .entry2NodeGroup(groupEntry) + .toIO + .chainError(s"Error when mapping server group entry into a Group instance. Entry: ${groupEntry}") + allNodes <- nodeFactRepo.getAll() + nodeIds = g.serverList.intersect(allNodes.keySet.toSet) + y = g.copy(serverList = nodeIds) + } yield (groupEntry, y) + }) + cats <- + ZIO.foreach(groups) { + case (groupEntry, g) => { + for { + parentCategoryEntry <- + con + .get(groupEntry.dn.getParent) + .notOptional(s"Parent category of entry with ID '${g.id.serialize}' was not found") + parentCategory <- + mapper + .entry2NodeGroupCategory(parentCategoryEntry) + .toIO + .chainError( + "Error when transforming LDAP entry %s into an active technique category".format(parentCategoryEntry) + ) + } yield { + parentCategory + } + } + } + result = cats.zip(groups).groupBy(_._1).map { case (cat, pairs) => (cat, pairs.map(_._2._2)) } + } yield { + result + } + } + } + def getNodeGroupOpt(id: NodeGroupId)(implicit qc: QueryContext): IOResult[Option[(NodeGroup, NodeGroupCategoryId)]] = { groupLibMutex.readLock(for { con <- ldap @@ -444,23 +490,6 @@ class RoLDAPNodeGroupRepository( } } - def getAllByIds(ids: Seq[NodeGroupId]): IOResult[Seq[NodeGroup]] = { - for { - con <- ldap - // for each directive entry, map it. if one fails, all fails - entries <- - groupLibMutex.readLock(con.searchSub(rudderDit.GROUP.dn, OR(ids.map(id => EQ(A_NODE_GROUP_UUID, id.serialize))*))) - groups <- ZIO.foreach(entries)(groupEntry => { - mapper - .entry2NodeGroup(groupEntry) - .toIO - .chainError(s"Error when transforming LDAP entry into a Group instance. Entry: ${groupEntry}") - }) - } yield { - groups - } - } - def getAllNodeIds(): IOResult[Map[NodeGroupId, Set[NodeId]]] = { for { con <- ldap diff --git a/webapp/sources/rudder/rudder-core/src/test/scala/com/normation/rudder/MockServices.scala b/webapp/sources/rudder/rudder-core/src/test/scala/com/normation/rudder/MockServices.scala index 5164c97dae9..39a16da8064 100644 --- a/webapp/sources/rudder/rudder-core/src/test/scala/com/normation/rudder/MockServices.scala +++ b/webapp/sources/rudder/rudder-core/src/test/scala/com/normation/rudder/MockServices.scala @@ -2381,8 +2381,17 @@ class MockNodeGroups(nodesRepo: MockNodes) { } yield cat } override def getAll(): IOResult[Seq[NodeGroup]] = categories.get.map(_.allGroups.values.map(_.nodeGroup).toSeq) - override def getAllByIds(ids: Seq[NodeGroupId]): IOResult[Seq[NodeGroup]] = { - categories.get.map(_.allGroups.values.map(_.nodeGroup).filter(g => ids.contains(g.id)).toSeq) + override def getGroupsByCategoryByIds(ids: Seq[NodeGroupId], includeSystem: Boolean = false)(implicit + qc: QueryContext + ): IOResult[Map[NodeGroupCategory, Seq[NodeGroup]]] = { + categories.get.map { root => + val groups = root.allGroups.values.map(_.nodeGroup).filter(g => ids.contains(g.id)).toSeq + val categories = groups.map(g => root.categoryByGroupId(g.id)).distinct + categories.map { c => + val cat = root.allCategories(c).toNodeGroupCategory + (cat, groups.filter(g => root.categoryByGroupId(g.id) == c)) + }.toMap + } } override def getAllNodeIds(): IOResult[Map[NodeGroupId, Set[NodeId]]] = diff --git a/webapp/sources/rudder/rudder-rest/src/main/scala/com/normation/rudder/rest/data/Compliance.scala b/webapp/sources/rudder/rudder-rest/src/main/scala/com/normation/rudder/rest/data/Compliance.scala index 95346bf2f10..301f9c2540b 100644 --- a/webapp/sources/rudder/rudder-rest/src/main/scala/com/normation/rudder/rest/data/Compliance.scala +++ b/webapp/sources/rudder/rudder-rest/src/main/scala/com/normation/rudder/rest/data/Compliance.scala @@ -48,6 +48,7 @@ import com.normation.rudder.domain.reports.ComplianceLevel import com.normation.rudder.reports.ComplianceModeName import com.normation.rudder.repository.FullActiveTechnique import enumeratum.* +import io.scalaland.chimney.Transformer import java.lang import net.liftweb.json.* import net.liftweb.json.JsonAST @@ -55,6 +56,8 @@ import net.liftweb.json.JsonDSL.* import org.apache.commons.csv.CSVFormat import org.apache.commons.csv.QuoteMode import scala.collection.immutable +import zio.json.DeriveJsonEncoder +import zio.json.JsonEncoder /** * Here, we want to present two views of compliance: @@ -129,6 +132,35 @@ final case class ByDirectiveByNodeRuleCompliance( components: Seq[ByRuleByNodeByDirectiveByComponentCompliance] ) +final case class ByNodeGroupFullCompliance( + id: String, + name: String, + category: String, + global: GenericCompliance, + targeted: GenericCompliance +) + +final case class GenericCompliance( + id: String, + name: String, + compliance: ComplianceLevel, + mode: ComplianceModeName, + complianceDetails: ComplianceSerializable +) +object GenericCompliance { + implicit def transformByNodeGroupCompliance(implicit + precision: CompliancePrecision + ): Transformer[ByNodeGroupCompliance, GenericCompliance] = { + Transformer + .define[ByNodeGroupCompliance, GenericCompliance] + .withFieldComputed( + _.complianceDetails, + b => ComplianceSerializable.fromPercent(CompliancePercent.fromLevels(b.compliance, precision)) + ) + .buildTransformer + } +} + final case class ByNodeGroupCompliance( id: String, name: String, @@ -511,6 +543,18 @@ object CsvCompliance { } object JsonCompliance { + implicit val complianceModeNameEncoder: JsonEncoder[ComplianceModeName] = JsonEncoder[String].contramap(_.name) + implicit val complianceSerializableEncoder: JsonEncoder[ComplianceSerializable] = DeriveJsonEncoder.gen[ComplianceSerializable] + + class ComplianceEncoders(implicit val precision: CompliancePrecision) { + implicit val complianceLevelEncoder: JsonEncoder[ComplianceLevel] = { + JsonEncoder[Double].contramap(_.complianceWithoutPending(precision)) + } + implicit val genericComplianceEncoder: JsonEncoder[GenericCompliance] = + DeriveJsonEncoder.gen[GenericCompliance] + implicit val byNodeGroupFullComplianceEncoder: JsonEncoder[ByNodeGroupFullCompliance] = + DeriveJsonEncoder.gen[ByNodeGroupFullCompliance] + } // global compliance implicit class JsonGlobalCompliance(val optCompliance: Option[(ComplianceLevel, Long)]) extends AnyVal { diff --git a/webapp/sources/rudder/rudder-rest/src/main/scala/com/normation/rudder/rest/lift/ComplianceApi.scala b/webapp/sources/rudder/rudder-rest/src/main/scala/com/normation/rudder/rest/lift/ComplianceApi.scala index addb2a5ee0a..be8f95d8089 100644 --- a/webapp/sources/rudder/rudder-rest/src/main/scala/com/normation/rudder/rest/lift/ComplianceApi.scala +++ b/webapp/sources/rudder/rudder-rest/src/main/scala/com/normation/rudder/rest/lift/ComplianceApi.scala @@ -41,8 +41,10 @@ import com.normation.box.* import com.normation.errors.* import com.normation.inventory.domain.NodeId import com.normation.rudder.api.ApiVersion +import com.normation.rudder.apidata.ZioJsonExtractor import com.normation.rudder.domain.logger.TimingDebugLogger import com.normation.rudder.domain.logger.TimingDebugLoggerPure +import com.normation.rudder.domain.nodes.NodeGroupCategory import com.normation.rudder.domain.nodes.NodeGroupCategoryId import com.normation.rudder.domain.nodes.NodeGroupId import com.normation.rudder.domain.policies.AllPolicyServers @@ -61,6 +63,7 @@ import com.normation.rudder.domain.policies.RuleTarget import com.normation.rudder.domain.policies.SimpleTarget import com.normation.rudder.domain.reports.BlockStatusReport import com.normation.rudder.domain.reports.ComplianceLevel +import com.normation.rudder.domain.reports.CompliancePercent import com.normation.rudder.domain.reports.CompliancePrecision import com.normation.rudder.domain.reports.ComponentStatusReport import com.normation.rudder.domain.reports.DirectiveStatusReport @@ -80,10 +83,12 @@ import com.normation.rudder.rest.ComplianceApi as API import com.normation.rudder.rest.RestExtractorService import com.normation.rudder.rest.RestUtils.* import com.normation.rudder.rest.data.* +import com.normation.rudder.rest.implicits.* import com.normation.rudder.services.reports.ReportingService import com.normation.rudder.services.reports.ReportingServiceUtils import com.normation.rudder.web.services.ComputePolicyMode import com.normation.zio.currentTimeMillis +import io.scalaland.chimney.syntax.* import net.liftweb.common.* import net.liftweb.http.LiftResponse import net.liftweb.http.PlainTextResponse @@ -98,10 +103,12 @@ import zio.syntax.* class ComplianceApi( restExtractorService: RestExtractorService, + zioJsonExtractor: ZioJsonExtractor, complianceService: ComplianceAPIService, readDirective: RoDirectiveRepository ) extends LiftApiModuleProvider[API] { + import ComplianceAPIService.* import CsvCompliance.* import JsonCompliance.* @@ -320,7 +327,7 @@ class ComplianceApi( object GetNodeGroupSummary extends LiftApiModule0 { val schema: API.GetNodeGroupComplianceSummary.type = API.GetNodeGroupComplianceSummary - val restExtractor = restExtractorService + val restExtractor = zioJsonExtractor def process0( version: ApiVersion, path: ApiPath, @@ -328,31 +335,18 @@ class ComplianceApi( params: DefaultParams, authzToken: AuthzToken ): LiftResponse = { - implicit val action = schema.name - implicit val prettify = params.prettify implicit val qc: QueryContext = authzToken.qc - (for { - precision <- restExtractor.extractPercentPrecision(req.params) - targets = req.params.getOrElse("groups", List.empty).flatMap { nodeGroups => - nodeGroups.split(",").toList.flatMap(parseSimpleTargetOrNodeGroupId(_).toOption) - } - - group <- complianceService.getNodeGroupComplianceSummary(targets, precision) - } yield { - JArray(group.toList.map { - case (id, (global, targeted)) => - (("id" -> id) ~ - ("targeted" -> targeted.toJson(1, precision.getOrElse(CompliancePrecision.Level2))) ~ - ("global" -> global.toJson(1, precision.getOrElse(CompliancePrecision.Level2)))) - }) - }) match { - case Full(groups) => - toJsonResponse(None, ("nodeGroups" -> groups)) - - case eb: EmptyBox => - val message = (eb ?~ (s"Could not get compliance summary")).messageChain - toJsonError(None, JString(message)) + withEncodersCtx( + restExtractor.extractCompliancePrecisionFromParams(req.params).map(_.getOrElse(CompliancePrecision.Level2)), + params, + schema + ) { encoders => + import encoders.* + complianceService + .getNodeGroupComplianceSummary(QueryFilter(req)) + .chainError("Could not get compliance summary") + .toLiftResponseOne(params, schema, _ => None) } } } @@ -546,11 +540,18 @@ class ComplianceApi( } } - private[this] def parseSimpleTargetOrNodeGroupId(str: String): PureResult[SimpleTarget] = { - // attempt to parse a "target" first because format is more specific - RuleTarget.unserOne(str) match { - case None => NodeGroupId.parse(str).map(GroupTarget(_)).left.map(Inconsistency(_)) - case Some(value) => Right(value) + // TODO: when migrating to scala 3 with implicit context function it should be possible to write by parameterizing with [A: JsonEncoder] + private[this] def withEncodersCtx( + precisionResult: PureResult[CompliancePrecision], + params: DefaultParams, + schema: EndpointSchema + )(body: ComplianceEncoders => LiftResponse): LiftResponse = { + implicit val prettify = params.prettify + precisionResult match { + case Right(precision) => + body(new ComplianceEncoders()(precision)) + case Left(e) => + RudderJsonResponse.internalError(None, RudderJsonResponse.ResponseSchema.fromSchema(schema), e.fullMsg) } } } @@ -1190,140 +1191,153 @@ class ComplianceAPIService( * Get global and targeted compliance at level 1 (without any details) with global compliance at left and targeted at right */ def getNodeGroupComplianceSummary( - targets: Seq[SimpleTarget], - precision: Option[CompliancePrecision] - )(implicit qc: QueryContext): Box[Map[String, (ByNodeGroupCompliance, ByNodeGroupCompliance)]] = { + filter: ComplianceAPIService.QueryFilter + )(implicit precision: CompliancePrecision, qc: QueryContext): IOResult[List[ByNodeGroupFullCompliance]] = { // container class to hold information for global targets final case class GlobalTargetInfo( name: String, + category: NodeGroupCategory, nodeIds: Set[NodeId], globalRulesByGroup: Map[RuleId, (Rule, Chunk[NodeId], Option[PolicyMode])], targetedRulesByGroup: Map[RuleId, (Rule, Chunk[NodeId], Option[PolicyMode])] ) - { - for { - t1 <- currentTimeMillis - nodeFacts <- nodeFactRepos.getAll() - nodeSettings = nodeFacts.mapValues(_.rudderSettings).toMap - t2 <- currentTimeMillis - _ <- TimingDebugLoggerPure.trace(s"getByNodeGroupCompliance - nodeFactRepo.getAll() in ${t2 - t1} ms") - - directiveLib <- directiveRepo.getFullDirectiveLibrary() - t3 <- currentTimeMillis - _ <- TimingDebugLoggerPure.trace(s"getByNodeGroupCompliance - getFullDirectiveLibrary in ${t3 - t2} ms") - - (nonGroupTargets, nodeGroupIds) = targets.partitionMap { - case GroupTarget(groupId) => Right(groupId) - case t: NonGroupRuleTarget => Left(t) - } - nodeGroupsInfo <- { - for { - nodeGroups <- nodeGroupRepo.getAllByIds(nodeGroupIds) - nodeGroupInfos = nodeGroups.map(g => g.id.serialize -> (g.name, g.serverList, GroupTarget(g.id))).toMap - - systemCategory <- nodeGroupRepo.getGroupCategory(NodeGroupCategoryId("SystemGroups")) - targetsInfos = { - nonGroupTargets - .flatMap(t => { - systemCategory.items - .find(_.target == t) - .map(i => i.target.target -> (i.name, targetServerList(t)(nodeSettings), t)) - }) - .toMap + for { + t1 <- currentTimeMillis + nodeFacts <- nodeFactRepos.getAll() + nodeSettings = nodeFacts.mapValues(_.rudderSettings).toMap + t2 <- currentTimeMillis + _ <- TimingDebugLoggerPure.trace(s"getByNodeGroupCompliance - nodeFactRepo.getAll() in ${t2 - t1} ms") + + directiveLib <- directiveRepo.getFullDirectiveLibrary() + t3 <- currentTimeMillis + _ <- TimingDebugLoggerPure.trace(s"getByNodeGroupCompliance - getFullDirectiveLibrary in ${t3 - t2} ms") + + (nonGroupTargets, nodeGroupIds) = filter.groups.partitionMap { + case GroupTarget(groupId) => Right(groupId) + case t: NonGroupRuleTarget => Left(t) + } + nodeGroupsByCat <- nodeGroupRepo.getGroupsByCategoryByIds(nodeGroupIds, includeSystem = true) + nodeGroupsGroupInfos = nodeGroupsByCat.toList.flatMap { case (cat, groups) => groups.map(cat -> _) }.map { + case (cat, g) => g.id.serialize -> (cat, g.name, g.serverList, GroupTarget(g.id)) + }.toMap + // all group info including ones for non-group targets (within SystemGroups category) + nodeGroupsInfo <- { + for { + systemCategory <- + ZIO + .fromOption(nodeGroupsByCat.keys.find(_.id == NodeGroupCategoryId("SystemGroups"))) + // system groups could not be included in the groups of the filter + .catchAll(_ => nodeGroupRepo.getGroupCategory(NodeGroupCategoryId("SystemGroups"))) + targetsInfos = { + nonGroupTargets + .flatMap(t => { + systemCategory.items + .find(_.target == t) + .map(i => i.target.target -> (systemCategory, i.name, targetServerList(t)(nodeSettings), t)) + }) + .toMap + } + } yield nodeGroupsGroupInfos ++ targetsInfos + } + t4 <- currentTimeMillis + _ <- TimingDebugLoggerPure.trace( + s"getByNodeGroupCompliance - nodeGroupRepo.getGroupsByCategoryByIds and transformations in ${t4 - t3} ms" + ) + + t5 <- currentTimeMillis + _ <- TimingDebugLoggerPure.trace(s"getByNodeGroupCompliance - getGlobalComplianceMode in ${t5 - t4} ms") + + rules <- rulesRepo.getAll() + t6 <- currentTimeMillis + _ <- TimingDebugLoggerPure.trace(s"getByNodeGroupCompliance - getAllRules in ${t6 - t5} ms") + + allGroups <- nodeGroupRepo.getAllNodeIdsChunk() + t7 <- currentTimeMillis + _ <- TimingDebugLoggerPure.trace(s"getByNodeGroupCompliance - nodeGroupRepo.getAllNodeIdsChunk in ${t7 - t6} ms") + + globalPolicyMode <- getGlobalPolicyMode + + // A map for constant time access for the set of targeted nodes and policy mode: a Map[RuleId, (Chunk[NodeId], Option[PolicyMode])] + // The set is reused to directly compute the policy mode of the rule + allRuleInfos = { + rules + .map(r => { + val targetedNodeIds = { + RoNodeGroupRepository.getNodeIdsChunk( + allGroups, + r.targets, + nodeFacts.mapValues(_.rudderSettings.isPolicyServer) + ) } - } yield nodeGroupInfos ++ targetsInfos - } - t4 <- currentTimeMillis - _ <- TimingDebugLoggerPure.trace(s"getByNodeGroupCompliance - nodeGroupRepo.getAllByIds in ${t4 - t3} ms") - - t5 <- currentTimeMillis - _ <- TimingDebugLoggerPure.trace(s"getByNodeGroupCompliance - getGlobalComplianceMode in ${t5 - t4} ms") - - rules <- rulesRepo.getAll() - t6 <- currentTimeMillis - _ <- TimingDebugLoggerPure.trace(s"getByNodeGroupCompliance - getAllRules in ${t6 - t5} ms") - - allGroups <- nodeGroupRepo.getAllNodeIdsChunk() - t7 <- currentTimeMillis - _ <- TimingDebugLoggerPure.trace(s"getByNodeGroupCompliance - nodeGroupRepo.getAllNodeIdsChunk in ${t7 - t6} ms") - - globalPolicyMode <- getGlobalPolicyMode - - // A map for constant time access for the set of targeted nodes and policy mode: a Map[RuleId, (Chunk[NodeId], Option[PolicyMode])] - // The set is reused to directly compute the policy mode of the rule - allRuleInfos = { - rules - .map(r => { - val targetedNodeIds = { - RoNodeGroupRepository.getNodeIdsChunk( - allGroups, - r.targets, - nodeFacts.mapValues(_.rudderSettings.isPolicyServer) - ) - } - val policyMode = - getRulePolicyMode(r, directiveLib.allDirectives, targetedNodeIds.toSet, nodeSettings, globalPolicyMode) + val policyMode = + getRulePolicyMode(r, directiveLib.allDirectives, targetedNodeIds.toSet, nodeSettings, globalPolicyMode) - (r.id, (targetedNodeIds, policyMode)) - }) - .toMap - } + (r.id, (targetedNodeIds, policyMode)) + }) + .toMap + } - // global compliance : filter our rules that are applicable to any node in this group - globalTargetInfos = nodeGroupsInfo.map { - case (g, (name, serverList, _)) => - val globalRulesByGroup = rules.flatMap { rule => - allRuleInfos.get(rule.id) match { - case None => None - case Some((nodeIds, policyMode)) => - if (nodeIds.exists(serverList.contains)) Some((rule.id, (rule, nodeIds, policyMode))) - else None - } - }.toMap - // targeted compliance : filter rules that only include this group in its targets - val targetedRulesByGroup = globalRulesByGroup.filter { - case (_, (rule, _, _)) => RuleTarget.merge(rule.targets).includes(nodeGroupsInfo(g)._3) + // global compliance : filter our rules that are applicable to any node in this group + globalTargetInfos = nodeGroupsInfo.map { + case (g, (cat, name, serverList, _)) => + val globalRulesByGroup = rules.flatMap { rule => + allRuleInfos.get(rule.id) match { + case None => None + case Some((nodeIds, policyMode)) => + if (nodeIds.exists(serverList.contains)) Some((rule.id, (rule, nodeIds, policyMode))) + else None } + }.toMap + // targeted compliance : filter rules that only include this group in its targets + val targetedRulesByGroup = globalRulesByGroup.filter { + case (_, (rule, _, _)) => RuleTarget.merge(rule.targets).includes(nodeGroupsInfo(g)._4) + } - (g, GlobalTargetInfo(name, serverList, globalRulesByGroup, targetedRulesByGroup)) - } + (g, GlobalTargetInfo(name, cat, serverList, globalRulesByGroup, targetedRulesByGroup)) + } - level = Some(1) + level = Some(1) - bothGlobalTargeted <- - ZIO.foreach(globalTargetInfos) { - case (id, info) => - ( - getByNodeGroupCompliance( - id, - info.name, - info.nodeIds, - directiveLib.allDirectives, - nodeFacts, - nodeSettings, - info.globalRulesByGroup, - level, - isGlobalCompliance = true - ) <&> getByNodeGroupCompliance( + fullCompliance <- + ZIO.foreach(globalTargetInfos.toList) { + case (id, info) => + ( + getByNodeGroupCompliance( + id, + info.name, + info.nodeIds, + directiveLib.allDirectives, + nodeFacts, + nodeSettings, + info.globalRulesByGroup, + level, + isGlobalCompliance = true + ) <&> getByNodeGroupCompliance( + id, + info.name, + info.nodeIds, + directiveLib.allDirectives, + nodeFacts, + nodeSettings, + info.targetedRulesByGroup, + level, + isGlobalCompliance = false + ) + ).map { + case (global, targeted) => + ByNodeGroupFullCompliance.apply( id, info.name, - info.nodeIds, - directiveLib.allDirectives, - nodeFacts, - nodeSettings, - info.targetedRulesByGroup, - level, - isGlobalCompliance = false + info.category.name, + global.transformInto[GenericCompliance], + targeted.transformInto[GenericCompliance] ) - ).map( - (id, _) - ) - } - } yield { - bothGlobalTargeted.toMap - } - }.toBox + } + } + } yield { + filter.apply(fullCompliance) + } } def getDirectivesCompliance(level: Option[Int])(implicit qc: QueryContext): Box[Seq[ByDirectiveCompliance]] = { @@ -1561,3 +1575,74 @@ class ComplianceAPIService( .map(allRuleObjects => directiveOverridesByRules.view.mapValues(_.map(overrideToTarget(_, allRuleObjects)))) } } + +object ComplianceAPIService { + + /** + * Query params supported by the compliance summary endpoint. + * Emptiness always means that there is no filter on the field + */ + case class QueryFilter( + groups: List[SimpleTarget], + limit: Option[Int], + offset: Option[Int], + order: Option[Ordering[ByNodeGroupFullCompliance]] + ) { + + def apply(compliances: List[ByNodeGroupFullCompliance]): List[ByNodeGroupFullCompliance] = { + val sorted = order.map(compliances.sorted(_)).getOrElse(compliances) + val dropped = offset.map(sorted.drop).getOrElse(sorted) + limit.map(dropped.take).getOrElse(dropped) + } + } + + object QueryFilter { + + def apply(req: Req): QueryFilter = { + val groups = req.params.getOrElse("groups", List.empty).flatMap { nodeGroups => + nodeGroups.split(",").toList.flatMap(parseSimpleTargetOrNodeGroupId(_).toOption) + } + val limit = req.params.get("limit").flatMap(_.headOption).flatMap(_.toIntOption) + val offset = req.params.get("offset").flatMap(_.headOption).flatMap(_.toIntOption) + val sort = req.params.get("sort").flatMap(_.headOption) + val order = req.params + .get("order") + .flatMap(_.headOption) + .flatMap(v => { + if (v == "desc") { + Some(()) + } else { + None + } + }) + val ordering: Option[Ordering[ByNodeGroupFullCompliance]] = { + implicit val complianceOrdering: Ordering[ComplianceLevel] = + Ordering.by(CompliancePercent.fromLevels(_, ComplianceLevel.PERCENT_PRECISION).compliance) + val asc: Option[Ordering[ByNodeGroupFullCompliance]] = sort match { + case Some("id") => + Some(Ordering.by(_.id)) + case Some("name") => + Some(Ordering.by(_.name)) + case Some("category") => + Some(Ordering.by(_.category)) + case Some("targeted") => + Some(Ordering.by(_.targeted.compliance)) + case Some("global") => + Some(Ordering.by(_.global.compliance)) + case _ => + None + } + order.map(_ => asc.map(_.reverse)).getOrElse(asc) + } + QueryFilter(groups, limit, offset, ordering) + } + } + + def parseSimpleTargetOrNodeGroupId(str: String): PureResult[SimpleTarget] = { + // attempt to parse a "target" first because format is more specific + RuleTarget.unserOne(str) match { + case None => NodeGroupId.parse(str).map(GroupTarget(_)).left.map(Inconsistency(_)) + case Some(value) => Right(value) + } + } +} diff --git a/webapp/sources/rudder/rudder-rest/src/test/scala/com/normation/rudder/MockServices.scala b/webapp/sources/rudder/rudder-rest/src/test/scala/com/normation/rudder/MockServices.scala index eab3d879968..eb569d4db74 100644 --- a/webapp/sources/rudder/rudder-rest/src/test/scala/com/normation/rudder/MockServices.scala +++ b/webapp/sources/rudder/rudder-rest/src/test/scala/com/normation/rudder/MockServices.scala @@ -225,8 +225,10 @@ class MockCompliance(mockDirectives: MockDirectives) { def categoryExists(id: NodeGroupCategoryId): IOResult[Boolean] = ??? def getNodeGroupCategory(id: NodeGroupId): IOResult[NodeGroupCategory] = ??? - def getAll(): IOResult[Seq[NodeGroup]] = ??? - def getAllByIds(ids: Seq[NodeGroupId]): IOResult[Seq[NodeGroup]] = ??? + def getAll(): IOResult[Seq[NodeGroup]] = ??? + def getGroupsByCategoryByIds(ids: Seq[NodeGroupId], includeSystem: Boolean = false)(implicit + qc: QueryContext + ): IOResult[Map[NodeGroupCategory, Seq[NodeGroup]]] = ??? def getAllNodeIds(): IOResult[Map[NodeGroupId, Set[NodeId]]] = ??? def getGroupsByCategory(includeSystem: Boolean)(implicit qc: QueryContext diff --git a/webapp/sources/rudder/rudder-rest/src/test/scala/com/normation/rudder/rest/RestTestSetUp.scala b/webapp/sources/rudder/rudder-rest/src/test/scala/com/normation/rudder/rest/RestTestSetUp.scala index a126e5939bc..be087c50acd 100644 --- a/webapp/sources/rudder/rudder-rest/src/test/scala/com/normation/rudder/rest/RestTestSetUp.scala +++ b/webapp/sources/rudder/rudder-rest/src/test/scala/com/normation/rudder/rest/RestTestSetUp.scala @@ -955,6 +955,7 @@ class RestTestSetUp { campaignApiModule.api, new ComplianceApi( restExtractorService, + zioJsonExtractor, complianceService.complianceAPIService, mockDirectives.directiveRepo ) diff --git a/webapp/sources/rudder/rudder-web/src/main/scala/bootstrap/liftweb/RudderConfig.scala b/webapp/sources/rudder/rudder-web/src/main/scala/bootstrap/liftweb/RudderConfig.scala index d777a595b90..ec2c20e0eed 100644 --- a/webapp/sources/rudder/rudder-web/src/main/scala/bootstrap/liftweb/RudderConfig.scala +++ b/webapp/sources/rudder/rudder-web/src/main/scala/bootstrap/liftweb/RudderConfig.scala @@ -2148,7 +2148,7 @@ object RudderConfigInit { stringUuidGenerator ) val modules = List( - new ComplianceApi(restExtractorService, complianceAPIService, roDirectiveRepository), + new ComplianceApi(restExtractorService, zioJsonExtractor, complianceAPIService, roDirectiveRepository), new GroupsApi( roLdapNodeGroupRepository, restExtractorService,