diff --git a/Dockerfile b/Dockerfile index 964627f..8d840a7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -12,10 +12,10 @@ LABEL org.opencontainers.image.vendor="neuland – Büro für Informatik GmbH" LABEL org.opencontainers.image.licenses="Apache-2.0" LABEL org.opencontainers.image.title="bandwhichd-server" LABEL org.opencontainers.image.description="bandwhichd server collecting measurements and calculating statistics" -LABEL org.opencontainers.image.version="0.6.0-rc9" +LABEL org.opencontainers.image.version="0.6.0-rc10" USER guest ENTRYPOINT ["/opt/java/openjdk/bin/java"] CMD ["-jar", "/opt/bandwhichd-server.jar"] EXPOSE 8080 STOPSIGNAL SIGTERM -COPY --from=build --chown=root:root /tmp/bandwhichd-server/target/scala-3.1.3/bandwhichd-server-assembly-0.6.0-rc9.jar /opt/bandwhichd-server.jar \ No newline at end of file +COPY --from=build --chown=root:root /tmp/bandwhichd-server/target/scala-3.1.3/bandwhichd-server-assembly-0.6.0-rc10.jar /opt/bandwhichd-server.jar \ No newline at end of file diff --git a/build.sbt b/build.sbt index 0eefec6..a7f47e0 100644 --- a/build.sbt +++ b/build.sbt @@ -2,7 +2,7 @@ lazy val root = (project in file(".")) .settings( organization := "de.neuland-bfi", name := "bandwhichd-server", - version := "0.6.0-rc9", + version := "0.6.0-rc10", scalaVersion := "3.1.3", Compile / scalaSource := baseDirectory.value / "src" / "main" / "scala", Test / scalaSource := baseDirectory.value / "src" / "test" / "scala", @@ -22,22 +22,22 @@ lazy val root = (project in file(".")) val oldStrategy = (ThisBuild / assemblyMergeStrategy).value oldStrategy(path) }, - libraryDependencies += "co.fs2" %% "fs2-core" % "3.2.12", - libraryDependencies += "co.fs2" %% "fs2-reactive-streams" % "3.2.12", - libraryDependencies += "com.comcast" %% "ip4s-core" % "3.1.3", - libraryDependencies += "com.comcast" %% "ip4s-test-kit" % "3.1.3" % "test", - libraryDependencies += "com.datastax.oss" % "java-driver-core" % "4.14.1", - libraryDependencies += "com.dimafeng" %% "testcontainers-scala-scalatest" % "0.40.10" % "test", - libraryDependencies += "io.circe" %% "circe-core" % "0.14.2", - libraryDependencies += "io.circe" %% "circe-parser" % "0.14.2", + libraryDependencies += "co.fs2" %% "fs2-core" % "3.3.0", + libraryDependencies += "co.fs2" %% "fs2-reactive-streams" % "3.3.0", + libraryDependencies += "com.comcast" %% "ip4s-core" % "3.2.0", + libraryDependencies += "com.comcast" %% "ip4s-test-kit" % "3.2.0" % "test", + libraryDependencies += "com.datastax.oss" % "java-driver-core" % "4.15.0", + libraryDependencies += "com.dimafeng" %% "testcontainers-scala-scalatest" % "0.40.11" % "test", + libraryDependencies += "io.circe" %% "circe-core" % "0.14.3", + libraryDependencies += "io.circe" %% "circe-parser" % "0.14.3", libraryDependencies += "org.http4s" %% "http4s-circe" % "1.0.0-M32", libraryDependencies += "org.http4s" %% "http4s-core" % "1.0.0-M32", libraryDependencies += "org.http4s" %% "http4s-dsl" % "1.0.0-M32", libraryDependencies += "org.http4s" %% "http4s-ember-server" % "1.0.0-M32", - libraryDependencies += "org.scalatest" %% "scalatest" % "3.2.12" % "test", - libraryDependencies += "org.scalatestplus" %% "scalacheck-1-16" % "3.2.12.0" % "test", - libraryDependencies += "org.slf4j" % "slf4j-simple" % "2.0.0" % "runtime", + libraryDependencies += "org.scalatest" %% "scalatest" % "3.2.14" % "test", + libraryDependencies += "org.scalatestplus" %% "scalacheck-1-16" % "3.2.14.0" % "test", + libraryDependencies += "org.slf4j" % "slf4j-simple" % "2.0.3" % "runtime", libraryDependencies += "org.typelevel" %% "cats-effect" % "3.3.14", libraryDependencies += "org.typelevel" %% "cats-effect-testing-scalatest" % "1.4.0" % "test", - libraryDependencies += "org.typelevel" %% "log4cats-slf4j" % "2.4.0" + libraryDependencies += "org.typelevel" %% "log4cats-slf4j" % "2.5.0" ) diff --git a/docker-compose.yml b/docker-compose.yml index 7bd4250..f46599f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -14,7 +14,7 @@ services: cassandra: - image: cassandra:4.0.4 + image: cassandra:4.1 ports: - 9042:9042 bandwhichd-server: diff --git a/src/main/scala/de/neuland/bandwhichd/server/adapter/in/v1/message/Message.scala b/src/main/scala/de/neuland/bandwhichd/server/adapter/in/v1/message/Message.scala index 0297b0e..bc615c4 100644 --- a/src/main/scala/de/neuland/bandwhichd/server/adapter/in/v1/message/Message.scala +++ b/src/main/scala/de/neuland/bandwhichd/server/adapter/in/v1/message/Message.scala @@ -57,9 +57,10 @@ object Message { } yield message given Codec[Measurement.NetworkConfiguration] = - Codec.forProduct5( + Codec.forProduct6( "machine_id", "timestamp", + "maybe_os_release", "hostname", "interfaces", "open_sockets" @@ -67,6 +68,7 @@ object Message { ( nc.machineId, nc.timing, + nc.maybeOsRelease, nc.hostname, nc.interfaces, nc.openSockets @@ -157,6 +159,9 @@ object Message { given Decoder[InterfaceName] = Decoder[String].map(InterfaceName.apply) given Encoder[MachineId] = Encoder[UUID].contramap(_.value) given Decoder[MachineId] = Decoder[UUID].map(MachineId.apply) + given Encoder[OsRelease.FileContents] = Encoder[String].contramap(_.value) + given Decoder[OsRelease.FileContents] = + Decoder[String].map(OsRelease.FileContents.apply) given Encoder[ProcessName] = Encoder[String].contramap(_.value) given Decoder[ProcessName] = Decoder[String].map(ProcessName.apply) given Encoder[Protocol] = Encoder[String].contramap(_ match diff --git a/src/main/scala/de/neuland/bandwhichd/server/adapter/in/v1/stats/StatsCodecs.scala b/src/main/scala/de/neuland/bandwhichd/server/adapter/in/v1/stats/StatsCodecs.scala index 9fbceb5..9f0bda0 100644 --- a/src/main/scala/de/neuland/bandwhichd/server/adapter/in/v1/stats/StatsCodecs.scala +++ b/src/main/scala/de/neuland/bandwhichd/server/adapter/in/v1/stats/StatsCodecs.scala @@ -6,35 +6,56 @@ import io.circe.{Encoder, Json} object StatsCodecs { val encoder: Encoder[MonitoredStats] = (stats: MonitoredStats) => - Json.obj( - "hosts" -> Json.fromFields( - stats.hosts - .map(monitoredHost => - monitoredHost.hostId.uuid.toString -> Json.obj( - "hostname" -> Json.fromString(monitoredHost.hostname.toString), - "additional_hostnames" -> Json.fromValues( - monitoredHost.additionalHostnames.map(additionalHostname => - Json.fromString(additionalHostname.toString) - ) - ), - "connections" -> stats - .connectionsFor(monitoredHost.hostId) - .fold(Json.obj())(hostIdsToConnections => { - Json.fromFields( - hostIdsToConnections.map[(String, Json)]((hostId, _) => { - hostId.uuid.toString -> Json.obj() - }) + Json + .obj( + "hosts" -> Json.fromFields( + stats.hosts + .map(monitoredHost => + monitoredHost.hostId.uuid.toString -> Json.obj( + "hostname" -> Json.fromString( + monitoredHost.hostname.toString + ), + "os_release" -> monitoredHost.maybeOsRelease.fold(Json.Null)( + osRelease => + Json.obj( + "pretty_name" -> osRelease.maybePrettyName + .fold(Json.Null)(prettyName => + Json.fromString(prettyName.value) + ), + "version_id" -> osRelease.maybeVersionId + .fold(Json.Null)(versionId => + Json.fromString(versionId.value) + ), + "id" -> osRelease.maybeId.fold(Json.Null)(id => + Json.fromString(id.value) + ) + ) + ), + "additional_hostnames" -> Json.fromValues( + monitoredHost.additionalHostnames.map(additionalHostname => + Json.fromString(additionalHostname.toString) ) - }) + ), + "connections" -> stats + .connectionsFor(monitoredHost.hostId) + .fold(Json.obj())(hostIdsToConnections => { + Json.fromFields( + hostIdsToConnections + .map[(String, Json)]((hostId, _) => { + hostId.uuid.toString -> Json.obj() + }) + ) + }) + ) ) - ) - ), - "unmonitoredHosts" -> Json.fromFields( - stats.unidentifiedRemoteHosts.map(unidentifiedRemoteHost => { - unidentifiedRemoteHost.hostId.uuid.toString -> Json.obj( - "host" -> Json.fromString(unidentifiedRemoteHost.host.toString) - ) - }) + ), + "unmonitoredHosts" -> Json.fromFields( + stats.unidentifiedRemoteHosts.map(unidentifiedRemoteHost => { + unidentifiedRemoteHost.hostId.uuid.toString -> Json.obj( + "host" -> Json.fromString(unidentifiedRemoteHost.host.toString) + ) + }) + ) ) - ) + .deepDropNullValues } diff --git a/src/main/scala/de/neuland/bandwhichd/server/adapter/out/CassandraMigration.scala b/src/main/scala/de/neuland/bandwhichd/server/adapter/out/CassandraMigration.scala index d73859e..bbdb21d 100644 --- a/src/main/scala/de/neuland/bandwhichd/server/adapter/out/CassandraMigration.scala +++ b/src/main/scala/de/neuland/bandwhichd/server/adapter/out/CassandraMigration.scala @@ -10,6 +10,12 @@ class CassandraMigration[F[_]: Async]( private val cassandraContext: CassandraContext[F] ) { def migrate(configuration: Configuration): F[Unit] = + for { + _ <- migrateV1(configuration) + _ <- migrateV2(configuration) + } yield () + + def migrateV1(configuration: Configuration): F[Unit] = for { _ <- createCidrType(configuration) _ <- createMeasurementNetworkConfigurationInterfaceType(configuration) @@ -18,6 +24,13 @@ class CassandraMigration[F[_]: Async]( _ <- createMeasurementsTable(configuration) } yield () + def migrateV2(configuration: Configuration): F[Unit] = + for { + _ <- addNetworkConfigurationMaybeOrReleaseToMeasurementsTable( + configuration + ) + } yield () + private def createMeasurementsTable( configuration: Configuration ): F[Unit] = @@ -111,4 +124,17 @@ class CassandraMigration[F[_]: Async]( .setTimeout(configuration.migrationQueryTimeout) .build() ) + + private def addNetworkConfigurationMaybeOrReleaseToMeasurementsTable( + configuration: Configuration + ): F[Unit] = + cassandraContext.executeRawExpectNoRow( + SimpleStatement + .builder( + "alter table measurements_by_date add if not exists network_configuration_maybe_os_release text" + ) + .setKeyspace(configuration.measurementsKeyspace) + .setTimeout(configuration.migrationQueryTimeout) + .build() + ) } diff --git a/src/main/scala/de/neuland/bandwhichd/server/adapter/out/measurement/MeasurementCassandraCodecs.scala b/src/main/scala/de/neuland/bandwhichd/server/adapter/out/measurement/MeasurementCassandraCodecs.scala index 0f0cda7..7552e1c 100644 --- a/src/main/scala/de/neuland/bandwhichd/server/adapter/out/measurement/MeasurementCassandraCodecs.scala +++ b/src/main/scala/de/neuland/bandwhichd/server/adapter/out/measurement/MeasurementCassandraCodecs.scala @@ -17,12 +17,13 @@ import scala.util.Try object MeasurementCassandraCodecs { given Codec[Measurement[Timing]] = - Codec.forProduct9( + Codec.forProduct10( "date", "timestamp", "end_timestamp", "machine_id", "measurement_type", + "network_configuration_maybe_os_release", "network_configuration_hostname", "network_configuration_interfaces", "network_configuration_open_sockets", @@ -34,6 +35,7 @@ object MeasurementCassandraCodecs { endTimestamp: Timing.Timestamp, machineId: MachineId, measurementType: String, + maybeOsRelease: String, hostname: Hostname, interfaces: Seq[Interface], openSockets: Seq[OpenSocket], @@ -44,6 +46,7 @@ object MeasurementCassandraCodecs { Measurement.NetworkConfiguration( machineId = machineId, timing = timestamp, + maybeOsRelease = Some(OsRelease.FileContents(maybeOsRelease)), hostname = hostname, interfaces = interfaces, openSockets = openSockets @@ -68,6 +71,7 @@ object MeasurementCassandraCodecs { case Measurement.NetworkConfiguration( machineId, timing, + maybeOsRelease, hostname, interfaces, openSockets @@ -78,6 +82,7 @@ object MeasurementCassandraCodecs { Timing.Timestamp(Instant.EPOCH), machineId, "network_configuration", + maybeOsRelease.fold("")(_.value), hostname, interfaces, openSockets, @@ -94,6 +99,7 @@ object MeasurementCassandraCodecs { Timing.Timestamp(timing.value.normalizedStop), machineId, "network_utilization", + "", Hostname.fromString("a").get, Seq.empty[Interface], Seq.empty[OpenSocket], diff --git a/src/main/scala/de/neuland/bandwhichd/server/domain/OsRelease.scala b/src/main/scala/de/neuland/bandwhichd/server/domain/OsRelease.scala new file mode 100644 index 0000000..c7d9b4f --- /dev/null +++ b/src/main/scala/de/neuland/bandwhichd/server/domain/OsRelease.scala @@ -0,0 +1,84 @@ +package de.neuland.bandwhichd.server.domain + +import scala.util.matching.Regex + +case class OsRelease( + maybeId: Option[OsRelease.Id], + maybeVersionId: Option[OsRelease.VersionId], + maybePrettyName: Option[OsRelease.PrettyName] +) + +object OsRelease { + def apply(fileContents: FileContents): OsRelease = { + import de.neuland.bandwhichd.server.domain.OsRelease.FileContents.findValue + + OsRelease( + maybeId = fileContents.findValue("ID").map(Id.apply), + maybeVersionId = + fileContents.findValue("VERSION_ID").map(VersionId.apply), + maybePrettyName = + fileContents.findValue("PRETTY_NAME").map(PrettyName.apply) + ) + } + + opaque type FileContents = String + + object FileContents { + def apply(value: String): FileContents = value + + private val rowRegex = + """^ *([a-zA-Z]+[a-zA-Z0-9_]*) *= *(?:"([^"]*)"|([a-zA-Z0-9]+)) *$""".r + + extension (fileContents: FileContents) { + def value: String = fileContents + + def parse: OsRelease = OsRelease.apply(fileContents) + + def findValue(key: String): Option[String] = + fileContents.value + .split("\\n") + .to(LazyList) + .flatMap(rowRegex.findFirstMatchIn) + .flatMap(_ match + case Regex.Groups(foundKey, quotedValue, null) + if key.equalsIgnoreCase(foundKey) => + Some(quotedValue) + case Regex.Groups(foundKey, null, unquotedValue) + if key.equalsIgnoreCase(foundKey) => + Some(unquotedValue) + case _ => None + ) + .headOption + } + } + + opaque type Id = String + + object Id { + def apply(value: String): Id = value + + extension (id: Id) { + def value: String = id + } + } + + opaque type VersionId = String + + object VersionId { + def apply(value: String): VersionId = value + + extension (versionId: VersionId) { + def value: String = versionId + } + } + + opaque type PrettyName = String + + object PrettyName { + def apply(value: String): PrettyName = value + + extension (prettyName: PrettyName) { + def value: String = prettyName + } + } +} diff --git a/src/main/scala/de/neuland/bandwhichd/server/domain/measurement/Measurement.scala b/src/main/scala/de/neuland/bandwhichd/server/domain/measurement/Measurement.scala index b81974b..ae9c9a3 100644 --- a/src/main/scala/de/neuland/bandwhichd/server/domain/measurement/Measurement.scala +++ b/src/main/scala/de/neuland/bandwhichd/server/domain/measurement/Measurement.scala @@ -16,6 +16,7 @@ object Measurement { case class NetworkConfiguration( machineId: MachineId, timing: Timing.Timestamp, + maybeOsRelease: Option[OsRelease.FileContents], hostname: Hostname, interfaces: Seq[Interface], openSockets: Seq[OpenSocket] diff --git a/src/main/scala/de/neuland/bandwhichd/server/domain/stats/Host.scala b/src/main/scala/de/neuland/bandwhichd/server/domain/stats/Host.scala index f7cd6ff..655f09f 100644 --- a/src/main/scala/de/neuland/bandwhichd/server/domain/stats/Host.scala +++ b/src/main/scala/de/neuland/bandwhichd/server/domain/stats/Host.scala @@ -45,6 +45,7 @@ object MachineIdHost { case class MonitoredHost( hostId: HostId.MachineId, + maybeOsRelease: Option[OsRelease], hostname: Hostname, additionalHostnames: Set[Hostname], interfaces: Set[Interface] diff --git a/src/main/scala/de/neuland/bandwhichd/server/domain/stats/Stats.scala b/src/main/scala/de/neuland/bandwhichd/server/domain/stats/Stats.scala index 84bd2a6..5eb1e08 100644 --- a/src/main/scala/de/neuland/bandwhichd/server/domain/stats/Stats.scala +++ b/src/main/scala/de/neuland/bandwhichd/server/domain/stats/Stats.scala @@ -70,6 +70,7 @@ object Stats { case Measurement.NetworkConfiguration( machineId, timing, + maybeOsRelease, hostname, interfaces, _ @@ -88,6 +89,7 @@ object Stats { Stats.Bundle( host = MonitoredHost( hostId = hostId, + maybeOsRelease = maybeOsRelease.map(_.parse), hostname = hostname, additionalHostnames = Set.empty, interfaces = interfaces.toSet @@ -99,6 +101,7 @@ object Stats { bundle.copy( host = MonitoredHost( hostId = hostId, + maybeOsRelease = maybeOsRelease.map(_.parse), hostname = hostname, additionalHostnames = bundle.host.hostnames - hostname, interfaces = bundle.host.interfaces ++ interfaces diff --git a/src/test/resources/de/neuland/bandwhichd/server/adapter/in/v1/message/bandwhichd/measurement/agent-network-configuration/v1/example.json b/src/test/resources/de/neuland/bandwhichd/server/adapter/in/v1/message/bandwhichd/measurement/agent-network-configuration/v1/example.json index 8ace49f..c5c4600 100644 --- a/src/test/resources/de/neuland/bandwhichd/server/adapter/in/v1/message/bandwhichd/measurement/agent-network-configuration/v1/example.json +++ b/src/test/resources/de/neuland/bandwhichd/server/adapter/in/v1/message/bandwhichd/measurement/agent-network-configuration/v1/example.json @@ -3,6 +3,7 @@ "content": { "machine_id": "c414c2da-714c-4b68-b97e-3f31e18053d2", "timestamp": "2022-05-06T15:14:51.742Z", + "maybe_os_release": "PRETTY_NAME=\"Debian GNU/Linux 11 (bullseye)\"\nNAME=\"Debian GNU/Linux\"\nVERSION_ID=\"11\"\nVERSION=\"11 (bullseye)\"\nVERSION_CODENAME=bullseye\nID=debian\nHOME_URL=\"https://www.debian.org/\"\nSUPPORT_URL=\"https://www.debian.org/support\"\nBUG_REPORT_URL=\"https://bugs.debian.org/\"", "hostname": "some-host.example.com", "interfaces": [ { diff --git a/src/test/scala/de/neuland/bandwhichd/server/BandwhichdServerApiV1Spec.scala b/src/test/scala/de/neuland/bandwhichd/server/BandwhichdServerApiV1Spec.scala index 6195fcb..da9ab76 100644 --- a/src/test/scala/de/neuland/bandwhichd/server/BandwhichdServerApiV1Spec.scala +++ b/src/test/scala/de/neuland/bandwhichd/server/BandwhichdServerApiV1Spec.scala @@ -298,6 +298,11 @@ class BandwhichdServerApiV1Spec jsonBody shouldBe obj( "hosts" -> obj( "c414c2da-714c-4b68-b97e-3f31e18053d2" -> obj( + "os_release" -> obj( + "pretty_name" -> fromString("Debian GNU/Linux 11 (bullseye)"), + "version_id" -> fromString("11"), + "id" -> fromString("debian") + ), "hostname" -> fromString("some-host.example.com"), "additional_hostnames" -> arr(), "connections" -> obj( diff --git a/src/test/scala/de/neuland/bandwhichd/server/domain/OsReleaseSpec.scala b/src/test/scala/de/neuland/bandwhichd/server/domain/OsReleaseSpec.scala new file mode 100644 index 0000000..df94a6e --- /dev/null +++ b/src/test/scala/de/neuland/bandwhichd/server/domain/OsReleaseSpec.scala @@ -0,0 +1,61 @@ +package de.neuland.bandwhichd.server.domain + +import org.scalatest.OptionValues +import org.scalatest.matchers.should.Matchers +import org.scalatest.wordspec.AnyWordSpec + +class OsReleaseSpec extends AnyWordSpec with Matchers with OptionValues { + + private val debian11FileContents = OsRelease.FileContents( + "PRETTY_NAME=\"Debian GNU/Linux 11 (bullseye)\"\nNAME=\"Debian GNU/Linux\"\nVERSION_ID=\"11\"\nVERSION=\"11 (bullseye)\"\nVERSION_CODENAME=bullseye\nID=debian\nHOME_URL=\"https://www.debian.org/\"\nSUPPORT_URL=\"https://www.debian.org/support\"\nBUG_REPORT_URL=\"https://bugs.debian.org/\"" + ) + + "OsRelease" should { + "be empty" when { + "file contents are empty" in { + // given + val fileContents = OsRelease.FileContents("") + + // when + val osRelease = OsRelease(fileContents = fileContents) + + // then + osRelease.maybeId shouldBe empty + osRelease.maybeVersionId shouldBe empty + osRelease.maybePrettyName shouldBe empty + } + } + + "have ID" when { + "file contents are from Debian GNU/Linux 11 (bullseye)" in { + // when + val osRelease = OsRelease(debian11FileContents) + + // then + osRelease.maybeId.value shouldBe OsRelease.Id("debian") + } + } + + "have version ID" when { + "file contents are from Debian GNU/Linux 11 (bullseye)" in { + // when + val osRelease = OsRelease(debian11FileContents) + + // then + osRelease.maybeVersionId.value shouldBe OsRelease.VersionId("11") + } + } + + "have pretty name" when { + "file contents are from Debian GNU/Linux 11 (bullseye)" in { + // when + val osRelease = OsRelease(debian11FileContents) + + // then + osRelease.maybePrettyName.value shouldBe OsRelease.PrettyName( + "Debian GNU/Linux 11 (bullseye)" + ) + } + } + } +} diff --git a/src/test/scala/de/neuland/bandwhichd/server/domain/measurement/MeasurementFixtures.scala b/src/test/scala/de/neuland/bandwhichd/server/domain/measurement/MeasurementFixtures.scala index 68b153f..08f599e 100644 --- a/src/test/scala/de/neuland/bandwhichd/server/domain/measurement/MeasurementFixtures.scala +++ b/src/test/scala/de/neuland/bandwhichd/server/domain/measurement/MeasurementFixtures.scala @@ -17,6 +17,11 @@ object MeasurementFixtures { MachineId(UUID.fromString("c414c2da-714c-4b68-b97e-3f31e18053d2")), timing = Timing.Timestamp(ZonedDateTime.parse("2022-05-06T15:14:51.742Z")), + maybeOsRelease = Some( + OsRelease.FileContents( + "PRETTY_NAME=\"Debian GNU/Linux 11 (bullseye)\"\nNAME=\"Debian GNU/Linux\"\nVERSION_ID=\"11\"\nVERSION=\"11 (bullseye)\"\nVERSION_CODENAME=bullseye\nID=debian\nHOME_URL=\"https://www.debian.org/\"\nSUPPORT_URL=\"https://www.debian.org/support\"\nBUG_REPORT_URL=\"https://bugs.debian.org/\"" + ) + ), hostname = Hostname.fromString("some-host.example.com").get, interfaces = Seq( Interface( @@ -234,6 +239,7 @@ object MeasurementFixtures { .ncGen() .copy( hostname = host"host0", + maybeOsRelease = None, interfaces = Seq( Interface( name = InterfaceName("eth0"), diff --git a/src/test/scala/de/neuland/bandwhichd/server/domain/stats/StatsSpec.scala b/src/test/scala/de/neuland/bandwhichd/server/domain/stats/StatsSpec.scala index f6cfbe2..e9bef7c 100644 --- a/src/test/scala/de/neuland/bandwhichd/server/domain/stats/StatsSpec.scala +++ b/src/test/scala/de/neuland/bandwhichd/server/domain/stats/StatsSpec.scala @@ -14,6 +14,7 @@ import fs2.Stream import org.scalacheck.Gen import org.scalatest.Assertion import org.scalatest.matchers.should.Matchers +import org.scalatest.OptionValues import org.scalatest.wordspec.{AnyWordSpec, AsyncWordSpec} import org.scalatestplus.scalacheck.ScalaCheckDrivenPropertyChecks @@ -23,7 +24,8 @@ import java.util.UUID class StatsSpec extends AnyWordSpec with ScalaCheckDrivenPropertyChecks - with Matchers { + with Matchers + with OptionValues { "Stats" when { "empty" should { "append configuration" in { @@ -37,6 +39,7 @@ class StatsSpec result.hosts should contain( MonitoredHost( hostId = HostId(nc.machineId), + maybeOsRelease = nc.maybeOsRelease.map(OsRelease.apply), hostname = nc.hostname, additionalHostnames = Set.empty, interfaces = nc.interfaces.toSet @@ -71,6 +74,15 @@ class StatsSpec timing = Timing.Timestamp( ZonedDateTime.parse("2022-05-18T18:09:50.34957395Z") ), + maybeOsRelease = OsRelease + .FileContents( + """ID=id + |VERSION_ID="version_id" + |#VERSION_ID="wrong_version_id" + | # PRETTY_NAME = "" + | PRETTY_NAME = "" """.stripMargin + ) + .some, hostname = Hostname.fromString("some-host.example.com").get, interfaces = Seq.empty, openSockets = Seq.empty @@ -88,6 +100,16 @@ class StatsSpec ) } + "have an os release" in { + // then + result.hosts should have size 1 + result.hosts.head.maybeOsRelease.value shouldBe OsRelease( + maybeId = OsRelease.Id("id").some, + maybeVersionId = OsRelease.VersionId("version_id").some, + maybePrettyName = OsRelease.PrettyName("").some + ) + } + "have the hostname" in { // then result.hosts should have size 1 @@ -107,6 +129,7 @@ class StatsSpec timing = Timing.Timestamp( ZonedDateTime.parse("2022-05-18T18:09:50.34957395Z") ), + maybeOsRelease = None, hostname = Hostname.fromString("some-host.example.com").get, interfaces = Seq.empty, openSockets = Seq.empty @@ -117,6 +140,7 @@ class StatsSpec timing = Timing.Timestamp( ZonedDateTime.parse("2022-05-18T18:09:50.34957395Z") ), + maybeOsRelease = None, hostname = Hostname.fromString("some-host.example.com").get, interfaces = Seq.empty, openSockets = Seq.empty @@ -147,6 +171,7 @@ class StatsSpec timing = Timing.Timestamp( ZonedDateTime.parse("2022-05-18T18:10:50.34957395Z") ), + maybeOsRelease = None, hostname = Hostname.fromString("another-host.example.com").get, interfaces = Seq.empty, openSockets = Seq.empty @@ -157,6 +182,7 @@ class StatsSpec timing = Timing.Timestamp( ZonedDateTime.parse("2022-05-18T18:09:50.34957395Z") ), + maybeOsRelease = None, hostname = Hostname.fromString("some-host.example.com").get, interfaces = Seq.empty, openSockets = Seq.empty @@ -227,6 +253,7 @@ class StatsSpec Measurement.NetworkConfiguration( machineId = machineId1, timing = start1, + maybeOsRelease = None, hostname = hostname1, interfaces = Seq( Interface( @@ -248,6 +275,7 @@ class StatsSpec Measurement.NetworkConfiguration( machineId = machineId2, timing = start2, + maybeOsRelease = None, hostname = hostname2, interfaces = Seq( Interface( diff --git a/src/test/scala/de/neuland/bandwhichd/server/lib/test/cassandra/CassandraContainer.scala b/src/test/scala/de/neuland/bandwhichd/server/lib/test/cassandra/CassandraContainer.scala index 497bc6e..f643857 100644 --- a/src/test/scala/de/neuland/bandwhichd/server/lib/test/cassandra/CassandraContainer.scala +++ b/src/test/scala/de/neuland/bandwhichd/server/lib/test/cassandra/CassandraContainer.scala @@ -16,7 +16,7 @@ case class CassandraContainer( object CassandraContainer { val defaultImageName: DockerImageName = - DockerImageName.parse("cassandra:4.0.4") + DockerImageName.parse("cassandra:4.1") val defaultImageCqlPort: Port = Port.fromInt(9042).get diff --git a/src/test/scala/de/neuland/bandwhichd/server/test/Arbitraries.scala b/src/test/scala/de/neuland/bandwhichd/server/test/Arbitraries.scala index a9eb98a..5e320ca 100644 --- a/src/test/scala/de/neuland/bandwhichd/server/test/Arbitraries.scala +++ b/src/test/scala/de/neuland/bandwhichd/server/test/Arbitraries.scala @@ -49,6 +49,14 @@ object Arbitraries { .oneOf("enp0s31f6", "lo", "virbr0", "tun0", "wlp3s0") .map(InterfaceName.apply) given Gen[MachineId] = Gen.uuid.map(MachineId.apply) + given Gen[OsRelease.FileContents] = + Gen + .oneOf( + "PRETTY_NAME=\"Debian GNU/Linux 11 (bullseye)\"\nNAME=\"Debian GNU/Linux\"\nVERSION_ID=\"11\"\nVERSION=\"11 (bullseye)\"\nVERSION_CODENAME=bullseye\nID=debian\nHOME_URL=\"https://www.debian.org/\"\nSUPPORT_URL=\"https://www.debian.org/support\"\nBUG_REPORT_URL=\"https://bugs.debian.org/\"", + "NAME=\"SLES\"\nVERSION=\"15-SP3\"\nVERSION_ID=\"15.3\"\nPRETTY_NAME=\"SUSE Linux Enterprise Server 15 SP3\"\nID=\"sles\"\nID_LIKE=\"suse\"\nANSI_COLOR=\"0;32\"\nCPE_NAME=\"cpe:/o:suse:sles:15:sp3\"\nDOCUMENTATION_URL=\"https://documentation.suse.com/\"", + "PRETTY_NAME=\"Debian GNU/Linux 8 (jessie)\"\nNAME=\"Debian GNU/Linux\"\nVERSION_ID=\"8\"\nVERSION=\"8 (jessie)\"\nID=debian\nHOME_URL=\"http://www.debian.org/\"\nSUPPORT_URL=\"http://www.debian.org/support\"\nBUG_REPORT_URL=\"https://bugs.debian.org/\"" + ) + .map(OsRelease.FileContents.apply) given Gen[ProcessName] = Gen .oneOf("dhclient", "java", "dnsmasq", "cupsd", "systemd-resolv") @@ -118,6 +126,7 @@ object Arbitraries { given Gen[Measurement.NetworkConfiguration] = for { machineId <- summon[Gen[MachineId]] timestamp <- summon[Gen[Timing.Timestamp]] + maybeOsRelease <- Gen.option(summon[Gen[OsRelease.FileContents]]) hostname <- Ip4sArbitraries.hostnameGenerator numberOfInterfaces <- Gen.chooseNum(1, 4) interfaces <- Gen.listOfN(numberOfInterfaces, summon[Gen[Interface]]) @@ -126,6 +135,7 @@ object Arbitraries { } yield Measurement.NetworkConfiguration( machineId = machineId, timing = timestamp, + maybeOsRelease = maybeOsRelease, hostname = hostname, interfaces = interfaces, openSockets = openSockets