From df8b61dc81a96b6218d9815b89ec0819ecf5aaf3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vincent=20Membr=C3=A9?= Date: Fri, 1 Dec 2023 15:16:19 +0100 Subject: [PATCH] Fixes #23967: Rudder score backend and API --- .../src/main/resources/reportsSchema.sql | 17 ++ .../domain/reports/ComplianceLevel.scala | 38 +++++ .../rudder/inventory/PostCommits.scala | 9 ++ .../rudder/score/ComplianceScore.scala | 83 ++++++++++ .../rudder/score/GlobalScoreRepository.scala | 115 ++++++++++++++ .../com/normation/rudder/score/Score.scala | 119 ++++++++++++++ .../rudder/score/ScoreRepository.scala | 130 ++++++++++++++++ .../normation/rudder/score/ScoreService.scala | 146 ++++++++++++++++++ .../rudder/score/SystemUpdateScore.scala | 109 +++++++++++++ .../reports/ReportingServiceImpl.scala | 60 ++++--- .../jdbc/ReportingServiceTest.scala | 2 + .../CachedFindRuleNodeStatusReportsTest.scala | 17 ++ .../rudder/rest/EndpointsDefinition.scala | 43 ++++-- .../rudder/rest/lift/CampaignApi.scala | 1 + .../normation/rudder/rest/lift/NodeApi.scala | 83 +++++++++- .../normation/rudder/rest/RestTestSetUp.scala | 3 +- .../rudder-web/src/main/elm/sources/Node.elm | 5 +- .../src/main/elm/sources/Node/View.elm | 4 +- .../src/main/elm/sources/Score/DataTypes.elm | 4 +- .../main/elm/sources/Score/JsonDecoder.elm | 4 +- .../src/main/elm/sources/Score/ViewUtils.elm | 4 +- .../main/elm/sources/SystemUpdateScore.elm | 84 ++++++++++ .../src/main/javascript/rudder/rudder-elm.js | 2 +- .../bootstrap/liftweb/RudderConfig.scala | 26 +++- .../rudder/web/services/DisplayNode.scala | 89 +++++++---- .../main/webapp/secure/nodeManager/node.html | 1 + 26 files changed, 1113 insertions(+), 85 deletions(-) create mode 100644 webapp/sources/rudder/rudder-core/src/main/scala/com/normation/rudder/score/ComplianceScore.scala create mode 100644 webapp/sources/rudder/rudder-core/src/main/scala/com/normation/rudder/score/GlobalScoreRepository.scala create mode 100644 webapp/sources/rudder/rudder-core/src/main/scala/com/normation/rudder/score/Score.scala create mode 100644 webapp/sources/rudder/rudder-core/src/main/scala/com/normation/rudder/score/ScoreRepository.scala create mode 100644 webapp/sources/rudder/rudder-core/src/main/scala/com/normation/rudder/score/ScoreService.scala create mode 100644 webapp/sources/rudder/rudder-core/src/main/scala/com/normation/rudder/score/SystemUpdateScore.scala create mode 100644 webapp/sources/rudder/rudder-web/src/main/elm/sources/SystemUpdateScore.elm diff --git a/webapp/sources/rudder/rudder-core/src/main/resources/reportsSchema.sql b/webapp/sources/rudder/rudder-core/src/main/resources/reportsSchema.sql index 3ba4252efb2..33a4b7e5d65 100644 --- a/webapp/sources/rudder/rudder-core/src/main/resources/reportsSchema.sql +++ b/webapp/sources/rudder/rudder-core/src/main/resources/reportsSchema.sql @@ -453,3 +453,20 @@ CREATE TABLE NodeFacts ( , acceptRefuseFact jsonb -- the big node fact data structure , deleteEvent jsonb -- { 'date': 'rfc3339 timestamp', 'actor': 'actor name' } ); + + +Create table GlobalScore ( + nodeId text primary key +, score text +, message text +, details jsonb +); + +Create table scoreDetails ( + nodeId text +, scoreId text +, score text +, message text +, details jsonb +, PRIMARY KEY (nodeId, scoreId) +); \ No newline at end of file diff --git a/webapp/sources/rudder/rudder-core/src/main/scala/com/normation/rudder/domain/reports/ComplianceLevel.scala b/webapp/sources/rudder/rudder-core/src/main/scala/com/normation/rudder/domain/reports/ComplianceLevel.scala index 90412a8fc83..fe4cab3b243 100644 --- a/webapp/sources/rudder/rudder-core/src/main/scala/com/normation/rudder/domain/reports/ComplianceLevel.scala +++ b/webapp/sources/rudder/rudder-core/src/main/scala/com/normation/rudder/domain/reports/ComplianceLevel.scala @@ -93,6 +93,44 @@ final case class CompliancePercent( val compliance = success + repaired + notApplicable + compliant + auditNotApplicable } +final case class ComplianceSerializable( + applying: Option[Double], + successNotApplicable: Option[Double], + successAlreadyOK: Option[Double], + successRepaired: Option[Double], + error: Option[Double], + auditCompliant: Option[Double], + auditNonCompliant: Option[Double], + auditError: Option[Double], + auditNotApplicable: Option[Double], + unexpectedUnknownComponent: Option[Double], + unexpectedMissingComponent: Option[Double], + noReport: Option[Double], + reportsDisabled: Option[Double], + badPolicyMode: Option[Double] +) + +object ComplianceSerializable { + def fromPercent(compliancePercent: CompliancePercent) = { + ComplianceSerializable( + if (compliancePercent.pending == 0) None else Some(compliancePercent.pending), + if (compliancePercent.notApplicable == 0) None else Some(compliancePercent.notApplicable), + if (compliancePercent.success == 0) None else Some(compliancePercent.success), + if (compliancePercent.repaired == 0) None else Some(compliancePercent.repaired), + if (compliancePercent.error == 0) None else Some(compliancePercent.error), + if (compliancePercent.compliant == 0) None else Some(compliancePercent.compliant), + if (compliancePercent.nonCompliant == 0) None else Some(compliancePercent.nonCompliant), + if (compliancePercent.auditError == 0) None else Some(compliancePercent.auditError), + if (compliancePercent.auditNotApplicable == 0) None else Some(compliancePercent.auditNotApplicable), + if (compliancePercent.unexpected == 0) None else Some(compliancePercent.unexpected), + if (compliancePercent.missing == 0) None else Some(compliancePercent.missing), + if (compliancePercent.noAnswer == 0) None else Some(compliancePercent.noAnswer), + if (compliancePercent.reportsDisabled == 0) None else Some(compliancePercent.reportsDisabled), + if (compliancePercent.badPolicyMode == 0) None else Some(compliancePercent.badPolicyMode) + ) + } +} + object CompliancePercent { // a correspondance array between worse order in `ReportType` and the order of fields in `ComplianceLevel` diff --git a/webapp/sources/rudder/rudder-core/src/main/scala/com/normation/rudder/inventory/PostCommits.scala b/webapp/sources/rudder/rudder-core/src/main/scala/com/normation/rudder/inventory/PostCommits.scala index 49dcd8ed2e9..5e82978087b 100644 --- a/webapp/sources/rudder/rudder-core/src/main/scala/com/normation/rudder/inventory/PostCommits.scala +++ b/webapp/sources/rudder/rudder-core/src/main/scala/com/normation/rudder/inventory/PostCommits.scala @@ -52,6 +52,8 @@ import com.normation.rudder.facts.nodes.QueryContext import com.normation.rudder.hooks.HookEnvPairs import com.normation.rudder.hooks.PureHooksLogger import com.normation.rudder.hooks.RunHooks +import com.normation.rudder.score.InventoryScoreEvent +import com.normation.rudder.score.ScoreServiceManager import com.normation.utils.StringUuidGenerator import com.normation.zio.currentTimeMillis import zio._ @@ -175,3 +177,10 @@ class TriggerPolicyGenerationPostCommit[A]( } else ZIO.unit) *> records.succeed } } + +class TriggerInventoryScorePostCommit[A](scoreServiceManager: ScoreServiceManager) extends PostCommit[A] { + override def name: String = "trigger Score computation on inventory update" + override def apply(inventory: Inventory, records: A): IOResult[A] = { + scoreServiceManager.handleEvent(InventoryScoreEvent(inventory.node.main.id, inventory)) *> records.succeed + } +} diff --git a/webapp/sources/rudder/rudder-core/src/main/scala/com/normation/rudder/score/ComplianceScore.scala b/webapp/sources/rudder/rudder-core/src/main/scala/com/normation/rudder/score/ComplianceScore.scala new file mode 100644 index 00000000000..5f42ecd8f8f --- /dev/null +++ b/webapp/sources/rudder/rudder-core/src/main/scala/com/normation/rudder/score/ComplianceScore.scala @@ -0,0 +1,83 @@ +/* + ************************************************************************************* + * Copyright 2024 Normation SAS + ************************************************************************************* + * + * This file is part of Rudder. + * + * Rudder is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * In accordance with the terms of section 7 (7. Additional Terms.) of + * the GNU General Public License version 3, the copyright holders add + * the following Additional permissions: + * Notwithstanding to the terms of section 5 (5. Conveying Modified Source + * Versions) and 6 (6. Conveying Non-Source Forms.) of the GNU General + * Public License version 3, when you create a Related Module, this + * Related Module is not considered as a part of the work and may be + * distributed under the license agreement of your choice. + * A "Related Module" means a set of sources files including their + * documentation that, without modification of the Source Code, enables + * supplementary functions or services in addition to those offered by + * the Software. + * + * Rudder is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Rudder. If not, see . + + * + ************************************************************************************* + */ + +package com.normation.rudder.score + +import com.normation.errors.PureResult +import com.normation.inventory.domain.InventoryError.Inconsistency +import com.normation.inventory.domain.NodeId +import com.normation.rudder.domain.reports.ComplianceSerializable +import com.normation.rudder.score.ScoreValue.A +import com.normation.rudder.score.ScoreValue.B +import com.normation.rudder.score.ScoreValue.C +import com.normation.rudder.score.ScoreValue.D +import com.normation.rudder.score.ScoreValue.E +import zio.json.DeriveJsonEncoder +import zio.json.EncoderOps +import zio.json.JsonEncoder + +object ComplianceScoreEventHandler extends ScoreEventHandler { + implicit val compliancePercentEncoder: JsonEncoder[ComplianceSerializable] = DeriveJsonEncoder.gen + def handle(event: ScoreEvent): PureResult[List[(NodeId, List[Score])]] = { + + event match { + case ComplianceScoreEvent(n, percent) => + (for { + p <- ComplianceSerializable.fromPercent(percent).toJsonAST + } yield { + val scoreId = "compliance" + val score = if (percent.compliance >= 100) { + Score(scoreId, A, "Node is compliant at 100%", p) + } else if (percent.compliance >= 75) { + Score(scoreId, B, "Node is compliant at least at 75%", p) + } else if (percent.compliance >= 50) { + ComplianceSerializable.fromPercent(percent) + Score(scoreId, C, "Node is compliant at least at 50%", p) + } else if (percent.compliance >= 25) { + Score(scoreId, D, "Node is compliant at least at 25%", p) + } else { + Score(scoreId, E, "Node is compliant at less then 25%", p) + } + ((n, score :: Nil) :: Nil) + }) match { + case Left(err) => Left(Inconsistency(err)) + case Right(r) => Right(r) + } + case _ => Right(Nil) + } + } +} diff --git a/webapp/sources/rudder/rudder-core/src/main/scala/com/normation/rudder/score/GlobalScoreRepository.scala b/webapp/sources/rudder/rudder-core/src/main/scala/com/normation/rudder/score/GlobalScoreRepository.scala new file mode 100644 index 00000000000..68b7340c808 --- /dev/null +++ b/webapp/sources/rudder/rudder-core/src/main/scala/com/normation/rudder/score/GlobalScoreRepository.scala @@ -0,0 +1,115 @@ +/* + ************************************************************************************* + * Copyright 2024 Normation SAS + ************************************************************************************* + * + * This file is part of Rudder. + * + * Rudder is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * In accordance with the terms of section 7 (7. Additional Terms.) of + * the GNU General Public License version 3, the copyright holders add + * the following Additional permissions: + * Notwithstanding to the terms of section 5 (5. Conveying Modified Source + * Versions) and 6 (6. Conveying Non-Source Forms.) of the GNU General + * Public License version 3, when you create a Related Module, this + * Related Module is not considered as a part of the work and may be + * distributed under the license agreement of your choice. + * A "Related Module" means a set of sources files including their + * documentation that, without modification of the Source Code, enables + * supplementary functions or services in addition to those offered by + * the Software. + * + * Rudder is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Rudder. If not, see . + + * + ************************************************************************************* + */ + +package com.normation.rudder.score + +import com.normation.errors.IOResult +import com.normation.inventory.domain.NodeId +import com.normation.rudder.db.Doobie +import doobie.implicits._ +import doobie.implicits.toSqlInterpolator +import zio.interop.catz._ + +trait GlobalScoreRepository { + def getAll(): IOResult[Map[NodeId, GlobalScore]] + def get(id: NodeId): IOResult[Option[GlobalScore]] + def delete(id: NodeId): IOResult[Unit] + def save(nodeId: NodeId, globalScore: GlobalScore): IOResult[(NodeId, GlobalScore)] +} + +object GlobalScoreRepositoryImpl { + + import ScoreSerializer._ + import com.normation.rudder.db.json.implicits._ + import doobie._ + implicit val getScoreValue: Get[ScoreValue] = Get[String].temap(ScoreValue.fromString) + implicit val putScoreValue: Put[ScoreValue] = Put[String].contramap(_.value) + + implicit val stateWrite: Meta[List[NoDetailsScore]] = new Meta(pgDecoderGet, pgEncoderPut) + + implicit val globalScoreWrite: Write[(NodeId, GlobalScore)] = { + Write[(String, ScoreValue, String, List[NoDetailsScore])].contramap { + case (nodeId: NodeId, score: GlobalScore) => + (nodeId.value, score.value, score.message, score.details) + } + } + + implicit val globalScoreRead: Read[GlobalScore] = { + Read[(ScoreValue, String, List[NoDetailsScore])].map { d: (ScoreValue, String, List[NoDetailsScore]) => + GlobalScore(d._1, d._2, d._3) + } + } + implicit val globalScoreWithIdRead: Read[(NodeId, GlobalScore)] = { + Read[(String, ScoreValue, String, List[NoDetailsScore])].map { d: (String, ScoreValue, String, List[NoDetailsScore]) => + (NodeId(d._1), GlobalScore(d._2, d._3, d._4)) + } + } + +} + +class GlobalScoreRepositoryImpl(doobie: Doobie) extends GlobalScoreRepository { + import GlobalScoreRepositoryImpl._ + import doobie._ + + def save(nodeId: NodeId, globalScore: GlobalScore): IOResult[(NodeId, GlobalScore)] = { + val query = { + sql"""insert into GlobalScore (nodeId, score, message, details) values (${(nodeId, globalScore)}) + | ON CONFLICT (nodeId) DO UPDATE + | SET score = ${globalScore.value}, message = ${globalScore.message}, details = ${globalScore.details} ; """.stripMargin + } + + transactIOResult(s"error when inserting global score for node '${nodeId.value}''")(xa => query.update.run.transact(xa)).map( + _ => (nodeId, globalScore) + ) + } + + override def getAll(): IOResult[Map[NodeId, GlobalScore]] = { + val q = sql"select nodeId, score, message, details from GlobalScore" + transactIOResult(s"error when getting global scores for node")(xa => q.query[(NodeId, GlobalScore)].to[List].transact(xa)) + .map(_.groupMapReduce(_._1)(_._2) { case (_, res) => res }) + } + + override def get(id: NodeId): IOResult[Option[GlobalScore]] = { + val q = sql"select score, message, details from GlobalScore where nodeId = ${id.value}" + transactIOResult(s"error when getting global score for node ${id.value}")(xa => q.query[GlobalScore].option.transact(xa)) + } + + override def delete(id: NodeId): IOResult[Unit] = { + val q = sql"delete from GlobalScore nodeId = ${id.value}" + transactIOResult(s"error when getting global score for node ${id.value}")(xa => q.update.run.transact(xa).unit) + } +} diff --git a/webapp/sources/rudder/rudder-core/src/main/scala/com/normation/rudder/score/Score.scala b/webapp/sources/rudder/rudder-core/src/main/scala/com/normation/rudder/score/Score.scala new file mode 100644 index 00000000000..aec7df8ded0 --- /dev/null +++ b/webapp/sources/rudder/rudder-core/src/main/scala/com/normation/rudder/score/Score.scala @@ -0,0 +1,119 @@ +/* + ************************************************************************************* + * Copyright 2024 Normation SAS + ************************************************************************************* + * + * This file is part of Rudder. + * + * Rudder is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * In accordance with the terms of section 7 (7. Additional Terms.) of + * the GNU General Public License version 3, the copyright holders add + * the following Additional permissions: + * Notwithstanding to the terms of section 5 (5. Conveying Modified Source + * Versions) and 6 (6. Conveying Non-Source Forms.) of the GNU General + * Public License version 3, when you create a Related Module, this + * Related Module is not considered as a part of the work and may be + * distributed under the license agreement of your choice. + * A "Related Module" means a set of sources files including their + * documentation that, without modification of the Source Code, enables + * supplementary functions or services in addition to those offered by + * the Software. + * + * Rudder is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Rudder. If not, see . + + * + ************************************************************************************* + */ + +package com.normation.rudder.score + +import com.normation.NamedZioLogger +import com.normation.errors.PureResult +import com.normation.inventory.domain.Inventory +import com.normation.inventory.domain.NodeId +import com.normation.rudder.domain.reports.CompliancePercent +import zio.json.DeriveJsonDecoder +import zio.json.DeriveJsonEncoder +import zio.json.JsonDecoder +import zio.json.JsonEncoder +import zio.json.ast.Json + +sealed trait ScoreValue { + def value: String +} + +object ScoreValue { + case object A extends ScoreValue { val value = "A" } + case object B extends ScoreValue { val value = "B" } + case object C extends ScoreValue { val value = "C" } + case object D extends ScoreValue { val value = "D" } + case object E extends ScoreValue { val value = "E" } + + val allValues: Set[ScoreValue] = ca.mrvisser.sealerate.values + + def fromString(s: String) = allValues.find(_.value == s.toUpperCase()) match { + case None => Left(s"${s} is not valid status value, accepted values are ${allValues.map(_.value).mkString(", ")}") + case Some(v) => Right(v) + } +} + +case class NoDetailsScore(scoreId: String, value: ScoreValue, message: String) +case class Score(scoreId: String, value: ScoreValue, message: String, details: Json) + +case class GlobalScore(value: ScoreValue, message: String, details: List[NoDetailsScore]) + +object GlobalScoreService { + def computeGlobalScore(oldScore: List[NoDetailsScore], scores: List[Score]): GlobalScore = { + + val correctScores = scores.foldRight(oldScore) { + case (newScore, acc) => + NoDetailsScore(newScore.scoreId, newScore.value, newScore.message) :: acc.filterNot(_.scoreId == newScore.scoreId) + } + import ScoreValue._ + val score = if (correctScores.exists(_.value == E)) { E } + else if (correctScores.exists(_.value == D)) { D } + else if (correctScores.exists(_.value == C)) { + C + } else if (correctScores.exists(_.value == B)) { + B + } else A + GlobalScore(score, s"There is at least a Score with ${score.value}", correctScores) + } +} + +trait ScoreEvent + +case class InventoryScoreEvent(nodeId: NodeId, inventory: Inventory) extends ScoreEvent +case class ComplianceScoreEvent(nodeId: NodeId, compliancePercent: CompliancePercent) extends ScoreEvent + +trait ScoreEventHandler { + def handle(event: ScoreEvent): PureResult[List[(NodeId, List[Score])]] +} + +object ScoreSerializer { + implicit val scoreValueEncoder: JsonEncoder[ScoreValue] = JsonEncoder[String].contramap(_.value) + implicit val scoreValueDecoder: JsonDecoder[ScoreValue] = JsonDecoder[String].mapOrFail(ScoreValue.fromString) + + implicit val noDetailsScoreEncoder: JsonEncoder[NoDetailsScore] = DeriveJsonEncoder.gen + implicit val noDetailsScoreDecoder: JsonDecoder[NoDetailsScore] = DeriveJsonDecoder.gen + + implicit val globalScoreEncoder: JsonEncoder[GlobalScore] = DeriveJsonEncoder.gen + implicit val globalScoreDecoder: JsonDecoder[GlobalScore] = DeriveJsonDecoder.gen + + implicit val jsonScoreEncoder: JsonEncoder[Score] = DeriveJsonEncoder.gen + implicit val jsonScoreDecoder: JsonDecoder[Score] = DeriveJsonDecoder.gen +} + +object ScoreLoggerPure extends NamedZioLogger { + override def loggerName: String = "score" +} diff --git a/webapp/sources/rudder/rudder-core/src/main/scala/com/normation/rudder/score/ScoreRepository.scala b/webapp/sources/rudder/rudder-core/src/main/scala/com/normation/rudder/score/ScoreRepository.scala new file mode 100644 index 00000000000..1e0e07a44c0 --- /dev/null +++ b/webapp/sources/rudder/rudder-core/src/main/scala/com/normation/rudder/score/ScoreRepository.scala @@ -0,0 +1,130 @@ +/* + ************************************************************************************* + * Copyright 2024 Normation SAS + ************************************************************************************* + * + * This file is part of Rudder. + * + * Rudder is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * In accordance with the terms of section 7 (7. Additional Terms.) of + * the GNU General Public License version 3, the copyright holders add + * the following Additional permissions: + * Notwithstanding to the terms of section 5 (5. Conveying Modified Source + * Versions) and 6 (6. Conveying Non-Source Forms.) of the GNU General + * Public License version 3, when you create a Related Module, this + * Related Module is not considered as a part of the work and may be + * distributed under the license agreement of your choice. + * A "Related Module" means a set of sources files including their + * documentation that, without modification of the Source Code, enables + * supplementary functions or services in addition to those offered by + * the Software. + * + * Rudder is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Rudder. If not, see . + + * + ************************************************************************************* + */ + +package com.normation.rudder.score + +import com.normation.errors.IOResult +import com.normation.inventory.domain.NodeId +import com.normation.rudder.db.Doobie +import doobie.Fragments +import doobie.Get +import doobie.Put +import doobie.Read +import doobie.Write +import doobie.implicits._ +import doobie.implicits.toSqlInterpolator +import zio.interop.catz._ +import zio.json.ast.Json + +trait ScoreRepository { + + def getAll(): IOResult[Map[NodeId, List[Score]]] + def getScore(nodeId: NodeId, scoreId: Option[String]): IOResult[List[Score]] + def getOneScore(nodeId: NodeId, scoreId: String): IOResult[Score] + def saveScore(nodeId: NodeId, score: Score): IOResult[Unit] + def deleteScore(nodeId: NodeId, scoreId: Option[String]): IOResult[Unit] + +} + +class ScoreRepositoryImpl(doobie: Doobie) extends ScoreRepository { + + import com.normation.rudder.db.json.implicits._ + implicit val getScoreValue: Get[ScoreValue] = Get[String].temap(ScoreValue.fromString) + implicit val putScoreValue: Put[ScoreValue] = Put[String].contramap(_.value) + + // implicit val stateWrite: Meta[Score] = new Meta(pgDecoderGet, pgEncoderPut) + + implicit val scoreWrite: Write[(NodeId, Score)] = { + Write[(String, String, ScoreValue, String, Json)].contramap { + case (nodeId: NodeId, score: Score) => + (nodeId.value, score.scoreId, score.value, score.message, score.details) + } + } + + implicit val scoreRead: Read[Score] = { + Read[(String, ScoreValue, String, Json)].map { d: (String, ScoreValue, String, Json) => Score(d._1, d._2, d._3, d._4) } + } + implicit val scoreWithIdRead: Read[(NodeId, Score)] = { + Read[(String, String, ScoreValue, String, Json)].map { d: (String, String, ScoreValue, String, Json) => + (NodeId(d._1), Score(d._2, d._3, d._4, d._5)) + } + } + + import doobie._ + override def getAll(): IOResult[Map[NodeId, List[Score]]] = { + val q = sql"select nodeId, scoreId, score, message, details from scoreDetails " + transactIOResult(s"error when getting scores for node")(xa => q.query[(NodeId, Score)].to[List].transact(xa)) + .map(_.groupMap(_._1)(_._2)) + } + + override def getScore(nodeId: NodeId, scoreId: Option[String]): IOResult[List[Score]] = { + + val whereNode = Some(fr"nodeId = ${nodeId.value}") + val whereName = scoreId.map(n => fr"scoreId = ${n}") + val where = Fragments.whereAndOpt(whereNode, whereName) + val q = sql"select scoreId, score, message, details from scoreDetails " ++ where + transactIOResult(s"error when getting scores for node")(xa => q.query[Score].to[List].transact(xa)) + } + + override def getOneScore(nodeId: NodeId, scoreId: String): IOResult[Score] = { + val whereNode = fr"nodeId = ${nodeId.value}" + val whereName = fr"scoreId = ${scoreId}" + val where = Fragments.whereAnd(whereNode, whereName) + val q = sql"select scoreId, score, message, details from scoreDetails " ++ where + transactIOResult(s"error when getting scores for node")(xa => q.query[Score].unique.transact(xa)) + } + + override def saveScore(nodeId: NodeId, score: Score): IOResult[Unit] = { + + val query = { + sql"""insert into scoreDetails (nodeId, scoreId, score, message, details) values (${(nodeId, score)}) + | ON CONFLICT (nodeId, scoreId) DO UPDATE + | SET score = ${score.value}, message = ${score.message}, details = ${score.details} ; """.stripMargin + } + + transactIOResult(s"error when inserting global score for node '${nodeId.value}''")(xa => query.update.run.transact(xa)).unit + + } + + override def deleteScore(nodeId: NodeId, scoreId: Option[String]): IOResult[Unit] = { + val whereNode = Some(fr"nodeId = ${nodeId.value}") + val whereName = scoreId.map(n => fr"scoreId = ${n}") + val where = Fragments.whereAndOpt(whereNode, whereName) + val q = sql"delete from scoreDetails " ++ where + transactIOResult(s"error when getting global score for node ${nodeId.value}")(xa => q.update.run.transact(xa).unit) + } +} diff --git a/webapp/sources/rudder/rudder-core/src/main/scala/com/normation/rudder/score/ScoreService.scala b/webapp/sources/rudder/rudder-core/src/main/scala/com/normation/rudder/score/ScoreService.scala new file mode 100644 index 00000000000..e3dd591b0d7 --- /dev/null +++ b/webapp/sources/rudder/rudder-core/src/main/scala/com/normation/rudder/score/ScoreService.scala @@ -0,0 +1,146 @@ +/* + ************************************************************************************* + * Copyright 2022 Normation SAS + ************************************************************************************* + * + * This file is part of Rudder. + * + * Rudder is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * In accordance with the terms of section 7 (7. Additional Terms.) of + * the GNU General Public License version 3, the copyright holders add + * the following Additional permissions: + * Notwithstanding to the terms of section 5 (5. Conveying Modified Source + * Versions) and 6 (6. Conveying Non-Source Forms.) of the GNU General + * Public License version 3, when you create a Related Module, this + * Related Module is not considered as a part of the work and may be + * distributed under the license agreement of your choice. + * A "Related Module" means a set of sources files including their + * documentation that, without modification of the Source Code, enables + * supplementary functions or services in addition to those offered by + * the Software. + * + * Rudder is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Rudder. If not, see . + + * + ************************************************************************************* + */ + +package com.normation.rudder.score + +import com.normation.errors.IOResult +import com.normation.inventory.domain.InventoryError.Inconsistency +import com.normation.inventory.domain.NodeId +import com.normation.zio._ +import zio._ +import zio.syntax.ToZio + +trait ScoreService { + def getAll(): IOResult[Map[NodeId, GlobalScore]] + def getGlobalScore(nodeId: NodeId): IOResult[GlobalScore] + def getScoreDetails(nodeId: NodeId): IOResult[List[Score]] + def cleanScore(name: String): IOResult[Unit] + def update(newScores: Map[NodeId, List[Score]]): IOResult[Unit] +} + +class ScoreServiceImpl(globalScoreRepository: GlobalScoreRepository, scoreRepository: ScoreRepository) extends ScoreService { + private[this] val cache: Ref[Map[NodeId, GlobalScore]] = globalScoreRepository.getAll().flatMap(Ref.make(_)).runNow + private[this] val scoreCache: Ref[Map[NodeId, List[Score]]] = scoreRepository.getAll().flatMap(Ref.make(_)).runNow + + def getAll(): IOResult[Map[NodeId, GlobalScore]] = cache.get + def getGlobalScore(nodeId: NodeId): IOResult[GlobalScore] = { + for { + c <- cache.get + res <- + c.get(nodeId) match { + case Some(g) => g.succeed + case None => Inconsistency(s"No global score for node ${nodeId.value}").fail + } + } yield { + res + } + } + + def getScoreDetails(nodeId: NodeId): IOResult[List[Score]] = { + for { + c <- scoreCache.get + res <- + c.get(nodeId) match { + case Some(g) => g.succeed + case None => Inconsistency(s"No score for node ${nodeId.value}").fail + } + } yield { + res + } + } + + def cleanScore(name: String): IOResult[Unit] = { + for { + _ <- cache.update(_.map { case (id, gscore) => (id, gscore.copy(details = gscore.details.filterNot(_.scoreId == name))) }) + } yield {} + } + + def update(newScores: Map[NodeId, List[Score]]): IOResult[Unit] = { + for { + c <- cache.get + updatedValue = (for { + (nodeId, newScores) <- newScores + } yield { + val oldScores = c.get(nodeId) match { + case None => Nil + case Some(oldScore) => oldScore.details + } + (nodeId, GlobalScoreService.computeGlobalScore(oldScores, newScores)) + }) + + updateScoreCache <- ZIO.foreach(newScores.toList) { + case (nodeId, scores) => + ZIO.foreach(scores)(score => { + scoreRepository.saveScore(nodeId, score).catchAll(err => ScoreLoggerPure.info(err.fullMsg)) *> + scoreCache.update(sc => + sc + ((nodeId, score :: sc.get(nodeId).getOrElse(Nil).filter(_.scoreId != score.scoreId))) + ) + }) + + } + updatedCache <- ZIO.foreach(updatedValue.toList) { + case (nodeId, score) => globalScoreRepository.save(nodeId, score) *> cache.update(_.+((nodeId, score))) + } + } yield {} + + } +} + +class ScoreServiceManager(readScore: ScoreService) { + + val handlers: Ref[List[ScoreEventHandler]] = + Ref.make(ComplianceScoreEventHandler :: SystemUpdateScoreHandler :: List.empty[ScoreEventHandler]).runNow + + def registerHandler(handler: ScoreEventHandler) = { + handlers.update(handler :: _) + } + + def handleEvent(scoreEvent: ScoreEvent): IOResult[Unit] = { + (for { + h <- handlers.get + _ <- ScoreLoggerPure.debug(s"Received new score event ${scoreEvent.getClass}") + _ <- ScoreLoggerPure.trace(s"event details: ${scoreEvent}") + handled <- ZIO.foreach(h)(_.handle(scoreEvent).toIO) + newScore = handled.flatMap(_.groupMapReduce(_._1)(_._2)(_ ++ _)).toMap + _ <- ScoreLoggerPure.debug(s"${newScore.size} score for event") + _ <- ScoreLoggerPure.ifTraceEnabled(ZIO.foreach(newScore.toList)(s => ScoreLoggerPure.trace(s"${s}"))) + _ <- readScore.update(newScore) + } yield {}).catchAll(err => + ScoreLoggerPure.error(s"An error occurred while treating score event of type '${scoreEvent.getClass}': ${err.fullMsg}") + ) + } +} diff --git a/webapp/sources/rudder/rudder-core/src/main/scala/com/normation/rudder/score/SystemUpdateScore.scala b/webapp/sources/rudder/rudder-core/src/main/scala/com/normation/rudder/score/SystemUpdateScore.scala new file mode 100644 index 00000000000..f6e452b7877 --- /dev/null +++ b/webapp/sources/rudder/rudder-core/src/main/scala/com/normation/rudder/score/SystemUpdateScore.scala @@ -0,0 +1,109 @@ +/* + ************************************************************************************* + * Copyright 2024 Normation SAS + ************************************************************************************* + * + * This file is part of Rudder. + * + * Rudder is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * In accordance with the terms of section 7 (7. Additional Terms.) of + * the GNU General Public License version 3, the copyright holders add + * the following Additional permissions: + * Notwithstanding to the terms of section 5 (5. Conveying Modified Source + * Versions) and 6 (6. Conveying Non-Source Forms.) of the GNU General + * Public License version 3, when you create a Related Module, this + * Related Module is not considered as a part of the work and may be + * distributed under the license agreement of your choice. + * A "Related Module" means a set of sources files including their + * documentation that, without modification of the Source Code, enables + * supplementary functions or services in addition to those offered by + * the Software. + * + * Rudder is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Rudder. If not, see . + + * + ************************************************************************************* + */ + +package com.normation.rudder.score + +import com.normation.errors.PureResult +import com.normation.inventory.domain.InventoryError.Inconsistency +import com.normation.inventory.domain.NodeId +import com.normation.inventory.domain.SoftwareUpdateKind +import com.normation.rudder.score.ScoreValue.A +import com.normation.rudder.score.ScoreValue.B +import com.normation.rudder.score.ScoreValue.C +import com.normation.rudder.score.ScoreValue.D +import com.normation.rudder.score.ScoreValue.E +import zio.json.DeriveJsonEncoder +import zio.json.EncoderOps +import zio.json.JsonEncoder + +case class SystemUpdateStats( + nbPackages: Int, + security: Option[Int], + updates: Option[Int], + defect: Option[Int], + enhancement: Option[Int], + other: Option[Int] +) + +object SystemUpdateScoreHandler extends ScoreEventHandler { + def handle(event: ScoreEvent): PureResult[List[(NodeId, List[Score])]] = { + implicit val compliancePercentEncoder: JsonEncoder[SystemUpdateStats] = DeriveJsonEncoder.gen + event match { + case InventoryScoreEvent(n, inventory) => + val sum = inventory.node.softwareUpdates.size + val security = inventory.node.softwareUpdates.count(_.kind == SoftwareUpdateKind.Security) + val patch = inventory.node.softwareUpdates.count(_.kind == SoftwareUpdateKind.None) + val defect = inventory.node.softwareUpdates.count(_.kind == SoftwareUpdateKind.Defect) + val enhancement = inventory.node.softwareUpdates.count(_.kind == SoftwareUpdateKind.Enhancement) + val other = inventory.node.softwareUpdates.count { s => + s.kind match { + case SoftwareUpdateKind.Other(_) => true + case _ => false + } + } + val s = SystemUpdateStats( + sum, + if (security > 0) Some(security) else None, + if (patch > 0) Some(patch) else None, + if (defect > 0) Some(defect) else None, + if (enhancement > 0) Some(enhancement) else None, + if (other > 0) Some(other) else None + ) + (for { + stats <- s.toJsonAST + } yield { + val scoreId = "system-updates" + val score = if (security == 0 && sum < 50) { + Score(scoreId, A, "Node has no security updates and less than 50 updates available", stats) + } else if (security < 5) { + Score(scoreId, B, s"Node has ${security} security updates available (less than 5)", stats) + } else if (security < 20) { + Score(scoreId, C, s"Node has ${security} security updates available (less than 20)", stats) + } else if (security < 50) { + Score(scoreId, D, s"Node has ${security} security updates available (less than 50)", stats) + } else { + Score(scoreId, E, s"Node has ${security} security updates available (more than 50)", stats) + } + ((n, score :: Nil) :: Nil) + }) match { + case Left(err) => Left(Inconsistency(err)) + case Right(r) => Right(r) + } + case _ => Right(Nil) + } + } +} diff --git a/webapp/sources/rudder/rudder-core/src/main/scala/com/normation/rudder/services/reports/ReportingServiceImpl.scala b/webapp/sources/rudder/rudder-core/src/main/scala/com/normation/rudder/services/reports/ReportingServiceImpl.scala index 28f9d21cb54..9a3b2d08cf5 100644 --- a/webapp/sources/rudder/rudder-core/src/main/scala/com/normation/rudder/services/reports/ReportingServiceImpl.scala +++ b/webapp/sources/rudder/rudder-core/src/main/scala/com/normation/rudder/services/reports/ReportingServiceImpl.scala @@ -60,6 +60,8 @@ import com.normation.rudder.reports.ReportsDisabled import com.normation.rudder.reports.execution.AgentRunId import com.normation.rudder.reports.execution.RoReportsExecutionRepository import com.normation.rudder.repository._ +import com.normation.rudder.score.ComplianceScoreEvent +import com.normation.rudder.score.ScoreServiceManager import com.normation.utils.Control.traverse import com.normation.zio._ import net.liftweb.common._ @@ -147,7 +149,8 @@ class CachedReportingServiceImpl( val defaultFindRuleNodeStatusReports: ReportingServiceImpl, val nodeFactRepository: NodeFactRepository, val batchSize: Int, - val complianceRepository: ComplianceRepository + val complianceRepository: ComplianceRepository, + val scoreServiceManager: ScoreServiceManager ) extends ReportingService with RuleOrNodeReportingServiceImpl with CachedFindRuleNodeStatusReports { val confExpectedRepo = defaultFindRuleNodeStatusReports.confExpectedRepo val directivesRepo = defaultFindRuleNodeStatusReports.directivesRepo @@ -352,6 +355,7 @@ trait CachedFindRuleNodeStatusReports def defaultFindRuleNodeStatusReports: DefaultFindRuleNodeStatusReports def nodeFactRepository: NodeFactRepository def batchSize: Int + def scoreServiceManager: ScoreServiceManager /** * The cache is managed node by node. @@ -414,7 +418,6 @@ trait CachedFindRuleNodeStatusReports import CacheComplianceQueueAction._ import CacheExpectedReportAction._ - ReportLoggerPure.Cache.debug(s"Performing action ${actions.headOption}") *> // get type of action (actions.headOption match { case None => ReportLoggerPure.Cache.debug("Nothing to do") @@ -424,15 +427,21 @@ trait CachedFindRuleNodeStatusReports ReportLoggerPure.Cache.debug(s"Compliance cache updated for nodes: ${actions.map(_.nodeId.value).mkString(", ")}") *> // all action should be homogeneous, but still, fails on other cases (for { - updates <- ZIO.foreach(actions) { - case a => - a match { - case x: UpdateCompliance => (x.nodeId, x.nodeCompliance).succeed - case x => - Inconsistency(s"Error: found an action of incorrect type in an 'update' for cache: ${x}").fail - } - } - _ <- IOResult.attempt { cache = cache ++ updates } + updates <- ZIO.foreach(actions) { + case a => + a match { + case x: UpdateCompliance => (x.nodeId, x.nodeCompliance).succeed + case x => + Inconsistency(s"Error: found an action of incorrect type in an 'update' for cache: ${x}").fail + } + } + scoreUpdates <- ZIO.foreach(updates) { + case (nodeId, compliance) => + val cp = compliance.compliance.computePercent() + val event = ComplianceScoreEvent(nodeId, cp) + scoreServiceManager.handleEvent(event) + } + _ <- IOResult.attempt { cache = cache ++ updates } } yield ()) case ExpectedReportAction((RemoveNodeInCache(_))) => @@ -454,15 +463,21 @@ trait CachedFindRuleNodeStatusReports for { x <- ZIO.foreach(impactedNodeIds.grouped(batchSize).to(Seq)) { updatedNodes => for { - updated <- defaultFindRuleNodeStatusReports - .findRuleNodeStatusReports(updatedNodes.toSet, Set())(QueryContext.systemQC) - .toIO - _ <- IOResult.attempt { - cache = cache ++ updated - } - _ <- ReportLoggerPure.Cache.debug( - s"Compliance cache recomputed for nodes: ${updated.keys.map(_.value).mkString(", ")}" - ) + updated <- defaultFindRuleNodeStatusReports + .findRuleNodeStatusReports(updatedNodes.toSet, Set())(QueryContext.systemQC) + .toIO + scoreUpdates <- ZIO.foreach(updated.toList) { + case (nodeId, compliance) => + val cp = compliance.compliance.computePercent() + val event = ComplianceScoreEvent(nodeId, cp) + scoreServiceManager.handleEvent(event) + } + _ <- IOResult.attempt { + cache = cache ++ updated + } + _ <- ReportLoggerPure.Cache.debug( + s"Compliance cache recomputed for nodes: ${updated.keys.map(_.value).mkString(", ")}" + ) } yield () } } yield () @@ -964,9 +979,8 @@ trait DefaultFindRuleNodeStatusReports extends ReportingService { // compute the status nodeStatusReports <- buildNodeStatusReports(uncomputedRuns, Set(), Set(), complianceMode.mode, unexpectedMode) - - t2 = System.currentTimeMillis - _ = TimingDebugLogger.debug(s"Compliance: compute compliance reports: ${t2 - t1}ms") + t2 = System.currentTimeMillis + _ = TimingDebugLogger.debug(s"Compliance: compute compliance reports: ${t2 - t1}ms") } yield { nodeStatusReports } diff --git a/webapp/sources/rudder/rudder-core/src/test/scala/com/normation/rudder/repository/jdbc/ReportingServiceTest.scala b/webapp/sources/rudder/rudder-core/src/test/scala/com/normation/rudder/repository/jdbc/ReportingServiceTest.scala index 0286c1e0ab3..9e6fa2863f3 100644 --- a/webapp/sources/rudder/rudder-core/src/test/scala/com/normation/rudder/repository/jdbc/ReportingServiceTest.scala +++ b/webapp/sources/rudder/rudder-core/src/test/scala/com/normation/rudder/repository/jdbc/ReportingServiceTest.scala @@ -66,6 +66,7 @@ import com.normation.rudder.repository.ComplianceRepository import com.normation.rudder.repository.FullActiveTechniqueCategory import com.normation.rudder.repository.RoDirectiveRepository import com.normation.rudder.repository.RoRuleRepository +import com.normation.rudder.score.ScoreServiceManager import com.normation.rudder.services.policies.NodeConfigData import com.normation.rudder.services.reports.CachedFindRuleNodeStatusReports import com.normation.rudder.services.reports.CachedNodeChangesServiceImpl @@ -176,6 +177,7 @@ class ReportingServiceTest extends DBCommon with BoxSpecMatcher { override def batchSize: Int = 5000 + override def scoreServiceManager: ScoreServiceManager = null } val RUDDER_JDBC_BATCH_MAX_SIZE = 5000 diff --git a/webapp/sources/rudder/rudder-core/src/test/scala/com/normation/rudder/services/reports/CachedFindRuleNodeStatusReportsTest.scala b/webapp/sources/rudder/rudder-core/src/test/scala/com/normation/rudder/services/reports/CachedFindRuleNodeStatusReportsTest.scala index 36f312af824..e4a23f32dd6 100644 --- a/webapp/sources/rudder/rudder-core/src/test/scala/com/normation/rudder/services/reports/CachedFindRuleNodeStatusReportsTest.scala +++ b/webapp/sources/rudder/rudder-core/src/test/scala/com/normation/rudder/services/reports/CachedFindRuleNodeStatusReportsTest.scala @@ -55,6 +55,10 @@ import com.normation.rudder.reports.GlobalComplianceMode import com.normation.rudder.reports.execution.RoReportsExecutionRepository import com.normation.rudder.repository.FindExpectedReportRepository import com.normation.rudder.repository.ReportsRepository +import com.normation.rudder.score.GlobalScore +import com.normation.rudder.score.Score +import com.normation.rudder.score.ScoreService +import com.normation.rudder.score.ScoreServiceManager import com.normation.rudder.services.policies.NodeConfigData import com.normation.zio._ import com.softwaremill.quicklens._ @@ -65,6 +69,7 @@ import org.junit.runner.RunWith import org.specs2.mutable._ import org.specs2.runner.JUnitRunner import zio.Chunk +import zio.syntax.ToZio /* * Test the cache behaviour @@ -193,6 +198,18 @@ class CachedFindRuleNodeStatusReportsTest extends Specification { def findStatusReportsForDirective(directiveId: DirectiveId)(implicit qc: QueryContext ): IOResult[Map[NodeId, NodeStatusReport]] = ??? + + override def scoreServiceManager: ScoreServiceManager = new ScoreServiceManager(new ScoreService { + override def getAll(): IOResult[Map[NodeId, GlobalScore]] = ??? + + override def getGlobalScore(nodeId: NodeId): IOResult[GlobalScore] = ??? + + override def getScoreDetails(nodeId: NodeId): IOResult[List[Score]] = ??? + + override def cleanScore(name: String): IOResult[Unit] = ??? + + override def update(newScores: Map[NodeId, List[Score]]): IOResult[Unit] = ().succeed + }) } implicit val qc: QueryContext = QueryContext.testQC diff --git a/webapp/sources/rudder/rudder-rest/src/main/scala/com/normation/rudder/rest/EndpointsDefinition.scala b/webapp/sources/rudder/rudder-rest/src/main/scala/com/normation/rudder/rest/EndpointsDefinition.scala index 4eb8bf3f135..ab96256d811 100644 --- a/webapp/sources/rudder/rudder-rest/src/main/scala/com/normation/rudder/rest/EndpointsDefinition.scala +++ b/webapp/sources/rudder/rudder-rest/src/main/scala/com/normation/rudder/rest/EndpointsDefinition.scala @@ -339,46 +339,69 @@ object DirectiveApi extends ApiModuleProvider[DirectiveApi] } sealed trait NodeApi extends EndpointSchema with SortIndex { - override def dataContainer = Some("nodes") + override def dataContainer: Option[String] = Some("nodes") } object NodeApi extends ApiModuleProvider[NodeApi] { - final case object ListAcceptedNodes extends NodeApi with GeneralApi with ZeroParam with StartsAtVersion2 with SortIndex { + final case object ListAcceptedNodes extends NodeApi with GeneralApi with ZeroParam with StartsAtVersion2 with SortIndex { val z = implicitly[Line].value val description = "List all accepted nodes with configurable details level" val (action, path) = GET / "nodes" } - final case object GetNodesStatus extends NodeApi with GeneralApi with ZeroParam with StartsAtVersion13 with SortIndex { + final case object GetNodesStatus extends NodeApi with GeneralApi with ZeroParam with StartsAtVersion13 with SortIndex { val z = implicitly[Line].value val description = "Get the status (pending, accepted, unknown) of the comma separated list of nodes given by `ids` parameter" val (action, path) = GET / "nodes" / "status" } - final case object ListPendingNodes extends NodeApi with GeneralApi with ZeroParam with StartsAtVersion2 with SortIndex { + final case object ListPendingNodes extends NodeApi with GeneralApi with ZeroParam with StartsAtVersion2 with SortIndex { val z = implicitly[Line].value val description = "List all pending nodes with configurable details level" val (action, path) = GET / "nodes" / "pending" } - final case object PendingNodeDetails extends NodeApi with GeneralApi with OneParam with StartsAtVersion2 with SortIndex { + final case object PendingNodeDetails extends NodeApi with GeneralApi with OneParam with StartsAtVersion2 with SortIndex { val z = implicitly[Line].value val description = "Get information about the given pending node" val (action, path) = GET / "nodes" / "pending" / "{id}" } - final case object NodeDetails extends NodeApi with GeneralApi with OneParam with StartsAtVersion2 with SortIndex { + final case object NodeDetails extends NodeApi with GeneralApi with OneParam with StartsAtVersion2 with SortIndex { val z = implicitly[Line].value val description = "Get information about the given accepted node" val (action, path) = GET / "nodes" / "{id}" } - final case object NodeInheritedProperties extends NodeApi with GeneralApi with OneParam with StartsAtVersion11 with SortIndex { + + final case object NodeInheritedProperties extends NodeApi with GeneralApi with OneParam with StartsAtVersion11 with SortIndex { val z = implicitly[Line].value - val description = "Get all proporeties for that node, included inherited ones" + val description = "Get all propreties for that node, included inherited ones" val (action, path) = GET / "nodes" / "{id}" / "inheritedProperties" } - final case object ApplyPolicyAllNodes extends NodeApi with GeneralApi with ZeroParam with StartsAtVersion8 with SortIndex { + + final case object NodeGlobalScore extends NodeApi with InternalApi with OneParam with StartsAtVersion19 with SortIndex { + val z = implicitly[Line].value + val description = "Get global score for a Node" + val (action, path) = GET / "nodes" / "{id}" / "score" + override def dataContainer = None + } + + final case object NodeScoreDetails extends NodeApi with InternalApi with OneParam with StartsAtVersion19 with SortIndex { + val z = implicitly[Line].value + val description = "Get all score details for a Node" + val (action, path) = GET / "nodes" / "{id}" / "score" / "details" + override def dataContainer = None + } + + final case object NodeScoreDetail extends NodeApi with InternalApi with TwoParam with StartsAtVersion19 with SortIndex { + val z = implicitly[Line].value + val description = "Get a score details for a Node" + val (action, path) = GET / "nodes" / "{id}" / "score" / "details" / "{name}" + override def dataContainer = Some("score") + } + + final case object ApplyPolicyAllNodes extends NodeApi with GeneralApi with ZeroParam with StartsAtVersion8 with SortIndex { val z = implicitly[Line].value val description = "Ask all nodes to start a run with the given policy" val (action, path) = POST / "nodes" / "applyPolicy" } - final case object ChangePendingNodeStatus extends NodeApi with GeneralApi with ZeroParam with StartsAtVersion2 with SortIndex { + final case object ChangePendingNodeStatus extends NodeApi with GeneralApi with ZeroParam with StartsAtVersion2 with SortIndex { val z = implicitly[Line].value val description = "Accept or refuse pending nodes" val (action, path) = POST / "nodes" / "pending" diff --git a/webapp/sources/rudder/rudder-rest/src/main/scala/com/normation/rudder/rest/lift/CampaignApi.scala b/webapp/sources/rudder/rudder-rest/src/main/scala/com/normation/rudder/rest/lift/CampaignApi.scala index 04c7255faa0..325ed5c07ef 100644 --- a/webapp/sources/rudder/rudder-rest/src/main/scala/com/normation/rudder/rest/lift/CampaignApi.scala +++ b/webapp/sources/rudder/rudder-rest/src/main/scala/com/normation/rudder/rest/lift/CampaignApi.scala @@ -1,4 +1,5 @@ package com.normation.rudder.rest.lift + import com.normation.errors.Unexpected import com.normation.rudder.api.ApiVersion import com.normation.rudder.apidata.ZioJsonExtractor diff --git a/webapp/sources/rudder/rudder-rest/src/main/scala/com/normation/rudder/rest/lift/NodeApi.scala b/webapp/sources/rudder/rudder-rest/src/main/scala/com/normation/rudder/rest/lift/NodeApi.scala index 22e9c1d0ee3..7a3a939b35f 100644 --- a/webapp/sources/rudder/rudder-rest/src/main/scala/com/normation/rudder/rest/lift/NodeApi.scala +++ b/webapp/sources/rudder/rudder-rest/src/main/scala/com/normation/rudder/rest/lift/NodeApi.scala @@ -103,6 +103,8 @@ import com.normation.rudder.rest.data.Rest import com.normation.rudder.rest.data.Rest.NodeDetails import com.normation.rudder.rest.data.Validation import com.normation.rudder.rest.data.Validation.NodeValidationError +import com.normation.rudder.score.ScoreSerializer +import com.normation.rudder.score.ScoreService import com.normation.rudder.services.nodes.MergeNodeProperties import com.normation.rudder.services.nodes.NodeInfoService import com.normation.rudder.services.queries._ @@ -168,6 +170,7 @@ class NodeApi( def getLiftEndpoints(): List[LiftApiModule] = { API.endpoints.map(e => { + e match { case API.ListPendingNodes => ListPendingNodes case API.NodeDetails => NodeDetails @@ -186,6 +189,9 @@ class NodeApi( case API.NodeDetailsSoftware => NodeDetailsSoftware case API.NodeDetailsProperty => NodeDetailsProperty case API.CreateNodes => CreateNodes + case API.NodeGlobalScore => GetNodeGlobalScore + case API.NodeScoreDetails => GetNodeScoreDetails + case API.NodeScoreDetail => GetNodeScoreDetail } }) } @@ -574,6 +580,71 @@ class NodeApi( } } + object GetNodeGlobalScore extends LiftApiModule { + val schema = API.NodeGlobalScore + val restExtractor = restExtractorService + + def process( + version: ApiVersion, + path: ApiPath, + id: String, + req: Req, + params: DefaultParams, + authzToken: AuthzToken + ): LiftResponse = { + import ScoreSerializer._ + import com.normation.rudder.rest.implicits._ + (for { + score <- nodeApiService.getNodeGlobalScore(NodeId(id)) + } yield { + score + }).toLiftResponseOne(params, schema, _ => Some(id)) + } + } + + object GetNodeScoreDetails extends LiftApiModule { + val schema = API.NodeScoreDetails + val restExtractor = restExtractorService + + def process( + version: ApiVersion, + path: ApiPath, + id: String, + req: Req, + params: DefaultParams, + authzToken: AuthzToken + ): LiftResponse = { + import ScoreSerializer._ + import com.normation.rudder.rest.implicits._ + nodeApiService.getNodeDetailsScore(NodeId(id)).toLiftResponseOne(params, schema, _ => Some(id)) + } + } + + object GetNodeScoreDetail extends LiftApiModuleString2 { + val schema = API.NodeScoreDetail + val restExtractor = restExtractorService + + def process( + version: ApiVersion, + path: ApiPath, + id: (String, String), + req: Req, + params: DefaultParams, + authzToken: AuthzToken + ): LiftResponse = { + // implicit val action = "getNodeGlobalScore" + // implicit val prettify = params.prettify + import ScoreSerializer._ + import com.normation.rudder.rest.implicits._ + val (nodeId, scoreId) = id + (for { + allDetails <- nodeApiService.getNodeDetailsScore(NodeId(nodeId)) + } yield { + allDetails.filter(_.scoreId == scoreId) + }).toLiftResponseOne(params, schema, _ => Some(nodeId)) + } + } + // WARNING : This is a READ ONLY action // No modifications will be performed // read_only user can access this endpoint @@ -710,7 +781,8 @@ class NodeApiService( acceptedNodeQueryProcessor: QueryProcessor, pendingNodeQueryProcessor: QueryChecker, getGlobalMode: () => Box[GlobalPolicyMode], - relayApiEndpoint: String + relayApiEndpoint: String, + scoreService: ScoreService ) { /// utility functions /// @@ -1080,6 +1152,8 @@ class NodeApiService( } def property(req: Req, property: String, inheritedValue: Boolean)(implicit qc: QueryContext) = { + // import com.normation.rudder.facts.nodes.NodeFactSerialisation.SimpleCodec.codecNodeProperty + for { optNodeIds <- req.json.flatMap(restExtractor.extractNodeIdsFromJson).toIO nodes <- optNodeIds match { @@ -1303,6 +1377,13 @@ class NodeApiService( } } + def getNodeGlobalScore(nodeId: NodeId) = { + scoreService.getGlobalScore(nodeId) + } + + def getNodeDetailsScore(nodeId: NodeId) = { + scoreService.getScoreDetails(nodeId) + } def queryNodes(query: Query, state: InventoryStatus, detailLevel: NodeDetailLevel, version: ApiVersion)(implicit prettify: Boolean, 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 6d7487cb63c..d4e533470d2 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 @@ -765,7 +765,8 @@ class RestTestSetUp { mockNodes.queryProcessor, null, () => Full(GlobalPolicyMode(Audit, PolicyModeOverrides.Always)), - "relay" + "relay", + null ) { implicit val testCC: ChangeContext = { ChangeContext( diff --git a/webapp/sources/rudder/rudder-web/src/main/elm/sources/Node.elm b/webapp/sources/rudder/rudder-web/src/main/elm/sources/Node.elm index f85ec35c9fc..fc5a5d3e2e2 100644 --- a/webapp/sources/rudder/rudder-web/src/main/elm/sources/Node.elm +++ b/webapp/sources/rudder/rudder-web/src/main/elm/sources/Node.elm @@ -6,7 +6,6 @@ import Html.Parser exposing (Node(..)) import Html.Parser.Util import Http exposing (..) import Json.Decode exposing (Value) -import Json.Encode import Result import Node.DataTypes exposing (..) @@ -40,7 +39,7 @@ update msg model = case res of Ok scoreDetails -> ( { model | details = scoreDetails } - , Cmd.batch (List.map (\d -> getDetails {name = d.name, details = d.details }) scoreDetails) + , Cmd.batch (List.map (\d -> getDetails {name = d.scoreId, details = d.details }) scoreDetails) ) Err err -> processApiError "Getting score details" err model @@ -58,7 +57,7 @@ update msg model = ( { model | detailsHtml = Dict.update name (always (Just html)) model.detailsHtml } , Cmd.none ) - Err err -> + Err _ -> (model, errorNotification ("Error when getting "++ name ++" score display") ) diff --git a/webapp/sources/rudder/rudder-web/src/main/elm/sources/Node/View.elm b/webapp/sources/rudder/rudder-web/src/main/elm/sources/Node/View.elm index a10db6096fe..2fe16c30cc8 100644 --- a/webapp/sources/rudder/rudder-web/src/main/elm/sources/Node/View.elm +++ b/webapp/sources/rudder/rudder-web/src/main/elm/sources/Node/View.elm @@ -15,5 +15,5 @@ view model = showScore : Model -> DetailedScore -> Html Msg showScore model score = div[class "d-flex mb-3 align-items-center"] - ( label[class "text-end"][text score.name] :: - (Dict.get score.name model.detailsHtml |> Maybe.withDefault [ small [] [text "No details yet"]]) ) \ No newline at end of file + ( label[class "text-end"][text score.scoreId] :: + (Dict.get score.scoreId model.detailsHtml |> Maybe.withDefault [ small [] [text "No details yet"]]) ) \ No newline at end of file diff --git a/webapp/sources/rudder/rudder-web/src/main/elm/sources/Score/DataTypes.elm b/webapp/sources/rudder/rudder-web/src/main/elm/sources/Score/DataTypes.elm index 10eff11ca32..c561089d442 100644 --- a/webapp/sources/rudder/rudder-web/src/main/elm/sources/Score/DataTypes.elm +++ b/webapp/sources/rudder/rudder-web/src/main/elm/sources/Score/DataTypes.elm @@ -21,13 +21,13 @@ type alias GlobalScore = type alias Score = { value : ScoreValue -- "A/B/C/D/E/F/-" - , name : String -- "compliance" + , scoreId : String -- "compliance" , message : String -- "un message en markdown" } type alias DetailedScore = { value : ScoreValue -- "A/B/C/D/E/F/-" - , name : String -- "compliance" + , scoreId : String -- "compliance" , message : String -- "un message en markdown" , details : Value } diff --git a/webapp/sources/rudder/rudder-web/src/main/elm/sources/Score/JsonDecoder.elm b/webapp/sources/rudder/rudder-web/src/main/elm/sources/Score/JsonDecoder.elm index 99741b58bc3..f53b8b210c9 100644 --- a/webapp/sources/rudder/rudder-web/src/main/elm/sources/Score/JsonDecoder.elm +++ b/webapp/sources/rudder/rudder-web/src/main/elm/sources/Score/JsonDecoder.elm @@ -23,7 +23,7 @@ decodeScore : Decoder Score decodeScore = succeed Score |> required "value" ( map toScoreValue string ) - |> required "name" string + |> required "scoreId" string |> required "message" string @@ -31,7 +31,7 @@ decodeDetailedScore : Decoder DetailedScore decodeDetailedScore = succeed DetailedScore |> required "value" ( map toScoreValue string ) - |> required "name" string + |> required "scoreId" string |> required "message" string |> required "details" value diff --git a/webapp/sources/rudder/rudder-web/src/main/elm/sources/Score/ViewUtils.elm b/webapp/sources/rudder/rudder-web/src/main/elm/sources/Score/ViewUtils.elm index 51b7698027d..4f04d3b38a1 100644 --- a/webapp/sources/rudder/rudder-web/src/main/elm/sources/Score/ViewUtils.elm +++ b/webapp/sources/rudder/rudder-web/src/main/elm/sources/Score/ViewUtils.elm @@ -38,8 +38,8 @@ scoreBreakdownList : List Score -> List (Html Msg) scoreBreakdownList scoreDetails = scoreDetails |> List.map(\sD -> div[class "d-flex flex-column pe-5 align-items-center"] - [ getScoreBadge sD.value (buildTooltipBadge sD.name sD.message) True - , label[class "text-center pt-2"][text (String.Extra.humanize sD.name)] + [ getScoreBadge sD.value (buildTooltipBadge sD.scoreId sD.message) True + , label[class "text-center pt-2"][text (String.Extra.humanize sD.scoreId)] ] ) diff --git a/webapp/sources/rudder/rudder-web/src/main/elm/sources/SystemUpdateScore.elm b/webapp/sources/rudder/rudder-web/src/main/elm/sources/SystemUpdateScore.elm new file mode 100644 index 00000000000..ec04d674555 --- /dev/null +++ b/webapp/sources/rudder/rudder-web/src/main/elm/sources/SystemUpdateScore.elm @@ -0,0 +1,84 @@ +port module SystemUpdateScore exposing (..) + +import Browser +import Html +import Html.String exposing (..) +import Html.String.Attributes exposing (class, title) +import Json.Decode exposing (..) +import Json.Decode.Pipeline exposing (..) +import String.Extra + +port sendHtml : String -> Cmd msg +port getValue : (Value -> msg) -> Sub msg + +type Msg = NewScore Value + +type alias SystemUpdateStats = + { nbPackages : Int, + security : Maybe Int, + patch : Maybe Int, + defect : Maybe Int, + enhancement : Maybe Int, + other : Maybe Int + } + +decodeSystemUpdateStats : Decoder SystemUpdateStats +decodeSystemUpdateStats = + succeed SystemUpdateStats + |> required "nbPackages" int + |> optional "security" (maybe int) Nothing + |> optional "updates" (maybe int) Nothing + |> optional "defect" (maybe int) Nothing + |> optional "enhancement" (maybe int) Nothing + |> optional "other" (maybe int) Nothing + +buildScoreDetails : SystemUpdateStats -> Html msg +buildScoreDetails details = + let + toBadge : String -> String -> Maybe Int -> Html msg + toBadge id iconClass value = + case value of + Just v -> + let + valueTxt = String.fromInt v + titleTxt = (String.Extra.humanize id) ++ ": " ++ valueTxt + in + span[class ("badge badge-" ++ id), title titleTxt][i[class ("fa fa-" ++ iconClass)][], text valueTxt] + Nothing -> text "" + in + div[] + [ toBadge "security" "warning" details.security + , toBadge "bugfix" "bug" details.defect + , toBadge "enhancement" "plus" details.enhancement + , toBadge "update" "box" details.patch + ] + +main = + Browser.element + { init = init + , view = always (Html.text "") + , update = update + , subscriptions = subscriptions + } + + +-- PORTS / SUBSCRIPTIONS +port errorNotification : String -> Cmd msg +subscriptions : () -> Sub Msg +subscriptions _ = getValue (NewScore) + +init : () -> ( (), Cmd Msg ) +init _ = ( (), Cmd.none ) + +update : Msg -> () -> ( () , Cmd Msg) +update msg model = + case msg of + NewScore value -> + case (Json.Decode.decodeValue decodeSystemUpdateStats value) of + Ok compliance -> + let + cmd = buildScoreDetails compliance |> Html.String.toString 0 |> sendHtml + in + (model, cmd) + Err err -> + (model, errorNotification(("Error while reading compliance score details, error is:" ++ (errorToString err)))) \ No newline at end of file diff --git a/webapp/sources/rudder/rudder-web/src/main/javascript/rudder/rudder-elm.js b/webapp/sources/rudder/rudder-web/src/main/javascript/rudder/rudder-elm.js index 2a99dfaf5d8..a557bbc4df8 100644 --- a/webapp/sources/rudder/rudder-web/src/main/javascript/rudder/rudder-elm.js +++ b/webapp/sources/rudder/rudder-web/src/main/javascript/rudder/rudder-elm.js @@ -1,5 +1,5 @@ var scoreDetailsDispatcher = {} ; - +var scoreDetailsApp ; var appNode, appNotif, createSuccessNotification, createErrorNotification, createInfoNotification; $(document).ready(function(){ 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 56b9d362050..82f75db2f7a 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 @@ -131,6 +131,7 @@ import com.normation.rudder.inventory.InventoryMover import com.normation.rudder.inventory.InventoryProcessor import com.normation.rudder.inventory.PostCommitInventoryHooks import com.normation.rudder.inventory.ProcessFile +import com.normation.rudder.inventory.TriggerInventoryScorePostCommit import com.normation.rudder.metrics._ import com.normation.rudder.migration.DefaultXmlEventLogMigration import com.normation.rudder.ncf @@ -163,6 +164,11 @@ import com.normation.rudder.rest.internal._ import com.normation.rudder.rest.lift import com.normation.rudder.rest.lift._ import com.normation.rudder.rule.category._ +import com.normation.rudder.rule.category.GitRuleCategoryArchiverImpl +import com.normation.rudder.score.GlobalScoreRepositoryImpl +import com.normation.rudder.score.ScoreRepositoryImpl +import com.normation.rudder.score.ScoreServiceImpl +import com.normation.rudder.score.ScoreServiceManager import com.normation.rudder.services._ import com.normation.rudder.services.eventlog._ import com.normation.rudder.services.eventlog.EventLogFactoryImpl @@ -1421,7 +1427,8 @@ case class RudderServiceApi( gitModificationRepository: GitModificationRepository, inventorySaver: NodeFactInventorySaver, inventoryDitService: InventoryDitService, - nodeFactRepository: NodeFactRepository + nodeFactRepository: NodeFactRepository, + scoreServiceManager: ScoreServiceManager ) /* @@ -1784,7 +1791,8 @@ object RudderConfigInit { queryProcessor, inventoryQueryChecker, () => configService.rudder_global_policy_mode().toBox, - RUDDER_RELAY_API + RUDDER_RELAY_API, + scoreService ) lazy val parameterApiService2 = { @@ -1971,6 +1979,7 @@ object RudderConfigInit { // new FactRepositoryPostCommit[Unit](factRepo, nodeFactInfoService) // deprecated: we use fact repo now // :: new PostCommitLogger(ldifInventoryLogger) + new TriggerInventoryScorePostCommit[Unit](scoreServiceManager) :: new PostCommitInventoryHooks[Unit](HOOKS_D, HOOKS_IGNORE_SUFFIXES) // removed: this is done as a callback of CoreNodeFactRepos // :: new TriggerPolicyGenerationPostCommit[Unit](asyncDeploymentAgent, uuidGen) @@ -3003,6 +3012,13 @@ object RudderConfigInit { ) } + /// score /// + + lazy val globalScoreRepository = new GlobalScoreRepositoryImpl(doobie) + lazy val scoreRepository = new ScoreRepositoryImpl(doobie) + lazy val scoreService = new ScoreServiceImpl(globalScoreRepository, scoreRepository) + lazy val scoreServiceManager: ScoreServiceManager = new ScoreServiceManager(scoreService) + /////// reporting /////// lazy val nodeConfigurationHashRepo: NodeConfigurationHashRepository = { @@ -3030,7 +3046,8 @@ object RudderConfigInit { nodeFactRepository, RUDDER_JDBC_BATCH_MAX_SIZE, // use same size as for SQL requests - complianceRepositoryImpl + complianceRepositoryImpl, + scoreServiceManager ) // to avoid a StackOverflowError, we set the compliance cache once it'z ready, // and can construct the nodeconfigurationservice without the comlpince cache @@ -3689,7 +3706,8 @@ object RudderConfigInit { gitModificationRepository, inventorySaver, inventoryDitService, - nodeFactRepository + nodeFactRepository, + scoreServiceManager ) // need to be done here to avoid cyclic dependencies diff --git a/webapp/sources/rudder/rudder-web/src/main/scala/com/normation/rudder/web/services/DisplayNode.scala b/webapp/sources/rudder/rudder-web/src/main/scala/com/normation/rudder/web/services/DisplayNode.scala index a0fe736b5f4..09f2c8d7840 100644 --- a/webapp/sources/rudder/rudder-web/src/main/scala/com/normation/rudder/web/services/DisplayNode.scala +++ b/webapp/sources/rudder/rudder-web/src/main/scala/com/normation/rudder/web/services/DisplayNode.scala @@ -456,10 +456,10 @@ object DisplayNode extends Loggable { (globalMode.mode, "

This mode is the globally defined default. You can change it in Settings.

") } } - val complianceScoreApp = { -
++ + val globalScoreApp = { +
++ Script(OnLoad(JsRaw(s""" - |var main = document.getElementById("nodecompliance-app") + |var main = document.getElementById("global-score-app") |var initValues = { | id : "${nodeFact.id.value}", | contextPath : contextPath, @@ -470,43 +470,64 @@ object DisplayNode extends Loggable { |}); |""".stripMargin))) } - val nodeApp = { + val complianceScoreApp = {
++ + Script( + OnLoad(JsRaw("""var complianceScoreMain = document.getElementById("compliance-app"); + |var complianceAppScore = Elm.ComplianceScore.init({node: complianceScoreMain, flags : {}}); + |scoreDetailsDispatcher["compliance"] = function(value){ complianceAppScore.ports.getValue.send(value) }; + |complianceAppScore.ports.sendHtml.subscribe(function(html) { + | scoreDetailsApp.ports.receiveDetails.send({name : "compliance",html : html}); + |}); + |complianceAppScore.ports.errorNotification.subscribe(function(str) { + | createErrorNotification(str) + |});""".stripMargin)) + ) + } + val systemUpdateApp = { +
++ + Script( + OnLoad( + JsRaw("""var systemUpdatesMain = document.getElementById("system-updates-app"); + |var systemUpdatesAppScore = Elm.SystemUpdateScore.init({node: systemUpdatesMain, flags : {}}); + |scoreDetailsDispatcher["system-updates"] = function(value){ systemUpdatesAppScore.ports.getValue.send(value) }; + |systemUpdatesAppScore.ports.sendHtml.subscribe(function(html) { + | scoreDetailsApp.ports.receiveDetails.send({name : "system-updates",html : html}); + |}); + |systemUpdatesAppScore.ports.errorNotification.subscribe(function(str) { + | createErrorNotification(str) + |});""".stripMargin) + ) + ) + } + val nodeApp = { +
++
++ Script( - OnLoad(JsRaw(s""" - |var complianceScoreMain = document.getElementById("compliance-app"); - |var complianceAppScore = Elm.ComplianceScore.init({node: complianceScoreMain, flags : {}}); - |scoreDetailsDispatcher["compliance"] = function(value){ complianceAppScore.ports.getValue.send(value) }; - |var main = document.getElementById("node-app") - |var initValues = { - | id : "${nodeFact.id.value}", - | contextPath : contextPath, - |}; - |var scoreDetailsApp = Elm.Node.init({node: main, flags: initValues}); - |scoreDetailsApp.ports.errorNotification.subscribe(function(str) { - | createErrorNotification(str) - |}); - |scoreDetailsApp.ports.getDetails.subscribe(function(data) { - | var name = data.name - | var value = data.details - | var detailsHandler = scoreDetailsDispatcher[name]; - | if (detailsHandler !== undefined) { - | detailsHandler(value) - | } - |}); - |complianceAppScore.ports.sendHtml.subscribe(function(html) { - | scoreDetailsApp.ports.receiveDetails.send({name : "compliance",html : html}); - |}); - |complianceAppScore.ports.errorNotification.subscribe(function(str) { - | createErrorNotification(str) - |}); - | - |""".stripMargin)) + OnLoad( + JsRaw(s""" + |var main = document.getElementById("node-app") + |var initValues = { + | id : "${nodeFact.id.value}", + | contextPath : contextPath, + |}; + |scoreDetailsApp = Elm.Node.init({node: main, flags: initValues}); + |scoreDetailsApp.ports.errorNotification.subscribe(function(str) { + | createErrorNotification(str) + |}); + |scoreDetailsApp.ports.getDetails.subscribe(function(data) { + | var name = data.name + | var value = data.details + | var detailsHandler = scoreDetailsDispatcher[name]; + | if (detailsHandler !== undefined) { + | detailsHandler(value) + | } + |});""".stripMargin) + ) ) }
- {complianceScoreApp} + {globalScoreApp ++ complianceScoreApp ++ systemUpdateApp}
{nodeApp} diff --git a/webapp/sources/rudder/rudder-web/src/main/webapp/secure/nodeManager/node.html b/webapp/sources/rudder/rudder-web/src/main/webapp/secure/nodeManager/node.html index 0e7f554023b..227d2893bcb 100644 --- a/webapp/sources/rudder/rudder-web/src/main/webapp/secure/nodeManager/node.html +++ b/webapp/sources/rudder/rudder-web/src/main/webapp/secure/nodeManager/node.html @@ -15,6 +15,7 @@ +