From 24ad1b49dbcec833afe610e4792b186a6e430bfc Mon Sep 17 00:00:00 2001 From: VictorCavichioli Date: Mon, 12 Aug 2024 11:32:48 -0300 Subject: [PATCH 1/6] Create Initial Project Structured --- .github/workflows/actions.yml | 130 ++-- CHANGES.md | 401 +---------- README.md | 14 +- application/pom.xml | 156 +++++ .../ecchronos/application/SpringBooter.java | 40 ++ .../ecchronos/application/config/Config.java | 55 ++ .../application/config/ConfigRefresher.java | 188 +++++ .../config/ConfigurationHelper.java | 114 +++ .../connection/AgentConnectionConfig.java | 556 +++++++++++++++ .../config/connection/Connection.java | 78 +++ .../config/connection/ConnectionConfig.java | 59 ++ .../connection/DistributedJmxConnection.java | 50 ++ .../DistributedNativeConnection.java | 66 ++ .../config/connection/package-info.java | 18 + .../application/config/package-info.java | 18 + .../config/rest/RestServerConfig.java | 50 ++ .../application/config/rest/package-info.java | 18 + .../config/security/CqlTLSConfig.java | 244 +++++++ .../config/security/Credentials.java | 97 +++ .../config/security/JmxTLSConfig.java | 132 ++++ .../security/ReloadingAuthProvider.java | 42 ++ .../security/ReloadingCertificateHandler.java | 266 +++++++ .../application/config/security/Security.java | 107 +++ .../config/security/package-info.java | 18 + .../exceptions/ConfigurationException.java | 39 ++ .../application/exceptions/package-info.java | 18 + .../ecchronos/application/package-info.java | 18 + .../providers/AgentJmxConnectionProvider.java | 161 +++++ .../AgentNativeConnectionProvider.java | 283 ++++++++ .../application/providers/package-info.java | 19 + .../application/spring/BeanConfigurator.java | 281 ++++++++ .../application/spring/package-info.java | 19 + application/src/main/resources/ecc.yml | 97 +++ application/src/main/resources/security.yml | 49 ++ .../application/config/TestConfig.java | 153 ++++ application/src/test/resources/all_set.yml | 48 ++ connection.impl/pom.xml | 123 ++++ .../impl/builders/ContactEndPoint.java | 85 +++ .../impl/builders/DistributedJmxBuilder.java | 260 +++++++ .../builders/DistributedNativeBuilder.java | 395 +++++++++++ .../impl/builders/package-info.java | 18 + .../connection/impl/enums/ConnectionType.java | 23 + .../connection/impl/enums/package-info.java | 18 + .../connection/impl/package-info.java | 18 + .../DistributedJmxConnectionProviderImpl.java | 140 ++++ ...stributedNativeConnectionProviderImpl.java | 93 +++ .../impl/providers/package-info.java | 18 + ...stributedNativeConnectionProviderImpl.java | 163 +++++ connection/pom.xml | 89 +++ .../connection/CertificateHandler.java | 32 + .../connection/DataCenterAwarePolicy.java | 281 ++++++++ .../connection/DataCenterAwareStatement.java | 289 ++++++++ .../DistributedJmxConnectionProvider.java | 36 + .../DistributedNativeConnectionProvider.java | 34 + .../connection/StatementDecorator.java | 27 + .../ecchronos/connection/package-info.java | 18 + data/pom.xml | 171 +++++ .../ecchronos/data/enums/NodeStatus.java | 24 + .../ecchronos/data/enums/package-info.java | 18 + .../data/exceptions/EcChronosException.java | 38 + .../data/exceptions/package-info.java | 18 + .../ecchronos/data/sync/EccNodesSync.java | 275 ++++++++ .../ecchronos/data/sync/package-info.java | 18 + .../ecchronos/data/sync/TestEccNodesSync.java | 147 ++++ .../data/utils/AbstractCassandraTest.java | 80 +++ docs/CONTRIBUTING.md | 3 +- docs/autogenerated/ECCHRONOS_SETTINGS.md | 330 --------- docs/autogenerated/ECCTOOL.md | 238 ------- docs/autogenerated/openapi.yaml | 547 --------------- pmd-rules.xml | 29 +- pom.xml | 661 ++++++++++++++++++ 71 files changed, 7242 insertions(+), 1617 deletions(-) create mode 100644 application/pom.xml create mode 100644 application/src/main/java/com/ericsson/bss/cassandra/ecchronos/application/SpringBooter.java create mode 100644 application/src/main/java/com/ericsson/bss/cassandra/ecchronos/application/config/Config.java create mode 100644 application/src/main/java/com/ericsson/bss/cassandra/ecchronos/application/config/ConfigRefresher.java create mode 100644 application/src/main/java/com/ericsson/bss/cassandra/ecchronos/application/config/ConfigurationHelper.java create mode 100644 application/src/main/java/com/ericsson/bss/cassandra/ecchronos/application/config/connection/AgentConnectionConfig.java create mode 100644 application/src/main/java/com/ericsson/bss/cassandra/ecchronos/application/config/connection/Connection.java create mode 100644 application/src/main/java/com/ericsson/bss/cassandra/ecchronos/application/config/connection/ConnectionConfig.java create mode 100644 application/src/main/java/com/ericsson/bss/cassandra/ecchronos/application/config/connection/DistributedJmxConnection.java create mode 100644 application/src/main/java/com/ericsson/bss/cassandra/ecchronos/application/config/connection/DistributedNativeConnection.java create mode 100644 application/src/main/java/com/ericsson/bss/cassandra/ecchronos/application/config/connection/package-info.java create mode 100644 application/src/main/java/com/ericsson/bss/cassandra/ecchronos/application/config/package-info.java create mode 100644 application/src/main/java/com/ericsson/bss/cassandra/ecchronos/application/config/rest/RestServerConfig.java create mode 100644 application/src/main/java/com/ericsson/bss/cassandra/ecchronos/application/config/rest/package-info.java create mode 100644 application/src/main/java/com/ericsson/bss/cassandra/ecchronos/application/config/security/CqlTLSConfig.java create mode 100644 application/src/main/java/com/ericsson/bss/cassandra/ecchronos/application/config/security/Credentials.java create mode 100644 application/src/main/java/com/ericsson/bss/cassandra/ecchronos/application/config/security/JmxTLSConfig.java create mode 100644 application/src/main/java/com/ericsson/bss/cassandra/ecchronos/application/config/security/ReloadingAuthProvider.java create mode 100644 application/src/main/java/com/ericsson/bss/cassandra/ecchronos/application/config/security/ReloadingCertificateHandler.java create mode 100644 application/src/main/java/com/ericsson/bss/cassandra/ecchronos/application/config/security/Security.java create mode 100644 application/src/main/java/com/ericsson/bss/cassandra/ecchronos/application/config/security/package-info.java create mode 100644 application/src/main/java/com/ericsson/bss/cassandra/ecchronos/application/exceptions/ConfigurationException.java create mode 100644 application/src/main/java/com/ericsson/bss/cassandra/ecchronos/application/exceptions/package-info.java create mode 100644 application/src/main/java/com/ericsson/bss/cassandra/ecchronos/application/package-info.java create mode 100644 application/src/main/java/com/ericsson/bss/cassandra/ecchronos/application/providers/AgentJmxConnectionProvider.java create mode 100644 application/src/main/java/com/ericsson/bss/cassandra/ecchronos/application/providers/AgentNativeConnectionProvider.java create mode 100644 application/src/main/java/com/ericsson/bss/cassandra/ecchronos/application/providers/package-info.java create mode 100644 application/src/main/java/com/ericsson/bss/cassandra/ecchronos/application/spring/BeanConfigurator.java create mode 100644 application/src/main/java/com/ericsson/bss/cassandra/ecchronos/application/spring/package-info.java create mode 100644 application/src/main/resources/ecc.yml create mode 100644 application/src/main/resources/security.yml create mode 100644 application/src/test/java/com/ericsson/bss/cassandra/ecchronos/application/config/TestConfig.java create mode 100644 application/src/test/resources/all_set.yml create mode 100644 connection.impl/pom.xml create mode 100644 connection.impl/src/main/java/com/ericsson/bss/cassandra/ecchronos/connection/impl/builders/ContactEndPoint.java create mode 100644 connection.impl/src/main/java/com/ericsson/bss/cassandra/ecchronos/connection/impl/builders/DistributedJmxBuilder.java create mode 100644 connection.impl/src/main/java/com/ericsson/bss/cassandra/ecchronos/connection/impl/builders/DistributedNativeBuilder.java create mode 100644 connection.impl/src/main/java/com/ericsson/bss/cassandra/ecchronos/connection/impl/builders/package-info.java create mode 100644 connection.impl/src/main/java/com/ericsson/bss/cassandra/ecchronos/connection/impl/enums/ConnectionType.java create mode 100644 connection.impl/src/main/java/com/ericsson/bss/cassandra/ecchronos/connection/impl/enums/package-info.java create mode 100644 connection.impl/src/main/java/com/ericsson/bss/cassandra/ecchronos/connection/impl/package-info.java create mode 100644 connection.impl/src/main/java/com/ericsson/bss/cassandra/ecchronos/connection/impl/providers/DistributedJmxConnectionProviderImpl.java create mode 100644 connection.impl/src/main/java/com/ericsson/bss/cassandra/ecchronos/connection/impl/providers/DistributedNativeConnectionProviderImpl.java create mode 100644 connection.impl/src/main/java/com/ericsson/bss/cassandra/ecchronos/connection/impl/providers/package-info.java create mode 100644 connection.impl/src/test/java/com/ericsson/bss/cassandra/ecchronos/connection/impl/providers/TestDistributedNativeConnectionProviderImpl.java create mode 100644 connection/pom.xml create mode 100644 connection/src/main/java/com/ericsson/bss/cassandra/ecchronos/connection/CertificateHandler.java create mode 100644 connection/src/main/java/com/ericsson/bss/cassandra/ecchronos/connection/DataCenterAwarePolicy.java create mode 100644 connection/src/main/java/com/ericsson/bss/cassandra/ecchronos/connection/DataCenterAwareStatement.java create mode 100644 connection/src/main/java/com/ericsson/bss/cassandra/ecchronos/connection/DistributedJmxConnectionProvider.java create mode 100644 connection/src/main/java/com/ericsson/bss/cassandra/ecchronos/connection/DistributedNativeConnectionProvider.java create mode 100644 connection/src/main/java/com/ericsson/bss/cassandra/ecchronos/connection/StatementDecorator.java create mode 100644 connection/src/main/java/com/ericsson/bss/cassandra/ecchronos/connection/package-info.java create mode 100644 data/pom.xml create mode 100644 data/src/main/java/com/ericsson/bss/cassandra/ecchronos/data/enums/NodeStatus.java create mode 100644 data/src/main/java/com/ericsson/bss/cassandra/ecchronos/data/enums/package-info.java create mode 100644 data/src/main/java/com/ericsson/bss/cassandra/ecchronos/data/exceptions/EcChronosException.java create mode 100644 data/src/main/java/com/ericsson/bss/cassandra/ecchronos/data/exceptions/package-info.java create mode 100644 data/src/main/java/com/ericsson/bss/cassandra/ecchronos/data/sync/EccNodesSync.java create mode 100644 data/src/main/java/com/ericsson/bss/cassandra/ecchronos/data/sync/package-info.java create mode 100644 data/src/test/java/com/ericsson/bss/cassandra/ecchronos/data/sync/TestEccNodesSync.java create mode 100644 data/src/test/java/com/ericsson/bss/cassandra/ecchronos/data/utils/AbstractCassandraTest.java delete mode 100644 docs/autogenerated/ECCHRONOS_SETTINGS.md delete mode 100644 docs/autogenerated/ECCTOOL.md delete mode 100644 docs/autogenerated/openapi.yaml create mode 100644 pom.xml diff --git a/.github/workflows/actions.yml b/.github/workflows/actions.yml index d55a55aab..6216db73c 100644 --- a/.github/workflows/actions.yml +++ b/.github/workflows/actions.yml @@ -1,89 +1,49 @@ -# name: Test report +name: Test report -# on: -# workflow_dispatch: -# paths-ignore: -# - '**.md' -# pull_request: -# paths-ignore: -# - '**.md' -# push: -# paths-ignore: -# - '**.md' +on: + workflow_dispatch: + paths-ignore: + - '**.md' + pull_request: + paths-ignore: + - '**.md' + push: + paths-ignore: + - '**.md' -# env: -# MAVEN_OPTS: -Dmaven.wagon.http.retryHandler.class=standard -Dmaven.wagon.http.retryHandler.count=3 -Dhttp.keepAlive=false -Dmaven.wagon.http.pool=false -Dmaven.wagon.httpconnectionManager.ttlSeconds=120 +env: + MAVEN_OPTS: -Dmaven.wagon.http.retryHandler.class=standard -Dmaven.wagon.http.retryHandler.count=3 -Dhttp.keepAlive=false -Dmaven.wagon.http.pool=false -Dmaven.wagon.httpconnectionManager.ttlSeconds=120 -# permissions: read-all +permissions: read-all -# jobs: -# tests: -# runs-on: ubuntu-20.04 -# strategy: -# fail-fast: false -# matrix: -# include: -# - name: "Unit tests" -# test_suite: 'test jacoco:report' -# - name: "Style check" -# test_suite: 'compile com.mycila:license-maven-plugin:check pmd:pmd pmd:cpd pmd:check pmd:cpd-check javadoc:jar' -# - name: "OSGi integration" -# test_suite: 'install -P docker-integration-test,osgi-integration-tests -DskipUTs' -# artifacts_dir: "osgi-integration/target" -# - name: "Standalone integration 4.0" -# test_suite: 'verify -P docker-integration-test,standalone-integration-tests -DskipUTs' -# artifacts_dir: "standalone-integration/target" -# - name: "Standalone integration 4.1" -# test_suite: 'verify -P docker-integration-test,standalone-integration-tests -Dit.cassandra.version=4.1 -DskipUTs' -# artifacts_dir: "standalone-integration/target" -# - name: "Standalone integration 5.0-alpha1" -# test_suite: 'verify -P docker-integration-test,standalone-integration-tests -Dit.cassandra.version=5.0-alpha1 -DskipUTs' -# artifacts_dir: "standalone-integration/target" -# - name: "Python integration" -# test_suite: 'verify -P docker-integration-test,python-integration-tests -DskipUTs' -# artifacts_dir: "ecchronos-binary/target" -# steps: -# - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 -# - name: Cache local Maven repository -# uses: actions/cache@13aacd865c20de90d75de3b17ebe84f7a17d57d2 # v4.0.0 -# with: -# path: ~/.m2/repository -# key: build-${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }} -# restore-keys: | -# build-${{ runner.os }}-maven- -# - name: Set up JDK -# uses: actions/setup-java@387ac29b308b003ca37ba93a6cab5eb57c8f5f93 # v4.0.0 -# with: -# java-version: 11 -# distribution: 'temurin' -# - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 -# - name: Set up Python 3.8 -# uses: actions/setup-python@0a5c61591373683505ea898e09a3ea4f39ef2b9c # v5.0.0 -# with: -# python-version: 3.8 -# - name: install dependencies -# run: mvn install -DskipTests=true -# - run: mvn $TEST_SUITE -B -# id: tests -# env: -# TEST_SUITE: ${{ matrix.test_suite }} -# - name: Upload artifacts -# if: ${{ failure() && steps.tests.conclusion == 'failure' }} -# uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3.1.3 -# with: -# name: ${{ matrix.name }}-cassandra-logs -# path: ${{ matrix.artifacts_dir }}/cassandra*.log -# if-no-files-found: 'ignore' -# - name: Upload coverage to Codecov -# uses: codecov/codecov-action@54bcd8715eee62d40e33596ef5e8f0f48dbbccab # v4.1.0 -# with: -# token: ${{ secrets.CODECOV_TOKEN }} -# fail_ci_if_error: false -# files: > -# ./rest/target/site/jacoco/jacoco.xml, -# ./core.osgi/target/site/jacoco/jacoco.xml, -# ./application/target/site/jacoco/jacoco.xml, -# ./osgi-integration/target/site/jacoco/jacoco.xml, -# ./core/target/site/jacoco/jacoco.xml, -# ./fm.impl/target/site/jacoco/jacoco.xml, -# ./connection/target/site/jacoco/jacoco.xml +jobs: + tests: + runs-on: ubuntu-20.04 + strategy: + fail-fast: false + matrix: + include: + - name: "Unit tests" + test_suite: 'test jacoco:report' + - name: "Style check" + test_suite: 'compile com.mycila:license-maven-plugin:check pmd:pmd pmd:cpd pmd:check pmd:cpd-check javadoc:jar' + steps: + - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + - name: Cache local Maven repository + uses: actions/cache@13aacd865c20de90d75de3b17ebe84f7a17d57d2 # v4.0.0 + with: + path: ~/.m2/repository + key: build-${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }} + restore-keys: | + build-${{ runner.os }}-maven- + - name: Set up JDK + uses: actions/setup-java@387ac29b308b003ca37ba93a6cab5eb57c8f5f93 # v4.0.0 + with: + java-version: 17 + distribution: 'temurin' + - name: install dependencies + run: mvn install -DskipTests=true + - run: mvn $TEST_SUITE -B + id: tests + env: + TEST_SUITE: ${{ matrix.test_suite }} diff --git a/CHANGES.md b/CHANGES.md index baf0c887f..09ecd754a 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,396 +1,11 @@ # Changes -## Version 5.0.5 -* Updata Mockito and JUnit versions - Issue #687 -* Metric status logger for troubleshooting - Issue #397 +## Version 1.0.0 (Not yet Release) -## Version 5.0.4 - -* ecChronos will break if repair interval is shorter than the initial delay - Issue #667 - -## Version 5.0.3 - -* Spring Framework URL Parsing with Host Validation is vulnerable - Issue #661 -* Possibility for repairs to never be triggered - Issue #264 - -## Version 5.0.2 - -* Containerized ecchronos restarting tomcat when Cassandra peer is overloaded - Issue #650 -* Bump tomcat to 9.0.86 - Issue #653 -* Bump springboot to 2.7.18 - Issue #653 - -## Version 5.0.1 - -* Improve hang preventing task - Issue #544 -* Improve Description of unwind_ratio - Issue #628 - -## Merged from Version 4.0 - -* Separate serial consistency configuration from remoteRouting functionality - Issue #633 - -## Version 5.0.0 - -* Build Ecchronos with Java 11 - Issue 616 -* Bump logback from 1.2.10 to 1.2.13 (CVE-2023-6378) - Issue #622 -* Progress for on demand repair jobs is either 0% or 100% - Issue #593 -* Make priority granularity configurable - Issue #599 -* Bump springboot from 2.7.12 to 2.7.17 - Issue #604 -* Bump io.micrometer from 1.9.2 to 1.9.16 - Issue #604 -* Bump io.dropwizard.metrics from 4.2.10 to 4.2.21 - Issue #604 -* Bump jackson-dataformat-yaml from 2.13.5 to 2.15.2 - Issue #602 -* Make locks dynamic based on TTL of lock table - Issue #543 -* Add new repair type parallel_vnode - Issue #554 -* Add validation of repair interval and alarms - Issue #560 -* Insert into repair history only on session finish - Issue #565 -* Use Caffeine caches instead of Guava - Issue #534 -* Validate TLS config for JMX and CQL - Issue #529 -* Add support for incremental repairs - Issue #31 -* Bump java driver from 4.14.1 to 4.17.0 -* Bump guava from 31.1 to 32.0.1 (CVE-2023-2976) -* Fix shebang in ecctool - Issue #504 -* Bump springboot from 2.7.5 to 2.7.12 - Issue #500 -* Drop support for Python < 3.8 - Issue #474 -* Drop support for Java 8 and Cassandra < 4 - Issue #493 -* Bump guava from 18.0 to 31.1 - Issue #491 -* Reread repair configuration when a node state changes - Issues #470 and #478 -* Support configuring backoff for failed jobs - Issue #475 -* Dropping keyspaces does not clean up schedules - Issue #469 - -### Merged from 1.2 - -* Fix calculation of tokens per repair - Issue #570 - -### Merged from 1.0 - -* Fix logging fault reporter raising duplicate alarm - Issue #557 -* Fix priority calculation for local queue - Issue #546 -* Skip unnecessary reads from repair history - Issue #548 -* Fix repair job priority - Issue #515 - -### Merged from 1.2 - -* Fix calculation of tokens per repair - Issue #570 - -### Merged from 1.0 - -* Fix logging fault reporter raising duplicate alarm - Issue #557 - -## Version 4.0.5 - -### Merged from 1.0 - -* Fix priority calculation for local queue - Issue #546 -* Skip unnecessary reads from repair history - Issue #548 - -## Version 4.0.4 - -### Merged from 1.0 - -* Fix repair job priority - Issue #515 - -## Version 4.0.3 - -* Bump jackson-databind from 2.13.4.1 to 2.13.4.2 -* Improve logging in repairState - Issue #463 -* Set name for all threads - Issue #459 -* Bump apache-karaf from 4.3.6 to 4.3.8 (CVE-2022-40145) - -## Version 4.0.2 - -* Add support for excluding metrics on tags - Issue #446 -* Fix ecctool start with jvm.options - Issue #433 -* Add time.since.last.repaired metric - Issue #423 -* Bump springboot from 2.7.2 to 2.7.5 - Issue #427 -* Use micrometer as metric framework - Issue #422 -* Allow global prefix for metrics - Issue #417 -* Bump prometheus simpleclient from 0.10.0 to 0.16.0 - Issue #416 -* Move exclusion of metrics to each reporter - Issue #411 -* Bump jackson-databind from 2.13.2.2 to 2.13.4.1 -* Default duration to GC grace, repairInfo for table - Issue #409 -* Don't create auth-provider if CQL credentials disabled - Issue #398 -* Add possibility to decide reporting for metrics - Issue #390 -* Update ecctool argparser descriptions - Issue #395 -* Disable driver metrics if statistics.enabled=false - Issue #391 -* Reload sslContext for cql client if certs change - Issue #329 -* Expose java driver metrics - Issue #368 -* Split metrics into separate connector - Issue #369 -* Add possibility to exclude metrics through config - Issue #367 -* Fix help for ecctool run-repair - Issue #365 -* Sort repair-info based on repaired-ratio - Issue #358 -* Support keyspaces and tables with camelCase - Issue #362 -* Fix limit for repair-info - Issue #359 -* Remove version override of log4j - Issue #356 -* Throw configexception if yaml config contains null - Issue #352 -* Add cluster-wide support for repair-info - Issue #156 -* Example size targets are incorrect in schedule.yml - Issue #337 -* Add repair-info - Issue #327 -* Add config to skip schedules of tables with TWCS - Issue #151 -* Add metric for failed and succeeded repair tasks - Issue #295 -* Remove deprecated v1 REST interface -* Migrate to datastax driver-4.14.1 - Issue #269 -* Add PEM format support - Issue #300 - -## Version 3.0.1 (Not yet released) - -### Merged from 1.2 - -* Fix calculation of tokens per repair - Issue #570 - -### Merged from 1.0 - -* Fix logging fault reporter raising duplicate alarm - Issue #557 -* Fix priority calculation for local queue - Issue #546 -* Skip unnecessary reads from repair history - Issue #548 -* Fix repair job priority - Issue #515 - -## Version 3.0.0 - -* Add support for repairs without keyspace/table - Issue #158 -* Add Cassandra health indicator and enable probes - Issue #192 -* Add support for clusterwide repairs - Issue #299 -* Add custom HTML error page -* Make fault reporter pluggable -* Fix JMX URI validation with new Java versions - Issue #306 -* Reworked rest interface - Issue #257 -* Bump springboot from 2.6.4 to 2.6.6 -* Offset next runtime with repair time taken - Issue #121 -* Remove deprecated scripts (ecc-schedule, ecc-status, ecc-config) -* Add blocked status - Issue #284 - -## Version 2.0.7 (Not yet released) - -### Merged from 1.2 - -* Fix calculation of tokens per repair - Issue #570 - -### Merged from 1.0 - -* Fix logging fault reporter raising duplicate alarm - Issue #557 -* Fix priority calculation for local queue - Issue #546 -* Skip unnecessary reads from repair history - Issue #548 -* Fix repair job priority - Issue #515 -* Fix malformed IPv6 for JMX - Issue #306 - -## Version 2.0.6 - -* Add ecc-schedule command - Issue #158 -* Consolidate all scripts into single script with subcommands - Issue #170 -* Bump springboot from 2.5.6 to 2.6.4 (CVE-2021-22060, CVE-2022-23181) -* Bump logback from 1.2.3 to 1.2.10 (CVE-2021-42550) -* Bump apache-karaf from 4.2.8 to 4.3.6 (CVE-2021-41766, CVE-2022-22932) -* Always create pidfile - Issue #253 -* Sort repair-status on newest first - Issue #261 - -## Version 2.0.5 - -* Step log4j-api version to 2.15.0 - Issue #245 - -## Version 2.0.4 - -* Handle endpoints with changing IP addresses - Issue #243 -* Log exceptions during creation/deletion of schedules -* Update gson to 2.8.9 - Issue #239 -* Update netty to 4.1.69.Final -* Many exceptions if Ongoing Repair jobs cannot be fetched - Issue #236 -* Update springbootframework to 2.5.6 - Issue #233 -* Update netty to 4.1.68.Final - Issue #228 -* Schedules are not deleted when dropping tables - Issue #230 -* Step simpleclient to 0.10.0 - -## Version 2.0.3 - -* Fixed signing issue of karaf feature artifact - -## Version 2.0.2 (Not Released) - -* Support certificatehandler pluggability -* Improve logging - Issue #191 -* Fix On Demand Repair Jobs always showing topology changed after restart -* Fix reoccurring flag in ecc-status showing incorrect value -* Update Netty io version to 4.1.62.Final -* Update commons-io to 2.7 -* Update spring-boot-dependencies to 2.5.3 - Issue #223 - -### Merged from 1.0 - -* Step karaf to 4.2.8 -* Improve Alarm logging - Issue #191 - -## Version 2.0.1 - -* Add possibilities to only take local locks - Issue #175 -* Remove default limit of ecc-status -* Process hangs on timeout - Issue #190 - -## Version 2.0.0 - -* OnDemand Job throws NPE when scheduled on non-existing table/keyspace - Issue #183 - -### Merged from 1.2 - -* Repairs not scheduled when statistics disabled - Issue #176 - -## Version 2.0.0-beta - -* Add Code Style - Issue #103 -* Avoid using concurrent map - Issue #101 -* Move alarm handling out of TableRepairJob -* Add State to each ScheduledJob -* Change executable file name from ecChronos to ecc -* Change configuration file name from ecChronos.cfg to ecc.cfg -* Add RepairJobView -* Add HTTP server with REST API - Issue #50 -* Expose metrics through JMX - Issue #75 -* Add ecc-config command that displays repair configuration -* Remove usage of joda time - Issue #95 -* Proper REST interface - Issue #109 -* Make scheduler interval configurable - Issue #122 -* Add manual repair - Issue #14 -* Use yaml format for configuration - Issue #126 -* Use springboot for REST server - Issue #111 -* Add Health Endpoint - Issue #131 -* Use table id in metric names - Issue #120 -* Add authentication for CQL and JMX - Issue #129 -* Add TLS support for CQL and JMX - Issue #129 -* Expose Springboot configuration - Issue #149 -* Per table configurations - Issue #119 - -### Merged from 1.2 - -* Add support for sub range repairs - Issue #96 -* Locks get stuck when unexpected exception occurs - Issue #177 - -### Merged from 1.1 - -* Add Karaf commands that exposes repair status - -### Merged from 1.0 - -* Dynamic license header check - Issue #37 -* Unwind ratio getting ignored - Issue #44 -* Reduce memory footprint - Issue #54 -* Locking failures log too much - Issue #58 -* Fix log file rotation - Issue #61 -* Correct initial replica repair group - Issue #60 -* Fix statistics when no table/data to repair - Issue #59 -* Cache locking failures to reduce unnecessary contention - Issue #70 -* Trigger table repairs more often - Issue #72 -* Reduce error logs to warn for some lock related failures -* Fix slow query of repair_history at start-up #86 -* Reduce cache refresh time in TimeBasedRunPolicy to quicker react to configuration changes -* Avoid concurrent modification exception in RSI#close - Issue #99 -* Support symlink of ecc binary - PR #114 -* Close file descriptors in background mode - PR #115 -* Add JVM options file -* Make policy changes quicker - Issue #117 - -## Version 1.2.0 (Not yet released) - -* Fix calculation of tokens per repair - Issue #570 -* Repairs not scheduled when statistics disabled - Issue #175 - -### Merged from 1.0 - -* Fix logging fault reporter raising duplicate alarm - Issue #557 -* Fix priority calculation for local queue - Issue #546 -* Skip unnecessary reads from repair history - Issue #548 -* Fix repair job priority - Issue #515 -* Fix malformed IPv6 for JMX - Issue #306 -* Step karaf to 4.2.8 -* Improve Alarm logging - Issue #191 -* Locks get stuck when unexpected exception occurs - Issue #177 - -## Version 1.1.4 (Not yet released) - -#### Merged from 1.0 - -* Fix logging fault reporter raising duplicate alarm - Issue #557 -* Fix priority calculation for local queue - Issue #546 -* Skip unnecessary reads from repair history - Issue #548 -* Fix repair job priority - Issue #515 -* Fix malformed IPv6 for JMX - Issue #306 - -## Version 1.1.3 - -* Step karaf to 4.2.8 -* Improve Alarm logging - Issue #191 -* Locks get stuck when unexpected exception occurs - Issue #177 - -## Version 1.1.2 - -### Merged from 1.0 - -* Add Code Style - Issue #103 -* Avoid using concurrent map - Issue #101 -* Avoid concurrent modification exception in RSI#close - Issue #99 -* Support symlink of ecc binary - PR #114 -* Close file descriptors in background mode - PR #115 -* Add JVM options file -* Make policy changes quicker - Issue #117 - -## Version 1.1.1 - -### Merged from 1.0 - -* Reduce cache refresh time in TimeBasedRunPolicy to quicker react to configuration changes - -## Version 1.1.0 - -* Add Karaf commands that exposes repair status - -### Merged from 1.0 - -* Fix slow query of repair_history at start-up #86 - -## Version 1.0.8 (Not yet released) - -* Fix logging fault reporter raising duplicate alarm - Issue #557 -* Fix priority calculation for local queue - Issue #546 -* Skip unnecessary reads from repair history - Issue #548 -* Fix repair job priority - Issue #515 -* Fix malformed IPv6 for JMX - Issue #306 -* Step karaf to 4.2.8 -* Improve Alarm logging - Issue #191 -* Locks get stuck when unexpected exception occurs - Issue #177 - -## Version 1.0.7 - -* Avoid concurrent modification exception in RSI#close - Issue #99 -* Support symlink of ecc binary - PR #114 -* Close file descriptors in background mode - PR #115 -* Add JVM options file -* Make policy changes quicker - Issue #117 - -## Version 1.0.6 - -* Reduce cache refresh time in TimeBasedRunPolicy to quicker react to configuration changes - -## Version 1.0.5 - -* Fix slow query of repair_history at start-up #86 - -## Version 1.0.4 - -* Reduce error logs to warn for some lock related failures - -## Version 1.0.3 - -* Trigger table repairs more often - Issue #72 -* Cache locking failures to reduce unnecessary contention - Issue #70 - -## Version 1.0.2 - -* Locking failures log too much - Issue #58 -* Fix log file rotation - Issue #61 -* Correct initial replica repair group - Issue #60 -* Fix statistics when no table/data to repair - Issue #59 - -## Version 1.0.1 - -* Dynamic license header check - Issue #37 -* Unwind ratio getting ignored - Issue #44 -* Reduce memory footprint - Issue #54 - -## Version 1.0.0 - -* First release +* Investigate Introduction of testContainers Issue #682 +* Create EccNodesSync Object to Represent Table nodes_sync - Issue #672 +* Expose AgentJMXConnectionProvider on Connection and Application Module - Issue #676 +* Create JMXAgentConfig to add Hosts in JMX Session Through ecc.yml - Issue #675 +* Expose AgentNativeConnectionProvider on Connection and Application Module - Issue #673 +* Create DatacenterAwareConfig to add Hosts in CQL Session Through ecc.yml - Issue #671 +* Create Initial project Structure for Agent - Issue #695 diff --git a/README.md b/README.md index c336d9db0..11f474f04 100644 --- a/README.md +++ b/README.md @@ -23,17 +23,15 @@ More information on the REST interface of ecChronos is described in [REST.md](do ## Prerequisites -* JDK 11 -* Python 3.8 +* JDK 17 ### Installation -Installation instructions can be found in [SETUP.md](docs/SETUP.md). +TO DO. ### Command line utility -In standalone installation, a command line utility called `ecctool` is provided. -For more information about `ecctool` refer to [ECCTOOL.md](docs/autogenerated/ECCTOOL.md). +TO DO. ### Getting Started @@ -41,11 +39,11 @@ Instructions on how to use ecChronos and configure it to suit your needs can be ### Upgrade -Upgrade instructions can be found in [UPGRADE.md](docs/UPGRADE.md). +TO DO. ### Compatibility with Cassandra versions -For information about which ecChronos versions have been tested with which Cassandra versions can be found in [COMPATIBILITY.md](docs/COMPATIBILITY.md) +TO DO. ## Contributing @@ -61,7 +59,7 @@ We try to adhere to [SemVer](http://semver.org) for versioning. ## Authors -* **Marcus Olsson** - *Initial work* - [emolsson](https://github.com/emolsson) +* **Victor Cavichioli** - *Initial work* - [ecavvic](https://github.com/VictorCavichioli) See also the list of [contributors](https://github.com/ericsson/ecchronos/contributors) who participated in this project. diff --git a/application/pom.xml b/application/pom.xml new file mode 100644 index 000000000..f5a8f7735 --- /dev/null +++ b/application/pom.xml @@ -0,0 +1,156 @@ + + + + 4.0.0 + + com.ericsson.bss.cassandra.ecchronos + agent + 1.0.0-SNAPSHOT + ../pom.xml + + + application + Standalone application of ecChronos agent + EcChronos Application + + + + + com.ericsson.bss.cassandra.ecchronos + connection + ${project.version} + + + + com.ericsson.bss.cassandra.ecchronos + connection.impl + ${project.version} + + + + com.ericsson.bss.cassandra.ecchronos + data + ${project.version} + + + + + org.springframework.boot + spring-boot-starter-web + + + + org.springframework.boot + spring-boot-starter-thymeleaf + + + + org.springdoc + springdoc-openapi-ui + + + + + com.datastax.oss + java-driver-core + + + + + org.slf4j + slf4j-api + + + + ch.qos.logback + logback-classic + + + + ch.qos.logback + logback-core + + + + + com.fasterxml.jackson.dataformat + jackson-dataformat-yaml + + + + com.fasterxml.jackson.core + jackson-databind + + + + org.yaml + snakeyaml + + + + + org.junit.vintage + junit-vintage-engine + test + + + + org.assertj + assertj-core + test + + + + org.mockito + mockito-core + test + + + + org.awaitility + awaitility + test + + + + nl.jqno.equalsverifier + equalsverifier + test + + + + + org.springframework + spring-test + test + + + org.springframework.boot + spring-boot-test + test + + + org.junit.jupiter + junit-jupiter + test + + + + \ No newline at end of file diff --git a/application/src/main/java/com/ericsson/bss/cassandra/ecchronos/application/SpringBooter.java b/application/src/main/java/com/ericsson/bss/cassandra/ecchronos/application/SpringBooter.java new file mode 100644 index 000000000..be74c3536 --- /dev/null +++ b/application/src/main/java/com/ericsson/bss/cassandra/ecchronos/application/SpringBooter.java @@ -0,0 +1,40 @@ +/* + * Copyright 2024 Telefonaktiebolaget LM Ericsson + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.ericsson.bss.cassandra.ecchronos.application; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.web.servlet.support.SpringBootServletInitializer; + +@SpringBootApplication +public class SpringBooter extends SpringBootServletInitializer +{ + private static final Logger LOG = LoggerFactory.getLogger(SpringBooter.class); + + public static void main(final String[] args) + { + try + { + SpringApplication.run(SpringBooter.class, args); + } + catch (Exception e) + { + LOG.error("Failed to initialize", e); + System.exit(1); + } + } +} diff --git a/application/src/main/java/com/ericsson/bss/cassandra/ecchronos/application/config/Config.java b/application/src/main/java/com/ericsson/bss/cassandra/ecchronos/application/config/Config.java new file mode 100644 index 000000000..c3f9ae78d --- /dev/null +++ b/application/src/main/java/com/ericsson/bss/cassandra/ecchronos/application/config/Config.java @@ -0,0 +1,55 @@ +/* + * Copyright 2024 Telefonaktiebolaget LM Ericsson + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.ericsson.bss.cassandra.ecchronos.application.config; + +import com.ericsson.bss.cassandra.ecchronos.application.config.connection.ConnectionConfig; +import com.ericsson.bss.cassandra.ecchronos.application.config.rest.RestServerConfig; +import com.fasterxml.jackson.annotation.JsonProperty; + +public class Config +{ + private ConnectionConfig myConnectionConfig = new ConnectionConfig(); + private RestServerConfig myRestServerConfig = new RestServerConfig(); + + @JsonProperty("connection") + public final ConnectionConfig getConnectionConfig() + { + return myConnectionConfig; + } + + @JsonProperty("connection") + public final void setConnectionConfig(final ConnectionConfig connectionConfig) + { + if (connectionConfig != null) + { + myConnectionConfig = connectionConfig; + } + } + + @JsonProperty("rest_server") + public final RestServerConfig getRestServer() + { + return myRestServerConfig; + } + + @JsonProperty("rest_server") + public final void setRestServerConfig(final RestServerConfig restServerConfig) + { + if (restServerConfig != null) + { + myRestServerConfig = restServerConfig; + } + } +} diff --git a/application/src/main/java/com/ericsson/bss/cassandra/ecchronos/application/config/ConfigRefresher.java b/application/src/main/java/com/ericsson/bss/cassandra/ecchronos/application/config/ConfigRefresher.java new file mode 100644 index 000000000..bca529fba --- /dev/null +++ b/application/src/main/java/com/ericsson/bss/cassandra/ecchronos/application/config/ConfigRefresher.java @@ -0,0 +1,188 @@ +/* + * Copyright 2024 Telefonaktiebolaget LM Ericsson + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.ericsson.bss.cassandra.ecchronos.application.config; + +import java.io.Closeable; +import java.io.IOException; +import java.nio.file.ClosedWatchServiceException; +import java.nio.file.FileSystems; +import java.nio.file.Path; +import java.nio.file.StandardWatchEventKinds; +import java.nio.file.WatchEvent; +import java.nio.file.WatchKey; +import java.nio.file.WatchService; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; + +import com.google.common.util.concurrent.ThreadFactoryBuilder; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.common.base.Preconditions; + +public class ConfigRefresher implements Closeable +{ + private static final Logger LOG = LoggerFactory.getLogger(ConfigRefresher.class); + + private static final int TEN_SECONDS = 10; + + private final ConcurrentMap knownConfigs = new ConcurrentHashMap<>(); + private final Path baseDirectory; + private final WatchService watcher; + + private final ExecutorService executor = Executors.newSingleThreadScheduledExecutor( + new ThreadFactoryBuilder().setNameFormat("ConfigRefresher-%d").build()); + + public ConfigRefresher(final Path theBaseDirectory) + { + this.baseDirectory = theBaseDirectory.toAbsolutePath(); + WatchService watchService = null; + + try + { + watchService = FileSystems.getDefault().newWatchService(); + this.baseDirectory.register(watchService, StandardWatchEventKinds.ENTRY_MODIFY, + StandardWatchEventKinds.ENTRY_CREATE, StandardWatchEventKinds.ENTRY_DELETE); + } + catch (IOException e) + { + LOG.error("Unable to register watch service, configuration refresh will not work", e); + } + + watcher = watchService; + + executor.submit(this::watchForEvents); + } + + public final void watch(final Path filePath, final Runnable onChange) + { + Path absoluteFilePath = filePath.toAbsolutePath(); + Preconditions.checkArgument(baseDirectory.equals(absoluteFilePath.getParent()), + String.format("Config file %s is not located in %s", absoluteFilePath, baseDirectory)); + + if (watcher == null) + { + return; + } + + knownConfigs.put(absoluteFilePath.getFileName(), onChange); + LOG.debug("Watching for changes in {}", absoluteFilePath); + } + + @Override + public final void close() + { + try + { + watcher.close(); + } + catch (IOException e) + { + LOG.error("Unable to close watcher"); + } + + executor.shutdownNow(); + + try + { + executor.awaitTermination(TEN_SECONDS, TimeUnit.SECONDS); + } + catch (InterruptedException e) + { + Thread.currentThread().interrupt(); + LOG.warn("Interrupted while waiting for config refresher shutdown", e); + } + } + + private void watchForEvents() + { + while (true) + { + WatchKey watchKey; + + try + { + watchKey = watcher.take(); + handleEvents(watchKey); + } + catch (InterruptedException e) + { + Thread.currentThread().interrupt(); + return; + } + catch (ClosedWatchServiceException e) + { + LOG.debug("Watch service has been closed"); + return; + } + catch (Exception e) + { + LOG.error("Encountered unexpected exception while watching for events", e); + } + } + } + + private void handleEvents(final WatchKey watchKey) + { + try + { + for (WatchEvent event : watchKey.pollEvents()) + { + WatchEvent.Kind kind = event.kind(); + + if (kind == StandardWatchEventKinds.OVERFLOW) + { + continue; + } + + Object context = event.context(); + + if (context instanceof Path) + { + handleEvent((Path) context); + } + else + { + LOG.warn("Unknown context {}", context); + } + } + } + finally + { + watchKey.reset(); + } + } + + private void handleEvent(final Path file) + { + LOG.debug("Received event for {}/{}", baseDirectory, file); + + Runnable onChange = knownConfigs.get(file); + if (onChange != null) + { + try + { + onChange.run(); + } + catch (Exception e) + { + LOG.error("Encountered unexpected exception while running callback for {}", file, e); + } + } + } +} diff --git a/application/src/main/java/com/ericsson/bss/cassandra/ecchronos/application/config/ConfigurationHelper.java b/application/src/main/java/com/ericsson/bss/cassandra/ecchronos/application/config/ConfigurationHelper.java new file mode 100644 index 000000000..aad9cf25c --- /dev/null +++ b/application/src/main/java/com/ericsson/bss/cassandra/ecchronos/application/config/ConfigurationHelper.java @@ -0,0 +1,114 @@ +/* + * Copyright 2024 Telefonaktiebolaget LM Ericsson + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.ericsson.bss.cassandra.ecchronos.application.config; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.FileSystems; +import java.nio.file.Path; + +import com.ericsson.bss.cassandra.ecchronos.application.exceptions.ConfigurationException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; + +public class ConfigurationHelper +{ + private static final String CONFIGURATION_DIRECTORY_PATH = "ecchronos.config"; + + public static final ConfigurationHelper DEFAULT_INSTANCE = new ConfigurationHelper(CONFIGURATION_DIRECTORY_PATH); + + private final String configurationDirectory; + private final boolean usePath; + + public ConfigurationHelper(final String theConfigurationDirectory) + { + this.configurationDirectory = theConfigurationDirectory; + this.usePath = System.getProperty(this.configurationDirectory) != null; + } + + public final boolean usePath() + { + return usePath; + } + + public final T getConfiguration(final String file, final Class clazz) throws ConfigurationException + { + if (usePath()) + { + return getConfiguration(configFile(file), clazz); + } + else + { + return getFileFromClassPath(file, clazz); + } + } + + public final File configFile(final String configFile) + { + return new File(getConfigPath().toFile(), configFile); + } + + public final Path getConfigPath() + { + return FileSystems.getDefault().getPath(System.getProperty(configurationDirectory)); + } + + private T getFileFromClassPath(final String file, final Class clazz) throws ConfigurationException + { + ClassLoader loader = ClassLoader.getSystemClassLoader(); + return getConfiguration(loader.getResourceAsStream(file), clazz); + } + + private T getConfiguration(final File configurationFile, final Class clazz) throws ConfigurationException + { + try + { + ObjectMapper objectMapper = new ObjectMapper(new YAMLFactory()); + + T config = objectMapper.readValue(configurationFile, clazz); + if (config == null) + { + throw new IOException("parsed config is null"); + } + return config; + } + catch (IOException e) + { + throw new ConfigurationException("Unable to load configuration file " + configurationFile, e); + } + } + + private T getConfiguration(final InputStream configurationFile, + final Class clazz) throws ConfigurationException + { + try + { + ObjectMapper objectMapper = new ObjectMapper(new YAMLFactory()); + + T config = objectMapper.readValue(configurationFile, clazz); + if (config == null) + { + throw new IOException("parsed config is null"); + } + return config; + } + catch (IOException e) + { + throw new ConfigurationException("Unable to load configuration file from classpath", e); + } + } +} + diff --git a/application/src/main/java/com/ericsson/bss/cassandra/ecchronos/application/config/connection/AgentConnectionConfig.java b/application/src/main/java/com/ericsson/bss/cassandra/ecchronos/application/config/connection/AgentConnectionConfig.java new file mode 100644 index 000000000..981317ae6 --- /dev/null +++ b/application/src/main/java/com/ericsson/bss/cassandra/ecchronos/application/config/connection/AgentConnectionConfig.java @@ -0,0 +1,556 @@ +/* + * Copyright 2024 Telefonaktiebolaget LM Ericsson + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.ericsson.bss.cassandra.ecchronos.application.config.connection; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import com.ericsson.bss.cassandra.ecchronos.application.exceptions.ConfigurationException; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Configuration class for setting up agent connections with different awareness levels (datacenter, rack, host). + */ +public final class AgentConnectionConfig +{ + private ConnectionType myType = ConnectionType.datacenterAware; + private String myLocalDatacenter; + private Map myContactPoints = new HashMap<>(); + private DatacenterAware myDatacenterAware = new DatacenterAware(); + private RackAware myRackAware = new RackAware(); + private HostAware myHostAware = new HostAware(); + + /** + * Default constructor for AgentConnectionConfig. + */ + public AgentConnectionConfig() + { + + } + + /** + * Gets the connection type. + * + * @return the connection type. + */ + @JsonProperty("type") + public ConnectionType getType() + { + return myType; + } + + /** + * Sets the connection type. + * + * @param type + * the connection type as a string. + * @throws ConfigurationException + * if the provided type is invalid. + */ + @JsonProperty("type") + public void setType(final String type) throws ConfigurationException + { + try + { + myType = ConnectionType.valueOf(type); + } + catch (IllegalArgumentException e) + { + throw new ConfigurationException( + "Invalid connection type: " + + + type + + + "\nAccepted configurations are: datacenterAware, rackAware, hostAware", e); + } + } + + /** + * Sets the local datacenter used for load-balancing policy. + * + * @param localDatacenter + * the local datacenter to set. + */ + @JsonProperty("localDatacenter") + public void setLocalDatacenter(final String localDatacenter) + { + myLocalDatacenter = localDatacenter; + } + + /** + * Gets the local datacenter used for load-balancing policy. + * + * @return the local datacenter. + */ + @JsonProperty("localDatacenter") + public String getLocalDatacenter() + { + return myLocalDatacenter; + } + + /** + * Gets the contact points. + * + * @return the contact points map. + */ + @JsonProperty("contactPoints") + public Map getContactPoints() + { + return myContactPoints; + } + + /** + * Sets the contact points. + * + * @param contactPoints + * a list of contact points. + */ + @JsonProperty("contactPoints") + public void setContactPoints(final List contactPoints) + { + if (contactPoints != null) + { + myContactPoints = contactPoints.stream().collect( + Collectors.toMap(Host::getHost, ks -> ks)); + } + } + + /** + * Sets the datacenter-aware configuration. + * + * @param datacenterAware + * the datacenter-aware configuration to set. + */ + @JsonProperty("datacenterAware") + public void setDatacenterAware(final DatacenterAware datacenterAware) + { + myDatacenterAware = datacenterAware; + } + + /** + * Gets the datacenter-aware configuration. + * + * @return the datacenter-aware configuration. + */ + @JsonProperty("datacenterAware") + public DatacenterAware getDatacenterAware() + { + return myDatacenterAware; + } + + /** + * Sets the rack-aware configuration. + * + * @param rackAware + * the rack-aware configuration to set. + */ + @JsonProperty("rackAware") + public void setRackAware(final RackAware rackAware) + { + myRackAware = rackAware; + } + + /** + * Gets the rack-aware configuration. + * + * @return the rack-aware configuration. + */ + @JsonProperty("rackAware") + public RackAware getRackAware() + { + return myRackAware; + } + + /** + * Sets the host-aware configuration. + * + * @param hostAware + * the host-aware configuration to set. + */ + @JsonProperty("hostAware") + public void setHostAware(final HostAware hostAware) + { + myHostAware = hostAware; + } + + /** + * Gets the host-aware configuration. + * + * @return the host-aware configuration. + */ + @JsonProperty("hostAware") + public HostAware getHostAware() + { + return myHostAware; + } + + /** + * Enum representing the connection types. + */ + public enum ConnectionType + { + /** + * ecChronos will register its control over all the nodes in the specified datacenter. + */ + datacenterAware, + + /** + * ecChronos is responsible only for a subset of racks specified in the declared list. + */ + rackAware, + + /** + * ecChronos is responsible only for the specified list of hosts. + */ + hostAware + } + + /** + * Configuration for datacenter-aware connections. + */ + public static final class DatacenterAware + { + private Map myDatacenters = new HashMap<>(); + + /** + * Default constructor for DatacenterAware. + */ + public DatacenterAware() + { + + } + + /** + * Gets the datacenters map. + * + * @return the datacenters map. + */ + @JsonProperty("datacenters") + public Map getDatacenters() + { + return myDatacenters; + } + + /** + * Sets the datacenters. + * + * @param datacenters + * a list of datacenters. + */ + @JsonProperty("datacenters") + public void setDatacenters(final List datacenters) + { + if (datacenters != null) + { + myDatacenters = datacenters.stream().collect( + Collectors.toMap(Datacenter::getName, ks -> ks)); + } + } + + /** + * Represents a datacenter. + */ + public static final class Datacenter + { + private String myName; + + /** + * Default constructor for Datacenter. + */ + public Datacenter() + { + + } + + /** + * Constructor with name. + * + * @param name + * the name of the datacenter. + */ + public Datacenter(final String name) + { + myName = name; + } + + /** + * Gets the name of the datacenter. + * + * @return the name of the datacenter. + */ + @JsonProperty("name") + public String getName() + { + return myName; + } + + /** + * Sets the name of the datacenter. + * + * @param name + * the name to set. + */ + @JsonProperty("name") + public void setName(final String name) + { + myName = name; + } + } + } + + /** + * Configuration for rack-aware connections. + */ + public static final class RackAware + { + private Map myRackAware = new HashMap<>(); + + /** + * Default constructor for RackAware. + */ + public RackAware() + { + + } + + /** + * Gets the racks map. + * + * @return the racks map. + */ + @JsonProperty("racks") + public Map getRacks() + { + return myRackAware; + } + + /** + * Sets the racks. + * + * @param rackAware + * a list of racks. + */ + @JsonProperty("racks") + public void setRacks(final List rackAware) + { + if (rackAware != null) + { + myRackAware = rackAware.stream().collect( + Collectors.toMap(Rack::getRackName, ks -> ks)); + } + } + + /** + * Represents a rack with a datacenter name and rack name. + */ + public static final class Rack + { + private String myDatacenterName; + private String myRackName; + + /** + * Default constructor for Rack. + */ + public Rack() + { + + } + + /** + * Constructor with datacenter name and rack name. + * + * @param datacenterName + * the datacenter name. + * @param rackName + * the rack name. + */ + public Rack(final String datacenterName, final String rackName) + { + myDatacenterName = datacenterName; + myRackName = rackName; + } + + /** + * Gets the datacenter name. + * + * @return the datacenter name. + */ + @JsonProperty("datacenterName") + public String getDatacenterName() + { + return myDatacenterName; + } + + /** + * Sets the datacenter name. + * + * @param datacenterName + * the datacenter name to set. + */ + @JsonProperty("datacenterName") + public void setDatacenterName(final String datacenterName) + { + myDatacenterName = datacenterName; + } + + /** + * Gets the rack name. + * + * @return the rack name. + */ + @JsonProperty("rackName") + public String getRackName() + { + return myRackName; + } + + /** + * Sets the rack name. + * + * @param rackName + * the rack name to set. + */ + @JsonProperty("rackName") + public void setRackName(final String rackName) + { + myRackName = rackName; + } + } + } + + /** + * Configuration for host-aware connections. + */ + public static final class HostAware + { + private Map myHosts = new HashMap<>(); + + /** + * Default constructor for HostAware. + */ + public HostAware() + { + + } + + /** + * Gets the hosts map. + * + * @return the hosts map. + */ + @JsonProperty("hosts") + public Map getHosts() + { + return myHosts; + } + + /** + * Sets the hosts. + * + * @param hosts + * a list of hosts. + */ + @JsonProperty("hosts") + public void setHosts(final List hosts) + { + if (hosts != null) + { + myHosts = hosts.stream().collect( + Collectors.toMap(Host::getHost, ks -> ks)); + } + } + } + + /** + * Represents a host configuration with a hostname and port. + */ + public static final class Host + { + private static final int DEFAULT_PORT = 9042; + private String myHost = "localhost"; + + private int myPort = DEFAULT_PORT; + + /** + * Default constructor for Host. Initializes the host with default values. + */ + public Host() + { + + } + + /** + * Constructs a Host with the specified hostname and port. + * + * @param host + * the hostname. + * @param port + * the port number. + */ + public Host(final String host, final int port) + { + myHost = host; + myPort = port; + } + + /** + * Gets the hostname of the host. + * + * @return the hostname. + */ + @JsonProperty("host") + public String getHost() + { + return myHost; + } + + /** + * Gets the port number of the host. + * + * @return the port number. + */ + @JsonProperty("port") + public int getPort() + { + return myPort; + } + + /** + * Sets the hostname of the host. + * + * @param host + * the hostname to set. + */ + @JsonProperty("host") + public void setHost(final String host) + { + myHost = host; + } + + /** + * Sets the port number of the host. + * + * @param port + * the port number to set. + */ + @JsonProperty("port") + public void setPort(final int port) + { + myPort = port; + } + } +} diff --git a/application/src/main/java/com/ericsson/bss/cassandra/ecchronos/application/config/connection/Connection.java b/application/src/main/java/com/ericsson/bss/cassandra/ecchronos/application/config/connection/Connection.java new file mode 100644 index 000000000..fd4cec1f4 --- /dev/null +++ b/application/src/main/java/com/ericsson/bss/cassandra/ecchronos/application/config/connection/Connection.java @@ -0,0 +1,78 @@ +/* + * Copyright 2024 Telefonaktiebolaget LM Ericsson + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.ericsson.bss.cassandra.ecchronos.application.config.connection; + +import com.ericsson.bss.cassandra.ecchronos.connection.CertificateHandler; +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.function.Supplier; + +public abstract class Connection +{ + private Class myProviderClass; + private Class myCertificateHandlerClass; + + @JsonProperty("provider") + public final Class getProviderClass() + { + return myProviderClass; + } + + @JsonProperty("provider") + public final void setProvider(final Class providerClass) throws NoSuchMethodException + { + providerClass.getDeclaredConstructor(expectedConstructor()); + + myProviderClass = providerClass; + } + + /** + * Set certification handler. + * + * @param certificateHandlerClass + * The certification handler. + * @throws NoSuchMethodException + * If the getDeclaredConstructor method was not found. + */ + @JsonProperty("certificateHandler") + public void setCertificateHandler(final Class certificateHandlerClass) + throws NoSuchMethodException + { + certificateHandlerClass.getDeclaredConstructor(expectedCertHandlerConstructor()); + + myCertificateHandlerClass = certificateHandlerClass; + } + + @JsonProperty("certificateHandler") + public final Class getCertificateHandlerClass() + { + return myCertificateHandlerClass; + } + + protected abstract Class[] expectedConstructor(); + + /** + * Get the expected certification handler. + * + * @return CertificateHandler + */ + protected Class[] expectedCertHandlerConstructor() + { + return new Class[] + { + Supplier.class + }; + } +} diff --git a/application/src/main/java/com/ericsson/bss/cassandra/ecchronos/application/config/connection/ConnectionConfig.java b/application/src/main/java/com/ericsson/bss/cassandra/ecchronos/application/config/connection/ConnectionConfig.java new file mode 100644 index 000000000..7ad8de2e1 --- /dev/null +++ b/application/src/main/java/com/ericsson/bss/cassandra/ecchronos/application/config/connection/ConnectionConfig.java @@ -0,0 +1,59 @@ +/* + * Copyright 2024 Telefonaktiebolaget LM Ericsson + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.ericsson.bss.cassandra.ecchronos.application.config.connection; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public class ConnectionConfig +{ + private DistributedNativeConnection myCqlConnection = new DistributedNativeConnection(); + private DistributedJmxConnection myJmxConnection = new DistributedJmxConnection(); + + @JsonProperty("cql") + public final DistributedNativeConnection getCqlConnection() + { + return myCqlConnection; + } + + @JsonProperty("jmx") + public final DistributedJmxConnection getJmxConnection() + { + return myJmxConnection; + } + + @JsonProperty("cql") + public final void setCqlConnection(final DistributedNativeConnection cqlConnection) + { + if (cqlConnection != null) + { + myCqlConnection = cqlConnection; + } + } + + @JsonProperty("jmx") + public final void setJmxConnection(final DistributedJmxConnection jmxConnection) + { + if (jmxConnection != null) + { + myJmxConnection = jmxConnection; + } + } + + @Override + public final String toString() + { + return String.format("Connection(cql=%s, jmx=%s)", myCqlConnection, myJmxConnection); + } +} diff --git a/application/src/main/java/com/ericsson/bss/cassandra/ecchronos/application/config/connection/DistributedJmxConnection.java b/application/src/main/java/com/ericsson/bss/cassandra/ecchronos/application/config/connection/DistributedJmxConnection.java new file mode 100644 index 000000000..1e4548791 --- /dev/null +++ b/application/src/main/java/com/ericsson/bss/cassandra/ecchronos/application/config/connection/DistributedJmxConnection.java @@ -0,0 +1,50 @@ +/* + * Copyright 2024 Telefonaktiebolaget LM Ericsson + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.ericsson.bss.cassandra.ecchronos.application.config.connection; + +import com.ericsson.bss.cassandra.ecchronos.application.providers.AgentJmxConnectionProvider; +import com.ericsson.bss.cassandra.ecchronos.connection.DistributedJmxConnectionProvider; + +import com.ericsson.bss.cassandra.ecchronos.connection.DistributedNativeConnectionProvider; +import com.ericsson.bss.cassandra.ecchronos.data.sync.EccNodesSync; +import java.util.function.Supplier; + +public class DistributedJmxConnection extends Connection +{ + public DistributedJmxConnection() + { + try + { + setProvider(AgentJmxConnectionProvider.class); + } + catch (NoSuchMethodException ignored) + { + // Do something useful ... + } + } + + /** + * @return + */ + @Override + protected Class[] expectedConstructor() + { + return new Class[] { + Supplier.class, + DistributedNativeConnectionProvider.class, + EccNodesSync.class + }; + } +} diff --git a/application/src/main/java/com/ericsson/bss/cassandra/ecchronos/application/config/connection/DistributedNativeConnection.java b/application/src/main/java/com/ericsson/bss/cassandra/ecchronos/application/config/connection/DistributedNativeConnection.java new file mode 100644 index 000000000..ecc7e0049 --- /dev/null +++ b/application/src/main/java/com/ericsson/bss/cassandra/ecchronos/application/config/connection/DistributedNativeConnection.java @@ -0,0 +1,66 @@ +/* + * Copyright 2024 Telefonaktiebolaget LM Ericsson + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.ericsson.bss.cassandra.ecchronos.application.config.connection; + +import com.ericsson.bss.cassandra.ecchronos.application.config.Config; +import com.ericsson.bss.cassandra.ecchronos.application.providers.AgentNativeConnectionProvider; +import com.ericsson.bss.cassandra.ecchronos.connection.CertificateHandler; +import com.ericsson.bss.cassandra.ecchronos.connection.DistributedNativeConnectionProvider; +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.function.Supplier; + +public class DistributedNativeConnection extends Connection +{ + private AgentConnectionConfig myAgentConnectionConfig = new AgentConnectionConfig(); + + public DistributedNativeConnection() + { + try + { + setProvider(AgentNativeConnectionProvider.class); + } + catch (NoSuchMethodException ignored) + { + // Do something useful ... + } + } + + @JsonProperty("agent") + public final AgentConnectionConfig getAgentConnectionConfig() + { + return myAgentConnectionConfig; + } + + @JsonProperty("agent") + public final void setAgentConnectionConfig(final AgentConnectionConfig agentConnectionConfig) + { + myAgentConnectionConfig = agentConnectionConfig; + } + + /** + * @return Class[] + */ + @Override + protected Class[] expectedConstructor() + { + return new Class[] + { + Config.class, + Supplier.class, + CertificateHandler.class + }; + } +} diff --git a/application/src/main/java/com/ericsson/bss/cassandra/ecchronos/application/config/connection/package-info.java b/application/src/main/java/com/ericsson/bss/cassandra/ecchronos/application/config/connection/package-info.java new file mode 100644 index 000000000..6cc872e1d --- /dev/null +++ b/application/src/main/java/com/ericsson/bss/cassandra/ecchronos/application/config/connection/package-info.java @@ -0,0 +1,18 @@ +/* + * Copyright 2024 Telefonaktiebolaget LM Ericsson + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * Contains configurations related to outbound connections (CQL and JMX). + */ +package com.ericsson.bss.cassandra.ecchronos.application.config.connection; diff --git a/application/src/main/java/com/ericsson/bss/cassandra/ecchronos/application/config/package-info.java b/application/src/main/java/com/ericsson/bss/cassandra/ecchronos/application/config/package-info.java new file mode 100644 index 000000000..29ffe6ac0 --- /dev/null +++ b/application/src/main/java/com/ericsson/bss/cassandra/ecchronos/application/config/package-info.java @@ -0,0 +1,18 @@ +/* + * Copyright 2024 Telefonaktiebolaget LM Ericsson + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * Contains configurations for ecChronos. + */ +package com.ericsson.bss.cassandra.ecchronos.application.config; diff --git a/application/src/main/java/com/ericsson/bss/cassandra/ecchronos/application/config/rest/RestServerConfig.java b/application/src/main/java/com/ericsson/bss/cassandra/ecchronos/application/config/rest/RestServerConfig.java new file mode 100644 index 000000000..39cb411ba --- /dev/null +++ b/application/src/main/java/com/ericsson/bss/cassandra/ecchronos/application/config/rest/RestServerConfig.java @@ -0,0 +1,50 @@ +/* + * Copyright 2024 Telefonaktiebolaget LM Ericsson + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.ericsson.bss.cassandra.ecchronos.application.config.rest; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public class RestServerConfig +{ + private static final String DEFAULT_HOST = "localhost"; + private static final int DEFAULT_PORT = 8080; + + private String myHost = DEFAULT_HOST; + private int myPort = DEFAULT_PORT; + + @JsonProperty("host") + public final String getHost() + { + return myHost; + } + + @JsonProperty("host") + public final void setHost(final String host) + { + myHost = host; + } + + @JsonProperty("port") + public final int getPort() + { + return myPort; + } + + @JsonProperty("port") + public final void setPort(final int port) + { + myPort = port; + } +} diff --git a/application/src/main/java/com/ericsson/bss/cassandra/ecchronos/application/config/rest/package-info.java b/application/src/main/java/com/ericsson/bss/cassandra/ecchronos/application/config/rest/package-info.java new file mode 100644 index 000000000..b65322434 --- /dev/null +++ b/application/src/main/java/com/ericsson/bss/cassandra/ecchronos/application/config/rest/package-info.java @@ -0,0 +1,18 @@ +/* + * Copyright 2024 Telefonaktiebolaget LM Ericsson + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * Contains configurations related to rest server. + */ +package com.ericsson.bss.cassandra.ecchronos.application.config.rest; diff --git a/application/src/main/java/com/ericsson/bss/cassandra/ecchronos/application/config/security/CqlTLSConfig.java b/application/src/main/java/com/ericsson/bss/cassandra/ecchronos/application/config/security/CqlTLSConfig.java new file mode 100644 index 000000000..0577e166e --- /dev/null +++ b/application/src/main/java/com/ericsson/bss/cassandra/ecchronos/application/config/security/CqlTLSConfig.java @@ -0,0 +1,244 @@ +/* + * Copyright 2024 Telefonaktiebolaget LM Ericsson + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.ericsson.bss.cassandra.ecchronos.application.config.security; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.Arrays; +import java.util.Objects; +import java.util.Optional; + +public class CqlTLSConfig +{ + private static final int HASH_SEED = 31; + + private final boolean myIsEnabled; + private String myKeyStorePath; + private String myKeyStorePassword; + private String myTrustStorePath; + private String myTrustStorePassword; + private String myStoreType; + private String myAlgorithm; + private String myCertificatePath; + private String myCertificatePrivateKeyPath; + private String myTrustCertificatePath; + private String myProtocol; + private String[] myCipherSuites; + private boolean myRequireEndpointVerification; + + @JsonCreator + public CqlTLSConfig(@JsonProperty("enabled") final boolean isEnabled, + @JsonProperty("keystore") final String keyStorePath, + @JsonProperty("keystore_password") final String keyStorePassword, + @JsonProperty("truststore") final String trustStorePath, + @JsonProperty("truststore_password") final String trustStorePassword, + @JsonProperty("certificate") final String certificatePath, + @JsonProperty("certificate_private_key") final String certificatePrivateKeyPath, + @JsonProperty("trust_certificate") final String trustCertificatePath) + { + myIsEnabled = isEnabled; + myKeyStorePath = keyStorePath; + myKeyStorePassword = keyStorePassword; + myTrustStorePath = trustStorePath; + myTrustStorePassword = trustStorePassword; + myCertificatePath = certificatePath; + myCertificatePrivateKeyPath = certificatePrivateKeyPath; + myTrustCertificatePath = trustCertificatePath; + if (myIsEnabled && !isKeyStoreConfigured() && !isCertificateConfigured()) + { + throw new IllegalArgumentException("Invalid CQL TLS config, you must either configure KeyStore or PEM based" + + " certificates."); + } + } + + private boolean isKeyStoreConfigured() + { + return myKeyStorePath != null && !myKeyStorePath.isEmpty() + && myKeyStorePassword != null && myKeyStorePassword != null + && myTrustStorePath != null && !myTrustStorePath.isEmpty() + && myTrustStorePassword != null && !myTrustStorePassword.isEmpty(); + } + + public final boolean isCertificateConfigured() + { + return getCertificatePath().isPresent() && getCertificatePrivateKeyPath().isPresent() + && getTrustCertificatePath().isPresent(); + } + + public CqlTLSConfig(final boolean isEnabled, final String keyStorePath, final String keyStorePassword, + final String trustStorePath, final String trustStorePassword) + { + myIsEnabled = isEnabled; + myKeyStorePath = keyStorePath; + myKeyStorePassword = keyStorePassword; + myTrustStorePath = trustStorePath; + myTrustStorePassword = trustStorePassword; + } + + public CqlTLSConfig(final boolean isEnabled, final String certificatePath, final String certificatePrivateKeyPath, + final String trustCertificatePath) + { + myIsEnabled = isEnabled; + myCertificatePath = certificatePath; + myCertificatePrivateKeyPath = certificatePrivateKeyPath; + myTrustCertificatePath = trustCertificatePath; + } + + public final boolean isEnabled() + { + return myIsEnabled; + } + + public final String getKeyStorePath() + { + return myKeyStorePath; + } + + public final String getKeyStorePassword() + { + return myKeyStorePassword; + } + + public final String getTrustStorePath() + { + return myTrustStorePath; + } + + public final String getTrustStorePassword() + { + return myTrustStorePassword; + } + + public final Optional getStoreType() + { + return Optional.ofNullable(myStoreType); + } + + @JsonProperty("store_type") + public final void setStoreType(final String storeType) + { + myStoreType = storeType; + } + + public final Optional getAlgorithm() + { + return Optional.ofNullable(myAlgorithm); + } + + @JsonProperty("algorithm") + public final void setAlgorithm(final String algorithm) + { + myAlgorithm = algorithm; + } + + public final Optional getCertificatePath() + { + return Optional.ofNullable(myCertificatePath); + } + + public final Optional getCertificatePrivateKeyPath() + { + return Optional.ofNullable(myCertificatePrivateKeyPath); + } + + public final Optional getTrustCertificatePath() + { + return Optional.ofNullable(myTrustCertificatePath); + } + + @JsonProperty("protocol") + public final void setProtocol(final String protocol) + { + myProtocol = protocol; + } + + public final Optional getCipherSuites() + { + if (myCipherSuites == null) + { + return Optional.empty(); + } + + return Optional.of(Arrays.copyOf(myCipherSuites, myCipherSuites.length)); + } + + @JsonProperty("cipher_suites") + public final void setCipherSuites(final String cipherSuites) + { + myCipherSuites = transformCiphers(cipherSuites); + } + + private static String[] transformCiphers(final String cipherSuites) + { + return cipherSuites == null ? null : cipherSuites.split(","); + } + + public final String[] getProtocols() + { + if (myProtocol == null) + { + return null; + } + return myProtocol.split(","); + } + + public final boolean requiresEndpointVerification() + { + return myRequireEndpointVerification; + } + + @JsonProperty("require_endpoint_verification") + public final void setRequireEndpointVerification(final boolean requireEndpointVerification) + { + myRequireEndpointVerification = requireEndpointVerification; + } + + @Override + public final boolean equals(final Object o) + { + if (this == o) + { + return true; + } + if (o == null || getClass() != o.getClass()) + { + return false; + } + CqlTLSConfig that = (CqlTLSConfig) o; + return myIsEnabled == that.myIsEnabled && myRequireEndpointVerification == that.myRequireEndpointVerification + && Objects.equals(myKeyStorePath, that.myKeyStorePath) + && Objects.equals(myKeyStorePassword, that.myKeyStorePassword) + && Objects.equals(myTrustStorePath, that.myTrustStorePath) + && Objects.equals(myTrustStorePassword, that.myTrustStorePassword) + && Objects.equals(myStoreType, that.myStoreType) + && Objects.equals(myAlgorithm, that.myAlgorithm) + && Objects.equals(myCertificatePath, that.myCertificatePath) + && Objects.equals(myCertificatePrivateKeyPath, that.myCertificatePrivateKeyPath) + && Objects.equals(myTrustCertificatePath, that.myTrustCertificatePath) + && Objects.equals(myProtocol, that.myProtocol) && Arrays.equals(myCipherSuites, that.myCipherSuites); + } + + @Override + public final int hashCode() + { + int result = Objects.hash(myIsEnabled, myKeyStorePath, myKeyStorePassword, myTrustStorePath, + myTrustStorePassword, + myStoreType, myAlgorithm, myCertificatePath, myCertificatePrivateKeyPath, myTrustCertificatePath, + myProtocol, myRequireEndpointVerification); + result = HASH_SEED * result + Arrays.hashCode(myCipherSuites); + return result; + } +} diff --git a/application/src/main/java/com/ericsson/bss/cassandra/ecchronos/application/config/security/Credentials.java b/application/src/main/java/com/ericsson/bss/cassandra/ecchronos/application/config/security/Credentials.java new file mode 100644 index 000000000..0bd1bc084 --- /dev/null +++ b/application/src/main/java/com/ericsson/bss/cassandra/ecchronos/application/config/security/Credentials.java @@ -0,0 +1,97 @@ +/* + * Copyright 2024 Telefonaktiebolaget LM Ericsson + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.ericsson.bss.cassandra.ecchronos.application.config.security; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.Objects; + +public class Credentials +{ + private boolean myIsEnabled; + private String myUsername; + private String myPassword; + + public Credentials() + { + // Default constructor + } + + public Credentials(final boolean enabled, final String username, final String password) + { + myIsEnabled = enabled; + myUsername = username; + myPassword = password; + } + + @JsonProperty("enabled") + public final boolean isEnabled() + { + return myIsEnabled; + } + + @JsonProperty("enabled") + public final void setEnabled(final boolean enabled) + { + myIsEnabled = enabled; + } + + @JsonProperty("username") + public final String getUsername() + { + return myUsername; + } + + @JsonProperty("username") + public final void setUsername(final String username) + { + myUsername = username; + } + + @JsonProperty("password") + public final String getPassword() + { + return myPassword; + } + + @JsonProperty("password") + public final void setPassword(final String password) + { + myPassword = password; + } + + @Override + public final boolean equals(final Object o) + { + if (this == o) + { + return true; + } + if (o == null || getClass() != o.getClass()) + { + return false; + } + Credentials that = (Credentials) o; + return myIsEnabled == that.myIsEnabled + && myUsername.equals(that.myUsername) + && myPassword.equals(that.myPassword); + } + + @Override + public final int hashCode() + { + return Objects.hash(myIsEnabled, myUsername, myPassword); + } +} diff --git a/application/src/main/java/com/ericsson/bss/cassandra/ecchronos/application/config/security/JmxTLSConfig.java b/application/src/main/java/com/ericsson/bss/cassandra/ecchronos/application/config/security/JmxTLSConfig.java new file mode 100644 index 000000000..7daa38943 --- /dev/null +++ b/application/src/main/java/com/ericsson/bss/cassandra/ecchronos/application/config/security/JmxTLSConfig.java @@ -0,0 +1,132 @@ +/* + * Copyright 2024 Telefonaktiebolaget LM Ericsson + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.ericsson.bss.cassandra.ecchronos.application.config.security; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.Objects; + +public class JmxTLSConfig +{ + private final boolean myIsEnabled; + private final String myKeyStorePath; + private final String myKeyStorePassword; + private final String myTrustStorePath; + private final String myTrustStorePassword; + private String myProtocol; + private String myCipherSuites; + + @JsonCreator + public JmxTLSConfig(@JsonProperty("enabled") final boolean isEnabled, + @JsonProperty("keystore") final String keyStorePath, + @JsonProperty("keystore_password") final String keyStorePassword, + @JsonProperty("truststore") final String trustStorePath, + @JsonProperty("truststore_password") final String trustStorePassword) + { + myIsEnabled = isEnabled; + myKeyStorePath = keyStorePath; + myKeyStorePassword = keyStorePassword; + myTrustStorePath = trustStorePath; + myTrustStorePassword = trustStorePassword; + if (myIsEnabled && !isKeyStoreConfigured()) + { + throw new IllegalArgumentException("Invalid JMX TLS config, you must configure KeyStore based" + + " certificates."); + } + } + + private boolean isKeyStoreConfigured() + { + return myKeyStorePath != null && !myKeyStorePath.isEmpty() + && myKeyStorePassword != null && myKeyStorePassword != null + && myTrustStorePath != null && !myTrustStorePath.isEmpty() + && myTrustStorePassword != null && !myTrustStorePassword.isEmpty(); + } + + public final boolean isEnabled() + { + return myIsEnabled; + } + + public final String getKeyStorePath() + { + return myKeyStorePath; + } + + public final String getKeyStorePassword() + { + return myKeyStorePassword; + } + + public final String getTrustStorePath() + { + return myTrustStorePath; + } + + public final String getTrustStorePassword() + { + return myTrustStorePassword; + } + + public final String getProtocol() + { + return myProtocol; + } + + @JsonProperty("protocol") + public final void setProtocol(final String protocol) + { + myProtocol = protocol; + } + + public final String getCipherSuites() + { + return myCipherSuites; + } + + @JsonProperty("cipher_suites") + public final void setCipherSuites(final String cipherSuites) + { + myCipherSuites = cipherSuites; + } + + @Override + public final boolean equals(final Object o) + { + if (this == o) + { + return true; + } + if (o == null || getClass() != o.getClass()) + { + return false; + } + JmxTLSConfig that = (JmxTLSConfig) o; + return myIsEnabled == that.myIsEnabled + && Objects.equals(myKeyStorePath, that.myKeyStorePath) + && Objects.equals(myKeyStorePassword, that.myKeyStorePassword) + && Objects.equals(myTrustStorePath, that.myTrustStorePath) + && Objects.equals(myTrustStorePassword, that.myTrustStorePassword) + && Objects.equals(myProtocol, that.myProtocol) && Objects.equals(myCipherSuites, that.myCipherSuites); + } + + @Override + public final int hashCode() + { + return Objects.hash(myIsEnabled, myKeyStorePath, myKeyStorePassword, myTrustStorePath, + myTrustStorePassword, myProtocol, myCipherSuites); + } +} diff --git a/application/src/main/java/com/ericsson/bss/cassandra/ecchronos/application/config/security/ReloadingAuthProvider.java b/application/src/main/java/com/ericsson/bss/cassandra/ecchronos/application/config/security/ReloadingAuthProvider.java new file mode 100644 index 000000000..ba2097747 --- /dev/null +++ b/application/src/main/java/com/ericsson/bss/cassandra/ecchronos/application/config/security/ReloadingAuthProvider.java @@ -0,0 +1,42 @@ +/* + * Copyright 2024 Telefonaktiebolaget LM Ericsson + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.ericsson.bss.cassandra.ecchronos.application.config.security; + +import com.datastax.oss.driver.api.core.auth.ProgrammaticPlainTextAuthProvider; +import com.datastax.oss.driver.api.core.metadata.EndPoint; + +import java.util.function.Supplier; + +public class ReloadingAuthProvider extends ProgrammaticPlainTextAuthProvider +{ + private final Supplier + credentialSupplier; + + public ReloadingAuthProvider( + final Supplier + aCredentialSupplier) + { + super(aCredentialSupplier.get().getUsername(), aCredentialSupplier.get().getPassword()); + this.credentialSupplier = aCredentialSupplier; + } + + @Override + protected final Credentials getCredentials(final EndPoint endPoint, final String serverAuthenticator) + { + com.ericsson.bss.cassandra.ecchronos.application.config.security.Credentials credentials = + credentialSupplier.get(); + return new Credentials(credentials.getUsername().toCharArray(), credentials.getPassword().toCharArray()); + } +} diff --git a/application/src/main/java/com/ericsson/bss/cassandra/ecchronos/application/config/security/ReloadingCertificateHandler.java b/application/src/main/java/com/ericsson/bss/cassandra/ecchronos/application/config/security/ReloadingCertificateHandler.java new file mode 100644 index 000000000..2ddeacb64 --- /dev/null +++ b/application/src/main/java/com/ericsson/bss/cassandra/ecchronos/application/config/security/ReloadingCertificateHandler.java @@ -0,0 +1,266 @@ +/* + * Copyright 2024 Telefonaktiebolaget LM Ericsson + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.ericsson.bss.cassandra.ecchronos.application.config.security; + +import com.datastax.oss.driver.api.core.metadata.EndPoint; +import com.ericsson.bss.cassandra.ecchronos.connection.CertificateHandler; +import io.netty.buffer.ByteBufAllocator; +import io.netty.handler.ssl.SslContext; +import io.netty.handler.ssl.SslContextBuilder; +import jakarta.xml.bind.DatatypeConverter; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.SSLEngine; +import javax.net.ssl.SSLParameters; +import javax.net.ssl.TrustManagerFactory; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.InetSocketAddress; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.security.KeyManagementException; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.UnrecoverableKeyException; +import java.security.cert.CertificateException; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Supplier; + +public class ReloadingCertificateHandler implements CertificateHandler +{ + private static final Logger LOG = LoggerFactory.getLogger(ReloadingCertificateHandler.class); + + private final AtomicReference currentContext = new AtomicReference<>(); + private final Supplier myCqlTLSConfigSupplier; + + public ReloadingCertificateHandler(final Supplier cqlTLSConfigSupplier) + { + this.myCqlTLSConfigSupplier = cqlTLSConfigSupplier; + } + + /** + * Create new SSL Engine. + * + * @param remoteEndpoint + * the remote endpoint. + * @return The SSLEngine. + */ + @Override + public SSLEngine newSslEngine(final EndPoint remoteEndpoint) + { + Context context = getContext(); + CqlTLSConfig tlsConfig = context.getTlsConfig(); + SslContext sslContext = context.getSSLContext(); + + SSLEngine sslEngine; + if (remoteEndpoint != null) + { + InetSocketAddress socketAddress = (InetSocketAddress) remoteEndpoint.resolve(); + sslEngine = sslContext.newEngine(ByteBufAllocator.DEFAULT, socketAddress.getHostName(), + socketAddress.getPort()); + } + else + { + sslEngine = sslContext.newEngine(ByteBufAllocator.DEFAULT); + } + sslEngine.setUseClientMode(true); + + if (tlsConfig.requiresEndpointVerification()) + { + SSLParameters sslParameters = new SSLParameters(); + sslParameters.setEndpointIdentificationAlgorithm("HTTPS"); + sslEngine.setSSLParameters(sslParameters); + } + tlsConfig.getCipherSuites().ifPresent(sslEngine::setEnabledCipherSuites); + return sslEngine; + } + + protected final Context getContext() + { + CqlTLSConfig tlsConfig = myCqlTLSConfigSupplier.get(); + Context context = currentContext.get(); + + try + { + while (context == null || !context.sameConfig(tlsConfig)) + { + Context newContext = new Context(tlsConfig); + if (currentContext.compareAndSet(context, newContext)) + { + context = newContext; + } + else + { + context = currentContext.get(); + } + tlsConfig = myCqlTLSConfigSupplier.get(); + } + } + catch (NoSuchAlgorithmException | IOException | UnrecoverableKeyException | CertificateException + | KeyStoreException | KeyManagementException e) + { + LOG.warn("Unable to create new SSL Context after configuration changed. Trying with the old one", e); + } + + return context; + } + + @Override + public void close() throws Exception + { + + } + + protected static final class Context + { + private final CqlTLSConfig myTlsConfig; + private final SslContext mySslContext; + private final Map myChecksums = new HashMap<>(); + + Context(final CqlTLSConfig tlsConfig) throws NoSuchAlgorithmException, IOException, UnrecoverableKeyException, + CertificateException, KeyStoreException, KeyManagementException + { + myTlsConfig = tlsConfig; + mySslContext = createSSLContext(myTlsConfig); + myChecksums.putAll(calculateChecksums(myTlsConfig)); + } + + CqlTLSConfig getTlsConfig() + { + return myTlsConfig; + } + + boolean sameConfig(final CqlTLSConfig newTLSConfig) throws IOException, NoSuchAlgorithmException + { + if (!myTlsConfig.equals(newTLSConfig)) + { + return false; + } + return checksumSame(newTLSConfig); + } + + private boolean checksumSame(final CqlTLSConfig newTLSConfig) throws IOException, NoSuchAlgorithmException + { + return myChecksums.equals(calculateChecksums(newTLSConfig)); + } + + private Map calculateChecksums(final CqlTLSConfig tlsConfig) + throws IOException, NoSuchAlgorithmException + { + Map checksums = new HashMap<>(); + if (tlsConfig.isCertificateConfigured()) + { + String certificate = tlsConfig.getCertificatePath().get(); + checksums.put(certificate, getChecksum(certificate)); + String certificatePrivateKey = tlsConfig.getCertificatePrivateKeyPath().get(); + checksums.put(certificatePrivateKey, getChecksum(certificatePrivateKey)); + String trustCertificate = tlsConfig.getTrustCertificatePath().get(); + checksums.put(trustCertificate, getChecksum(trustCertificate)); + } + else + { + String keyStore = tlsConfig.getKeyStorePath(); + checksums.put(keyStore, getChecksum(keyStore)); + String trustStore = tlsConfig.getTrustStorePath(); + checksums.put(trustStore, getChecksum(trustStore)); + } + return checksums; + } + + private String getChecksum(final String file) throws IOException, NoSuchAlgorithmException + { + MessageDigest md5 = MessageDigest.getInstance("MD5"); + byte[] digestBytes = md5.digest(Files.readAllBytes(Paths.get(file))); + return DatatypeConverter.printHexBinary(digestBytes); + } + + SslContext getSSLContext() + { + return mySslContext; + } + } + + protected static SslContext createSSLContext(final CqlTLSConfig tlsConfig) throws IOException, + NoSuchAlgorithmException, + KeyStoreException, + CertificateException, + UnrecoverableKeyException + { + + SslContextBuilder builder = SslContextBuilder.forClient(); + if (tlsConfig.isCertificateConfigured()) + { + File certificateFile = new File(tlsConfig.getCertificatePath().get()); + File certificatePrivateKeyFile = new File(tlsConfig.getCertificatePrivateKeyPath().get()); + File trustCertificateFile = new File(tlsConfig.getTrustCertificatePath().get()); + + builder.keyManager(certificateFile, certificatePrivateKeyFile); + builder.trustManager(trustCertificateFile); + } + else + { + KeyManagerFactory keyManagerFactory = getKeyManagerFactory(tlsConfig); + TrustManagerFactory trustManagerFactory = getTrustManagerFactory(tlsConfig); + builder.keyManager(keyManagerFactory); + builder.trustManager(trustManagerFactory); + } + if (tlsConfig.getCipherSuites().isPresent()) + { + builder.ciphers(Arrays.asList(tlsConfig.getCipherSuites().get())); + } + return builder.protocols(tlsConfig.getProtocols()).build(); + } + + protected static KeyManagerFactory getKeyManagerFactory(final CqlTLSConfig tlsConfig) throws IOException, + NoSuchAlgorithmException, KeyStoreException, CertificateException, UnrecoverableKeyException + { + String algorithm = tlsConfig.getAlgorithm().orElse(KeyManagerFactory.getDefaultAlgorithm()); + char[] keystorePassword = tlsConfig.getKeyStorePassword().toCharArray(); + + try (InputStream keystoreFile = new FileInputStream(tlsConfig.getKeyStorePath())) + { + KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(algorithm); + KeyStore keyStore = KeyStore.getInstance(tlsConfig.getStoreType().orElse("JKS")); + keyStore.load(keystoreFile, keystorePassword); + keyManagerFactory.init(keyStore, keystorePassword); + return keyManagerFactory; + } + } + + protected static TrustManagerFactory getTrustManagerFactory(final CqlTLSConfig tlsConfig) + throws IOException, NoSuchAlgorithmException, KeyStoreException, CertificateException + { + String algorithm = tlsConfig.getAlgorithm().orElse(TrustManagerFactory.getDefaultAlgorithm()); + char[] truststorePassword = tlsConfig.getTrustStorePassword().toCharArray(); + + try (InputStream truststoreFile = new FileInputStream(tlsConfig.getTrustStorePath())) + { + TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(algorithm); + KeyStore keyStore = KeyStore.getInstance(tlsConfig.getStoreType().orElse("JKS")); + keyStore.load(truststoreFile, truststorePassword); + trustManagerFactory.init(keyStore); + return trustManagerFactory; + } + } +} diff --git a/application/src/main/java/com/ericsson/bss/cassandra/ecchronos/application/config/security/Security.java b/application/src/main/java/com/ericsson/bss/cassandra/ecchronos/application/config/security/Security.java new file mode 100644 index 000000000..1d976ab54 --- /dev/null +++ b/application/src/main/java/com/ericsson/bss/cassandra/ecchronos/application/config/security/Security.java @@ -0,0 +1,107 @@ +/* + * Copyright 2023 Telefonaktiebolaget LM Ericsson + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.ericsson.bss.cassandra.ecchronos.application.config.security; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public final class Security +{ + private CqlSecurity myCqlSecurity; + private JmxSecurity myJmxSecurity; + + @JsonProperty("cql") + public CqlSecurity getCqlSecurity() + { + return myCqlSecurity; + } + + @JsonProperty("cql") + public void setCqlSecurity(final CqlSecurity cqlSecurity) + { + myCqlSecurity = cqlSecurity; + } + + @JsonProperty("jmx") + public JmxSecurity getJmxSecurity() + { + return myJmxSecurity; + } + + @JsonProperty("jmx") + public void setJmxSecurity(final JmxSecurity jmxSecurity) + { + myJmxSecurity = jmxSecurity; + } + + public static final class CqlSecurity + { + private Credentials myCqlCredentials; + private CqlTLSConfig myCqlTlsConfig; + + @JsonProperty("credentials") + public Credentials getCqlCredentials() + { + return myCqlCredentials; + } + + @JsonProperty("credentials") + public void setCqlCredentials(final Credentials cqlCredentials) + { + myCqlCredentials = cqlCredentials; + } + + @JsonProperty("tls") + public CqlTLSConfig getCqlTlsConfig() + { + return myCqlTlsConfig; + } + + @JsonProperty("tls") + public void setCqlTlsConfig(final CqlTLSConfig cqlTlsConfig) + { + myCqlTlsConfig = cqlTlsConfig; + } + } + + public static final class JmxSecurity + { + private Credentials myJmxCredentials; + private JmxTLSConfig myJmxTlsConfig; + + @JsonProperty("credentials") + public Credentials getJmxCredentials() + { + return myJmxCredentials; + } + + @JsonProperty("credentials") + public void setJmxCredentials(final Credentials jmxCredentials) + { + myJmxCredentials = jmxCredentials; + } + + @JsonProperty("tls") + public JmxTLSConfig getJmxTlsConfig() + { + return myJmxTlsConfig; + } + + @JsonProperty("tls") + public void setJmxTlsConfig(final JmxTLSConfig jmxTlsConfig) + { + myJmxTlsConfig = jmxTlsConfig; + } + } +} diff --git a/application/src/main/java/com/ericsson/bss/cassandra/ecchronos/application/config/security/package-info.java b/application/src/main/java/com/ericsson/bss/cassandra/ecchronos/application/config/security/package-info.java new file mode 100644 index 000000000..4fe1b3f92 --- /dev/null +++ b/application/src/main/java/com/ericsson/bss/cassandra/ecchronos/application/config/security/package-info.java @@ -0,0 +1,18 @@ +/* + * Copyright 2024 Telefonaktiebolaget LM Ericsson + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * Contains configurations related to security for outbound connections (CQL/JMX). + */ +package com.ericsson.bss.cassandra.ecchronos.application.config.security; diff --git a/application/src/main/java/com/ericsson/bss/cassandra/ecchronos/application/exceptions/ConfigurationException.java b/application/src/main/java/com/ericsson/bss/cassandra/ecchronos/application/exceptions/ConfigurationException.java new file mode 100644 index 000000000..5c41c3234 --- /dev/null +++ b/application/src/main/java/com/ericsson/bss/cassandra/ecchronos/application/exceptions/ConfigurationException.java @@ -0,0 +1,39 @@ +/* + * Copyright 2024 Telefonaktiebolaget LM Ericsson + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.ericsson.bss.cassandra.ecchronos.application.exceptions; + +public class ConfigurationException extends Exception +{ + private static final long serialVersionUID = -2269440899665538081L; + + public ConfigurationException() + { + } + + public ConfigurationException(final String message) + { + super(message); + } + + public ConfigurationException(final String message, final Throwable cause) + { + super(message, cause); + } + + public ConfigurationException(final Throwable cause) + { + super(cause); + } +} diff --git a/application/src/main/java/com/ericsson/bss/cassandra/ecchronos/application/exceptions/package-info.java b/application/src/main/java/com/ericsson/bss/cassandra/ecchronos/application/exceptions/package-info.java new file mode 100644 index 000000000..358e33b66 --- /dev/null +++ b/application/src/main/java/com/ericsson/bss/cassandra/ecchronos/application/exceptions/package-info.java @@ -0,0 +1,18 @@ +/* + * Copyright 2024 Telefonaktiebolaget LM Ericsson + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * Contains exceptions related to outbound connections (CQL and JMX). + */ +package com.ericsson.bss.cassandra.ecchronos.application.exceptions; diff --git a/application/src/main/java/com/ericsson/bss/cassandra/ecchronos/application/package-info.java b/application/src/main/java/com/ericsson/bss/cassandra/ecchronos/application/package-info.java new file mode 100644 index 000000000..a84eebbf0 --- /dev/null +++ b/application/src/main/java/com/ericsson/bss/cassandra/ecchronos/application/package-info.java @@ -0,0 +1,18 @@ +/* + * Copyright 2024 Telefonaktiebolaget LM Ericsson + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * Contains classes that make ecChronos a standalone springboot application. + */ +package com.ericsson.bss.cassandra.ecchronos.application; diff --git a/application/src/main/java/com/ericsson/bss/cassandra/ecchronos/application/providers/AgentJmxConnectionProvider.java b/application/src/main/java/com/ericsson/bss/cassandra/ecchronos/application/providers/AgentJmxConnectionProvider.java new file mode 100644 index 000000000..bf551f8e8 --- /dev/null +++ b/application/src/main/java/com/ericsson/bss/cassandra/ecchronos/application/providers/AgentJmxConnectionProvider.java @@ -0,0 +1,161 @@ +/* + * Copyright 2024 Telefonaktiebolaget LM Ericsson + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.ericsson.bss.cassandra.ecchronos.application.providers; + +import com.ericsson.bss.cassandra.ecchronos.application.config.security.Credentials; +import com.ericsson.bss.cassandra.ecchronos.application.config.security.JmxTLSConfig; +import com.ericsson.bss.cassandra.ecchronos.application.config.security.Security; +import com.ericsson.bss.cassandra.ecchronos.connection.DistributedJmxConnectionProvider; +import com.ericsson.bss.cassandra.ecchronos.connection.DistributedNativeConnectionProvider; +import com.ericsson.bss.cassandra.ecchronos.connection.impl.providers.DistributedJmxConnectionProviderImpl; +import com.ericsson.bss.cassandra.ecchronos.data.sync.EccNodesSync; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.management.remote.JMXConnector; +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Supplier; + +/** + * Default JmxConnection provider used to create JMX connections. + */ +public class AgentJmxConnectionProvider implements DistributedJmxConnectionProvider +{ + private static final Logger LOG = LoggerFactory.getLogger(AgentJmxConnectionProvider.class); + private final DistributedJmxConnectionProviderImpl myDistributedJmxConnectionProviderImpl; + + /** + * Constructs an {@code AgentJmxConnectionProvider} with the specified parameters. + * + * @param jmxSecurity + * a {@link Supplier} providing the JMX security configuration. + * @param distributedNativeConnectionProvider + * the provider responsible for managing native connections in a distributed environment. + * @param eccNodesSync + * an {@link EccNodesSync} instance for synchronizing ECC nodes. + * @throws IOException + * if an I/O error occurs during the initialization of the JMX connection provider. + */ + public AgentJmxConnectionProvider( + final Supplier jmxSecurity, + final DistributedNativeConnectionProvider distributedNativeConnectionProvider, + final EccNodesSync eccNodesSync + ) throws IOException + { + Supplier credentials = () -> convertCredentials(jmxSecurity); + Supplier> tls = () -> convertTls(jmxSecurity); + + LOG.info("Creating DistributedJmxConnectionConfig"); + myDistributedJmxConnectionProviderImpl = DistributedJmxConnectionProviderImpl.builder() + .withCqlSession(distributedNativeConnectionProvider.getCqlSession()) + .withNodesList(distributedNativeConnectionProvider.getNodes()) + .withCredentials(credentials) + .withEccNodesSync(eccNodesSync) + .withTLS(tls) + .build(); + } + + private Map convertTls(final Supplier jmxSecurity) + { + JmxTLSConfig tlsConfig = jmxSecurity.get().getJmxTlsConfig(); + if (!tlsConfig.isEnabled()) + { + return new HashMap<>(); + } + + Map config = new HashMap<>(); + if (tlsConfig.getProtocol() != null) + { + config.put("com.sun.management.jmxremote.ssl.enabled.protocols", tlsConfig.getProtocol()); + } + if (tlsConfig.getCipherSuites() != null) + { + config.put("com.sun.management.jmxremote.ssl.enabled.cipher.suites", tlsConfig.getCipherSuites()); + } + config.put("javax.net.ssl.keyStore", tlsConfig.getKeyStorePath()); + config.put("javax.net.ssl.keyStorePassword", tlsConfig.getKeyStorePassword()); + config.put("javax.net.ssl.trustStore", tlsConfig.getTrustStorePath()); + config.put("javax.net.ssl.trustStorePassword", tlsConfig.getTrustStorePassword()); + + return config; + } + + private String[] convertCredentials(final Supplier jmxSecurity) + { + Credentials credentials = jmxSecurity.get().getJmxCredentials(); + if (!credentials.isEnabled()) + { + return null; + } + return new String[] { + credentials.getUsername(), credentials.getPassword() + }; + } + + /** + * Retrieves the current map of JMX connections. + * + * @return a {@link ConcurrentHashMap} containing the JMX connections, where the key is the UUID of the node and the + * value is the {@link JMXConnector}. + */ + @Override + public ConcurrentHashMap getJmxConnections() + { + return myDistributedJmxConnectionProviderImpl.getJmxConnections(); + } + + /** + * Retrieves the JMX connector associated with the specified node ID. + * + * @param nodeID + * the UUID of the node for which to retrieve the JMX connector. + * @return the {@link JMXConnector} associated with the given node ID. + */ + @Override + public JMXConnector getJmxConnector(final UUID nodeID) + { + return myDistributedJmxConnectionProviderImpl.getJmxConnector(nodeID); + } + + /** + * Closes the JMX connection associated with the specified node ID. + * + * @param nodeID + * the UUID of the node for which to close the JMX connection. + * @throws IOException + * if an I/O error occurs while closing the connection. + */ + @Override + public void close(final UUID nodeID) throws IOException + { + myDistributedJmxConnectionProviderImpl.close(nodeID); + } + + /** + * Closes all JMX connections managed by this provider. + * + * @throws IOException + * if an I/O error occurs while closing the connections. + */ + @Override + public void close() throws IOException + { + myDistributedJmxConnectionProviderImpl.close(); + } +} diff --git a/application/src/main/java/com/ericsson/bss/cassandra/ecchronos/application/providers/AgentNativeConnectionProvider.java b/application/src/main/java/com/ericsson/bss/cassandra/ecchronos/application/providers/AgentNativeConnectionProvider.java new file mode 100644 index 000000000..bab6a24dc --- /dev/null +++ b/application/src/main/java/com/ericsson/bss/cassandra/ecchronos/application/providers/AgentNativeConnectionProvider.java @@ -0,0 +1,283 @@ +/* + * Copyright 2024 Telefonaktiebolaget LM Ericsson + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.ericsson.bss.cassandra.ecchronos.application.providers; + +import com.datastax.oss.driver.api.core.AllNodesFailedException; +import com.datastax.oss.driver.api.core.CqlSession; +import com.datastax.oss.driver.api.core.auth.AuthProvider; +import com.datastax.oss.driver.api.core.metadata.Node; +import com.datastax.oss.driver.api.core.ssl.SslEngineFactory; +import com.ericsson.bss.cassandra.ecchronos.application.config.Config; +import com.ericsson.bss.cassandra.ecchronos.application.config.connection.AgentConnectionConfig; +import com.ericsson.bss.cassandra.ecchronos.application.config.security.ReloadingAuthProvider; +import com.ericsson.bss.cassandra.ecchronos.application.config.security.Security; +import com.ericsson.bss.cassandra.ecchronos.connection.CertificateHandler; +import com.ericsson.bss.cassandra.ecchronos.connection.DistributedNativeConnectionProvider; +import com.ericsson.bss.cassandra.ecchronos.connection.impl.builders.DistributedNativeBuilder; +import com.ericsson.bss.cassandra.ecchronos.connection.impl.providers.DistributedNativeConnectionProviderImpl; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Supplier; + +/** + * The {@code AgentNativeConnectionProvider} class is responsible for establishing and managing native connections to + * Cassandra nodes based on the provided configuration. This class integrates security configurations, such as + * authentication and TLS, and supports different connection types like datacenter-aware, rack-aware, and host-aware. + */ +public class AgentNativeConnectionProvider implements DistributedNativeConnectionProvider +{ + private static final Logger LOG = LoggerFactory.getLogger(AgentNativeConnectionProvider.class); + + private final DistributedNativeConnectionProviderImpl myDistributedNativeConnectionProviderImpl; + + /** + * Constructs an {@code AgentNativeConnectionProvider} with the specified configuration, security supplier, and + * certificate handler. + * + * @param config + * the configuration object containing the connection settings. + * @param cqlSecuritySupplier + * a {@link Supplier} providing the CQL security settings. + * @param certificateHandler + * the handler for managing SSL/TLS certificates. + */ + public AgentNativeConnectionProvider( + final Config config, + final Supplier cqlSecuritySupplier, + final CertificateHandler certificateHandler + ) + { + AgentConnectionConfig agentConnectionConfig = config.getConnectionConfig().getCqlConnection() + .getAgentConnectionConfig(); + Security.CqlSecurity cqlSecurity = cqlSecuritySupplier.get(); + boolean authEnabled = cqlSecurity.getCqlCredentials().isEnabled(); + boolean tlsEnabled = cqlSecurity.getCqlTlsConfig().isEnabled(); + AuthProvider authProvider = null; + if (authEnabled) + { + authProvider = new ReloadingAuthProvider(() -> cqlSecuritySupplier.get().getCqlCredentials()); + } + + SslEngineFactory sslEngineFactory = null; + if (tlsEnabled) + { + sslEngineFactory = certificateHandler; + } + + DistributedNativeBuilder nativeConnectionBuilder = + DistributedNativeConnectionProviderImpl.builder() + .withInitialContactPoints(resolveInitialContactPoints(agentConnectionConfig.getContactPoints())) + .withAgentType(agentConnectionConfig.getType().toString()) + .withLocalDatacenter(agentConnectionConfig.getLocalDatacenter()) + .withAuthProvider(authProvider) + .withSslEngineFactory(sslEngineFactory); + LOG.info("Preparing Agent Connection Config"); + nativeConnectionBuilder = resolveAgentProviderBuilder(nativeConnectionBuilder, agentConnectionConfig); + LOG.info("Establishing Connection With Nodes"); + myDistributedNativeConnectionProviderImpl = tryEstablishConnection(nativeConnectionBuilder); + } + + /** + * Resolves the connection provider builder based on the specified agent connection configuration. This method + * configures the builder with the appropriate connection type (datacenter-aware, rack-aware, or host-aware). + * + * @param builder + * the {@link DistributedNativeBuilder} instance to configure. + * @param agentConnectionConfig + * the connection configuration object. + * @return the configured {@link DistributedNativeBuilder}. + */ + public final DistributedNativeBuilder resolveAgentProviderBuilder( + final DistributedNativeBuilder builder, + final AgentConnectionConfig agentConnectionConfig + ) + { + switch (agentConnectionConfig.getType()) + { + case datacenterAware: + LOG.info("Using DatacenterAware as Agent Config"); + return builder.withDatacenterAware(resolveDatacenterAware( + agentConnectionConfig.getDatacenterAware())); + case rackAware: + LOG.info("Using RackAware as Agent Config"); + return builder.withRackAware(resolveRackAware( + agentConnectionConfig.getRackAware())); + case hostAware: + LOG.info("Using HostAware as Agent Config"); + return builder.withHostAware(resolveHostAware( + agentConnectionConfig.getHostAware())); + default: + } + return builder; + } + + /** + * Resolves the initial contact points from the provided map of host configurations. + * + * @param contactPoints + * a map containing the host configurations. + * @return a list of {@link InetSocketAddress} representing the resolved contact points. + */ + public final List resolveInitialContactPoints( + final Map contactPoints + ) + { + List resolvedContactPoints = new ArrayList<>(); + for (AgentConnectionConfig.Host host : contactPoints.values()) + { + InetSocketAddress tmpAddress = new InetSocketAddress(host.getHost(), host.getPort()); + resolvedContactPoints.add(tmpAddress); + } + return resolvedContactPoints; + } + + /** + * Resolves the datacenter-aware configuration from the specified {@link AgentConnectionConfig.DatacenterAware} + * object. + * + * @param datacenterAware + * the datacenter-aware configuration object. + * @return a list of datacenter names. + */ + public final List resolveDatacenterAware(final AgentConnectionConfig.DatacenterAware datacenterAware) + { + List datacenterNames = new ArrayList<>(); + for + ( + AgentConnectionConfig.DatacenterAware.Datacenter datacenter + : + datacenterAware.getDatacenters().values()) + { + datacenterNames.add(datacenter.getName()); + } + return datacenterNames; + } + + /** + * Resolves the rack-aware configuration from the specified {@link AgentConnectionConfig.RackAware} object. + * + * @param rackAware + * the rack-aware configuration object. + * @return a list of maps containing datacenter and rack information. + */ + public final List> resolveRackAware(final AgentConnectionConfig.RackAware rackAware) + { + List> rackList = new ArrayList<>(); + for + ( + AgentConnectionConfig.RackAware.Rack rack + : + rackAware.getRacks().values() + ) + { + Map rackInfo = new HashMap<>(); + rackInfo.put("datacenterName", rack.getDatacenterName()); + rackInfo.put("rackName", rack.getRackName()); + rackList.add(rackInfo); + } + return rackList; + } + + /** + * Resolves the host-aware configuration from the specified {@link AgentConnectionConfig.HostAware} object. + * + * @param hostAware + * the host-aware configuration object. + * @return a list of {@link InetSocketAddress} representing the resolved hosts. + */ + public final List resolveHostAware(final AgentConnectionConfig.HostAware hostAware) + { + List resolvedHosts = new ArrayList<>(); + for + ( + AgentConnectionConfig.Host host + : + hostAware.getHosts().values() + ) + { + InetSocketAddress tmpAddress = new InetSocketAddress(host.getHost(), host.getPort()); + resolvedHosts.add(tmpAddress); + } + return resolvedHosts; + } + + /** + * Attempts to establish a connection to Cassandra nodes using the provided builder. This method handles exceptions + * and logs errors if the connection fails. + * + * @param builder + * the {@link DistributedNativeBuilder} used to establish the connection. + * @return the established {@link DistributedNativeConnectionProviderImpl}. + * @throws AllNodesFailedException + * if all nodes fail to connect. + * @throws IllegalStateException + * if the connection is in an illegal state. + */ + public final DistributedNativeConnectionProviderImpl tryEstablishConnection( + final DistributedNativeBuilder builder + ) throws AllNodesFailedException, IllegalStateException + { + try + { + return builder.build(); + } + catch (AllNodesFailedException | IllegalStateException e) + { + LOG.error("Unexpected interrupt while trying to connect to Cassandra. Reason: ", e); + throw e; + } + } + + /** + * Retrieves the CQL session associated with this connection provider. + * + * @return the {@link CqlSession} instance. + */ + @Override + public CqlSession getCqlSession() + { + return myDistributedNativeConnectionProviderImpl.getCqlSession(); + } + + /** + * Retrieves the list of nodes connected by this provider. + * + * @return a list of {@link Node} instances. + */ + @Override + public List getNodes() + { + return myDistributedNativeConnectionProviderImpl.getNodes(); + } + + /** + * Closes all resources and connections managed by this provider. + * + * @throws IOException + * if an I/O error occurs while closing the connections. + */ + @Override + public void close() throws IOException + { + myDistributedNativeConnectionProviderImpl.close(); + } +} diff --git a/application/src/main/java/com/ericsson/bss/cassandra/ecchronos/application/providers/package-info.java b/application/src/main/java/com/ericsson/bss/cassandra/ecchronos/application/providers/package-info.java new file mode 100644 index 000000000..02cf97b4e --- /dev/null +++ b/application/src/main/java/com/ericsson/bss/cassandra/ecchronos/application/providers/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright 2023 Telefonaktiebolaget LM Ericsson + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Contains the providers used to create connections (CQL and JMX). + */ +package com.ericsson.bss.cassandra.ecchronos.application.providers; diff --git a/application/src/main/java/com/ericsson/bss/cassandra/ecchronos/application/spring/BeanConfigurator.java b/application/src/main/java/com/ericsson/bss/cassandra/ecchronos/application/spring/BeanConfigurator.java new file mode 100644 index 000000000..dc769cc52 --- /dev/null +++ b/application/src/main/java/com/ericsson/bss/cassandra/ecchronos/application/spring/BeanConfigurator.java @@ -0,0 +1,281 @@ +/* + * Copyright 2024 Telefonaktiebolaget LM Ericsson + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.ericsson.bss.cassandra.ecchronos.application.spring; + +import com.ericsson.bss.cassandra.ecchronos.application.config.security.CqlTLSConfig; +import com.ericsson.bss.cassandra.ecchronos.application.config.security.ReloadingCertificateHandler; +import com.ericsson.bss.cassandra.ecchronos.application.providers.AgentJmxConnectionProvider; +import com.ericsson.bss.cassandra.ecchronos.connection.DistributedJmxConnectionProvider; +import com.ericsson.bss.cassandra.ecchronos.data.exceptions.EcChronosException; +import com.ericsson.bss.cassandra.ecchronos.data.sync.EccNodesSync; + +import java.net.InetAddress; +import java.io.IOException; +import java.net.UnknownHostException; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Consumer; +import java.util.function.Supplier; + +import com.ericsson.bss.cassandra.ecchronos.application.config.Config; +import com.ericsson.bss.cassandra.ecchronos.application.config.ConfigRefresher; +import com.ericsson.bss.cassandra.ecchronos.application.config.ConfigurationHelper; +import com.ericsson.bss.cassandra.ecchronos.application.config.security.Security; +import com.ericsson.bss.cassandra.ecchronos.application.exceptions.ConfigurationException; +import com.ericsson.bss.cassandra.ecchronos.application.providers.AgentNativeConnectionProvider; +import com.ericsson.bss.cassandra.ecchronos.connection.CertificateHandler; +import com.ericsson.bss.cassandra.ecchronos.connection.DistributedNativeConnectionProvider; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory; +import org.springframework.boot.web.servlet.server.ConfigurableServletWebServerFactory; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.format.FormatterRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; +import org.springframework.boot.convert.ApplicationConversionService; + +/** + * The {@code BeanConfigurator} class is responsible for configuring and managing beans within a Spring application + * context, particularly related to Cassandra connections and security configurations. It also provides support for + * refreshing security settings dynamically based on configuration file changes. + */ +@Configuration +public class BeanConfigurator +{ + private static final Logger LOG = LoggerFactory.getLogger(BeanConfigurator.class); + + private static final String CONFIGURATION_FILE = "ecc.yml"; + private static final String SECURITY_FILE = "security.yml"; + private static final String ECCHORONS_ID_PRE_STRING = "ecchronos-"; + + private final AtomicReference cqlSecurity = new AtomicReference<>(); + private final AtomicReference jmxSecurity = new AtomicReference<>(); + private final ConfigRefresher configRefresher; + private final String ecChronosID; + + /** + * Constructs a new {@code BeanConfigurator} and initializes the configuration and security settings. If the + * application is configured to use a specific path, the configuration refresher is initialized to watch for changes + * in security settings. + * + * @throws ConfigurationException + * if there is an error loading the configuration files. + * @throws UnknownHostException + * if the local host name cannot be determined. + */ + public BeanConfigurator() throws ConfigurationException, UnknownHostException + { + if (ConfigurationHelper.DEFAULT_INSTANCE.usePath()) + { + configRefresher = new ConfigRefresher(ConfigurationHelper.DEFAULT_INSTANCE.getConfigPath()); + configRefresher.watch(ConfigurationHelper.DEFAULT_INSTANCE.configFile(SECURITY_FILE).toPath(), + () -> refreshSecurityConfig(cqlSecurity::set, jmxSecurity::set)); + } + else + { + configRefresher = null; + } + Security security = getSecurityConfig(); + cqlSecurity.set(security.getCqlSecurity()); + jmxSecurity.set(security.getJmxSecurity()); + ecChronosID = ECCHORONS_ID_PRE_STRING.concat(InetAddress.getLocalHost().getHostName()); + } + + /** + * Closes the {@code ConfigRefresher} and releases any resources held by it. + */ + public final void close() + { + if (configRefresher != null) + { + configRefresher.close(); + } + } + + /** + * Provides a {@link Config} bean that represents the application configuration. + * + * @return the {@link Config} object. + * @throws ConfigurationException + * if there is an error loading the configuration. + */ + @Bean + public Config config() throws ConfigurationException + { + return getConfiguration(); + } + + /** + * Provides a {@link WebMvcConfigurer} bean to configure formatters and converters for the Spring MVC framework. + * + * @return a {@link WebMvcConfigurer} object. + */ + @Bean + public WebMvcConfigurer conversionConfigurer() //Add application converters to web so springboot can convert in REST + { + return new WebMvcConfigurer() + { + @Override + public void addFormatters(final FormatterRegistry registry) + { + ApplicationConversionService.configure(registry); + } + }; + } + + /** + * Configures the embedded web server factory with the host and port specified in the application configuration. + * + * @param config + * the {@link Config} object containing the server configuration. + * @return a configured {@link ConfigurableServletWebServerFactory}. + * @throws UnknownHostException + * if the specified host cannot be resolved. + */ + @Bean + public ConfigurableServletWebServerFactory webServerFactory(final Config config) throws UnknownHostException + { + TomcatServletWebServerFactory factory = new TomcatServletWebServerFactory(); + factory.setAddress(InetAddress.getByName(config.getRestServer().getHost())); + factory.setPort(config.getRestServer().getPort()); + return factory; + } + + /** + * Provides a {@link DistributedNativeConnectionProvider} bean to manage Cassandra native connections. + * + * @param config + * the {@link Config} object containing the Cassandra connection configuration. + * @return a {@link DistributedNativeConnectionProvider} instance. + */ + @Bean + public DistributedNativeConnectionProvider distributedNativeConnectionProvider( + final Config config + ) + { + return getDistributedNativeConnection(config, cqlSecurity::get); + } + + /** + * Provides an {@link EccNodesSync} bean for synchronizing nodes in an ecChronos environment. + * + * @param distributedNativeConnectionProvider + * the provider for Cassandra native connections. + * @return an {@link EccNodesSync} instance. + * @throws UnknownHostException + * if the local host name cannot be determined. + * @throws EcChronosException + * if there is an error during node synchronization. + */ + @Bean + public EccNodesSync eccNodesSync( + final DistributedNativeConnectionProvider distributedNativeConnectionProvider + ) throws UnknownHostException, EcChronosException + { + return getEccNodesSync(distributedNativeConnectionProvider); + } + + /** + * Provides a {@link DistributedJmxConnectionProvider} bean for managing JMX connections to Cassandra nodes. + * + * @param distributedNativeConnectionProvider + * the provider for Cassandra native connections. + * @param eccNodesSync + * the {@link EccNodesSync} instance for node synchronization. + * @return a {@link DistributedJmxConnectionProvider} instance. + * @throws IOException + * if there is an error creating the JMX connection provider. + */ + @Bean + public DistributedJmxConnectionProvider distributedJmxConnectionProvider( + final DistributedNativeConnectionProvider distributedNativeConnectionProvider, + final EccNodesSync eccNodesSync + ) throws IOException + { + return getDistributedJmxConnection( + jmxSecurity::get, distributedNativeConnectionProvider, eccNodesSync); + } + + private Security getSecurityConfig() throws ConfigurationException + { + return ConfigurationHelper.DEFAULT_INSTANCE.getConfiguration(SECURITY_FILE, Security.class); + } + + private Config getConfiguration() throws ConfigurationException + { + return ConfigurationHelper.DEFAULT_INSTANCE.getConfiguration(CONFIGURATION_FILE, Config.class); + } + + private DistributedNativeConnectionProvider getDistributedNativeConnection( + final Config config, + final Supplier securitySupplier + ) + { + Supplier tlsSupplier = () -> securitySupplier.get().getCqlTlsConfig(); + CertificateHandler certificateHandler = createCertificateHandler(tlsSupplier); + return new AgentNativeConnectionProvider(config, securitySupplier, certificateHandler); + } + + private DistributedJmxConnectionProvider getDistributedJmxConnection( + final Supplier securitySupplier, + final DistributedNativeConnectionProvider distributedNativeConnectionProvider, + final EccNodesSync eccNodesSync + ) throws IOException + { + return new AgentJmxConnectionProvider( + securitySupplier, distributedNativeConnectionProvider, eccNodesSync); + } + + private void refreshSecurityConfig( + final Consumer cqlSetter, + final Consumer jmxSetter + ) + { + try + { + Security security = getSecurityConfig(); + cqlSetter.accept(security.getCqlSecurity()); + jmxSetter.accept(security.getJmxSecurity()); + } + catch (ConfigurationException e) + { + LOG.warn("Unable to refresh security config"); + } + } + + private static CertificateHandler createCertificateHandler( + final Supplier tlsSupplier + ) + { + return new ReloadingCertificateHandler(tlsSupplier); + } + + private EccNodesSync getEccNodesSync( + final DistributedNativeConnectionProvider distributedNativeConnectionProvider + ) throws UnknownHostException, EcChronosException + { + LOG.info("Creating ecChronos nodes_sync bean"); + EccNodesSync myEccNodesSync = EccNodesSync.newBuilder() + .withInitialNodesList(distributedNativeConnectionProvider.getNodes()) + .withSession(distributedNativeConnectionProvider.getCqlSession()) + .withEcchronosID(ecChronosID) + .build(); + LOG.info("ecChronos nodes_sync bean created with success"); + LOG.info("Starting to acquire nodes"); + myEccNodesSync.acquireNodes(); + LOG.info("Nodes acquired with success"); + return myEccNodesSync; + } +} diff --git a/application/src/main/java/com/ericsson/bss/cassandra/ecchronos/application/spring/package-info.java b/application/src/main/java/com/ericsson/bss/cassandra/ecchronos/application/spring/package-info.java new file mode 100644 index 000000000..352ce601f --- /dev/null +++ b/application/src/main/java/com/ericsson/bss/cassandra/ecchronos/application/spring/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright 2024 Telefonaktiebolaget LM Ericsson + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * Contains integration between ecChronos and springboot. + * This package only contain classes that are using spring annotations. + */ +package com.ericsson.bss.cassandra.ecchronos.application.spring; diff --git a/application/src/main/resources/ecc.yml b/application/src/main/resources/ecc.yml new file mode 100644 index 000000000..beb9077d8 --- /dev/null +++ b/application/src/main/resources/ecc.yml @@ -0,0 +1,97 @@ +# +# Copyright 2024 Telefonaktiebolaget LM Ericsson +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +### ecChronos agent configuration + +## Connection +## Properties for connection to Cassandra nodes +## +connection: + cql: + ## Configuration to define Agent Strategy and hosts for ecChronos + ## to connect to. The application will use the configurations + ## specified below, connecting to the listed hosts; + agent: + ## Define the Agent strategy, it can be + ## - datacenterAware; + ## - rackAware; and + ## - hostAware. + type: datacenterAware + ## Specifies the datacenter that is considered "local" by the load balancing policy, + ## The specified datacenter should match with the contact point datacenter + localDatacenter: datacenter1 + ## Initial contact points list for ecChronos + ## to establish first connection with Cassandra. + contactPoints: + - host: 127.0.0.1 + port: 9042 + - host: 127.0.0.2 + port: 9042 + ## Configuration to define datacenters for ecchronos + ## to connect to, datacenterAware enable means that + ## ecChronos will be responsible for all nodes in the + ## datacenter list. + datacenterAware: + datacenters: + - name: datacenter1 + - name: datacenter2 + ## Configuration to define racks for ecchronos + ## to connect to, rackAware enable means that + ## ecChronos will be responsible for all nodes in the + ## rack list. + rackAware: + racks: + - datacenterName: datacenter1 + rackName: rack1 + - datacenterName: datacenter1 + rackName: rack2 + ## Configuration to define hosts for ecchronos + ## to connect to, hostAware enable means that + ## ecChronos will be responsible just for the + ## specified hosts list. + hostAware: + hosts: + - host: 127.0.0.1 + port: 9042 + - host: 127.0.0.2 + port: 9042 + - host: 127.0.0.3 + port: 9042 + - host: 127.0.0.4 + port: 9042 + ## + ## The class used to provide CQL connections to Apache Cassandra. + ## The default provider will be used unless another is specified. + ## The default provider will be used unless another is specified. + ## + provider: com.ericsson.bss.cassandra.ecchronos.application.providers.AgentNativeConnectionProvider + ## + ## The class used to provide an SSL context to the NativeConnectionProvider. + ## Extending this allows to manipulate the SSLEngine and SSLParameters. + ## + certificateHandler: com.ericsson.bss.cassandra.ecchronos.application.config.security.ReloadingCertificateHandler + jmx: + ## + ## The class used to provide JMX connections to Apache Cassandra. + ## The default provider will be used unless another is specified. + ## + provider: com.ericsson.bss.cassandra.ecchronos.application.providers.AgentJmxConnectionProvider + +rest_server: + ## + ## The host and port used for the HTTP server + ## + host: localhost + port: 8080 diff --git a/application/src/main/resources/security.yml b/application/src/main/resources/security.yml new file mode 100644 index 000000000..64b4b43d4 --- /dev/null +++ b/application/src/main/resources/security.yml @@ -0,0 +1,49 @@ +# +# Copyright 2024 Telefonaktiebolaget LM Ericsson +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +cql: + credentials: + enabled: true + username: cassandra + password: cassandra + tls: + enabled: false + keystore: /path/to/keystore + keystore_password: ecchronos + truststore: /path/to/truststore + truststore_password: ecchronos + certificate: + certificate_private_key: + trust_certificate: + protocol: TLSv1.2 + algorithm: + store_type: JKS + cipher_suites: + require_endpoint_verification: false + + +jmx: + credentials: + enabled: true + username: cassandra + password: cassandra + tls: + enabled: false + keystore: /path/to/keystore + keystore_password: ecchronos + truststore: /path/to/truststore + truststore_password: ecchronos + protocol: TLSv1.2 + cipher_suites: diff --git a/application/src/test/java/com/ericsson/bss/cassandra/ecchronos/application/config/TestConfig.java b/application/src/test/java/com/ericsson/bss/cassandra/ecchronos/application/config/TestConfig.java new file mode 100644 index 000000000..95fb42db2 --- /dev/null +++ b/application/src/test/java/com/ericsson/bss/cassandra/ecchronos/application/config/TestConfig.java @@ -0,0 +1,153 @@ +/* + * Copyright 2024 Telefonaktiebolaget LM Ericsson + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.ericsson.bss.cassandra.ecchronos.application.config; + +import com.ericsson.bss.cassandra.ecchronos.application.config.connection.AgentConnectionConfig; +import com.ericsson.bss.cassandra.ecchronos.application.config.connection.ConnectionConfig; +import com.ericsson.bss.cassandra.ecchronos.application.config.connection.DistributedNativeConnection; +import com.ericsson.bss.cassandra.ecchronos.application.exceptions.ConfigurationException; +import com.ericsson.bss.cassandra.ecchronos.application.providers.AgentNativeConnectionProvider; + +import com.fasterxml.jackson.core.exc.StreamReadException; +import com.fasterxml.jackson.databind.DatabindException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; + +import org.junit.Before; +import org.junit.Test; + +import java.io.File; +import java.io.IOException; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.Assert.assertThrows; + +public class TestConfig +{ + private static final String DEFAULT_AGENT_FILE_NAME = "all_set.yml"; + private static Config config; + private static DistributedNativeConnection nativeConnection; + + @Before + public void setup() throws StreamReadException, DatabindException, IOException + { + ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); + + File file = new File(classLoader.getResource(DEFAULT_AGENT_FILE_NAME).getFile()); + + ObjectMapper objectMapper = new ObjectMapper(new YAMLFactory()); + + config = objectMapper.readValue(file, Config.class); + + ConnectionConfig connection = config.getConnectionConfig(); + + nativeConnection = connection.getCqlConnection(); + } + + @Test + public void testDefaultAgentType() + { + assertThat(nativeConnection.getAgentConnectionConfig().getType()).isEqualTo(AgentConnectionConfig.ConnectionType.datacenterAware); + } + + @Test + public void testLocalDatacenter() + { + assertThat(nativeConnection.getAgentConnectionConfig().getLocalDatacenter()).isEqualTo("datacenter1"); + } + + @Test + public void testDefaultContactPoints() + { + assertThat(nativeConnection.getAgentConnectionConfig()).isNotNull(); + assertThat(nativeConnection.getAgentConnectionConfig().getContactPoints().get("127.0.0.1").getPort()).isEqualTo(9042); + assertThat(nativeConnection.getAgentConnectionConfig().getContactPoints().get("127.0.0.2").getPort()).isEqualTo(9042); + assertThat(nativeConnection.getAgentConnectionConfig().getContactPoints().size()).isEqualTo(2); + } + + @Test + public void testDefaultDatacenterAware() + { + assertThat(nativeConnection.getAgentConnectionConfig().getDatacenterAware()).isNotNull(); + assertThat(nativeConnection + .getAgentConnectionConfig() + .getDatacenterAware() + .getDatacenters() + .get("datacenter1").getName()).isEqualTo("datacenter1"); + } + + @Test + public void testDefaultRackAware() + { + assertThat(nativeConnection.getAgentConnectionConfig().getRackAware()).isNotNull(); + assertThat(nativeConnection + .getAgentConnectionConfig() + .getRackAware() + .getRacks().get("rack1") + .getDatacenterName() + ).isEqualTo("datacenter1"); + } + + @Test + public void testDefaultHostAware() + { + assertThat(nativeConnection.getAgentConnectionConfig().getHostAware()).isNotNull(); + assertThat(nativeConnection + .getAgentConnectionConfig() + .getHostAware().getHosts() + .get("127.0.0.1").getPort()) + .isEqualTo(9042); + + assertThat(nativeConnection + .getAgentConnectionConfig() + .getHostAware().getHosts() + .get("127.0.0.2").getPort()) + .isEqualTo(9042); + + assertThat(nativeConnection + .getAgentConnectionConfig() + .getHostAware().getHosts() + .get("127.0.0.3").getPort()) + .isEqualTo(9042); + + assertThat(nativeConnection + .getAgentConnectionConfig() + .getHostAware().getHosts() + .get("127.0.0.4").getPort()) + .isEqualTo(9042); + } + + @Test + public void testAgentProviderConfig() + { + Class providerClass = nativeConnection.getProviderClass(); + assertThat(providerClass).isEqualTo(AgentNativeConnectionProvider.class); + } + + @Test + public void testConfigurationExceptionForWrongAgentType() + { + assertThrows(ConfigurationException.class, () -> { + nativeConnection.getAgentConnectionConfig().setType("wrongType"); + }); + } + + @Test + public void testRestServerConfig() + { + assertThat(config.getRestServer().getHost()).isEqualTo("127.0.0.2"); + assertThat(config.getRestServer().getPort()).isEqualTo(8081); + } +} \ No newline at end of file diff --git a/application/src/test/resources/all_set.yml b/application/src/test/resources/all_set.yml new file mode 100644 index 000000000..673fbce13 --- /dev/null +++ b/application/src/test/resources/all_set.yml @@ -0,0 +1,48 @@ +# +# Copyright 2024 Telefonaktiebolaget LM Ericsson +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +connection: + cql: + agent: + type: datacenterAware + localDatacenter: datacenter1 + contactPoints: + - host: 127.0.0.1 + port: 9042 + - host: 127.0.0.2 + port: 9042 + datacenterAware: + datacenters: + - name: datacenter1 + rackAware: + racks: + - datacenterName: datacenter1 + rackName: rack1 + hostAware: + hosts: + - host: 127.0.0.1 + port: 9042 + - host: 127.0.0.2 + port: 9042 + - host: 127.0.0.3 + port: 9042 + - host: 127.0.0.4 + port: 9042 + provider: com.ericsson.bss.cassandra.ecchronos.application.providers.AgentNativeConnectionProvider + jmx: + +rest_server: + host: 127.0.0.2 + port: 8081 \ No newline at end of file diff --git a/connection.impl/pom.xml b/connection.impl/pom.xml new file mode 100644 index 000000000..9dbd0ca47 --- /dev/null +++ b/connection.impl/pom.xml @@ -0,0 +1,123 @@ + + + + 4.0.0 + + com.ericsson.bss.cassandra.ecchronos + agent + 1.0.0-SNAPSHOT + + + connection.impl + + + + + com.ericsson.bss.cassandra.ecchronos + connection + ${project.version} + + + + com.ericsson.bss.cassandra.ecchronos + data + ${project.version} + + + + com.datastax.oss + java-driver-core + + + + com.datastax.oss + java-driver-metrics-micrometer + + + + + org.junit.vintage + junit-vintage-engine + test + + + + org.slf4j + slf4j-api + + + + org.mockito + mockito-core + test + + + + org.assertj + assertj-core + test + + + + + + + + org.apache.maven.plugins + maven-shade-plugin + + + package + + shade + + + + + com.datastax.oss:java-driver-metrics-micrometer + + + + + + + + org.apache.felix + maven-bundle-plugin + + true + META-INF + + + + + com.ericsson.bss.cassandra.ecchronos.connection.impl.*, + com.datastax.oss.driver.internal.metrics.micrometer.* + + + + + + + + \ No newline at end of file diff --git a/connection.impl/src/main/java/com/ericsson/bss/cassandra/ecchronos/connection/impl/builders/ContactEndPoint.java b/connection.impl/src/main/java/com/ericsson/bss/cassandra/ecchronos/connection/impl/builders/ContactEndPoint.java new file mode 100644 index 000000000..27b74dd39 --- /dev/null +++ b/connection.impl/src/main/java/com/ericsson/bss/cassandra/ecchronos/connection/impl/builders/ContactEndPoint.java @@ -0,0 +1,85 @@ +/* + * Copyright 2024 Telefonaktiebolaget LM Ericsson + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.ericsson.bss.cassandra.ecchronos.connection.impl.builders; + +import com.datastax.oss.driver.api.core.metadata.EndPoint; + +import java.net.InetSocketAddress; +import java.util.Objects; + +public class ContactEndPoint implements EndPoint +{ + private final String hostName; + private final String metricPrefix; + private final int port; + private volatile InetSocketAddress lastResolvedAddress; + + public ContactEndPoint(final String aHostName, final int aPort) + { + this.hostName = aHostName; + this.port = aPort; + this.metricPrefix = buildMetricPrefix(aHostName, aPort); + } + + @Override + public final InetSocketAddress resolve() + { + lastResolvedAddress = new InetSocketAddress(hostName, port); + return lastResolvedAddress; + } + + @Override + public final String asMetricPrefix() + { + return metricPrefix; + } + + @Override + public final boolean equals(final Object o) + { + if (this == o) + { + return true; + } + if (o == null || getClass() != o.getClass()) + { + return false; + } + ContactEndPoint that = (ContactEndPoint) o; + return port == that.port && hostName.equals(that.hostName); + } + + @Override + public final int hashCode() + { + return Objects.hash(hostName, port); + } + + @Override + public final String toString() + { + if (lastResolvedAddress != null) + { + return lastResolvedAddress.toString(); + } + return String.format("%s:%d", hostName, port); + } + + private static String buildMetricPrefix(final String host, final int port) + { + return host.replace('.', '_') + ':' + port; + } +} + diff --git a/connection.impl/src/main/java/com/ericsson/bss/cassandra/ecchronos/connection/impl/builders/DistributedJmxBuilder.java b/connection.impl/src/main/java/com/ericsson/bss/cassandra/ecchronos/connection/impl/builders/DistributedJmxBuilder.java new file mode 100644 index 000000000..f212d625d --- /dev/null +++ b/connection.impl/src/main/java/com/ericsson/bss/cassandra/ecchronos/connection/impl/builders/DistributedJmxBuilder.java @@ -0,0 +1,260 @@ +/* + * Copyright 2024 Telefonaktiebolaget LM Ericsson + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.ericsson.bss.cassandra.ecchronos.connection.impl.builders; + +import com.datastax.oss.driver.api.core.CqlSession; +import com.datastax.oss.driver.api.core.cql.Row; +import com.datastax.oss.driver.api.core.cql.SimpleStatement; +import com.datastax.oss.driver.api.core.metadata.Node; +import com.ericsson.bss.cassandra.ecchronos.connection.impl.providers.DistributedJmxConnectionProviderImpl; +import com.ericsson.bss.cassandra.ecchronos.data.enums.NodeStatus; +import com.ericsson.bss.cassandra.ecchronos.data.exceptions.EcChronosException; +import com.ericsson.bss.cassandra.ecchronos.data.sync.EccNodesSync; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.management.remote.JMXConnector; +import javax.management.remote.JMXConnectorFactory; +import javax.management.remote.JMXServiceURL; +import javax.rmi.ssl.SslRMIClientSocketFactory; +import java.io.IOException; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.UUID; +import java.util.HashMap; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Supplier; + +public class DistributedJmxBuilder +{ + private static final Logger LOG = LoggerFactory.getLogger(DistributedJmxBuilder.class); + private static final String JMX_FORMAT_URL = "service:jmx:rmi:///jndi/rmi://%s:%d/jmxrmi"; + private static final int DEFAULT_PORT = 7199; + + private CqlSession mySession; + private List myNodesList; + private final ConcurrentHashMap myJMXConnections = new ConcurrentHashMap<>(); + private Supplier myCredentialsSupplier; + private Supplier> myTLSSupplier; + + private EccNodesSync myEccNodesSync; + + /** + * Set the CQL session to be used by the DistributedJmxBuilder. + * + * @param session + * the CqlSession instance to be used for communication with Cassandra. + * @return the current instance of DistributedJmxBuilder for chaining. + */ + public final DistributedJmxBuilder withCqlSession(final CqlSession session) + { + mySession = session; + return this; + } + + /** + * Set the list of nodes to be used by the DistributedJmxBuilder. + * + * @param nodesList + * a List of Node instances representing the Cassandra nodes to connect to. + * @return the current instance of DistributedJmxBuilder for chaining. + */ + public final DistributedJmxBuilder withNodesList(final List nodesList) + { + myNodesList = nodesList; + return this; + } + + /** + * Set the credentials supplier to be used by the DistributedJmxBuilder. + * + * @param credentials + * a Supplier that provides an array of Strings containing the username and password. + * @return the current instance of DistributedJmxBuilder for chaining. + */ + public final DistributedJmxBuilder withCredentials(final Supplier credentials) + { + myCredentialsSupplier = credentials; + return this; + } + + /** + * Set the TLS settings supplier to be used by the DistributedJmxBuilder. + * + * @param tlsSupplier + * a Supplier that provides a Map containing TLS settings. + * @return the current instance of DistributedJmxBuilder for chaining. + */ + public final DistributedJmxBuilder withTLS(final Supplier> tlsSupplier) + { + myTLSSupplier = tlsSupplier; + return this; + } + + /** + * Set the EccNodesSync instance to be used by the DistributedJmxBuilder. + * + * @param eccNodesSync + * the EccNodesSync instance that handles synchronization of ECC nodes. + * @return the current instance of DistributedJmxBuilder for chaining. + */ + public final DistributedJmxBuilder withEccNodesSync(final EccNodesSync eccNodesSync) + { + myEccNodesSync = eccNodesSync; + return this; + } + + /** + * Build the DistributedJmxConnectionProviderImpl instance. + * + * @return a new instance of DistributedJmxConnectionProviderImpl initialized with the current settings. + * @throws IOException + * if an I/O error occurs during the creation of connections. + */ + public final DistributedJmxConnectionProviderImpl build() throws IOException + { + createConnections(); + return new DistributedJmxConnectionProviderImpl( + myNodesList, + myJMXConnections + ); + } + + private void createConnections() throws IOException + { + for (Node node : myNodesList) + { + LOG.info("Creating connection with node {}", node.getHostId()); + try + { + reconnect(node); + LOG.info("Connection created with success"); + } + catch (EcChronosException e) + { + LOG.info("Unable to connect with node {} connection refused: {}", node.getHostId(), e.getMessage()); + } + } + } + + private void reconnect(final Node node) throws IOException, EcChronosException + { + String host = node.getBroadcastRpcAddress().get().getHostString(); + Integer port = getJMXPort(node); + if (host.contains(":")) + { + // Use square brackets to surround IPv6 addresses + host = "[" + host + "]"; + } + + LOG.info("Starting to instantiate JMXService with host: {} and port: {}", host, port); + JMXServiceURL jmxUrl = new JMXServiceURL(String.format(JMX_FORMAT_URL, host, port)); + LOG.debug("Connecting JMX through {}, credentials: {}, tls: {}", jmxUrl, isAuthEnabled(), isTLSEnabled()); + JMXConnector jmxConnector = JMXConnectorFactory.connect(jmxUrl, createJMXEnv()); + if (isConnected(jmxConnector)) + { + LOG.info("Connected JMX for {}", jmxUrl); + myEccNodesSync.updateNodeStatus(NodeStatus.AVAILABLE, node.getDatacenter(), node.getHostId()); + myJMXConnections.put(Objects.requireNonNull(node.getHostId()), jmxConnector); + } + else + { + myEccNodesSync.updateNodeStatus(NodeStatus.UNAVAILABLE, node.getDatacenter(), node.getHostId()); + } + } + + private Map createJMXEnv() + { + Map env = new HashMap<>(); + String[] credentials = getCredentialsConfig(); + Map tls = getTLSConfig(); + if (credentials != null) + { + env.put(JMXConnector.CREDENTIALS, credentials); + } + + if (!tls.isEmpty()) + { + for (Map.Entry configEntry : tls.entrySet()) + { + String key = configEntry.getKey(); + String value = configEntry.getValue(); + + if (!value.isEmpty()) + { + System.setProperty(key, value); + } + else + { + System.clearProperty(key); + } + } + env.put("com.sun.jndi.rmi.factory.socket", new SslRMIClientSocketFactory()); + } + return env; + } + + private String[] getCredentialsConfig() + { + return myCredentialsSupplier.get(); + } + + private Map getTLSConfig() + { + return myTLSSupplier.get(); + } + + private boolean isAuthEnabled() + { + return getCredentialsConfig() != null; + } + + private boolean isTLSEnabled() + { + return !getTLSConfig().isEmpty(); + } + + private Integer getJMXPort(final Node node) + { + SimpleStatement simpleStatement = SimpleStatement + .builder("SELECT value FROM system_views.system_properties WHERE name = 'cassandra.jmx.remote.port';") + .setNode(node) + .build(); + Row row = mySession.execute(simpleStatement).one(); + if (row != null) + { + return Integer.parseInt(Objects.requireNonNull(row.getString("value"))); + } + else + { + return DEFAULT_PORT; + } + } + + private static boolean isConnected(final JMXConnector jmxConnector) + { + try + { + jmxConnector.getConnectionId(); + } + catch (IOException e) + { + return false; + } + + return true; + } +} diff --git a/connection.impl/src/main/java/com/ericsson/bss/cassandra/ecchronos/connection/impl/builders/DistributedNativeBuilder.java b/connection.impl/src/main/java/com/ericsson/bss/cassandra/ecchronos/connection/impl/builders/DistributedNativeBuilder.java new file mode 100644 index 000000000..2eb23b7e2 --- /dev/null +++ b/connection.impl/src/main/java/com/ericsson/bss/cassandra/ecchronos/connection/impl/builders/DistributedNativeBuilder.java @@ -0,0 +1,395 @@ +/* + * Copyright 2024 Telefonaktiebolaget LM Ericsson + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.ericsson.bss.cassandra.ecchronos.connection.impl.builders; + +import com.ericsson.bss.cassandra.ecchronos.connection.DataCenterAwarePolicy; +import com.ericsson.bss.cassandra.ecchronos.connection.impl.enums.ConnectionType; + +import com.datastax.oss.driver.api.core.CqlSession; +import com.datastax.oss.driver.api.core.CqlSessionBuilder; +import com.datastax.oss.driver.api.core.auth.AuthProvider; +import com.datastax.oss.driver.api.core.config.DefaultDriverOption; +import com.datastax.oss.driver.api.core.config.DriverConfigLoader; +import com.datastax.oss.driver.api.core.config.ProgrammaticDriverConfigLoaderBuilder; +import com.datastax.oss.driver.api.core.metadata.Node; +import com.datastax.oss.driver.api.core.metadata.NodeStateListener; +import com.datastax.oss.driver.api.core.metadata.schema.SchemaChangeListener; +import com.datastax.oss.driver.api.core.ssl.SslEngineFactory; +import com.datastax.oss.driver.shaded.guava.common.annotations.VisibleForTesting; +import com.datastax.oss.driver.api.core.metrics.DefaultSessionMetric; +import com.ericsson.bss.cassandra.ecchronos.connection.impl.providers.DistributedNativeConnectionProviderImpl; +import com.google.common.collect.ImmutableList; + +import java.net.InetSocketAddress; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class DistributedNativeBuilder +{ + private static final Logger LOG = LoggerFactory.getLogger(DistributedNativeBuilder.class); + + private static final List SCHEMA_REFRESHED_KEYSPACES = ImmutableList.of("/.*/", "!system", + "!system_distributed", "!system_schema", "!system_traces", "!system_views", "!system_virtual_schema"); + + private static final List SESSION_METRICS = Arrays.asList(DefaultSessionMetric.BYTES_RECEIVED.getPath(), + DefaultSessionMetric.BYTES_SENT.getPath(), DefaultSessionMetric.CONNECTED_NODES.getPath(), + DefaultSessionMetric.CQL_REQUESTS.getPath(), DefaultSessionMetric.CQL_CLIENT_TIMEOUTS.getPath(), + DefaultSessionMetric.CQL_PREPARED_CACHE_SIZE.getPath(), DefaultSessionMetric.THROTTLING_DELAY.getPath(), + DefaultSessionMetric.THROTTLING_QUEUE_SIZE.getPath(), DefaultSessionMetric.THROTTLING_ERRORS.getPath()); + + private static final int MAX_NODES_PER_DC = 999; + private ConnectionType myType = ConnectionType.datacenterAware; + private List myInitialContactPoints = new ArrayList<>(); + private String myLocalDatacenter = "datacenter1"; + + private List myDatacenterAware = new ArrayList<>(); + private List> myRackAware = new ArrayList<>(); + private List myHostAware = new ArrayList<>(); + + private boolean myIsMetricsEnabled = true; + private AuthProvider myAuthProvider = null; + private SslEngineFactory mySslEngineFactory = null; + private SchemaChangeListener mySchemaChangeListener = null; + private NodeStateListener myNodeStateListener = null; + + /** + * Sets the initial contact points for the distributed native connection. + * + * @param initialContactPoints + * the list of initial contact points as {@link InetSocketAddress} instances. + * @return the current instance of {@link DistributedNativeBuilder}. + */ + public final DistributedNativeBuilder withInitialContactPoints(final List initialContactPoints) + { + myInitialContactPoints = initialContactPoints; + return this; + } + + /** + * Sets the type of the agent for the distributed native connection. + * + * @param type + * the type of the agent as a {@link String}. + * @return the current instance of {@link DistributedNativeBuilder}. + */ + public final DistributedNativeBuilder withAgentType(final String type) + { + myType = ConnectionType.valueOf(type); + return this; + } + + /** + * Sets the local datacenter for the distributed native connection. + * + * @param localDatacenter + * the name of the local datacenter. + * @return the current instance of {@link DistributedNativeBuilder}. + */ + public final DistributedNativeBuilder withLocalDatacenter(final String localDatacenter) + { + myLocalDatacenter = localDatacenter; + return this; + } + + /** + * Sets the datacenter awareness for the distributed native connection. + * + * @param datacentersInfo + * a list of datacenter information as {@link String}. + * @return the current instance of {@link DistributedNativeBuilder}. + */ + public final DistributedNativeBuilder withDatacenterAware(final List datacentersInfo) + { + myDatacenterAware = datacentersInfo; + return this; + } + + /** + * Sets the rack awareness for the distributed native connection. + * + * @param racksInfo + * a list of rack information as {@link Map} of strings. + * @return the current instance of {@link DistributedNativeBuilder}. + */ + public final DistributedNativeBuilder withRackAware(final List> racksInfo) + { + myRackAware = racksInfo; + return this; + } + + /** + * Sets the host awareness for the distributed native connection. + * + * @param hostsInfo + * a list of host information as {@link InetSocketAddress} instances. + * @return the current instance of {@link DistributedNativeBuilder}. + */ + public final DistributedNativeBuilder withHostAware(final List hostsInfo) + { + myHostAware = hostsInfo; + return this; + } + + /** + * Sets the authentication provider for the distributed native connection. + * + * @param authProvider + * the {@link AuthProvider} to use for authentication. + * @return the current instance of {@link DistributedNativeBuilder}. + */ + public final DistributedNativeBuilder withAuthProvider(final AuthProvider authProvider) + { + myAuthProvider = authProvider; + return this; + } + + /** + * Sets the SSL engine factory for the distributed native connection. + * + * @param sslEngineFactory + * the {@link SslEngineFactory} to use for SSL connections. + * @return the current instance of {@link DistributedNativeBuilder}. + */ + public final DistributedNativeBuilder withSslEngineFactory(final SslEngineFactory sslEngineFactory) + { + this.mySslEngineFactory = sslEngineFactory; + return this; + } + + /** + * Sets the schema change listener for the distributed native connection. + * + * @param schemaChangeListener + * the {@link SchemaChangeListener} to handle schema changes. + * @return the current instance of {@link DistributedNativeBuilder}. + */ + public final DistributedNativeBuilder withSchemaChangeListener(final SchemaChangeListener schemaChangeListener) + { + mySchemaChangeListener = schemaChangeListener; + return this; + } + + /** + * Sets the node state listener for the distributed native connection. + * + * @param nodeStateListener + * the {@link NodeStateListener} to handle node state changes. + * @return the current instance of {@link DistributedNativeBuilder}. + */ + public final DistributedNativeBuilder withNodeStateListener(final NodeStateListener nodeStateListener) + { + myNodeStateListener = nodeStateListener; + return this; + } + + /** + * Enables or disables metrics for the distributed native connection. + * + * @param enabled + * true to enable metrics, false to disable. + * @return the current instance of {@link DistributedNativeBuilder}. + */ + public final DistributedNativeBuilder withMetricsEnabled(final boolean enabled) + { + myIsMetricsEnabled = enabled; + return this; + } + + /** + * Builds and returns a {@link DistributedNativeConnectionProviderImpl} instance. + * + * @return a new instance of {@link DistributedNativeConnectionProviderImpl}. + */ + public final DistributedNativeConnectionProviderImpl build() + { + LOG.info("Creating Session With Initial Contact Points"); + CqlSession session = createSession(this); + LOG.info("Requesting Nodes List"); + List nodesList = createNodesList(session); + LOG.info("Nodes list was created with success"); + return new DistributedNativeConnectionProviderImpl(session, nodesList); + } + + private List createNodesList(final CqlSession session) + { + List tmpNodeList = new ArrayList<>(); + switch (myType) + { + case datacenterAware: + tmpNodeList = resolveDatacenterNodes(session, myDatacenterAware); + return tmpNodeList; + + case rackAware: + tmpNodeList = resolveRackNodes(session, myRackAware); + return tmpNodeList; + + case hostAware: + tmpNodeList = resolveHostAware(session, myHostAware); + return tmpNodeList; + + default: + } + return tmpNodeList; + } + + private CqlSession createSession(final DistributedNativeBuilder builder) + { + CqlSessionBuilder sessionBuilder = fromBuilder(builder); + + DriverConfigLoader driverConfigLoader = loaderBuilder(builder).build(); + LOG.debug("Driver configuration: {}", driverConfigLoader.getInitialConfig().getDefaultProfile().entrySet()); + sessionBuilder.withConfigLoader(driverConfigLoader); + return sessionBuilder.build(); + } + + private List resolveDatacenterNodes(final CqlSession session, final List datacenterNames) + { + Set datacenterNameSet = new HashSet<>(datacenterNames); + List nodesList = new ArrayList<>(); + Collection nodes = session.getMetadata().getNodes().values(); + + for (Node node : nodes) + { + if (datacenterNameSet.contains(node.getDatacenter())) + { + nodesList.add(node); + } + } + return nodesList; + } + + private List resolveRackNodes(final CqlSession session, final List> rackInfo) + { + Set> racksInfoSet = new HashSet<>(rackInfo); + List nodesList = new ArrayList<>(); + Collection nodes = session.getMetadata().getNodes().values(); + + for (Node node : nodes) + { + Map tmpRackInfo = new HashMap<>(); + tmpRackInfo.put("datacenterName", node.getDatacenter()); + tmpRackInfo.put("rackName", node.getRack()); + if (racksInfoSet.contains(tmpRackInfo)) + { + nodesList.add(node); + } + } + return nodesList; + } + + private List resolveHostAware(final CqlSession session, final List hostsInfo) + { + Set hostsInfoSet = new HashSet<>(hostsInfo); + List nodesList = new ArrayList<>(); + Collection nodes = session.getMetadata().getNodes().values(); + for (Node node : nodes) + { + InetSocketAddress tmpAddress = (InetSocketAddress) node.getEndPoint().resolve(); + if (hostsInfoSet.contains(tmpAddress)) + { + nodesList.add(node); + } + } + return nodesList; + } + + private static ProgrammaticDriverConfigLoaderBuilder loaderBuilder( + final DistributedNativeBuilder builder + ) + { + ProgrammaticDriverConfigLoaderBuilder loaderBuilder = DriverConfigLoader.programmaticBuilder() + .withStringList(DefaultDriverOption.METADATA_SCHEMA_REFRESHED_KEYSPACES, + SCHEMA_REFRESHED_KEYSPACES); + loaderBuilder.withString(DefaultDriverOption.LOAD_BALANCING_POLICY_CLASS, + DataCenterAwarePolicy.class.getCanonicalName()); + loaderBuilder.withInt(DefaultDriverOption.LOAD_BALANCING_DC_FAILOVER_MAX_NODES_PER_REMOTE_DC, + MAX_NODES_PER_DC); + if (builder.myIsMetricsEnabled) + { + loaderBuilder.withStringList(DefaultDriverOption.METRICS_SESSION_ENABLED, SESSION_METRICS); + loaderBuilder.withString(DefaultDriverOption.METRICS_FACTORY_CLASS, "MicrometerMetricsFactory"); + loaderBuilder.withString(DefaultDriverOption.METRICS_ID_GENERATOR_CLASS, "TaggingMetricIdGenerator"); + } + return loaderBuilder; + } + + private static CqlSessionBuilder fromBuilder(final DistributedNativeBuilder builder) + { + return CqlSession.builder() + .addContactPoints(builder.myInitialContactPoints) + .withLocalDatacenter(builder.myLocalDatacenter) + .withAuthProvider(builder.myAuthProvider) + .withSslEngineFactory(builder.mySslEngineFactory) + .withSchemaChangeListener(builder.mySchemaChangeListener) + .withNodeStateListener(builder.myNodeStateListener); + } + + /** + * Resolves nodes in the specified datacenters for testing purposes. This method delegates to + * {@link #resolveDatacenterNodes(CqlSession, List)}. + * + * @param session + * the {@link CqlSession} used to connect to the cluster. + * @param datacenterNames + * the list of datacenter names to resolve nodes for. + * @return a list of {@link Node} instances representing the resolved nodes. + */ + @VisibleForTesting + public final List testResolveDatacenterNodes(final CqlSession session, final List datacenterNames) + { + return resolveDatacenterNodes(session, datacenterNames); + } + + /** + * Resolves nodes in the specified racks for testing purposes. This method delegates to + * {@link #resolveRackNodes(CqlSession, List)}. + * + * @param session + * the {@link CqlSession} used to connect to the cluster. + * @param rackInfo + * a list of maps representing rack information. + * @return a list of {@link Node} instances representing the resolved nodes. + */ + @VisibleForTesting + public final List testResolveRackNodes(final CqlSession session, final List> rackInfo) + { + return resolveRackNodes(session, rackInfo); + } + + /** + * Resolves nodes based on host awareness for testing purposes. This method delegates to + * {@link #resolveHostAware(CqlSession, List)}. + * + * @param session + * the {@link CqlSession} used to connect to the cluster. + * @param hostsInfo + * a list of {@link InetSocketAddress} representing host information. + * @return a list of {@link Node} instances representing the resolved nodes. + */ + @VisibleForTesting + public final List testResolveHostAware(final CqlSession session, final List hostsInfo) + { + return resolveHostAware(session, hostsInfo); + } +} diff --git a/connection.impl/src/main/java/com/ericsson/bss/cassandra/ecchronos/connection/impl/builders/package-info.java b/connection.impl/src/main/java/com/ericsson/bss/cassandra/ecchronos/connection/impl/builders/package-info.java new file mode 100644 index 000000000..aedf702f6 --- /dev/null +++ b/connection.impl/src/main/java/com/ericsson/bss/cassandra/ecchronos/connection/impl/builders/package-info.java @@ -0,0 +1,18 @@ +/* + * Copyright 2024 Telefonaktiebolaget LM Ericsson + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * Contains builder classes for connections implementations (CQL and JMX). + */ +package com.ericsson.bss.cassandra.ecchronos.connection.impl.builders; diff --git a/connection.impl/src/main/java/com/ericsson/bss/cassandra/ecchronos/connection/impl/enums/ConnectionType.java b/connection.impl/src/main/java/com/ericsson/bss/cassandra/ecchronos/connection/impl/enums/ConnectionType.java new file mode 100644 index 000000000..f09c1216a --- /dev/null +++ b/connection.impl/src/main/java/com/ericsson/bss/cassandra/ecchronos/connection/impl/enums/ConnectionType.java @@ -0,0 +1,23 @@ +/* + * Copyright 2024 Telefonaktiebolaget LM Ericsson + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.ericsson.bss.cassandra.ecchronos.connection.impl.enums; + +/** + * The type of agent strategy to use for repair management. + */ +public enum ConnectionType +{ + datacenterAware, rackAware, hostAware +} diff --git a/connection.impl/src/main/java/com/ericsson/bss/cassandra/ecchronos/connection/impl/enums/package-info.java b/connection.impl/src/main/java/com/ericsson/bss/cassandra/ecchronos/connection/impl/enums/package-info.java new file mode 100644 index 000000000..123d0854b --- /dev/null +++ b/connection.impl/src/main/java/com/ericsson/bss/cassandra/ecchronos/connection/impl/enums/package-info.java @@ -0,0 +1,18 @@ +/* + * Copyright 2024 Telefonaktiebolaget LM Ericsson + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * Contains enums definitions for connections configs (CQL and JMX). + */ +package com.ericsson.bss.cassandra.ecchronos.connection.impl.enums; diff --git a/connection.impl/src/main/java/com/ericsson/bss/cassandra/ecchronos/connection/impl/package-info.java b/connection.impl/src/main/java/com/ericsson/bss/cassandra/ecchronos/connection/impl/package-info.java new file mode 100644 index 000000000..4800bcc2e --- /dev/null +++ b/connection.impl/src/main/java/com/ericsson/bss/cassandra/ecchronos/connection/impl/package-info.java @@ -0,0 +1,18 @@ +/* + * Copyright 2024 Telefonaktiebolaget LM Ericsson + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * Contains the API for outgoing connections (CQL and JMX) towards Cassandra. + */ +package com.ericsson.bss.cassandra.ecchronos.connection.impl; diff --git a/connection.impl/src/main/java/com/ericsson/bss/cassandra/ecchronos/connection/impl/providers/DistributedJmxConnectionProviderImpl.java b/connection.impl/src/main/java/com/ericsson/bss/cassandra/ecchronos/connection/impl/providers/DistributedJmxConnectionProviderImpl.java new file mode 100644 index 000000000..88bbbb6d8 --- /dev/null +++ b/connection.impl/src/main/java/com/ericsson/bss/cassandra/ecchronos/connection/impl/providers/DistributedJmxConnectionProviderImpl.java @@ -0,0 +1,140 @@ +/* + * Copyright 2024 Telefonaktiebolaget LM Ericsson + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.ericsson.bss.cassandra.ecchronos.connection.impl.providers; + +import java.io.IOException; +import java.util.List; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; + +import javax.management.remote.JMXConnector; + +import com.ericsson.bss.cassandra.ecchronos.connection.DistributedJmxConnectionProvider; +import com.ericsson.bss.cassandra.ecchronos.connection.impl.builders.DistributedJmxBuilder; + +import com.datastax.oss.driver.api.core.metadata.Node; + +public class DistributedJmxConnectionProviderImpl implements DistributedJmxConnectionProvider +{ + private final List myNodesList; + private final ConcurrentHashMap myJMXConnections; + + /** + * Constructs a DistributedJmxConnectionProviderImpl with the specified list of nodes and JMX connections. + * + * @param nodesList + * the list of Node objects representing the nodes to manage JMX connections for. + * @param jmxConnections + * a ConcurrentHashMap mapping each node's UUID to its corresponding JMXConnector. + */ + public DistributedJmxConnectionProviderImpl( + final List nodesList, + final ConcurrentHashMap jmxConnections + ) + { + myNodesList = nodesList; + myJMXConnections = jmxConnections; + } + + private static boolean isConnected(final JMXConnector jmxConnector) + { + try + { + jmxConnector.getConnectionId(); + } + catch (IOException e) + { + return false; + } + + return true; + } + + /** + * Checks if the JMX connection for the specified node is active. + * + * @param nodeID + * the UUID of the node to check the connection status for. + * @return true if the JMX connection for the specified node is active, false otherwise. + */ + public boolean isConnected(final UUID nodeID) + { + return isConnected(myJMXConnections.get(nodeID)); + } + + /** + * Creates and returns a new instance of the DistributedJmxBuilder. + * + * @return a new DistributedJmxBuilder instance. + */ + public static DistributedJmxBuilder builder() + { + return new DistributedJmxBuilder(); + } + + /** + * Get the map of JMX connections. + * + * @return a ConcurrentHashMap where the key is the UUID of a node and the value is the corresponding JMXConnector. + */ + @Override + public ConcurrentHashMap getJmxConnections() + { + return myJMXConnections; + } + + /** + * Get the JMXConnector for a specific node. + * + * @param nodeID + * the UUID of the node for which to retrieve the JMXConnector. + * @return the JMXConnector associated with the specified nodeID, or null if no such connection exists. + */ + @Override + public JMXConnector getJmxConnector(final UUID nodeID) + { + return myJMXConnections.get(nodeID); + } + + /** + * Close all JMX connections. + * + * @throws IOException + * if an I/O error occurs during the closing of connections. + */ + @Override + public void close() throws IOException + { + for (int i = 0; i <= myNodesList.size(); i++) + { + close(myNodesList.get(i).getHostId()); + } + } + + /** + * Close the JMX connection for a specific node. + * + * @param nodeID + * the UUID of the node whose JMX connection should be closed. + * @throws IOException + * if an I/O error occurs while closing the connection. + */ + @Override + public void close(final UUID nodeID) throws IOException + { + myJMXConnections.get(nodeID).close(); + } + +} diff --git a/connection.impl/src/main/java/com/ericsson/bss/cassandra/ecchronos/connection/impl/providers/DistributedNativeConnectionProviderImpl.java b/connection.impl/src/main/java/com/ericsson/bss/cassandra/ecchronos/connection/impl/providers/DistributedNativeConnectionProviderImpl.java new file mode 100644 index 000000000..8d4ccebd3 --- /dev/null +++ b/connection.impl/src/main/java/com/ericsson/bss/cassandra/ecchronos/connection/impl/providers/DistributedNativeConnectionProviderImpl.java @@ -0,0 +1,93 @@ +/* + * Copyright 2024 Telefonaktiebolaget LM Ericsson + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.ericsson.bss.cassandra.ecchronos.connection.impl.providers; + +import com.datastax.oss.driver.api.core.CqlSession; +import com.datastax.oss.driver.api.core.metadata.Node; +import com.ericsson.bss.cassandra.ecchronos.connection.DistributedNativeConnectionProvider; +import com.ericsson.bss.cassandra.ecchronos.connection.impl.builders.DistributedNativeBuilder; + +import java.io.IOException; +import java.util.List; + +public class DistributedNativeConnectionProviderImpl implements DistributedNativeConnectionProvider +{ + private final CqlSession mySession; + private final List myNodes; + + /** + * Constructs a new {@code DistributedNativeConnectionProviderImpl} with the specified {@link CqlSession} and list + * of {@link Node} instances. + * + * @param session + * the {@link CqlSession} used for communication with the Cassandra cluster. + * @param nodesList + * the list of {@link Node} instances representing the nodes in the cluster. + */ + public DistributedNativeConnectionProviderImpl( + final CqlSession session, + final List nodesList + ) + { + mySession = session; + myNodes = nodesList; + } + + /** + * Returns the {@link CqlSession} associated with this connection provider. + * + * @return the {@link CqlSession} used for communication with the Cassandra cluster. + */ + @Override + public CqlSession getCqlSession() + { + return mySession; + } + + /** + * Returns the list of {@link Node} instances generated based on the agent connection type. + * + * @return a {@link List} of {@link Node} instances representing the nodes in the cluster. + */ + @Override + public List getNodes() + { + return myNodes; + } + + /** + * Closes the {@link CqlSession} associated with this connection provider. + * + * @throws IOException + * if an I/O error occurs while closing the session. + */ + @Override + public void close() throws IOException + { + mySession.close(); + } + + /** + * Creates a new instance of {@link DistributedNativeBuilder} for building + * {@link DistributedNativeConnectionProviderImpl} objects. + * + * @return a new {@link DistributedNativeBuilder} instance. + */ + public static DistributedNativeBuilder builder() + { + return new DistributedNativeBuilder(); + } + +} diff --git a/connection.impl/src/main/java/com/ericsson/bss/cassandra/ecchronos/connection/impl/providers/package-info.java b/connection.impl/src/main/java/com/ericsson/bss/cassandra/ecchronos/connection/impl/providers/package-info.java new file mode 100644 index 000000000..094e01017 --- /dev/null +++ b/connection.impl/src/main/java/com/ericsson/bss/cassandra/ecchronos/connection/impl/providers/package-info.java @@ -0,0 +1,18 @@ +/* + * Copyright 2024 Telefonaktiebolaget LM Ericsson + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * Contains the default implementation of API for outgoing connections (CQL and JMX) towards Cassandra. + */ +package com.ericsson.bss.cassandra.ecchronos.connection.impl.providers; diff --git a/connection.impl/src/test/java/com/ericsson/bss/cassandra/ecchronos/connection/impl/providers/TestDistributedNativeConnectionProviderImpl.java b/connection.impl/src/test/java/com/ericsson/bss/cassandra/ecchronos/connection/impl/providers/TestDistributedNativeConnectionProviderImpl.java new file mode 100644 index 000000000..2a4307651 --- /dev/null +++ b/connection.impl/src/test/java/com/ericsson/bss/cassandra/ecchronos/connection/impl/providers/TestDistributedNativeConnectionProviderImpl.java @@ -0,0 +1,163 @@ +/* + * Copyright 2024 Telefonaktiebolaget LM Ericsson + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.ericsson.bss.cassandra.ecchronos.connection.impl.providers; + +import com.ericsson.bss.cassandra.ecchronos.connection.impl.builders.ContactEndPoint; +import com.ericsson.bss.cassandra.ecchronos.connection.impl.builders.DistributedNativeBuilder; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.Assert.*; +import static org.mockito.Mockito.*; + + +import com.datastax.oss.driver.api.core.CqlSession; +import com.datastax.oss.driver.api.core.metadata.Metadata; +import com.datastax.oss.driver.api.core.metadata.Node; +import com.datastax.oss.driver.api.core.metadata.NodeState; + +import java.net.InetSocketAddress; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +@RunWith(MockitoJUnitRunner.Silent.class) +public class TestDistributedNativeConnectionProviderImpl +{ + @Mock + private CqlSession mySessionMock; + + @Mock + private Metadata myMetadataMock; + + private final Map myNodes = new HashMap<>(); + + @Mock + private Node mockNodeDC1Rack1; + + @Mock + private Node mockNodeDC1Rack2; + + @Mock + private Node mockNodeDC2Rack1; + + @Mock + private Node mockNodeDC2Rack2; + + private final ContactEndPoint endPointNodeDC1Rack1 = new ContactEndPoint("127.0.0.1", 9042); + + private final ContactEndPoint endPointNodeDC1rack2 = new ContactEndPoint("127.0.0.2", 9042); + + private final ContactEndPoint endPointNodeDC2Rack1 = new ContactEndPoint("127.0.0.3", 9042); + + private final ContactEndPoint endPointNodeDC2Rack2 = new ContactEndPoint("127.0.0.4", 9042); + + private final List contactPoints = new ArrayList<>(); + + @Before + public void setup() + { + contactPoints.add(new InetSocketAddress("127.0.0.1", 9042)); + contactPoints.add(new InetSocketAddress("127.0.0.2", 9042)); + + when(mockNodeDC1Rack1.getDatacenter()).thenReturn("datacenter1"); + when(mockNodeDC1Rack2.getDatacenter()).thenReturn("datacenter1"); + when(mockNodeDC2Rack1.getDatacenter()).thenReturn("datacenter2"); + when(mockNodeDC2Rack2.getDatacenter()).thenReturn("datacenter2"); + + when(mockNodeDC1Rack1.getRack()).thenReturn("rack1"); + when(mockNodeDC1Rack2.getRack()).thenReturn("rack2"); + when(mockNodeDC2Rack1.getRack()).thenReturn("rack1"); + when(mockNodeDC2Rack2.getRack()).thenReturn("rack2"); + + when(mockNodeDC1Rack1.getEndPoint()).thenReturn(endPointNodeDC1Rack1); + when(mockNodeDC1Rack2.getEndPoint()).thenReturn(endPointNodeDC1rack2); + when(mockNodeDC2Rack1.getEndPoint()).thenReturn(endPointNodeDC2Rack1); + when(mockNodeDC2Rack2.getEndPoint()).thenReturn(endPointNodeDC2Rack2); + + when(mockNodeDC1Rack1.getState()).thenReturn(NodeState.UP); + when(mockNodeDC1Rack2.getState()).thenReturn(NodeState.UP); + when(mockNodeDC2Rack1.getState()).thenReturn(NodeState.UP); + when(mockNodeDC2Rack2.getState()).thenReturn(NodeState.UP); + + myNodes.put(UUID.randomUUID(), mockNodeDC1Rack1); + myNodes.put(UUID.randomUUID(), mockNodeDC1Rack2); + myNodes.put(UUID.randomUUID(), mockNodeDC2Rack1); + myNodes.put(UUID.randomUUID(), mockNodeDC2Rack2); + + when(myMetadataMock.getNodes()).thenReturn(myNodes); + when(mySessionMock.getMetadata()).thenReturn(myMetadataMock); + } + + @Test + public void testResolveDatacenterAware() + { + List datacentersInfo = new ArrayList<>(); + datacentersInfo.add("datacenter1"); + + DistributedNativeBuilder provider = DistributedNativeConnectionProviderImpl.builder() + .withInitialContactPoints(contactPoints) + .withDatacenterAware(datacentersInfo); + + List realNodesList = provider.testResolveDatacenterNodes(mySessionMock, datacentersInfo); + assertThat(realNodesList) + .extracting(Node::getDatacenter) + .containsOnly("datacenter1"); + assertEquals(2, realNodesList.size()); + } + + @Test + public void testResolveRackAware() throws SecurityException, IllegalArgumentException + { + List> rackList = new ArrayList<>(); + Map rackInfo = new HashMap<>(); + rackInfo.put("datacenterName", "datacenter1"); + rackInfo.put("rackName", "rack1"); + rackList.add(rackInfo); + + DistributedNativeBuilder provider = DistributedNativeConnectionProviderImpl.builder() + .withInitialContactPoints(contactPoints) + .withAgentType("rackAware") + .withRackAware(rackList); + + List realNodesList = provider.testResolveRackNodes(mySessionMock, rackList); + assertThat(realNodesList) + .extracting(Node::getRack) + .containsOnly("rack1"); + assertEquals(1, realNodesList.size()); + } + + @Test + public void testResolveHostAware() + { + List hostList = new ArrayList<>(); + hostList.add(new InetSocketAddress("127.0.0.1", 9042)); + hostList.add(new InetSocketAddress("127.0.0.2", 9042)); + hostList.add(new InetSocketAddress("127.0.0.3", 9042)); + DistributedNativeBuilder provider = DistributedNativeConnectionProviderImpl.builder() + .withInitialContactPoints(contactPoints) + .withAgentType("hostAware") + .withHostAware(hostList); + List realNodesList = provider.testResolveHostAware(mySessionMock, hostList); + assertEquals(3, realNodesList.size()); + } +} \ No newline at end of file diff --git a/connection/pom.xml b/connection/pom.xml new file mode 100644 index 000000000..5ce8de648 --- /dev/null +++ b/connection/pom.xml @@ -0,0 +1,89 @@ + + + + 4.0.0 + + com.ericsson.bss.cassandra.ecchronos + agent + 1.0.0-SNAPSHOT + + + connection + + bundle + Interfaces that ecChronos rely on to connect to Apache Cassandra + EcChronos Connection + + + + + com.datastax.oss + java-driver-core + + + + + org.slf4j + slf4j-api + + + + + com.google.guava + guava + + + + + org.junit.vintage + junit-vintage-engine + test + + + + org.mockito + mockito-core + test + + + + org.assertj + assertj-core + test + + + + + + + org.apache.felix + maven-bundle-plugin + + + + maven-compiler-plugin + + ${maven.compiler.source} + ${maven.compiler.target} + + + + + \ No newline at end of file diff --git a/connection/src/main/java/com/ericsson/bss/cassandra/ecchronos/connection/CertificateHandler.java b/connection/src/main/java/com/ericsson/bss/cassandra/ecchronos/connection/CertificateHandler.java new file mode 100644 index 000000000..695fbd579 --- /dev/null +++ b/connection/src/main/java/com/ericsson/bss/cassandra/ecchronos/connection/CertificateHandler.java @@ -0,0 +1,32 @@ +/* + * Copyright 2024 Telefonaktiebolaget LM Ericsson + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.ericsson.bss.cassandra.ecchronos.connection; + +import com.datastax.oss.driver.api.core.metadata.EndPoint; +import com.datastax.oss.driver.api.core.ssl.SslEngineFactory; + +import javax.net.ssl.SSLEngine; + +/** + * SSL Context provider. + */ +public interface CertificateHandler extends SslEngineFactory +{ + @Override + SSLEngine newSslEngine(EndPoint remoteEndpoint); + + @Override + void close() throws Exception; +} diff --git a/connection/src/main/java/com/ericsson/bss/cassandra/ecchronos/connection/DataCenterAwarePolicy.java b/connection/src/main/java/com/ericsson/bss/cassandra/ecchronos/connection/DataCenterAwarePolicy.java new file mode 100644 index 000000000..d69755132 --- /dev/null +++ b/connection/src/main/java/com/ericsson/bss/cassandra/ecchronos/connection/DataCenterAwarePolicy.java @@ -0,0 +1,281 @@ +/* + * Copyright 2024 Telefonaktiebolaget LM Ericsson + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.ericsson.bss.cassandra.ecchronos.connection; + +import java.nio.ByteBuffer; +import java.security.SecureRandom; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Queue; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.atomic.AtomicInteger; + +import com.datastax.oss.driver.api.core.CqlIdentifier; +import com.datastax.oss.driver.api.core.context.DriverContext; +import com.datastax.oss.driver.api.core.loadbalancing.NodeDistance; +import com.datastax.oss.driver.api.core.metadata.Node; +import com.datastax.oss.driver.api.core.metadata.NodeState; +import com.datastax.oss.driver.api.core.session.Request; +import com.datastax.oss.driver.api.core.session.Session; +import com.datastax.oss.driver.internal.core.loadbalancing.DefaultLoadBalancingPolicy; +import com.google.common.base.Joiner; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * A custom load balancing policy inspired by com.datastax.driver.core.policies.DCAwareRoundRobinPolicy and + * com.datastax.driver.core.policies.TokenAwarePolicy but extended to allow the local data center to be replaced with a + * specified data center when creating a new plan. + */ +public class DataCenterAwarePolicy extends DefaultLoadBalancingPolicy +{ + private static final Logger LOG = LoggerFactory.getLogger(DataCenterAwarePolicy.class); + + private final ConcurrentMap> myPerDcLiveNodes = new ConcurrentHashMap<>(); + private final AtomicInteger myIndex = new AtomicInteger(); + + public DataCenterAwarePolicy(final DriverContext context, final String profileName) + { + super(context, profileName); + } + + @Override + public final void init(final Map nodes, final DistanceReporter distanceReporter) + { + super.init(nodes, distanceReporter); + LOG.info("Using provided data-center name '{}' for DataCenterAwareLoadBalancingPolicy", getLocalDatacenter()); + + ArrayList notInLocalDC = new ArrayList<>(); + + for (Node node : nodes.values()) + { + String dc = getDc(node); + + if (!dc.equals(getLocalDatacenter())) + { + notInLocalDC.add(String.format("%s (%s)", node, dc)); + } + + CopyOnWriteArrayList nodeList = myPerDcLiveNodes.get(dc); + if (nodeList == null) + { + myPerDcLiveNodes.put(dc, new CopyOnWriteArrayList<>(Collections.singletonList(node))); + } + else + { + nodeList.addIfAbsent(node); + } + } + + if (!notInLocalDC.isEmpty()) + { + String nonLocalHosts = Joiner.on(",").join(notInLocalDC); + LOG.warn("Some contact points ({}) don't match local data center ({})", + nonLocalHosts, + getLocalDatacenter()); + } + + myIndex.set(new SecureRandom().nextInt(Math.max(nodes.size(), 1))); + } + + /** + * Returns the hosts to use for a new query. + *

+ * The returned plan will first return replicas (whose {@code HostDistance} is {@code LOCAL}) for the query if it + * can determine them (i.e. mainly if {@code statement.getRoutingKey()} is not {@code null}). Following what it will + * return hosts whose {@code HostDistance} is {@code LOCAL} according to a Round-robin algorithm. If no specific + * data center is asked for the child policy is used. + * + * @param request + * the query for which to build the plan. + * @return the new query plan. + */ + @Override + public Queue newQueryPlan(final Request request, final Session session) + { + final String dataCenter; + + if (request instanceof DataCenterAwareStatement) + { + dataCenter = ((DataCenterAwareStatement) request).getDataCenter(); + } + else + { + return super.newQueryPlan(request, session); + } + + ByteBuffer partitionKey = request.getRoutingKey(); + CqlIdentifier keyspace = request.getRoutingKeyspace(); + if (partitionKey == null || keyspace == null) + { + return getFallbackQueryPlan(dataCenter); + } + final Set replicas = session.getMetadata().getTokenMap() + .orElseThrow(IllegalStateException::new) + .getReplicas(keyspace, partitionKey); + if (replicas.isEmpty()) + { + return getFallbackQueryPlan(dataCenter); + } + + return getQueryPlan(dataCenter, replicas); + } + + private Queue getQueryPlan(final String datacenter, final Set replicas) + { + Queue queue = new ConcurrentLinkedQueue(); + for (Node node : replicas) + { + if (node.getState().equals(NodeState.UP) && distance(node, datacenter).equals(NodeDistance.LOCAL)) + { + queue.add(node); + } + } + // Skip if it was already a local replica + Queue fallbackQueue = getFallbackQueryPlan(datacenter); + fallbackQueue.stream().filter(n -> !queue.contains(n)).forEachOrdered(n -> queue.add(n)); + return queue; + } + + /** + * Return the {@link NodeDistance} for the provided host according to the selected data center. + * + * @param node + * the node of which to return the distance of. + * @param dataCenter + * the selected data center. + * @return the HostDistance to {@code host}. + */ + public NodeDistance distance(final Node node, final String dataCenter) + { + String dc = getDc(node); + if (dc.equals(dataCenter)) + { + return NodeDistance.LOCAL; + } + + CopyOnWriteArrayList dcNodes = myPerDcLiveNodes.get(dc); + if (dcNodes == null) + { + return NodeDistance.IGNORED; + } + + return dcNodes.contains(node) ? NodeDistance.REMOTE : NodeDistance.IGNORED; + } + + private Queue getFallbackQueryPlan(final String dataCenter) + { + CopyOnWriteArrayList localLiveNodes = myPerDcLiveNodes.get(dataCenter); + final List nodes = localLiveNodes == null ? Collections.emptyList() : cloneList(localLiveNodes); + final int startIndex = myIndex.getAndIncrement(); + int index = startIndex; + int remainingLocal = nodes.size(); + Queue queue = new ConcurrentLinkedQueue<>(); + while (remainingLocal > 0) + { + remainingLocal--; + int count = index++ % nodes.size(); + if (count < 0) + { + count += nodes.size(); + } + queue.add(nodes.get(count)); + } + return queue; + } + + @SuppressWarnings("unchecked") + private static CopyOnWriteArrayList cloneList(final CopyOnWriteArrayList list) + { + return (CopyOnWriteArrayList) list.clone(); + } + + @Override + public final void onUp(final Node node) + { + super.onUp(node); + markAsUp(node); + } + + private void markAsUp(final Node node) + { + String dc = getDc(node); + + CopyOnWriteArrayList dcNodes = myPerDcLiveNodes.get(dc); + if (dcNodes == null) + { + CopyOnWriteArrayList newMap = new CopyOnWriteArrayList<>(Collections.singletonList(node)); + dcNodes = myPerDcLiveNodes.putIfAbsent(dc, newMap); + // If we've successfully put our new node, we're good, otherwise we've been beaten so continue + if (dcNodes == null) + { + return; + } + } + dcNodes.addIfAbsent(node); + } + + @Override + public final void onDown(final Node node) + { + super.onDown(node); + markAsDown(node); + } + + private void markAsDown(final Node node) + { + CopyOnWriteArrayList dcNodes = myPerDcLiveNodes.get(getDc(node)); + if (dcNodes != null) + { + dcNodes.remove(node); + } + } + + private String getDc(final Node node) + { + String dc = node.getDatacenter(); + return dc == null ? getLocalDatacenter() : dc; + } + + @Override + public final void onAdd(final Node node) + { + super.onAdd(node); + markAsUp(node); + } + + @Override + public final void onRemove(final Node node) + { + super.onRemove(node); + markAsDown(node); + } + + /** + * Only for test purposes. + */ + ConcurrentMap> getPerDcLiveNodes() + { + return myPerDcLiveNodes; + } +} + diff --git a/connection/src/main/java/com/ericsson/bss/cassandra/ecchronos/connection/DataCenterAwareStatement.java b/connection/src/main/java/com/ericsson/bss/cassandra/ecchronos/connection/DataCenterAwareStatement.java new file mode 100644 index 000000000..7f3511afc --- /dev/null +++ b/connection/src/main/java/com/ericsson/bss/cassandra/ecchronos/connection/DataCenterAwareStatement.java @@ -0,0 +1,289 @@ +/* + * Copyright 2024 Telefonaktiebolaget LM Ericsson + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.ericsson.bss.cassandra.ecchronos.connection; + +import com.datastax.oss.driver.api.core.ConsistencyLevel; +import com.datastax.oss.driver.api.core.CqlIdentifier; +import com.datastax.oss.driver.api.core.ProtocolVersion; +import com.datastax.oss.driver.api.core.config.DriverExecutionProfile; +import com.datastax.oss.driver.api.core.cql.BoundStatement; +import com.datastax.oss.driver.api.core.cql.PreparedStatement; +import com.datastax.oss.driver.api.core.metadata.Node; +import com.datastax.oss.driver.api.core.metadata.token.Token; +import com.datastax.oss.driver.api.core.type.DataType; +import com.datastax.oss.driver.api.core.type.codec.registry.CodecRegistry; + +import java.nio.ByteBuffer; +import java.time.Duration; +import java.util.List; +import java.util.Map; + +public class DataCenterAwareStatement implements BoundStatement +{ + private final String myDataCenter; + private final BoundStatement myBoundStatement; + + public DataCenterAwareStatement(final BoundStatement statement, final String dataCenter) + { + myBoundStatement = statement; + myDataCenter = dataCenter; + } + + public final String getDataCenter() + { + return myDataCenter; + } + + @Override + public final PreparedStatement getPreparedStatement() + { + return myBoundStatement.getPreparedStatement(); + } + + @Override + public final List getValues() + { + return myBoundStatement.getValues(); + } + + @Override + public final BoundStatement setExecutionProfileName(final String newConfigProfileName) + { + return myBoundStatement.setExecutionProfileName(newConfigProfileName); + } + + @Override + public final BoundStatement setExecutionProfile(final DriverExecutionProfile newProfile) + { + return myBoundStatement.setExecutionProfile(newProfile); + } + + @Override + public final BoundStatement setRoutingKeyspace(final CqlIdentifier newRoutingKeyspace) + { + return myBoundStatement.setRoutingKeyspace(newRoutingKeyspace); + } + + @Override + public final BoundStatement setNode(final Node node) + { + return myBoundStatement.setNode(node); + } + + @Override + public final BoundStatement setRoutingKey(final ByteBuffer newRoutingKey) + { + return myBoundStatement.setRoutingKey(newRoutingKey); + } + + @Override + public final BoundStatement setRoutingToken(final Token newRoutingToken) + { + return myBoundStatement.setRoutingToken(newRoutingToken); + } + + @Override + public final BoundStatement setCustomPayload(final Map newCustomPayload) + { + return myBoundStatement.setCustomPayload(newCustomPayload); + } + + @Override + public final BoundStatement setIdempotent(final Boolean newIdempotence) + { + return myBoundStatement.setIdempotent(newIdempotence); + } + + @Override + public final BoundStatement setTracing(final boolean newTracing) + { + return myBoundStatement.setTracing(newTracing); + } + + @Override + public final long getQueryTimestamp() + { + return myBoundStatement.getQueryTimestamp(); + } + + @Override + public final BoundStatement setQueryTimestamp(final long newTimestamp) + { + return myBoundStatement.setQueryTimestamp(newTimestamp); + } + + @Override + public final BoundStatement setTimeout(final Duration newTimeout) + { + return myBoundStatement.setTimeout(newTimeout); + } + + @Override + public final ByteBuffer getPagingState() + { + return myBoundStatement.getPagingState(); + } + + @Override + public final BoundStatement setPagingState(final ByteBuffer newPagingState) + { + return myBoundStatement.setPagingState(newPagingState); + } + + @Override + public final int getPageSize() + { + return myBoundStatement.getPageSize(); + } + + @Override + public final BoundStatement setPageSize(final int newPageSize) + { + return myBoundStatement.setPageSize(newPageSize); + } + + @Override + public final ConsistencyLevel getConsistencyLevel() + { + return myBoundStatement.getConsistencyLevel(); + } + + @Override + public final BoundStatement setConsistencyLevel(final ConsistencyLevel newConsistencyLevel) + { + return myBoundStatement.setConsistencyLevel(newConsistencyLevel); + } + + @Override + public final ConsistencyLevel getSerialConsistencyLevel() + { + return myBoundStatement.getSerialConsistencyLevel(); + } + + @Override + public final BoundStatement setSerialConsistencyLevel(final ConsistencyLevel newSerialConsistencyLevel) + { + return myBoundStatement.setSerialConsistencyLevel(newSerialConsistencyLevel); + } + + @Override + public final boolean isTracing() + { + return myBoundStatement.isTracing(); + } + + @Override + public final int firstIndexOf(final String name) + { + return myBoundStatement.firstIndexOf(name); + } + + @Override + public final int firstIndexOf(final CqlIdentifier id) + { + return myBoundStatement.firstIndexOf(id); + } + + @Override + public final ByteBuffer getBytesUnsafe(final int i) + { + return myBoundStatement.getBytesUnsafe(i); + } + + @Override + public final BoundStatement setBytesUnsafe(final int i, final ByteBuffer v) + { + return myBoundStatement.setBytesUnsafe(i, v); + } + + @Override + public final int size() + { + return myBoundStatement.size(); + } + + @Override + public final DataType getType(final int i) + { + return myBoundStatement.getType(i); + } + + @Override + public final CodecRegistry codecRegistry() + { + return myBoundStatement.codecRegistry(); + } + + @Override + public final ProtocolVersion protocolVersion() + { + return myBoundStatement.protocolVersion(); + } + + @Override + public final String getExecutionProfileName() + { + return myBoundStatement.getExecutionProfileName(); + } + + @Override + public final DriverExecutionProfile getExecutionProfile() + { + return myBoundStatement.getExecutionProfile(); + } + + @Override + public final CqlIdentifier getRoutingKeyspace() + { + return myBoundStatement.getRoutingKeyspace(); + } + + @Override + public final ByteBuffer getRoutingKey() + { + return myBoundStatement.getRoutingKey(); + } + + @Override + public final Token getRoutingToken() + { + return myBoundStatement.getRoutingToken(); + } + + @Override + public final Map getCustomPayload() + { + return myBoundStatement.getCustomPayload(); + } + + @Override + public final Boolean isIdempotent() + { + return myBoundStatement.isIdempotent(); + } + + @Override + public final Duration getTimeout() + { + return myBoundStatement.getTimeout(); + } + + @Override + public final Node getNode() + { + return myBoundStatement.getNode(); + } +} + diff --git a/connection/src/main/java/com/ericsson/bss/cassandra/ecchronos/connection/DistributedJmxConnectionProvider.java b/connection/src/main/java/com/ericsson/bss/cassandra/ecchronos/connection/DistributedJmxConnectionProvider.java new file mode 100644 index 000000000..7fad4ba98 --- /dev/null +++ b/connection/src/main/java/com/ericsson/bss/cassandra/ecchronos/connection/DistributedJmxConnectionProvider.java @@ -0,0 +1,36 @@ +/* + * Copyright 2024 Telefonaktiebolaget LM Ericsson + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.ericsson.bss.cassandra.ecchronos.connection; + +import java.io.Closeable; +import java.io.IOException; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; + +import javax.management.remote.JMXConnector; + +public interface DistributedJmxConnectionProvider extends Closeable +{ + ConcurrentHashMap getJmxConnections(); + + JMXConnector getJmxConnector(UUID nodeID); + + @Override + default void close() throws IOException + { + } + + void close(UUID nodeID) throws IOException; +} diff --git a/connection/src/main/java/com/ericsson/bss/cassandra/ecchronos/connection/DistributedNativeConnectionProvider.java b/connection/src/main/java/com/ericsson/bss/cassandra/ecchronos/connection/DistributedNativeConnectionProvider.java new file mode 100644 index 000000000..45dfb2ac2 --- /dev/null +++ b/connection/src/main/java/com/ericsson/bss/cassandra/ecchronos/connection/DistributedNativeConnectionProvider.java @@ -0,0 +1,34 @@ +/* + * Copyright 2024 Telefonaktiebolaget LM Ericsson + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.ericsson.bss.cassandra.ecchronos.connection; + +import java.io.Closeable; +import java.io.IOException; +import java.util.List; + +import com.datastax.oss.driver.api.core.CqlSession; +import com.datastax.oss.driver.api.core.metadata.Node; + +public interface DistributedNativeConnectionProvider extends Closeable +{ + CqlSession getCqlSession(); + + List getNodes(); + + @Override + default void close() throws IOException + { + } +} diff --git a/connection/src/main/java/com/ericsson/bss/cassandra/ecchronos/connection/StatementDecorator.java b/connection/src/main/java/com/ericsson/bss/cassandra/ecchronos/connection/StatementDecorator.java new file mode 100644 index 000000000..f3c0a49c5 --- /dev/null +++ b/connection/src/main/java/com/ericsson/bss/cassandra/ecchronos/connection/StatementDecorator.java @@ -0,0 +1,27 @@ +/* + * Copyright 2024 Telefonaktiebolaget LM Ericsson + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.ericsson.bss.cassandra.ecchronos.connection; + +import com.datastax.oss.driver.api.core.cql.Statement; + +public interface StatementDecorator +{ + /** + * Decorates a statement before sending it over to the server. + * @param statement The original statement + * @return The decorated statement + */ + Statement apply(Statement statement); +} diff --git a/connection/src/main/java/com/ericsson/bss/cassandra/ecchronos/connection/package-info.java b/connection/src/main/java/com/ericsson/bss/cassandra/ecchronos/connection/package-info.java new file mode 100644 index 000000000..1f067bbd1 --- /dev/null +++ b/connection/src/main/java/com/ericsson/bss/cassandra/ecchronos/connection/package-info.java @@ -0,0 +1,18 @@ +/* + * Copyright 2024 Telefonaktiebolaget LM Ericsson + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * Contains the API for outgoing connections (CQL and JMX) towards Cassandra. + */ +package com.ericsson.bss.cassandra.ecchronos.connection; diff --git a/data/pom.xml b/data/pom.xml new file mode 100644 index 000000000..b765ab205 --- /dev/null +++ b/data/pom.xml @@ -0,0 +1,171 @@ + + + + 4.0.0 + + com.ericsson.bss.cassandra.ecchronos + agent + 1.0.0-SNAPSHOT + + + data + + + + + com.ericsson.bss.cassandra.ecchronos + connection + ${project.version} + + + + io.micrometer + micrometer-core + + + + + jakarta.validation + jakarta.validation-api + + + + + com.datastax.oss + java-driver-core + + + + com.datastax.oss + java-driver-query-builder + + + + com.google.guava + guava + + + + com.github.ben-manes.caffeine + caffeine + + + + + org.slf4j + slf4j-api + + + + + org.junit.vintage + junit-vintage-engine + test + + + + commons-io + commons-io + test + + + + org.awaitility + awaitility + test + + + + org.mockito + mockito-core + test + + + + org.assertj + assertj-core + test + + + + net.jcip + jcip-annotations + test + + + + nl.jqno.equalsverifier + equalsverifier + test + + + + + org.springframework + spring-test + test + + + org.springframework.boot + spring-boot-test + test + + + org.testcontainers + cassandra + test + + + + + + + org.apache.maven.plugins + maven-dependency-plugin + + + dependencies + generate-sources + + tree + + + compile + target/dependency-tree.txt + + + + + + + org.apache.felix + maven-bundle-plugin + + true + META-INF + + com.ericsson.bss.cassandra.ecchronos.data.* + + + + + + + \ No newline at end of file diff --git a/data/src/main/java/com/ericsson/bss/cassandra/ecchronos/data/enums/NodeStatus.java b/data/src/main/java/com/ericsson/bss/cassandra/ecchronos/data/enums/NodeStatus.java new file mode 100644 index 000000000..706a5998a --- /dev/null +++ b/data/src/main/java/com/ericsson/bss/cassandra/ecchronos/data/enums/NodeStatus.java @@ -0,0 +1,24 @@ +/* + * Copyright 2024 Telefonaktiebolaget LM Ericsson + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.ericsson.bss.cassandra.ecchronos.data.enums; + +/** + * The status of nodes after creating jmx connection. + */ +public enum NodeStatus +{ + UNAVAILABLE, + AVAILABLE +} diff --git a/data/src/main/java/com/ericsson/bss/cassandra/ecchronos/data/enums/package-info.java b/data/src/main/java/com/ericsson/bss/cassandra/ecchronos/data/enums/package-info.java new file mode 100644 index 000000000..e643c759a --- /dev/null +++ b/data/src/main/java/com/ericsson/bss/cassandra/ecchronos/data/enums/package-info.java @@ -0,0 +1,18 @@ +/* + * Copyright 2024 Telefonaktiebolaget LM Ericsson + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * Contains enums definitions for data configs. + */ +package com.ericsson.bss.cassandra.ecchronos.data.enums; diff --git a/data/src/main/java/com/ericsson/bss/cassandra/ecchronos/data/exceptions/EcChronosException.java b/data/src/main/java/com/ericsson/bss/cassandra/ecchronos/data/exceptions/EcChronosException.java new file mode 100644 index 000000000..ee5e65983 --- /dev/null +++ b/data/src/main/java/com/ericsson/bss/cassandra/ecchronos/data/exceptions/EcChronosException.java @@ -0,0 +1,38 @@ +/* + * Copyright 2024 Telefonaktiebolaget LM Ericsson + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.ericsson.bss.cassandra.ecchronos.data.exceptions; + +/** + * Generic exception thrown by schedulers to signal that something went wrong. + */ +public class EcChronosException extends Exception +{ + private static final long serialVersionUID = 1148561336907867613L; + + public EcChronosException(final String message) + { + super(message); + } + + public EcChronosException(final Throwable t) + { + super(t); + } + + public EcChronosException(final String message, final Throwable t) + { + super(message, t); + } +} diff --git a/data/src/main/java/com/ericsson/bss/cassandra/ecchronos/data/exceptions/package-info.java b/data/src/main/java/com/ericsson/bss/cassandra/ecchronos/data/exceptions/package-info.java new file mode 100644 index 000000000..6a241f019 --- /dev/null +++ b/data/src/main/java/com/ericsson/bss/cassandra/ecchronos/data/exceptions/package-info.java @@ -0,0 +1,18 @@ +/* + * Copyright 2024 Telefonaktiebolaget LM Ericsson + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * Contains exceptions related to I/O in data tables. + */ +package com.ericsson.bss.cassandra.ecchronos.data.exceptions; diff --git a/data/src/main/java/com/ericsson/bss/cassandra/ecchronos/data/sync/EccNodesSync.java b/data/src/main/java/com/ericsson/bss/cassandra/ecchronos/data/sync/EccNodesSync.java new file mode 100644 index 000000000..976828805 --- /dev/null +++ b/data/src/main/java/com/ericsson/bss/cassandra/ecchronos/data/sync/EccNodesSync.java @@ -0,0 +1,275 @@ +/* + * Copyright 2024 Telefonaktiebolaget LM Ericsson + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.ericsson.bss.cassandra.ecchronos.data.sync; + +import com.datastax.oss.driver.api.core.ConsistencyLevel; +import com.datastax.oss.driver.api.core.CqlSession; +import com.datastax.oss.driver.api.core.cql.BoundStatement; +import com.datastax.oss.driver.api.core.cql.PreparedStatement; +import com.datastax.oss.driver.api.core.cql.ResultSet; +import com.datastax.oss.driver.api.core.metadata.Node; +import com.datastax.oss.driver.api.querybuilder.QueryBuilder; +import com.datastax.oss.driver.shaded.guava.common.annotations.VisibleForTesting; +import com.ericsson.bss.cassandra.ecchronos.data.enums.NodeStatus; +import com.ericsson.bss.cassandra.ecchronos.data.exceptions.EcChronosException; +import com.google.common.base.Preconditions; + +import java.net.UnknownHostException; +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.List; +import java.util.UUID; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + + +import static com.datastax.oss.driver.api.querybuilder.QueryBuilder.bindMarker; + +/** + * CQL Definition for nodes_sync table. CREATE TABLE ecchronos_agent.nodes_sync ( ecchronos_id TEXT, datacenter_name + * TEXT, node_id UUID, node_endpoint TEXT, node_status TEXT, last_connection TIMESTAMP, next_connection TIMESTAMP, + * PRIMARY KEY ( ecchronos_id, datacenter_name, node_id ) ) WITH CLUSTERING ORDER BY( datacenter_name DESC, node_id DESC + * ); + */ +public final class EccNodesSync +{ + private static final Logger LOG = LoggerFactory.getLogger(EccNodesSync.class); + + private static final Integer DEFAULT_CONNECTION_DELAY_IN_MINUTES = 30; + private static final String COLUMN_ECCHRONOS_ID = "ecchronos_id"; + private static final String COLUMN_DC_NAME = "datacenter_name"; + private static final String COLUMN_NODE_ID = "node_id"; + private static final String COLUMN_NODE_ENDPOINT = "node_endpoint"; + private static final String COLUMN_NODE_STATUS = "node_status"; + private static final String COLUMN_LAST_CONNECTION = "last_connection"; + private static final String COLUMN_NEXT_CONNECTION = "next_connection"; + + private static final String KEYSPACE_NAME = "ecchronos"; + private static final String TABLE_NAME = "nodes_sync"; + + private final CqlSession mySession; + private final List myNodesList; + private final String ecChronosID; + + private final PreparedStatement myCreateStatement; + private final PreparedStatement myUpdateStatusStatement; + + private EccNodesSync(final Builder builder) throws UnknownHostException + { + mySession = Preconditions.checkNotNull(builder.mySession, "Session cannot be null"); + myNodesList = Preconditions + .checkNotNull(builder.initialNodesList, "Nodes list cannot be null"); + myCreateStatement = mySession.prepare(QueryBuilder.insertInto(KEYSPACE_NAME, TABLE_NAME) + .value(COLUMN_ECCHRONOS_ID, bindMarker()) + .value(COLUMN_DC_NAME, bindMarker()) + .value(COLUMN_NODE_ENDPOINT, bindMarker()) + .value(COLUMN_NODE_STATUS, bindMarker()) + .value(COLUMN_LAST_CONNECTION, bindMarker()) + .value(COLUMN_NEXT_CONNECTION, bindMarker()) + .value(COLUMN_NODE_ID, bindMarker()) + .build() + .setConsistencyLevel(ConsistencyLevel.LOCAL_QUORUM)); + myUpdateStatusStatement = mySession.prepare(QueryBuilder.update(KEYSPACE_NAME, TABLE_NAME) + .setColumn(COLUMN_NODE_STATUS, bindMarker()) + .setColumn(COLUMN_LAST_CONNECTION, bindMarker()) + .setColumn(COLUMN_NEXT_CONNECTION, bindMarker()) + .whereColumn(COLUMN_ECCHRONOS_ID).isEqualTo(bindMarker()) + .whereColumn(COLUMN_DC_NAME).isEqualTo(bindMarker()) + .whereColumn(COLUMN_NODE_ID).isEqualTo(bindMarker()) + .build() + .setConsistencyLevel(ConsistencyLevel.LOCAL_QUORUM)); + ecChronosID = builder.myEcchronosID; + } + + public void acquireNodes() throws EcChronosException + { + if (myNodesList.isEmpty()) + { + throw new EcChronosException("Cannot Acquire Nodes because there is no nodes to be acquired"); + } + for (Node node : myNodesList) + { + LOG.info( + "Preparing to acquire node {} with endpoint {} and Datacenter {}", + node.getHostId(), + node.getEndPoint(), + node.getDatacenter()); + ResultSet tmpResultSet = acquireNode(node); + if (tmpResultSet.wasApplied()) + { + LOG.info("Node successfully acquired by instance {}", ecChronosID); + } + else + { + LOG.error("Unable to acquire node {}", node.getHostId()); + } + } + } + + private ResultSet acquireNode(final Node node) + { + return insertNodeInfo( + node.getDatacenter(), + node.getEndPoint().toString(), + node.getState().toString(), + Instant.now(), + Instant.now().plus(DEFAULT_CONNECTION_DELAY_IN_MINUTES, ChronoUnit.MINUTES), + node.getHostId()); + } + + @VisibleForTesting + public ResultSet verifyAcquireNode(final Node node) + { + return acquireNode(node); + } + + private ResultSet insertNodeInfo( + final String datacenterName, + final String nodeEndpoint, + final String nodeStatus, + final Instant lastConnection, + final Instant nextConnection, + final UUID nodeID + ) + { + BoundStatement insertNodeSyncInfo = myCreateStatement.bind(ecChronosID, + datacenterName, nodeEndpoint, nodeStatus, lastConnection, nextConnection, nodeID); + return execute(insertNodeSyncInfo); + } + + public ResultSet updateNodeStatus( + final NodeStatus nodeStatus, + final String datacenterName, + final UUID nodeID + ) + { + ResultSet tmpResultSet = updateNodeStateStatement(nodeStatus, datacenterName, nodeID); + if (tmpResultSet.wasApplied()) + { + LOG.info("Node {} successfully upgraded", nodeID); + } + else + { + LOG.error("Unable to update node {}", nodeID); + } + return tmpResultSet; + } + + private ResultSet updateNodeStateStatement( + final NodeStatus nodeStatus, + final String datacenterName, + final UUID nodeID + ) + { + BoundStatement updateNodeStatus = myUpdateStatusStatement.bind( + nodeStatus.toString(), + Instant.now(), + Instant.now().plus(DEFAULT_CONNECTION_DELAY_IN_MINUTES, ChronoUnit.MINUTES), + ecChronosID, + datacenterName, + nodeID + ); + return execute(updateNodeStatus); + } + + @VisibleForTesting + public ResultSet verifyInsertNodeInfo( + final String datacenterName, + final String nodeEndpoint, + final String nodeStatus, + final Instant lastConnection, + final Instant nextConnection, + final UUID nodeID + ) + { + return insertNodeInfo( + datacenterName, + nodeEndpoint, + nodeStatus, + lastConnection, + nextConnection, + nodeID + ); + } + + public ResultSet execute(final BoundStatement statement) + { + return mySession.execute(statement); + } + + public static Builder newBuilder() + { + return new Builder(); + } + + public static class Builder + { + private CqlSession mySession; + private List initialNodesList; + private String myEcchronosID; + + /** + * Builds EccNodesSync with session. + * + * @param session + * Session object + * @return Builder + */ + public Builder withSession(final CqlSession session) + { + this.mySession = session; + return this; + } + + /** + * Builds EccNodesSync with nodes list. + * + * @param nodes + * nodes list + * @return Builder + */ + public Builder withInitialNodesList(final List nodes) + { + this.initialNodesList = nodes; + return this; + } + + /** + * Builds EccNodesSync with ecchronosID. + * + * @param echronosID + * ecchronos ID generated by BeanConfigurator. + * @return Builder + */ + public Builder withEcchronosID(final String echronosID) + { + this.myEcchronosID = echronosID; + return this; + } + + /** + * Builds EccNodesSync. + * + * @return Builder + * @throws UnknownHostException + */ + public EccNodesSync build() throws UnknownHostException + { + return new EccNodesSync(this); + } + } +} + diff --git a/data/src/main/java/com/ericsson/bss/cassandra/ecchronos/data/sync/package-info.java b/data/src/main/java/com/ericsson/bss/cassandra/ecchronos/data/sync/package-info.java new file mode 100644 index 000000000..ed86a2740 --- /dev/null +++ b/data/src/main/java/com/ericsson/bss/cassandra/ecchronos/data/sync/package-info.java @@ -0,0 +1,18 @@ +/* + * Copyright 2024 Telefonaktiebolaget LM Ericsson + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * Contains the representation of nodes_sync table. + */ +package com.ericsson.bss.cassandra.ecchronos.data.sync; diff --git a/data/src/test/java/com/ericsson/bss/cassandra/ecchronos/data/sync/TestEccNodesSync.java b/data/src/test/java/com/ericsson/bss/cassandra/ecchronos/data/sync/TestEccNodesSync.java new file mode 100644 index 000000000..df0337a51 --- /dev/null +++ b/data/src/test/java/com/ericsson/bss/cassandra/ecchronos/data/sync/TestEccNodesSync.java @@ -0,0 +1,147 @@ +/* + * Copyright 2024 Telefonaktiebolaget LM Ericsson + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.ericsson.bss.cassandra.ecchronos.data.sync; + +import com.datastax.oss.driver.api.core.cql.ResultSet; +import com.datastax.oss.driver.api.core.cql.SimpleStatement; +import com.datastax.oss.driver.api.core.metadata.Node; + +import com.ericsson.bss.cassandra.ecchronos.data.enums.NodeStatus; +import com.ericsson.bss.cassandra.ecchronos.data.exceptions.EcChronosException; +import com.ericsson.bss.cassandra.ecchronos.data.utils.AbstractCassandraTest; +import java.io.IOException; +import net.jcip.annotations.NotThreadSafe; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import java.net.UnknownHostException; +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + + +import static org.junit.Assert.*; + +@NotThreadSafe +public class TestEccNodesSync extends AbstractCassandraTest +{ + private static final String ECCHRONOS_KEYSPACE = "ecchronos"; + + private EccNodesSync eccNodesSync; + private final List nodesList = getNativeConnectionProvider().getNodes(); + private final UUID nodeID = UUID.randomUUID(); + private final String datacenterName = "datacenter1"; + + @Before + public void setup() throws IOException + { + mySession.execute(String.format( + "CREATE KEYSPACE IF NOT EXISTS %s WITH replication = {'class': 'NetworkTopologyStrategy', 'DC1': 1}", + ECCHRONOS_KEYSPACE)); + String query = String.format( + "CREATE TABLE IF NOT EXISTS %s.nodes_sync(" + + "ecchronos_id TEXT, " + + "datacenter_name TEXT, " + + "node_id UUID, " + + "node_endpoint TEXT, " + + "node_status TEXT, " + + "last_connection TIMESTAMP, " + + "next_connection TIMESTAMP, " + + "PRIMARY KEY(ecchronos_id, datacenter_name, node_id));", + ECCHRONOS_KEYSPACE + ); + + mySession.execute(query); + + eccNodesSync = EccNodesSync.newBuilder() + .withSession(mySession) + .withInitialNodesList(nodesList) + .withEcchronosID("ecchronos-test").build(); + } + + @After + public void testCleanup() + { + mySession.execute(SimpleStatement.newInstance( + String.format("TRUNCATE %s.%s", ECCHRONOS_KEYSPACE, "nodes_sync"))); + } + + @Test + public void testAcquireNode() + { + ResultSet result = eccNodesSync.verifyAcquireNode(nodesList.get(0)); + assertNotNull(result); + } + + @Test + public void testInsertNodeInfo() + { + + String nodeEndpoint = "127.0.0.1"; + String nodeStatus = "UP"; + Instant lastConnection = Instant.now(); + Instant nextConnection = lastConnection.plus(30, ChronoUnit.MINUTES); + + ResultSet result = eccNodesSync.verifyInsertNodeInfo(datacenterName, nodeEndpoint, + nodeStatus, lastConnection, nextConnection, nodeID); + assertNotNull(result); + } + + @Test + public void testUpdateNodeStatus() + { + ResultSet resultSet = eccNodesSync.updateNodeStatus(NodeStatus.AVAILABLE, datacenterName, nodeID); + assertNotNull(resultSet); + assertTrue(resultSet.wasApplied()); + } + + @Test + public void testEccNodesWithNullList() + { + EccNodesSync.Builder tmpEccNodesSyncBuilder = EccNodesSync.newBuilder() + .withSession(mySession) + .withInitialNodesList(null); + NullPointerException exception = assertThrows( + NullPointerException.class, tmpEccNodesSyncBuilder::build); + assertEquals("Nodes list cannot be null", exception.getMessage()); + } + + @Test + public void testAcquiredNodesWithEmptyList() throws UnknownHostException + { + EccNodesSync tmpEccNodesSync = EccNodesSync.newBuilder() + .withSession(mySession) + .withInitialNodesList(new ArrayList<>()).build(); + EcChronosException exception = assertThrows( + EcChronosException.class, tmpEccNodesSync::acquireNodes); + assertEquals( + "Cannot Acquire Nodes because there is no nodes to be acquired", + exception.getMessage()); + } + + @Test + public void testEccNodesWithNullSession() + { + EccNodesSync.Builder tmpEccNodesSyncBuilder = EccNodesSync.newBuilder() + .withSession(null) + .withInitialNodesList(nodesList); + NullPointerException exception = assertThrows( + NullPointerException.class, tmpEccNodesSyncBuilder::build); + assertEquals("Session cannot be null", exception.getMessage()); + } +} diff --git a/data/src/test/java/com/ericsson/bss/cassandra/ecchronos/data/utils/AbstractCassandraTest.java b/data/src/test/java/com/ericsson/bss/cassandra/ecchronos/data/utils/AbstractCassandraTest.java new file mode 100644 index 000000000..681a95426 --- /dev/null +++ b/data/src/test/java/com/ericsson/bss/cassandra/ecchronos/data/utils/AbstractCassandraTest.java @@ -0,0 +1,80 @@ +/* + * Copyright 2024 Telefonaktiebolaget LM Ericsson + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.ericsson.bss.cassandra.ecchronos.data.utils; + +import com.datastax.oss.driver.api.core.CqlSession; +import com.datastax.oss.driver.api.core.metadata.Node; +import com.ericsson.bss.cassandra.ecchronos.connection.DistributedNativeConnectionProvider; +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.testcontainers.containers.CassandraContainer; +import org.testcontainers.utility.DockerImageName; + +import java.util.ArrayList; +import java.util.List; + +public class AbstractCassandraTest +{ + private static final List> nodes = new ArrayList<>(); + protected static CqlSession mySession; + + private static DistributedNativeConnectionProvider myNativeConnectionProvider; + + @BeforeClass + public static void setUpCluster() + { + CassandraContainer node = new CassandraContainer<>(DockerImageName.parse("cassandra:4.1.5")) + .withExposedPorts(9042, 7000) + .withEnv("CASSANDRA_DC", "DC1") + .withEnv("CASSANDRA_ENDPOINT_SNITCH", "GossipingPropertyFileSnitch") + .withEnv("CASSANDRA_CLUSTER_NAME", "TestCluster"); + nodes.add(node); + node.start(); + mySession = CqlSession.builder() + .addContactPoint(node.getContactPoint()) + .withLocalDatacenter(node.getLocalDatacenter()) + .build(); + List nodesList = new ArrayList<>(mySession.getMetadata().getNodes().values()); + myNativeConnectionProvider = new DistributedNativeConnectionProvider() + { + @Override + public CqlSession getCqlSession() + { + return mySession; + } + + @Override + public List getNodes() + { + return nodesList; + } + }; + } + + @AfterClass + public static void tearDownCluster() + { + // Stop all nodes + for (CassandraContainer node : nodes) + { + node.stop(); + } + } + + public static DistributedNativeConnectionProvider getNativeConnectionProvider() + { + return myNativeConnectionProvider; + } +} diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md index 026d0af45..39ada764c 100644 --- a/docs/CONTRIBUTING.md +++ b/docs/CONTRIBUTING.md @@ -9,7 +9,7 @@ For keeping track of the history it is recommended that most communication is pe ### Prerequisites * Maven -* JDK11 +* JDK17 * Docker (for test setup) * Python @@ -38,7 +38,6 @@ If you encounter a PMD rule that seems odd or non-relevant feel free to discuss #### Built with * [Maven](https://maven.apache.org) - Dependency and build management -* [docker-maven-plugin](https://github.com/fabric8io/docker-maven-plugin) - For integration tests ### REST API diff --git a/docs/autogenerated/ECCHRONOS_SETTINGS.md b/docs/autogenerated/ECCHRONOS_SETTINGS.md deleted file mode 100644 index 0747339c0..000000000 --- a/docs/autogenerated/ECCHRONOS_SETTINGS.md +++ /dev/null @@ -1,330 +0,0 @@ -```yaml -### ecChronos configuration - -## Connection -## Properties for connection to the local node -## -connection: - cql: - ## - ## Host and port properties for CQL. - ## Primarily used by the default connection provider - ## - host: localhost - port: 9042 - ## - ## Connection Timeout for a CQL attempt. - ## Specify a time to wait for cassandra to come up. - ## Connection is tried based on retry policy delay calculations. Each connection attempt will use the timeout to calculate CQL connection process delay. - ## - timeout: - time: 60 - unit: seconds - retryPolicy: - ## Max number of attempts ecChronos will try to connect with Cassandra. - maxAttempts: 5 - ## Delay use to wait between an attempt and another, this value will be multiplied by the current attempt count powered by two. - ## If the current attempt is 4 and the default delay is 5 seconds, so ((4(attempt) x 2) x 5(default delay)) = 40 seconds. - ## If the calculated delay is greater than maxDelay, maxDelay will be used instead of the calculated delay. - delay: 5 - ## Maximum delay before the next connection attempt is made. - ## Setting it as 0 will disable maxDelay and the delay interval will - ## be calculated based on the attempt count and the default delay. - maxDelay: 30 - unit: seconds - ## - ## The class used to provide CQL connections to Apache Cassandra. - ## The default provider will be used unless another is specified. - ## - provider: com.ericsson.bss.cassandra.ecchronos.application.DefaultNativeConnectionProvider - ## - ## The class used to provide an SSL context to the NativeConnectionProvider. - ## Extending this allows to manipulate the SSLEngine and SSLParameters. - ## - certificateHandler: com.ericsson.bss.cassandra.ecchronos.application.ReloadingCertificateHandler - ## - ## The class used to decorate CQL statements. - ## The default no-op decorator will be used unless another is specified. - ## - decoratorClass: com.ericsson.bss.cassandra.ecchronos.application.NoopStatementDecorator - ## - ## Allow routing requests directly to a remote datacenter. - ## This allows locks for other datacenters to be taken in that datacenter instead of via the local datacenter. - ## If clients are prevented from connecting directly to Cassandra nodes in other sites this is not possible. - ## If remote routing is disabled, instead SERIAL consistency will be used for those request. - ## - remoteRouting: true - jmx: - ## - ## Host and port properties for JMX. - ## Primarily used by the default connection provider. - ## - host: localhost - port: 7199 - ## - ## The class used to provide JMX connections to Apache Cassandra. - ## The default provider will be used unless another is specified. - ## - provider: com.ericsson.bss.cassandra.ecchronos.application.DefaultJmxConnectionProvider - -## Repair configuration -## This section defines default repair behavior for all tables. -## -repair: - ## - ## A class for providing repair configuration for tables. - ## The default FileBasedRepairConfiguration uses a schedule.yml file to define per-table configurations. - ## - provider: com.ericsson.bss.cassandra.ecchronos.application.FileBasedRepairConfiguration - ## - ## How often repairs should be triggered for tables. - ## - interval: - time: 7 - unit: days - ## - ## Initial delay for new tables. New tables are always assumed to have been repaired in the past by the interval. - ## However, a delay can be set for the first repair. This will not affect subsequent repairs and defaults to one day. - ## - initial_delay: - time: 1 - unit: days - ## - ## The unit of time granularity for priority calculation, can be HOURS, MINUTES, or SECONDS. - ## This unit is used in the calculation of priority. - ## Default is HOURS for backward compatibility. - ## Ensure to pause repair operations prior to changing the granularity. - ## Not doing so may lead to inconsistencies as some ecchronos instances - ## could have different priorities compared to others for the same repair. - ## Possible values are HOURS, MINUTES, or SECONDS. - ## - priority: - granularity_unit: HOURS - ## - ## Specifies the type of lock to use for repairs. - ## "vnode" will lock each node involved in a repair individually and increase the number of - ## parallel repairs that can run in a single data center. - ## "datacenter" will lock each data center involved in a repair and only allow a single repair per data center. - ## "datacenter_and_vnode" will combine both options and allow a smooth transition between them without allowing - ## multiple repairs to run concurrently on a single node. - ## - lock_type: vnode - ## - ## Alarms are triggered when tables have not been repaired for a long amount of time. - ## The warning alarm is meant to indicate early that repairs are falling behind. - ## The error alarm is meant to indicate that gc_grace has passed between repairs. - ## - ## With the defaults where repairs triggers once every 7 days for each table a warning alarm would be raised - ## if the table has not been properly repaired within one full day. - ## - alarm: - ## - ## The class used for fault reporting - ## The default LoggingFaultReporter will log when alarm is raised/ceased - ## - faultReporter: com.ericsson.bss.cassandra.ecchronos.fm.impl.LoggingFaultReporter - ## - ## If a table has not been repaired for the following duration an warning alarm will be raised. - ## The schedule will be marked as late if the table has not been repaired within this interval. - ## - warn: - time: 8 - unit: days - ## - ## If a table has not been repaired for the following duration an error alarm will be raised. - ## The schedule will be marked as overdue if the table has not been repaired within this interval. - ## - error: - time: 10 - unit: days - ## - ## Specifies the unwind ratio to smooth out the load that repairs generate. - ## This value is a ratio between 0 -> 100% of the execution time of a repair session. - ## - ## 100% means that the executor will wait to run the next session for as long time as the previous session took. - ## The 'unwind_ratio' setting configures the wait time between repair tasks as a proportion of the previous task's execution time. - ## - ## Examples: - ## - unwind_ratio: 0 - ## Explanation: No wait time between tasks. The next task starts immediately after the previous one finishes. - ## Total Repair Time: T1 (10s) + T2 (20s) = 30 seconds. - ## - ## - unwind_ratio: 1.0 (100%) - ## Explanation: The wait time after each task equals its duration. - ## Total Repair Time: T1 (10s + 10s wait) + T2 (20s + 20s wait) = 60 seconds. - ## - ## - unwind_ratio: 0.5 (50%) - ## Explanation: The wait time is half of the task's duration. - ## Total Repair Time: T1 (10s + 5s wait) + T2 (20s + 10s wait) = 45 seconds. - ## - ## A higher 'unwind_ratio' reduces system load by adding longer waits, but increases total repair time. - ## A lower 'unwind_ratio' speeds up repairs but may increase system load. - ## - unwind_ratio: 0.0 - ## - ## Specifies the lookback time for when the repair_history table is queried to get initial repair state at startup. - ## The time should match the "expected TTL" of the system_distributed.repair_history table. - ## - history_lookback: - time: 30 - unit: days - ## - ## Specifies a target for how much data each repair session should process. - ## This is only supported if using 'vnode' as repair_type. - ## This is an estimation assuming uniform data distribution among partition keys. - ## The value should be either a number or a number with a unit of measurement: - ## 12 (12 B) - ## 12k (12 KiB) - ## 12m (12 MiB) - ## 12g (12 GiB) - ## - size_target: - ## - ## Specifies the repair history provider used to determine repair state. - ## The "cassandra" provider uses the repair history generated by the database. - ## The "upgrade" provider is an intermediate state reading history from "cassandra" and producing history for "ecc" - ## The "ecc" provider maintains and uses an internal repair history in a dedicated table. - ## The main context for the "ecc" provider is an environment where the ip address of nodes might change. - ## Possible values are "ecc", "upgrade" and "cassandra". - ## - ## The keyspace parameter is only used by "ecc" and "upgrade" and points to the keyspace where the custom - ## 'repair_history' table is located. - ## - history: - provider: ecc - keyspace: ecchronos - ## - ## Specifies if tables with TWCS (TimeWindowCompactionStrategy) should be ignored for repair - ## - ignore_twcs_tables: false - ## - ## Specifies the backoff time for a job. - ## This is the time that the job will wait before trying to run again after failing. - ## - backoff: - time: 30 - unit: MINUTES - ## - ## Specifies the default repair_type. - ## Possible values are: vnode, parallel_vnode, incremental - ## vnode = repair 1 vnode at a time (supports size_target to split the vnode further, in this case there will be 1 repair session per subrange) - ## parallel_vnode = repair vnodes in parallel, this will combine vnodes into a single repair session per repair group - ## incremental = repair vnodes incrementally (incremental repair) - ## - repair_type: vnode - -statistics: - enabled: true - ## - ## Decides how statistics should be exposed. - ## If all reporting is disabled, the statistics will be disabled as well. - ## - reporting: - jmx: - enabled: true - ## - ## The metrics can be excluded on name and on tag values using quoted regular expressions. - ## Exclusion on name should be done without the prefix. - ## If an exclusion is without tags, then metric matching the name will be excluded. - ## If both name and tags are specified, then the metric must match both to be excluded. - ## If multiple tags are specified, the metric must match all tags to be excluded. - ## By default, no metrics are excluded. - ## For list of available metrics and tags refer to the documentation. - ## - excludedMetrics: [] - file: - enabled: true - ## - ## The metrics can be excluded on name and on tag values using quoted regular expressions. - ## Exclusion on name should be done without the prefix. - ## If an exclusion is without tags, then metric matching the name will be excluded. - ## If both name and tags are specified, then the metric must match both to be excluded. - ## If multiple tags are specified, the metric must match all tags to be excluded. - ## By default, no metrics are excluded. - ## For list of available metrics and tags refer to the documentation. - ## - excludedMetrics: [] - http: - enabled: true - ## - ## The metrics can be excluded on name and on tag values using quoted regular expressions. - ## Exclusion on name should be done without the prefix. - ## If an exclusion is without tags, then metric matching the name will be excluded. - ## If both name and tags are specified, then the metric must match both to be excluded. - ## If multiple tags are specified, the metric must match all tags to be excluded. - ## By default, no metrics are excluded. - ## For list of available metrics and tags refer to the documentation. - ## - excludedMetrics: [] - directory: ./statistics - ## - ## Prefix all metrics with below string - ## The prefix cannot start or end with a dot or any other path separator. - ## - prefix: '' - ## - ## Number of repair failures before status logger logs metrics in debug logs - ## The number is used to trigger a status once number of failures is breached in a time window mentioned below - ## - repair_failures_count: 5 - ## - ## Time window over which to track repair failures in node for trigger status logger messages in debug log - ## - repair_failures_time_window: - time: 30 - unit: minutes - ## - ## Trigger interval for metric inspection. - ## This time should always be lesser than repair_failures_time_window - ## - trigger_interval_for_metric_inspection: - time: 5 - unit: seconds - -lock_factory: - cas: - ## - ## The keyspace used for the CAS lock factory tables. - ## - keyspace: ecchronos - ## - ## The number of seconds until the lock failure cache expires. - ## If an attempt to secure a lock is unsuccessful, - ## all subsequent attempts will be failed until - ## the cache expiration time is reached. - ## - cache_expiry_time_in_seconds: 30 - ## - ## Allow to override consistency level for LWT (lightweight transactions). Possible values are: - ## "DEFAULT" - Use consistency level based on remoteRouting. - ## "SERIAL" - Use SERIAL consistency for LWT regardless of remoteRouting. - ## "LOCAL" - Use LOCAL_SERIAL consistency for LWT regardless of remoteRouting. - ## - ## if you use remoteRouting: false and LOCAL then all locks will be taken locally - ## in DC. I.e There's a risk that multiple nodes in different datacenters will be able to lock the - ## same nodes causing multiple repairs on the same range/node at the same time. - ## - consistencySerial: "DEFAULT" - -run_policy: - time_based: - ## - ## The keyspace used for the time based run policy tables. - ## - keyspace: ecchronos - -scheduler: - ## - ## Specifies the frequency the scheduler checks for work to be done - ## - frequency: - time: 30 - unit: SECONDS - -rest_server: - ## - ## The host and port used for the HTTP server - ## - host: localhost - port: 8080 -``` diff --git a/docs/autogenerated/ECCTOOL.md b/docs/autogenerated/ECCTOOL.md deleted file mode 100644 index 6dd973488..000000000 --- a/docs/autogenerated/ECCTOOL.md +++ /dev/null @@ -1,238 +0,0 @@ -# ecctool - -ecctool is a command line utility which can be used to perform actions towards a local ecChronos instance. The actions are implemented in form of subcommands with arguments. All visualization is displayed in form of human-readable tables. - -```console -usage: ecctool [-h] - {repairs,schedules,run-repair,repair-info,start,stop,status} - ... -``` - - -### -h, --help -show this help message and exit - -## ecctool repair-info - -Get information about repairs for tables. The repair information is based on repair history, meaning that both manual repairs and schedules will contribute to the repair information. This subcommand requires the user to provide either –since or –duration if –keyspace and –table is not provided. If repair info is fetched for a specific table using –keyspace and –table, the duration will default to the table’s GC_GRACE_SECONDS. - -```console -usage: ecctool repair-info [-h] [-k KEYSPACE] [-t TABLE] [-s SINCE] - [-d DURATION] [--local] [-u URL] [-l LIMIT] -``` - - -### -h, --help -show this help message and exit - - -### -k <keyspace>, --keyspace <keyspace> -Show repair information for all tables in the specified keyspace. - - -### -t <table>, --table <table> -Show repair information for the specified table. Keyspace argument -k or –keyspace becomes mandatory if using this argument. - - -### -s <since>, --since <since> -Show repair information since the specified date to now. Date must be specified in ISO8601 format. The time-window will be since to now. Mandatory if –duration or –keyspace and –table is not specified. - - -### -d <duration>, --duration <duration> -Show repair information for the duration. Duration can be specified as ISO8601 format or as simple format in form: 5s, 5m, 5h, 5d. The time-window will be now-duration to now. Mandatory if –since or –keyspace and –table is not specified. - - -### --local -Show repair information only for the local node. - - -### -u <url>, --url <url> -The ecChronos host to connect to, specified in the format [http:/](http:/)/<host>:<port>. - - -### -l <limit>, --limit <limit> -Limits the number of rows printed in the output. Specified as a number, -1 to disable limit. - -## ecctool repairs - -Show the status of all manual repairs. This subcommand has no mandatory parameters. - -```console -usage: ecctool repairs [-h] [-k KEYSPACE] [-t TABLE] [-u URL] [-i ID] - [-l LIMIT] [--hostid HOSTID] -``` - - -### -h, --help -show this help message and exit - - -### -k <keyspace>, --keyspace <keyspace> -Show repairs for the specified keyspace. This argument is mutually exclusive with -i and –id. - - -### -t <table>, --table <table> -Show repairs for the specified table. Keyspace argument -k or –keyspace becomes mandatory if using this argument. This argument is mutually exclusive with -i and –id. - - -### -u <url>, --url <url> -The ecChronos host to connect to, specified in the format [http:/](http:/)/<host>:<port>. - - -### -i <id>, --id <id> -Show repairs matching the specified ID. This argument is mutually exclusive with -k, –keyspace, -t and –table. - - -### -l <limit>, --limit <limit> -Limits the number of rows printed in the output. Specified as a number, -1 to disable limit. - - -### --hostid <hostid> -Show repairs for the specified host id. The host id corresponds to the Cassandra instance ecChronos is connected to. - -## ecctool run-repair - -Run a manual repair. The manual repair will be triggered in ecChronos. EcChronos will perform repair through Cassandra JMX interface. This subcommand has no mandatory parameters. - -```console -usage: ecctool run-repair [-h] [-u URL] [--local] [-r REPAIR_TYPE] - [-k KEYSPACE] [-t TABLE] -``` - - -### -h, --help -show this help message and exit - - -### -u <url>, --url <url> -The ecChronos host to connect to, specified in the format [http:/](http:/)/<host>:<port>. - - -### --local -Run repair for the local node only, i.e repair will only be performed for the ranges that the local node is a replica for. - - -### -r <repair_type>, --repair_type <repair_type> -The type of the repair, possible values are ‘vnode’, ‘parallel_vnode’, ‘incremental’ - - -### -k <keyspace>, --keyspace <keyspace> -Run repair for the specified keyspace. Repair will be run for all tables within the keyspace with replication factor higher than 1. - - -### -t <table>, --table <table> -Run repair for the specified table. Keyspace argument -k or –keyspace becomes mandatory if using this argument. - -## ecctool schedules - -Show the status of schedules. This subcommand has no mandatory parameters. - -```console -usage: ecctool schedules [-h] [-k KEYSPACE] [-t TABLE] [-u URL] [-i ID] [-f] - [-l LIMIT] -``` - - -### -h, --help -show this help message and exit - - -### -k <keyspace>, --keyspace <keyspace> -Show schedules for the specified keyspace. This argument is mutually exclusive with -i and –id. - - -### -t <table>, --table <table> -Show schedules for the specified table. Keyspace argument -k or –keyspace becomes mandatory if using this argument. This argument is mutually exclusive with -i and –id. - - -### -u <url>, --url <url> -The ecChronos host to connect to, specified in the format [http:/](http:/)/<host>:<port>. - - -### -i <id>, --id <id> -Show schedules matching the specified ID. This argument is mutually exclusive with -k, –keyspace, -t and –table. - - -### -f, --full -Show full schedules, can only be used with -i or –id. Full schedules include schedule configuration and repair state per vnode. - - -### -l <limit>, --limit <limit> -Limits the number of rows printed in the output. Specified as a number, -1 to disable limit. - -## ecctool start - -Start the ecChronos service. This subcommand has no mandatory parameters. - -```console -usage: ecctool start [-h] [-f] [-p PIDFILE] -``` - - -### -h, --help -show this help message and exit - - -### -f, --foreground -Start the ecChronos instance in foreground mode (exec in current terminal and log to stdout) - - -### -p <pidfile>, --pidfile <pidfile> -Start the ecChronos instance and store the pid in the specified pid file. - -## ecctool status - -View status of ecChronos instance. This subcommand has no mandatory parameters. - -```console -usage: ecctool status [-h] [-u URL] -``` - - -### -h, --help -show this help message and exit - - -### -u <url>, --url <url> -The ecChronos host to connect to, specified in the format [http:/](http:/)/<host>:<port>. - -## ecctool stop - -Stop the ecChronos instance. Stopping of ecChronos is done by using kill with SIGTERM signal (same as kill in shell) for the pid. This subcommand has no mandatory parameters. - -```console -usage: ecctool stop [-h] [-p PIDFILE] -``` - - -### -h, --help -show this help message and exit - - -### -p <pidfile>, --pidfile <pidfile> -Stops the ecChronos instance by pid fetched from the specified pid file. - -# Examples - -For example usage and explanation about output refer to [ECCTOOL_EXAMPLES.md](../ECCTOOL_EXAMPLES.md) - -## ecctool running-job - -Show which (if any) job that is currently running. - -```console -usage: ecctool running-job [-h] [-u URL] - -Show which (if any) job is currently running - -optional arguments: - -h, --help show this help message and exit - -u URL, --url URL The ecChronos host to connect to, specified in the format http://:. -``` - -### -h, --help -show this help message and exit - - -### -u <url>, --url <url> -The ecChronos host to connect to, specified in the format [http:/](http:/)/<host>:<port>. diff --git a/docs/autogenerated/openapi.yaml b/docs/autogenerated/openapi.yaml deleted file mode 100644 index 26b1405b6..000000000 --- a/docs/autogenerated/openapi.yaml +++ /dev/null @@ -1,547 +0,0 @@ -openapi: 3.0.1 -info: - title: REST API - license: - name: Apache 2.0 - url: https://www.apache.org/licenses/LICENSE-2.0 - version: 1.0.0 -servers: -- url: https://localhost:8080 - description: Generated server url -tags: -- name: Repair-Management - description: Management of repairs -- name: Metrics - description: Retrieve metrics about ecChronos -- name: Actuator - description: Monitor and interact - externalDocs: - description: Spring Boot Actuator Web API Documentation - url: https://docs.spring.io/spring-boot/docs/current/actuator-api/html/ -paths: - /repair-management/v2/repairs: - get: - tags: - - Repair-Management - summary: Get manual repairs. - description: Get manual repairs which are running/completed/failed. - operationId: get-repairs - parameters: - - name: keyspace - in: query - description: "Only return repairs matching the keyspace, mandatory if 'table'\ - \ is provided." - required: false - schema: - type: string - - name: table - in: query - description: Only return repairs matching the table. - required: false - schema: - type: string - - name: hostId - in: query - description: Only return repairs matching the hostId. - required: false - schema: - type: string - responses: - "200": - description: OK - content: - application/json: - schema: - type: array - items: - $ref: '#/components/schemas/OnDemandRepair' - post: - tags: - - Repair-Management - summary: Run a manual repair. - description: "Run a manual repair, if 'isLocal' is not provided this will run\ - \ a cluster-wide repair." - operationId: run-repair - parameters: - - name: keyspace - in: query - description: "The keyspace to run repair for, mandatory if 'table' is provided." - required: false - schema: - type: string - - name: table - in: query - description: The table to run repair for. - required: false - schema: - type: string - - name: repairType - in: query - description: "The type of the repair, defaults to vnode." - required: false - schema: - type: string - enum: - - VNODE - - PARALLEL_VNODE - - INCREMENTAL - - name: isLocal - in: query - description: "Decides if the repair should be only for the local node, i.e\ - \ not cluster-wide." - required: false - schema: - type: boolean - responses: - "200": - description: OK - content: - application/json: - schema: - type: array - items: - $ref: '#/components/schemas/OnDemandRepair' - /repair-management/v2/schedules: - get: - tags: - - Repair-Management - summary: Get schedules - description: Get schedules - operationId: get-schedules - parameters: - - name: keyspace - in: query - description: "Filter schedules based on this keyspace, mandatory if 'table'\ - \ is provided." - required: false - schema: - type: string - - name: table - in: query - description: Filter schedules based on this table. - required: false - schema: - type: string - responses: - "200": - description: OK - content: - application/json: - schema: - type: array - items: - $ref: '#/components/schemas/Schedule' - /repair-management/v2/schedules/{id}: - get: - tags: - - Repair-Management - summary: Get schedules matching the id. - description: Get schedules matching the id. - operationId: get-schedules-by-id - parameters: - - name: id - in: path - description: The id of the schedule. - required: true - schema: - type: string - - name: full - in: query - description: Decides if a 'full schedule' should be returned. - required: false - schema: - type: boolean - responses: - "200": - description: OK - content: - application/json: - schema: - $ref: '#/components/schemas/Schedule' - /repair-management/v2/repairs/{id}: - get: - tags: - - Repair-Management - summary: Get manual repairs matching the id. - description: Get manual repairs matching the id which are running/completed/failed. - operationId: get-repairs-by-id - parameters: - - name: id - in: path - description: Only return repairs matching the id. - required: true - schema: - type: string - - name: hostId - in: query - description: Only return repairs matching the hostId. - required: false - schema: - type: string - responses: - "200": - description: OK - content: - application/json: - schema: - type: array - items: - $ref: '#/components/schemas/OnDemandRepair' - /repair-management/v2/repairInfo: - get: - tags: - - Repair-Management - summary: Get repair information - description: "Get repair information, if keyspace and table are provided while\ - \ duration and since are not, the duration will default to GC_GRACE_SECONDS\ - \ of the table. This operation might take time depending on the provided params\ - \ since it's based on the repair history." - operationId: get-repair-info - parameters: - - name: keyspace - in: query - description: "Only return repair-info matching the keyspace, mandatory if\ - \ 'table' is provided." - required: false - schema: - type: string - - name: table - in: query - description: Only return repair-info matching the table. - required: false - schema: - type: string - - name: since - in: query - description: "Since time, can be specified as ISO8601 date or as milliseconds\ - \ since epoch. Required if keyspace and table or duration is not specified." - required: false - schema: - type: string - - name: duration - in: query - description: "Duration, can be specified as either a simple duration like\ - \ '30s' or as ISO8601 duration 'pt30s'. Required if keyspace and table or\ - \ since is not specified." - required: false - schema: - type: string - - name: isLocal - in: query - description: Decides if the repair-info should be calculated for the local - node only. - required: false - schema: - type: boolean - responses: - "200": - description: OK - content: - application/json: - schema: - $ref: '#/components/schemas/RepairInfo' - /metrics: - get: - tags: - - Metrics - summary: Get metrics - description: Get metrics in the specified format - operationId: metrics - parameters: - - name: Accept - in: header - required: false - schema: - type: string - default: text/plain; version=0.0.4; charset=utf-8 - - name: "name[]" - in: query - description: Filter metrics based on these names. - required: false - schema: - uniqueItems: true - type: array - items: - type: string - default: [] - responses: - "200": - description: OK - content: - text/plain;version=0.0.4;charset=utf-8: - schema: - type: string - application/openmetrics-text;version=1.0.0;charset=utf-8: - schema: - type: string - /actuator: - get: - tags: - - Actuator - summary: Actuator root web endpoint - operationId: links - responses: - "200": - description: OK - content: - application/vnd.spring-boot.actuator.v3+json: - schema: - type: object - additionalProperties: - type: object - additionalProperties: - $ref: '#/components/schemas/Link' - application/vnd.spring-boot.actuator.v2+json: - schema: - type: object - additionalProperties: - type: object - additionalProperties: - $ref: '#/components/schemas/Link' - application/json: - schema: - type: object - additionalProperties: - type: object - additionalProperties: - $ref: '#/components/schemas/Link' - /actuator/health: - get: - tags: - - Actuator - summary: Actuator web endpoint 'health' - operationId: health - responses: - "200": - description: OK - content: - application/vnd.spring-boot.actuator.v3+json: - schema: - type: object - application/vnd.spring-boot.actuator.v2+json: - schema: - type: object - application/json: - schema: - type: object - /actuator/health/**: - get: - tags: - - Actuator - summary: Actuator web endpoint 'health-path' - operationId: health-path - responses: - "200": - description: OK - content: - application/vnd.spring-boot.actuator.v3+json: - schema: - type: object - application/vnd.spring-boot.actuator.v2+json: - schema: - type: object - application/json: - schema: - type: object -components: - schemas: - OnDemandRepair: - required: - - completedAt - - hostId - - id - - keyspace - - repairType - - repairedRatio - - status - - table - type: object - properties: - id: - type: string - format: uuid - hostId: - type: string - format: uuid - keyspace: - type: string - table: - type: string - status: - type: string - enum: - - COMPLETED - - IN_QUEUE - - WARNING - - ERROR - - BLOCKED - repairedRatio: - maximum: 1 - minimum: 0 - type: number - format: double - completedAt: - minimum: -1 - type: integer - format: int64 - repairType: - type: string - enum: - - VNODE - - PARALLEL_VNODE - - INCREMENTAL - Schedule: - required: - - config - - id - - keyspace - - lastRepairedAtInMs - - nextRepairInMs - - repairType - - repairedRatio - - status - - table - type: object - properties: - id: - type: string - format: uuid - keyspace: - type: string - table: - type: string - status: - type: string - enum: - - COMPLETED - - ON_TIME - - LATE - - OVERDUE - - BLOCKED - repairedRatio: - maximum: 1 - minimum: 0 - type: number - format: double - lastRepairedAtInMs: - type: integer - format: int64 - nextRepairInMs: - type: integer - format: int64 - config: - $ref: '#/components/schemas/ScheduleConfig' - repairType: - type: string - enum: - - VNODE - - PARALLEL_VNODE - - INCREMENTAL - virtualNodeStates: - type: array - items: - $ref: '#/components/schemas/VirtualNodeState' - ScheduleConfig: - required: - - errorTimeInMs - - intervalInMs - - parallelism - - unwindRatio - - warningTimeInMs - type: object - properties: - intervalInMs: - minimum: 0 - type: integer - format: int64 - unwindRatio: - minimum: 0 - type: number - format: double - warningTimeInMs: - minimum: 0 - type: integer - format: int64 - errorTimeInMs: - minimum: 0 - type: integer - format: int64 - parallelism: - type: string - enum: - - PARALLEL - VirtualNodeState: - required: - - endToken - - lastRepairedAtInMs - - repaired - - replicas - - startToken - type: object - properties: - startToken: - minimum: -9223372036854775808 - type: integer - format: int64 - endToken: - maximum: 9223372036854775807 - type: integer - format: int64 - replicas: - uniqueItems: true - type: array - items: - type: string - lastRepairedAtInMs: - minimum: 0 - type: integer - format: int64 - repaired: - type: boolean - RepairInfo: - required: - - repairStats - - sinceInMs - - toInMs - type: object - properties: - sinceInMs: - minimum: 0 - type: integer - format: int64 - toInMs: - minimum: 0 - type: integer - format: int64 - repairStats: - type: array - items: - $ref: '#/components/schemas/RepairStats' - RepairStats: - required: - - keyspace - - repairTimeTakenMs - - repairedRatio - - table - type: object - properties: - keyspace: - type: string - table: - type: string - repairedRatio: - maximum: 1 - minimum: 0 - type: number - format: double - repairTimeTakenMs: - minimum: 0 - type: integer - format: int64 - Link: - type: object - properties: - href: - type: string - templated: - type: boolean diff --git a/pmd-rules.xml b/pmd-rules.xml index 5858cf92b..6485e7866 100644 --- a/pmd-rules.xml +++ b/pmd-rules.xml @@ -1,7 +1,7 @@ - - - - + + + + + + + + + + - - - - - + + + - + diff --git a/pom.xml b/pom.xml new file mode 100644 index 000000000..af7b56c06 --- /dev/null +++ b/pom.xml @@ -0,0 +1,661 @@ + + + + 4.0.0 + + com.ericsson.bss.cassandra.ecchronos + agent + 1.0.0-SNAPSHOT + pom + + Ericsson Cassandra Chronos Agent + A distributed agent repair scheduler for Apache Cassandra. + + + + Apache License, Version 2.0 + https://www.apache.org/licenses/LICENSE-2.0.txt + + + + + + Victor Cavichioli + victor.cavichioli@ericsson.com + Ericsson AB + http://www.ericsson.com + + + + + Ericsson AB + http://www.ericsson.com + + + + + ossrh + https://oss.sonatype.org/content/repositories/snapshots/ + + + ossrh + https://oss.sonatype.org/service/local/staging/deploy/maven2/ + + + + + https://github.com/ericsson/ecchronos + scm:git:git@github.com:ericsson/ecchronos.git + scm:git:git@github.com:ericsson/ecchronos.git + HEAD + + + + + connection + connection.impl + application + data + + + + + 17 + 17 + 17 + UTF-8 + + + 3.3.2 + 10.1.26 + 1.8.0 + 6.1.11 + + + 4.17.0 + 32.0.1-jre + 1.5.0 + 1.4.2 + 25.1-jre + 3.1.8 + 1.0.1 + 2.1.12 + 2.0.13 + 1.5.6 + + + 2.17.2 + 2.17.2 + 1.33 + + + 5.12.0 + 3.26.3 + 5.7.0 + 2.16.1 + 3.16.1 + 1.0 + 1.18.3 + + + 3.8.0 + + 2.5.2 + + 2.22.1 + + 2.22.1 + + 3.6.0 + + 3.2.0 + + 3.24.0 + 3.0.0 + 3.1.0 + + 3.5.0 + + 3.4.2 + 3.4.1 + 5.1.8 + 3.0 + 1.2 + + 4.2.1 + 0.8.4 + 3.0.0 + 1.19 + + 2.5.3 + + 2.8.2 + + 3.0.1 + + 3.0.1 + + 1.6 + + false + ${skipTests} + + + + + + + org.apache.tomcat.embed + tomcat-embed-core + ${tomcat.version} + + + + org.apache.tomcat.embed + tomcat-embed-el + ${tomcat.version} + + + + org.apache.tomcat.embed + tomcat-embed-websocket + ${tomcat.version} + + + + org.springframework.boot + spring-boot-dependencies + ${org.springframework.boot.version} + pom + import + + + + org.springdoc + springdoc-openapi-ui + ${org.springdoc.openapi-ui.version} + + + + org.springframework + spring-web + ${org.springframework.web.version} + + + + + com.datastax.oss + java-driver-core + ${cassandra.driver.core.version} + + + com.github.jnr + jnr-ffi + + + com.github.jnr + jnr-posix + + + com.github.stephenc.jcip + jcip-annotations + + + com.github.spotbugs + spotbugs-annotations + + + io.dropwizard.metrics + metrics-core + + + org.hdrhistogram + HdrHistogram + + + + + com.datastax.oss + java-driver-metrics-micrometer + ${cassandra.driver.core.version} + + + com.datastax.oss + java-driver-query-builder + ${cassandra.driver.core.version} + + + com.github.stephenc.jcip + jcip-annotations + + + com.github.spotbugs + spotbugs-annotations + + + + + + com.github.ben-manes.caffeine + caffeine + ${caffeine.version} + + + com.google.guava + guava + ${guava.version} + + + com.google.guava + failureaccess + ${failureaccess.version} + + + + com.datastax.oss + native-protocol + ${cassandra.driver.protocol.version} + + + + com.datastax.oss + java-driver-shaded-guava + ${cassandra.driver.shaded.guava.version} + + + + com.typesafe + config + ${com.typesafe.config.version} + + + + org.hdrhistogram + HdrHistogram + ${org.hdrhistogram.version} + + + + org.slf4j + slf4j-api + ${slf4j.version} + + + + ch.qos.logback + logback-classic + ${logback.version} + + + + ch.qos.logback + logback-core + ${logback.version} + + + + + com.fasterxml.jackson.core + jackson-core + ${com.fasterxml.jackson.core.version} + + + + com.fasterxml.jackson.core + jackson-annotations + ${com.fasterxml.jackson.core.version} + + + + com.fasterxml.jackson.core + jackson-databind + ${com.fasterxml.jackson.core.version} + + + + com.fasterxml.jackson.dataformat + jackson-dataformat-yaml + ${com.fasterxml.jackson.dataformat.version} + + + + org.yaml + snakeyaml + ${org.yaml.snakeyaml.version} + + + + + org.mockito + mockito-core + ${mockito.core.version} + test + + + + org.assertj + assertj-core + ${assertj.version} + test + + + + org.junit + junit-bom + ${junit.version} + pom + import + + + + commons-io + commons-io + ${org.apache.commons.io.version} + test + + + + nl.jqno.equalsverifier + equalsverifier + ${equalsverifier.version} + test + + + + net.jcip + jcip-annotations + ${jcip.version} + test + + + + org.testcontainers + cassandra + ${testcontainers.version} + test + + + + + + + + + maven-pmd-plugin + ${org.apache.maven.plugins-maven-pmd-plugin.version} + + + pmd-rules.xml + + true + + + + + org.apache.maven.plugins + maven-shade-plugin + ${org.apache.maven.plugins-maven-shade-plugin.version} + + + + org.codehaus.mojo + license-maven-plugin + ${org.codehaus.mojo.license-maven-plugin.version} + + + + maven-surefire-plugin + ${org.apache.maven.plugins.maven-surefire-plugin.version} + + ${skipUTs} + + -Djdk.attach.allowAttachSelf=true + --add-exports java.base/jdk.internal.misc=ALL-UNNAMED + --add-exports java.base/jdk.internal.ref=ALL-UNNAMED + --add-exports java.base/sun.nio.ch=ALL-UNNAMED + --add-exports java.management.rmi/com.sun.jmx.remote.internal.rmi=ALL-UNNAMED + --add-exports java.rmi/sun.rmi.registry=ALL-UNNAMED + --add-exports java.rmi/sun.rmi.server=ALL-UNNAMED + --add-exports java.sql/java.sql=ALL-UNNAMED + --add-opens java.base/java.lang.module=ALL-UNNAMED + --add-opens java.base/jdk.internal.loader=ALL-UNNAMED + --add-opens java.base/jdk.internal.ref=ALL-UNNAMED + --add-opens java.base/jdk.internal.reflect=ALL-UNNAMED + --add-opens java.base/jdk.internal.math=ALL-UNNAMED + --add-opens java.base/jdk.internal.module=ALL-UNNAMED + --add-opens java.base/jdk.internal.util.jar=ALL-UNNAMED + --add-opens jdk.management/com.sun.management.internal=ALL-UNNAMED + --add-opens=java.base/java.io=ALL-UNNAMED + --add-opens=java.base/sun.nio.ch=ALL-UNNAMED + --add-opens=java.base/java.util.concurrent=ALL-UNNAMED + --add-opens=java.base/java.util=ALL-UNNAMED + --add-opens=java.base/java.util.concurrent.atomic=ALL-UNNAMED + --add-opens=java.base/java.lang=ALL-UNNAMED + --add-opens java.base/java.nio=ALL-UNNAMED + + + + + + maven-failsafe-plugin + ${org.apache.maven.plugins.maven-failsafe-plugin.version} + + + + maven-compiler-plugin + ${org.apache.maven.plugins.maven-compiler-plugin.version} + + ${maven.compiler.source} + ${maven.compiler.target} + + + + + maven-install-plugin + ${org.apache.maven.plugins.maven-install-plugin.version} + + + + maven-assembly-plugin + ${org.apache.maven.plugins.maven-assembly-plugin.version} + + true + + + + + org.apache.karaf.tooling + karaf-maven-plugin + ${org.apache.karaf.tooling.karaf-maven-plugin.version} + + + + org.apache.felix + maven-bundle-plugin + ${org.apache.felix.maven-bundle-plugin.version} + + + + org.springframework.boot + spring-boot-maven-plugin + + + + org.apache.servicemix.tooling + depends-maven-plugin + ${org.apache.servicemix.tooling.depends-maven-plugin.version} + + + + com.mycila + license-maven-plugin + ${com.mycila.license-maven-plugin.version} + + + + + SLASHSTAR_STYLE + + true + true + + + YEAR + 2019 + [0-9-]+ + + + + CODEOWNERS + code_style.xml + check_style.xml + .github/workflows/actions.yml + .github/workflows/scorecard.yml + + + + + + maven-release-plugin + ${org.apache.maven.plugins.maven-release-plugin.version} + + false + release + deploy + + + + + maven-deploy-plugin + ${org.apache.maven.plugins.maven-deploy-plugin.version} + + + + maven-source-plugin + ${org.apache.maven.plugins.maven-source-plugin.version} + + + + maven-javadoc-plugin + ${org.apache.maven.plugins.maven-javadoc-plugin.version} + + + com.ericsson.bss.cassandra.ecchronos.application + + + + + + maven-gpg-plugin + ${org.apache.maven.plugins.maven-gpg-plugin.version} + + + + maven-dependency-plugin + ${org.apache.maven.plugins-maven-dependency-plugin.version} + + + + maven-resources-plugin + ${org.apache.maven.plugins-maven-resources-plugin.version} + + + + org.codehaus.mojo + exec-maven-plugin + ${org.codehaus.mojo.exec-maven-plugin.version} + + + + org.apache.maven.plugins + maven-jar-plugin + ${org.apache.maven.plugins-maven-jar-plugin.version} + + + + + + + org.apache.felix + maven-bundle-plugin + true + + + org.apache.maven.plugins + maven-checkstyle-plugin + ${org.apache.maven.plugins-maven-checkstyle-plugin.version} + + UTF-8 + UTF-8 + true + check_style.xml + true + + + + + check + + + + + + org.jacoco + jacoco-maven-plugin + ${org.jacoco.jacoco-maven-plugin.version} + + + prepare-agent + + prepare-agent + + + + + + + maven-compiler-plugin + + + + + + + + org.apache.maven.plugins + maven-jxr-plugin + ${org.apache.maven.plugin.maven-jxr-plugin.version} + + + + \ No newline at end of file From fe811031d46405033910d4ee1fb313b23d4fdbe6 Mon Sep 17 00:00:00 2001 From: VictorCavichioli Date: Wed, 14 Aug 2024 10:59:28 -0300 Subject: [PATCH 2/6] Improve JMXConnection Exception Handler and DatacenterAwarePolicy --- application/pom.xml | 11 +- .../connection/AgentConnectionConfig.java | 26 ++ application/src/main/resources/ecc.yml | 7 +- .../application/config/TestConfig.java | 8 +- .../config/TestConfigRefresher.java | 115 ++++++ .../config/security/CertUtils.java | 345 ++++++++++++++++++ .../security/TestReloadingAuthProvider.java | 135 +++++++ .../TestReloadingCertificateHandler.java | 183 ++++++++++ .../config/security/TestSecurity.java | 144 ++++++++ application/src/test/resources/all_set.yml | 1 + .../security/enabled_keystore_jmxandcql.yml | 45 +++ .../security/enabled_nokeystore_nopem_cql.yml | 25 ++ .../security/enabled_nokeystore_nopem_jmx.yml | 24 ++ .../resources/security/enabled_pem_cql.yml | 28 ++ .../resources/security/enabled_pem_jmx.yml | 28 ++ .../impl/builders/DistributedJmxBuilder.java | 44 ++- .../builders/DistributedNativeBuilder.java | 27 +- .../connection/TestDataCenterAwarePolicy.java | 322 ++++++++++++++++ .../TestDataCenterAwareStatement.java | 32 ++ 19 files changed, 1523 insertions(+), 27 deletions(-) create mode 100644 application/src/test/java/com/ericsson/bss/cassandra/ecchronos/application/config/TestConfigRefresher.java create mode 100644 application/src/test/java/com/ericsson/bss/cassandra/ecchronos/application/config/security/CertUtils.java create mode 100644 application/src/test/java/com/ericsson/bss/cassandra/ecchronos/application/config/security/TestReloadingAuthProvider.java create mode 100644 application/src/test/java/com/ericsson/bss/cassandra/ecchronos/application/config/security/TestReloadingCertificateHandler.java create mode 100644 application/src/test/java/com/ericsson/bss/cassandra/ecchronos/application/config/security/TestSecurity.java create mode 100644 application/src/test/resources/security/enabled_keystore_jmxandcql.yml create mode 100644 application/src/test/resources/security/enabled_nokeystore_nopem_cql.yml create mode 100644 application/src/test/resources/security/enabled_nokeystore_nopem_jmx.yml create mode 100644 application/src/test/resources/security/enabled_pem_cql.yml create mode 100644 application/src/test/resources/security/enabled_pem_jmx.yml create mode 100644 connection/src/test/java/com/ericsson/bss/cassandra/ecchronos/connection/TestDataCenterAwarePolicy.java create mode 100644 connection/src/test/java/com/ericsson/bss/cassandra/ecchronos/connection/TestDataCenterAwareStatement.java diff --git a/application/pom.xml b/application/pom.xml index f5a8f7735..175ba585d 100644 --- a/application/pom.xml +++ b/application/pom.xml @@ -99,11 +99,6 @@ jackson-databind - - org.yaml - snakeyaml - - org.junit.vintage @@ -151,6 +146,12 @@ junit-jupiter test + + org.bouncycastle + bcpkix-jdk15on + 1.64 + test + \ No newline at end of file diff --git a/application/src/main/java/com/ericsson/bss/cassandra/ecchronos/application/config/connection/AgentConnectionConfig.java b/application/src/main/java/com/ericsson/bss/cassandra/ecchronos/application/config/connection/AgentConnectionConfig.java index 981317ae6..3be179727 100644 --- a/application/src/main/java/com/ericsson/bss/cassandra/ecchronos/application/config/connection/AgentConnectionConfig.java +++ b/application/src/main/java/com/ericsson/bss/cassandra/ecchronos/application/config/connection/AgentConnectionConfig.java @@ -19,7 +19,9 @@ import java.util.Map; import java.util.stream.Collectors; +import com.datastax.oss.driver.internal.core.loadbalancing.DefaultLoadBalancingPolicy; import com.ericsson.bss.cassandra.ecchronos.application.exceptions.ConfigurationException; +import com.ericsson.bss.cassandra.ecchronos.connection.DataCenterAwarePolicy; import com.fasterxml.jackson.annotation.JsonProperty; /** @@ -33,6 +35,7 @@ public final class AgentConnectionConfig private DatacenterAware myDatacenterAware = new DatacenterAware(); private RackAware myRackAware = new RackAware(); private HostAware myHostAware = new HostAware(); + private Class myDatacenterAwarePolicy = DataCenterAwarePolicy.class; /** * Default constructor for AgentConnectionConfig. @@ -102,6 +105,29 @@ public String getLocalDatacenter() return myLocalDatacenter; } + /** + * Gets the DataCenterAwarePolicy used for load-balancing policy. + * + * @return the DataCenterAwarePolicy. + */ + @JsonProperty("datacenterAwarePolicy") + public final Class getDatacenterAwarePolicy() + { + return myDatacenterAwarePolicy; + } + + /** + * Sets the DataCenterAwarePolicy used for load-balancing policy. + * + * @param datacenterAwarePolicy + * the DataCenterAwarePolicy to set. + */ + @JsonProperty("datacenterAwarePolicy") + public final void setDatacenterAwarePolicy(final Class datacenterAwarePolicy) throws NoSuchMethodException + { + myDatacenterAwarePolicy = datacenterAwarePolicy; + } + /** * Gets the contact points. * diff --git a/application/src/main/resources/ecc.yml b/application/src/main/resources/ecc.yml index beb9077d8..34a0f9701 100644 --- a/application/src/main/resources/ecc.yml +++ b/application/src/main/resources/ecc.yml @@ -32,6 +32,10 @@ connection: ## Specifies the datacenter that is considered "local" by the load balancing policy, ## The specified datacenter should match with the contact point datacenter localDatacenter: datacenter1 + ## A custom load balancing policy extended to allow the local + ## datacenter to be replaced with a specified data center when + ## creating a new plan, this is used just if the agent.type is datacenterAware + datacenterAwarePolicy: com.ericsson.bss.cassandra.ecchronos.connection.DataCenterAwarePolicy ## Initial contact points list for ecChronos ## to establish first connection with Cassandra. contactPoints: @@ -50,7 +54,8 @@ connection: ## Configuration to define racks for ecchronos ## to connect to, rackAware enable means that ## ecChronos will be responsible for all nodes in the - ## rack list. + ## rack list, this configuration is designed to manage + ## racks on the same datacenter. rackAware: racks: - datacenterName: datacenter1 diff --git a/application/src/test/java/com/ericsson/bss/cassandra/ecchronos/application/config/TestConfig.java b/application/src/test/java/com/ericsson/bss/cassandra/ecchronos/application/config/TestConfig.java index 95fb42db2..f299ebe48 100644 --- a/application/src/test/java/com/ericsson/bss/cassandra/ecchronos/application/config/TestConfig.java +++ b/application/src/test/java/com/ericsson/bss/cassandra/ecchronos/application/config/TestConfig.java @@ -19,7 +19,7 @@ import com.ericsson.bss.cassandra.ecchronos.application.config.connection.DistributedNativeConnection; import com.ericsson.bss.cassandra.ecchronos.application.exceptions.ConfigurationException; import com.ericsson.bss.cassandra.ecchronos.application.providers.AgentNativeConnectionProvider; - +import com.ericsson.bss.cassandra.ecchronos.connection.DataCenterAwarePolicy; import com.fasterxml.jackson.core.exc.StreamReadException; import com.fasterxml.jackson.databind.DatabindException; import com.fasterxml.jackson.databind.ObjectMapper; @@ -150,4 +150,10 @@ public void testRestServerConfig() assertThat(config.getRestServer().getHost()).isEqualTo("127.0.0.2"); assertThat(config.getRestServer().getPort()).isEqualTo(8081); } + + @Test + public void testDefaultLoadBalancingPolicy() + { + assertThat(nativeConnection.getAgentConnectionConfig().getDatacenterAwarePolicy()).isEqualTo(DataCenterAwarePolicy.class); + } } \ No newline at end of file diff --git a/application/src/test/java/com/ericsson/bss/cassandra/ecchronos/application/config/TestConfigRefresher.java b/application/src/test/java/com/ericsson/bss/cassandra/ecchronos/application/config/TestConfigRefresher.java new file mode 100644 index 000000000..e7077792a --- /dev/null +++ b/application/src/test/java/com/ericsson/bss/cassandra/ecchronos/application/config/TestConfigRefresher.java @@ -0,0 +1,115 @@ +/* + * Copyright 2024 Telefonaktiebolaget LM Ericsson + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.ericsson.bss.cassandra.ecchronos.application.config; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; + +import java.io.*; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +public class TestConfigRefresher +{ + @Rule + public TemporaryFolder temporaryFolder = new TemporaryFolder(); + + @Test + public void testGetFileContent() throws Exception + { + File file = temporaryFolder.newFile(); + + try (ConfigRefresher configRefresher = new ConfigRefresher(temporaryFolder.getRoot().toPath())) + { + AtomicReference reference = new AtomicReference<>(readFileContent(file)); + + configRefresher.watch(file.toPath(), () -> reference.set(readFileContent(file))); + + writeToFile(file, "some content"); + await().atMost(1, TimeUnit.SECONDS).until(() -> "some content".equals(reference.get())); + + writeToFile(file, "some new content"); + await().atMost(1, TimeUnit.SECONDS).until(() -> "some new content".equals(reference.get())); + } + } + + @Test + public void testRunnableThrowsAtFirstUpdate() throws Exception + { + File file = temporaryFolder.newFile(); + + AtomicBoolean shouldThrow = new AtomicBoolean(true); + + try (ConfigRefresher configRefresher = new ConfigRefresher(temporaryFolder.getRoot().toPath())) + { + AtomicReference reference = new AtomicReference<>(readFileContent(file)); + + configRefresher.watch(file.toPath(), () -> { + if (shouldThrow.get()) + { + throw new NullPointerException(); + } + + reference.set(readFileContent(file)); + }); + + writeToFile(file, "some content"); + + Thread.sleep(100); + + assertThat(reference).hasValue(""); + + shouldThrow.set(false); + + writeToFile(file, "some new content"); + await().atMost(1, TimeUnit.SECONDS).until(() -> "some new content".equals(reference.get())); + } + } + + private void writeToFile(File file, String content) throws IOException + { + try (FileWriter fileWriter = new FileWriter(file)) + { + fileWriter.write(content); + } + } + + private String readFileContent(File file) + { + try (FileReader fileReader = new FileReader(file); + BufferedReader bufferedReader = new BufferedReader(fileReader)) + { + StringBuilder result = new StringBuilder(); + String line; + while ((line = bufferedReader.readLine()) != null) + { + result.append(line); + } + return result.toString(); + } + catch (IOException e) + { + e.printStackTrace(); + } + + return null; + } +} + diff --git a/application/src/test/java/com/ericsson/bss/cassandra/ecchronos/application/config/security/CertUtils.java b/application/src/test/java/com/ericsson/bss/cassandra/ecchronos/application/config/security/CertUtils.java new file mode 100644 index 000000000..74081e8ae --- /dev/null +++ b/application/src/test/java/com/ericsson/bss/cassandra/ecchronos/application/config/security/CertUtils.java @@ -0,0 +1,345 @@ +/* + * Copyright 2024 Telefonaktiebolaget LM Ericsson + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.ericsson.bss.cassandra.ecchronos.application.config.security; + +import org.bouncycastle.asn1.oiw.OIWObjectIdentifiers; +import org.bouncycastle.asn1.pkcs.PrivateKeyInfo; +import org.bouncycastle.asn1.x500.X500Name; +import org.bouncycastle.asn1.x509.AlgorithmIdentifier; +import org.bouncycastle.asn1.x509.AuthorityKeyIdentifier; +import org.bouncycastle.asn1.x509.BasicConstraints; +import org.bouncycastle.asn1.x509.Extension; +import org.bouncycastle.asn1.x509.SubjectKeyIdentifier; +import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo; +import org.bouncycastle.cert.CertIOException; +import org.bouncycastle.cert.X509ExtensionUtils; +import org.bouncycastle.cert.X509v3CertificateBuilder; +import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter; +import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.bouncycastle.openssl.PEMParser; +import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter; +import org.bouncycastle.operator.ContentSigner; +import org.bouncycastle.operator.DigestCalculator; +import org.bouncycastle.operator.OperatorCreationException; +import org.bouncycastle.operator.bc.BcDigestCalculatorProvider; +import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.math.BigInteger; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.Security; +import java.security.cert.Certificate; +import java.security.cert.CertificateEncodingException; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; +import java.time.Instant; +import java.util.Base64; +import java.util.Date; + +public class CertUtils +{ + public static final String RSA_ALGORITHM_NAME = "RSA"; + public static final String EC_ALGORITHM_NAME = "ECDSA"; + //When changing keysize make sure you know what you are doing. + //Too big keysize will slow keypair generation by ALOT. + //2048 for RSA is not secure enough in real world, but since this is only for tests it's perfectly fine. + private static final int RSA_KEY_SIZE = 2048; + private static final int EC_KEY_SIZE = 384; + private static final int PEM_ENCODED_LINE_LENGTH = 64; + private static final String DEFAULT_CA_ALIAS = "cacert"; + private static final String DEFAULT_CERT_ALIAS = "cert"; + private static final String RSA_HASH_ALGORITHM = "SHA256WithRSA"; + private static final String EC_HASH_ALGORITHM = "SHA256withECDSA"; + private static final String BEGIN_CERTIFICATE = "-----BEGIN CERTIFICATE-----\n"; + private static final String END_CERTIFICATE = "\n-----END CERTIFICATE-----\n"; + private static final String BEGIN_PRIVATE_KEY = "-----BEGIN PRIVATE KEY-----\n"; + private static final String END_PRIVATE_KEY = "\n-----END PRIVATE KEY-----\n"; + + public CertUtils() + { + Security.addProvider(new org.bouncycastle.jce.provider.BouncyCastleProvider()); + } + + /** + * Create certificate signed by the provided CA. + * + * @param commonName The common name to use. + * @param notBefore The notBefore to use for certificate (from). + * @param notAfter The notAfter to use for certificate (to). + * @param caCertificateFile Path to the CA certificate file. + * @param caCertificatePrivateKeyFile Path to the CA certificate private key file. + * @param certificateOutputFile The output file for the certificate in PEM format. + * @param privateKeyOutputFile The output file for the certificate private key in PEM format. + */ + public void createCertificate(String commonName, Date notBefore, Date notAfter, String caCertificateFile, + String caCertificatePrivateKeyFile, String certificateOutputFile, String privateKeyOutputFile) + { + try + { + PrivateKey caPrivateKey = getPrivateKey(Paths.get(caCertificatePrivateKeyFile).toFile()); + String caAlgorithm = caPrivateKey.getAlgorithm(); + + KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance(caAlgorithm); + keyPairGenerator.initialize(getKeySize(caAlgorithm)); + KeyPair keyPair = keyPairGenerator.generateKeyPair(); + + X509Certificate caCertificate = getCertificate(Paths.get(caCertificateFile).toFile()); + PublicKey caPublicKey = caCertificate.getPublicKey(); + X509Certificate certificate = generate(getHashAlgorithm(caAlgorithm), keyPair.getPublic(), caPrivateKey, + caPublicKey, commonName, caCertificate.getSubjectDN().getName().replace("CN=", ""), notBefore, + notAfter, false); + + storeCertificate(certificate, Paths.get(certificateOutputFile)); + storePrivateKey(keyPair.getPrivate(), Paths.get(privateKeyOutputFile)); + } + catch (Exception e) + { + throw new IllegalStateException(e); + } + } + + /** + * Create a keystore file using the provided certificate. + * + * @param certFile Path to the certificate file in PEM format. + * @param certPrivateKey Path to the certificate private key file in PEM format. + * @param keyStoreType The keystore type to use for the keystore (PKCS12 or JKS). + * @param keyStorePassword The keystore password to use for the keystore. + * @param outputFile The output file where the keystore will be stored. + */ + public void createKeyStore(String certFile, String certPrivateKey, String keyStoreType, String keyStorePassword, + String outputFile) + { + try + { + X509Certificate certificate = getCertificate(Paths.get(certFile).toFile()); + PrivateKey privateKey = getPrivateKey(Paths.get(certPrivateKey).toFile()); + storeKeyStore(certificate, privateKey, keyStoreType, keyStorePassword, Paths.get(outputFile)); + } + catch (Exception e) + { + throw new IllegalStateException(e); + } + } + + private PrivateKey getPrivateKey(File file) throws IOException + { + try(InputStream in = new FileInputStream(file)) + { + PEMParser pemParser = new PEMParser(new InputStreamReader(in)); + Object object = pemParser.readObject(); + JcaPEMKeyConverter converter = new JcaPEMKeyConverter().setProvider("BC"); + PrivateKeyInfo caPrivateKeyInfo = (PrivateKeyInfo) object; + return converter.getPrivateKey(caPrivateKeyInfo); + } + } + + private void storeKeyStore(X509Certificate certificate, PrivateKey privateKey, String keyStoreType, + String keyStorePassword, Path file) + throws KeyStoreException, IOException, CertificateException, NoSuchAlgorithmException + { + KeyStore keyStore = KeyStore.getInstance(keyStoreType); + keyStore.load(null, null); + keyStore.setKeyEntry(DEFAULT_CERT_ALIAS, privateKey, keyStorePassword.toCharArray(), + new Certificate[] { certificate }); + try(OutputStream out = new FileOutputStream(file.toFile())) + { + keyStore.store(out, keyStorePassword.toCharArray()); + } + } + + /** + * Create a truststore file using the provided certificate. + * + * @param certFile Path to the certificate/CA certificate file in PEM format. + * @param trustStoreType The truststore type to use for the keystore (PKCS12 or JKS). + * @param trustStorePassword The truststore password to use for the keystore. + * @param outputFile The output file where the truststore will be stored. + */ + public void createTrustStore(String certFile, String trustStoreType, String trustStorePassword, String outputFile) + { + try + { + X509Certificate certificate = getCertificate(Paths.get(certFile).toFile()); + storeTrustStore(certificate, trustStoreType, trustStorePassword, Paths.get(outputFile)); + } + catch (Exception e) + { + throw new IllegalStateException(e); + } + } + + private X509Certificate getCertificate(File file) throws IOException, CertificateException + { + try(InputStream in = new FileInputStream(file)) + { + CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509"); + X509Certificate certificate = (X509Certificate) certificateFactory.generateCertificate(in); + return certificate; + } + } + + private void storeTrustStore(X509Certificate certificate, String trustStoreType, String trustStorePassword, + Path file) throws KeyStoreException, IOException, CertificateException, NoSuchAlgorithmException + { + KeyStore trustStore = KeyStore.getInstance(trustStoreType); + trustStore.load(null, null); + trustStore.setCertificateEntry(DEFAULT_CA_ALIAS, certificate); + try(OutputStream out = new FileOutputStream(file.toFile())) + { + trustStore.store(out, trustStorePassword.toCharArray()); + } + } + + /** + * Create a self-signed CA certificate. + * + * @param commonName The common name to use. + * @param notBefore The notBefore to use for certificate (from). + * @param notAfter The notAfter to use for certificate (to). + * @param algorithm The algorithm to use, supported algorithms: 'RSA' or 'EC'. + * @param certificateOutputFile The output file for the certificate in PEM format. + * @param privateKeyOutputFile The output file for the certificate private key in PEM format. + */ + public void createSelfSignedCACertificate(String commonName, Date notBefore, Date notAfter, String algorithm, + String certificateOutputFile, String privateKeyOutputFile) + { + try + { + KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance(algorithm); + keyPairGenerator.initialize(getKeySize(algorithm)); + KeyPair keyPair = keyPairGenerator.generateKeyPair(); + X509Certificate certificate = generate(getHashAlgorithm(algorithm), keyPair.getPublic(), + keyPair.getPrivate(), keyPair.getPublic(), commonName, commonName, notBefore, notAfter, true); + storeCertificate(certificate, Paths.get(certificateOutputFile)); + storePrivateKey(keyPair.getPrivate(), Paths.get(privateKeyOutputFile)); + } + catch (Exception e) + { + throw new IllegalStateException(e); + } + } + + private int getKeySize(String algorithm) + { + int keySize; + if (RSA_ALGORITHM_NAME.equalsIgnoreCase(algorithm)) + { + keySize = RSA_KEY_SIZE; + } + else if ("EC".equalsIgnoreCase(algorithm) || EC_ALGORITHM_NAME.equalsIgnoreCase(algorithm)) + { + keySize = EC_KEY_SIZE; + } + else + { + throw new IllegalArgumentException("Algorithm '" + algorithm + "' is not supported."); + } + return keySize; + } + + private String getHashAlgorithm(String algorithm) + { + String hashAlgorithm; + if (RSA_ALGORITHM_NAME.equalsIgnoreCase(algorithm)) + { + hashAlgorithm = RSA_HASH_ALGORITHM; + } + else if ("EC".equalsIgnoreCase(algorithm) || EC_ALGORITHM_NAME.equalsIgnoreCase(algorithm)) + { + hashAlgorithm = EC_HASH_ALGORITHM; + } + else + { + throw new IllegalArgumentException("Algorithm '" + algorithm + "' is not supported."); + } + return hashAlgorithm; + } + + private X509Certificate generate(String hashAlgorithm, PublicKey certificatePublicKey, PrivateKey caPrivateKey, + PublicKey caPublicKey, String subjectCN, String issuerCN, Date notBefore, Date notAfter, boolean isCA) + throws OperatorCreationException, CertIOException, CertificateException + { + Instant now = Instant.now(); + ContentSigner contentSigner = new JcaContentSignerBuilder(hashAlgorithm).build(caPrivateKey); + X500Name subjectX500Name = new X500Name("CN=" + subjectCN); + X500Name issuerX500Name = new X500Name("CN=" + issuerCN); + X509v3CertificateBuilder certificateBuilder = new JcaX509v3CertificateBuilder(issuerX500Name, + BigInteger.valueOf(now.toEpochMilli()), notBefore, notAfter, subjectX500Name, certificatePublicKey); + certificateBuilder.addExtension(Extension.subjectKeyIdentifier, false, + createSubjectKeyIdentifier(certificatePublicKey)) + .addExtension(Extension.authorityKeyIdentifier, false, createAuthorityKeyIdentifier(caPublicKey)) + .addExtension(Extension.basicConstraints, true, new BasicConstraints(isCA)); + return new JcaX509CertificateConverter().setProvider(new BouncyCastleProvider()) + .getCertificate(certificateBuilder.build(contentSigner)); + } + + private SubjectKeyIdentifier createSubjectKeyIdentifier(final PublicKey publicKey) throws OperatorCreationException + { + final SubjectPublicKeyInfo publicKeyInfo = SubjectPublicKeyInfo.getInstance(publicKey.getEncoded()); + final DigestCalculator digCalc = new BcDigestCalculatorProvider().get( + new AlgorithmIdentifier(OIWObjectIdentifiers.idSHA1)); + + return new X509ExtensionUtils(digCalc).createSubjectKeyIdentifier(publicKeyInfo); + } + + private static AuthorityKeyIdentifier createAuthorityKeyIdentifier(final PublicKey publicKey) + throws OperatorCreationException + { + final SubjectPublicKeyInfo publicKeyInfo = SubjectPublicKeyInfo.getInstance(publicKey.getEncoded()); + final DigestCalculator digCalc = new BcDigestCalculatorProvider().get( + new AlgorithmIdentifier(OIWObjectIdentifiers.idSHA1)); + + return new X509ExtensionUtils(digCalc).createAuthorityKeyIdentifier(publicKeyInfo); + } + + private void storeCertificate(X509Certificate x509Certificate, Path file) + throws CertificateEncodingException, IOException + { + String encodedCertificate = new String( + Base64.getMimeEncoder(PEM_ENCODED_LINE_LENGTH, "\n".getBytes(StandardCharsets.UTF_8)) + .encode(x509Certificate.getEncoded())); + Files.write(file, + BEGIN_CERTIFICATE.concat(encodedCertificate).concat(END_CERTIFICATE).getBytes(StandardCharsets.UTF_8)); + } + + private void storePrivateKey(PrivateKey privateKey, Path file) throws IOException + { + String encodedPrivateKey = new String( + Base64.getMimeEncoder(PEM_ENCODED_LINE_LENGTH, "\n".getBytes(StandardCharsets.UTF_8)) + .encode(privateKey.getEncoded())); + Files.write(file, + BEGIN_PRIVATE_KEY.concat(encodedPrivateKey).concat(END_PRIVATE_KEY).getBytes(StandardCharsets.UTF_8)); + } +} + diff --git a/application/src/test/java/com/ericsson/bss/cassandra/ecchronos/application/config/security/TestReloadingAuthProvider.java b/application/src/test/java/com/ericsson/bss/cassandra/ecchronos/application/config/security/TestReloadingAuthProvider.java new file mode 100644 index 000000000..14a94f4f8 --- /dev/null +++ b/application/src/test/java/com/ericsson/bss/cassandra/ecchronos/application/config/security/TestReloadingAuthProvider.java @@ -0,0 +1,135 @@ +/* + * Copyright 2024 Telefonaktiebolaget LM Ericsson + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.ericsson.bss.cassandra.ecchronos.application.config.security; + +import com.datastax.oss.driver.api.core.auth.AuthProvider; +import com.datastax.oss.driver.api.core.auth.Authenticator; +import com.datastax.oss.driver.api.core.metadata.EndPoint; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +import java.io.ByteArrayOutputStream; +import java.nio.ByteBuffer; +import java.nio.charset.Charset; +import java.util.function.Supplier; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class TestReloadingAuthProvider +{ + private static final Charset UTF8 = Charset.forName("UTF-8"); + + private static final int POS_USERNAME = 1; + private static final int POS_PASSWORD = 2; + + private static final String SERVER_AUTHENTICATOR = "org.apache.cassandra.auth.PasswordAuthenticator"; + + @Mock + private Supplier credentialsSupplier; + + @Mock + private EndPoint endPoint; + + @Test + public void testCorrectResponse() + { + when(credentialsSupplier.get()).thenReturn(new Credentials(true, "username", "password")); + AuthProvider authProvider = new ReloadingAuthProvider(credentialsSupplier); + + Authenticator authenticator = authProvider.newAuthenticator(endPoint, SERVER_AUTHENTICATOR); + + authenticator.initialResponse().thenAccept(b -> + { + assertThat(getUsername(b)).isEqualTo("username"); + assertThat(getPassword(b)).isEqualTo("password"); + }); + } + + @Test + public void testChangingResponse() + { + when(credentialsSupplier.get()) + .thenReturn(new Credentials(true, "username", "password")) + .thenReturn(new Credentials(true, "new_user", "new_password")); + AuthProvider authProvider = new ReloadingAuthProvider(credentialsSupplier); + + Authenticator authenticator = authProvider.newAuthenticator(endPoint, SERVER_AUTHENTICATOR); + + authenticator.initialResponse().thenAccept(b -> + { + assertThat(getUsername(b)).isEqualTo("username"); + assertThat(getPassword(b)).isEqualTo("password"); + }); + + authenticator = authProvider.newAuthenticator(endPoint, SERVER_AUTHENTICATOR); + + authenticator.initialResponse().thenAccept(b -> + { + assertThat(getUsername(b)).isEqualTo("new_user"); + assertThat(getPassword(b)).isEqualTo("new_password"); + }); + } + + private String getUsername(ByteBuffer response) + { + return readField(response, POS_USERNAME); + } + + private String getPassword(ByteBuffer response) + { + return readField(response, POS_PASSWORD); + } + + private String readField(ByteBuffer response, int pos) + { + ByteBuffer buffer = readUntil(response, pos); + + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + while (buffer.hasRemaining()) + { + byte next = buffer.get(); + if (next == 0) + { + break; + } + baos.write(next); + } + return new String(baos.toByteArray(), UTF8); + } + + private ByteBuffer readUntil(ByteBuffer response, int pos) + { + ByteBuffer result = response.duplicate(); + result.rewind(); + + int zeroFound = 0; + + // Read the buffer until we are at the expected position + while (zeroFound < pos && result.hasRemaining()) + { + if (result.get() == 0) + { + zeroFound++; + } + } + + return result; + } +} + diff --git a/application/src/test/java/com/ericsson/bss/cassandra/ecchronos/application/config/security/TestReloadingCertificateHandler.java b/application/src/test/java/com/ericsson/bss/cassandra/ecchronos/application/config/security/TestReloadingCertificateHandler.java new file mode 100644 index 000000000..ddb45b16e --- /dev/null +++ b/application/src/test/java/com/ericsson/bss/cassandra/ecchronos/application/config/security/TestReloadingCertificateHandler.java @@ -0,0 +1,183 @@ +/* + * Copyright 2024 Telefonaktiebolaget LM Ericsson + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.ericsson.bss.cassandra.ecchronos.application.config.security; + +import org.junit.BeforeClass; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +import java.io.IOException; +import java.nio.file.Paths; +import java.time.Instant; +import java.util.Date; + +import static org.assertj.core.api.Assertions.assertThat; + +public class TestReloadingCertificateHandler +{ + private static final String KEYSTORE_PASSWORD = "ecctest"; + private static final String STORE_TYPE_JKS = "JKS"; + + private static CertUtils certUtils = new CertUtils(); + private static String certDirectory; + private static String clientCaCert; + private static String clientCaCertKey; + private static String clientCert; + private static String clientCertKey; + private static String clientKeyStore; + private static String clientTrustStore; + + //Using this we "modify" the config/keystore/certificate + private String protocolVersion = "TLSv1.2"; + + @BeforeClass + public static void initOnce() throws IOException + { + TemporaryFolder temporaryFolder = new TemporaryFolder(); + temporaryFolder.create(); + certDirectory = temporaryFolder.getRoot().getPath(); + setupCerts(); + } + + private static void setupCerts() + { + Date notBefore = Date.from(Instant.now()); + Date notAfter = Date.from(Instant.now().plus(java.time.Duration.ofHours(1))); + + clientCaCert = Paths.get(certDirectory, "clientca.crt").toString(); + clientCaCertKey = Paths.get(certDirectory, "clientca.key").toString(); + certUtils.createSelfSignedCACertificate("clientCA", notBefore, notAfter, "RSA", clientCaCert, clientCaCertKey); + clientCert = Paths.get(certDirectory, "client.crt").toString(); + clientCertKey = Paths.get(certDirectory, "client.key").toString(); + certUtils.createCertificate("client", notBefore, notAfter, clientCaCert, clientCaCertKey, clientCert, clientCertKey); + clientKeyStore = Paths.get(certDirectory, "client.keystore").toString(); + certUtils.createKeyStore(clientCert, clientCertKey, STORE_TYPE_JKS, KEYSTORE_PASSWORD, clientKeyStore); + clientTrustStore = Paths.get(certDirectory, "client.truststore").toString(); + certUtils.createTrustStore(clientCaCert, STORE_TYPE_JKS, KEYSTORE_PASSWORD, clientTrustStore); + } + + @Test + public void testNewSslEngineSameContextWhenConfigDoesNotChangeKeyStore() + { + ReloadingCertificateHandler reloadingCertificateHandler = new ReloadingCertificateHandler(() -> getTLSConfigWithKeyStore()); + + reloadingCertificateHandler.newSslEngine(null); + ReloadingCertificateHandler.Context oldContext = reloadingCertificateHandler.getContext(); + + reloadingCertificateHandler.newSslEngine(null); + ReloadingCertificateHandler.Context newContext = reloadingCertificateHandler.getContext(); + + assertThat(newContext).isEqualTo(oldContext); + } + + @Test + public void testNewSslEngineDifferentContextWhenConfigChangesKeyStore() + { + ReloadingCertificateHandler reloadingCertificateHandler = new ReloadingCertificateHandler(() -> getTLSConfigWithKeyStore()); + + reloadingCertificateHandler.newSslEngine(null); + ReloadingCertificateHandler.Context oldContext = reloadingCertificateHandler.getContext(); + + //Change protocolVersion to simulate a change in the config + protocolVersion = "TLSv1.1,TLSv1.2"; + + reloadingCertificateHandler.newSslEngine(null); + ReloadingCertificateHandler.Context newContext = reloadingCertificateHandler.getContext(); + + assertThat(newContext).isNotEqualTo(oldContext); + } + + @Test + public void testNewSslEngineDifferentContextWhenKeyStoreChanges() + { + ReloadingCertificateHandler reloadingCertificateHandler = new ReloadingCertificateHandler(() -> getTLSConfigWithKeyStore()); + + reloadingCertificateHandler.newSslEngine(null); + ReloadingCertificateHandler.Context oldContext = reloadingCertificateHandler.getContext(); + + //Create certificates again to simulate renewal + setupCerts(); + + reloadingCertificateHandler.newSslEngine(null); + ReloadingCertificateHandler.Context newContext = reloadingCertificateHandler.getContext(); + + assertThat(newContext).isNotEqualTo(oldContext); + } + + @Test + public void testNewSslEngineSameContextWhenConfigDoesNotChangePEM() + { + ReloadingCertificateHandler reloadingCertificateHandler = new ReloadingCertificateHandler(() -> getTLSConfigWithPEMFiles()); + + reloadingCertificateHandler.newSslEngine(null); + ReloadingCertificateHandler.Context oldContext = reloadingCertificateHandler.getContext(); + + reloadingCertificateHandler.newSslEngine(null); + ReloadingCertificateHandler.Context newContext = reloadingCertificateHandler.getContext(); + + assertThat(newContext).isEqualTo(oldContext); + } + + @Test + public void testNewSslEngineDifferentContextWhenConfigChangesPEM() + { + ReloadingCertificateHandler reloadingCertificateHandler = new ReloadingCertificateHandler(() -> getTLSConfigWithPEMFiles()); + + reloadingCertificateHandler.newSslEngine(null); + ReloadingCertificateHandler.Context oldContext = reloadingCertificateHandler.getContext(); + + //Change protocolVersion to simulate a change in the config + protocolVersion = "TLSv1.1,TLSv1.2"; + + reloadingCertificateHandler.newSslEngine(null); + ReloadingCertificateHandler.Context newContext = reloadingCertificateHandler.getContext(); + + assertThat(newContext).isNotEqualTo(oldContext); + } + + @Test + public void testNewSslEngineDifferentContextWhenCertificateChangesPEM() + { + ReloadingCertificateHandler reloadingCertificateHandler = new ReloadingCertificateHandler(() -> getTLSConfigWithPEMFiles()); + + reloadingCertificateHandler.newSslEngine(null); + ReloadingCertificateHandler.Context oldContext = reloadingCertificateHandler.getContext(); + + //Create certificates again to simulate renewal + setupCerts(); + + reloadingCertificateHandler.newSslEngine(null); + ReloadingCertificateHandler.Context newContext = reloadingCertificateHandler.getContext(); + + assertThat(newContext).isNotEqualTo(oldContext); + } + + private CqlTLSConfig getTLSConfigWithKeyStore() + { + CqlTLSConfig cqlTLSConfig = new CqlTLSConfig(true, clientKeyStore, KEYSTORE_PASSWORD, clientTrustStore, + KEYSTORE_PASSWORD); + cqlTLSConfig.setStoreType(STORE_TYPE_JKS); + cqlTLSConfig.setProtocol(protocolVersion); + return cqlTLSConfig; + } + + private CqlTLSConfig getTLSConfigWithPEMFiles() + { + CqlTLSConfig cqlTLSConfig = new CqlTLSConfig(true, clientCert, clientCertKey, clientCaCert); + cqlTLSConfig.setProtocol(protocolVersion); + return cqlTLSConfig; + } +} + diff --git a/application/src/test/java/com/ericsson/bss/cassandra/ecchronos/application/config/security/TestSecurity.java b/application/src/test/java/com/ericsson/bss/cassandra/ecchronos/application/config/security/TestSecurity.java new file mode 100644 index 000000000..fa75624c4 --- /dev/null +++ b/application/src/test/java/com/ericsson/bss/cassandra/ecchronos/application/config/security/TestSecurity.java @@ -0,0 +1,144 @@ +/* + * Copyright 2024 Telefonaktiebolaget LM Ericsson + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.ericsson.bss.cassandra.ecchronos.application.config.security; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.exc.ValueInstantiationException; +import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; +import org.junit.Test; + +import java.io.File; +import java.io.IOException; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +public class TestSecurity +{ + @Test + public void testDefault() throws Exception + { + ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); + File file = new File(classLoader.getResource("security.yml").getFile()); + + ObjectMapper objectMapper = new ObjectMapper(new YAMLFactory()); + + Security config = objectMapper.readValue(file, Security.class); + + Credentials expectedCredentials = new Credentials(true, "cassandra", "cassandra"); + + CqlTLSConfig cqlTLSConfig = new CqlTLSConfig(false, "/path/to/keystore", "ecchronos", + "/path/to/truststore", "ecchronos"); + cqlTLSConfig.setStoreType("JKS"); + cqlTLSConfig.setProtocol("TLSv1.2"); + + JmxTLSConfig jmxTLSConfig = new JmxTLSConfig(false, "/path/to/keystore", + "ecchronos", "/path/to/truststore", "ecchronos"); + jmxTLSConfig.setProtocol("TLSv1.2"); + jmxTLSConfig.setCipherSuites(null); + + assertThat(config.getCqlSecurity().getCqlCredentials()).isEqualTo(expectedCredentials); + assertThat(config.getJmxSecurity().getJmxCredentials()).isEqualTo(expectedCredentials); + assertThat(config.getCqlSecurity().getCqlTlsConfig()).isEqualTo(cqlTLSConfig); + assertThat(config.getJmxSecurity().getJmxTlsConfig()).isEqualTo(jmxTLSConfig); + } + + @Test + public void testCqlAndJmxEnabledKeyStore() throws Exception + { + ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); + File file = new File(classLoader.getResource("security/enabled_keystore_jmxandcql.yml").getFile()); + + ObjectMapper objectMapper = new ObjectMapper(new YAMLFactory()); + + Security config = objectMapper.readValue(file, Security.class); + + Credentials expectedCqlCredentials = new Credentials(true, "cqluser", "cqlpassword"); + Credentials expectedJmxCredentials = new Credentials(true, "jmxuser", "jmxpassword"); + + CqlTLSConfig cqlTLSConfig = new CqlTLSConfig(true, "/path/to/cql/keystore", + "cqlkeystorepassword", "/path/to/cql/truststore","cqltruststorepassword"); + cqlTLSConfig.setStoreType("JKS"); + cqlTLSConfig.setAlgorithm("SunX509"); + cqlTLSConfig.setProtocol("TLSv1.2"); + cqlTLSConfig.setCipherSuites("VALID_CIPHER_SUITE,VALID_CIPHER_SUITE2"); + cqlTLSConfig.setRequireEndpointVerification(true); + + JmxTLSConfig jmxTLSConfig = new JmxTLSConfig(true, "/path/to/jmx/keystore", + "jmxkeystorepassword", "/path/to/jmx/truststore", "jmxtruststorepassword"); + jmxTLSConfig.setProtocol("TLSv1.2"); + jmxTLSConfig.setCipherSuites("VALID_CIPHER_SUITE,VALID_CIPHER_SUITE2"); + + assertThat(config.getCqlSecurity().getCqlCredentials()).isEqualTo(expectedCqlCredentials); + assertThat(config.getJmxSecurity().getJmxCredentials()).isEqualTo(expectedJmxCredentials); + assertThat(config.getCqlSecurity().getCqlTlsConfig()).isEqualTo(cqlTLSConfig); + assertThat(config.getJmxSecurity().getJmxTlsConfig()).isEqualTo(jmxTLSConfig); + } + + @Test + public void testCqlEnabledWithCertificate() throws Exception + { + ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); + File file = new File(classLoader.getResource("security/enabled_pem_cql.yml").getFile()); + + ObjectMapper objectMapper = new ObjectMapper(new YAMLFactory()); + + Security config = objectMapper.readValue(file, Security.class); + + Credentials expectedCqlCredentials = new Credentials(true, "cqluser", "cqlpassword"); + + CqlTLSConfig cqlTLSConfig = new CqlTLSConfig(true, "/path/to/cql/certificate", + "/path/to/cql/certificate_key", "/path/to/cql/certificate_authorities"); + cqlTLSConfig.setProtocol("TLSv1.2"); + cqlTLSConfig.setCipherSuites("VALID_CIPHER_SUITE,VALID_CIPHER_SUITE2"); + cqlTLSConfig.setRequireEndpointVerification(true); + + assertThat(config.getCqlSecurity().getCqlCredentials()).isEqualTo(expectedCqlCredentials); + assertThat(config.getCqlSecurity().getCqlTlsConfig()).isEqualTo(cqlTLSConfig); + } + + @Test + public void testJmxEnabledWithCertificate() + { + ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); + File file = new File(classLoader.getResource("security/enabled_pem_jmx.yml").getFile()); + + ObjectMapper objectMapper = new ObjectMapper(new YAMLFactory()); + + assertThatExceptionOfType(IOException.class).isThrownBy(() -> objectMapper.readValue(file, Security.class)); + } + + @Test + public void testCqlEnabledWithNothing() + { + ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); + File file = new File(classLoader.getResource("security/enabled_nokeystore_nopem_cql.yml").getFile()); + + ObjectMapper objectMapper = new ObjectMapper(new YAMLFactory()); + + assertThatExceptionOfType(ValueInstantiationException.class).isThrownBy(() -> objectMapper.readValue(file, Security.class)); + } + + @Test + public void testJmxEnabledWithNothing() + { + ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); + File file = new File(classLoader.getResource("security/enabled_nokeystore_nopem_jmx.yml").getFile()); + + ObjectMapper objectMapper = new ObjectMapper(new YAMLFactory()); + + assertThatExceptionOfType(ValueInstantiationException.class).isThrownBy(() -> objectMapper.readValue(file, Security.class)); + } +} diff --git a/application/src/test/resources/all_set.yml b/application/src/test/resources/all_set.yml index 673fbce13..e7bd0aad3 100644 --- a/application/src/test/resources/all_set.yml +++ b/application/src/test/resources/all_set.yml @@ -18,6 +18,7 @@ connection: agent: type: datacenterAware localDatacenter: datacenter1 + datacenterAwarePolicy: com.ericsson.bss.cassandra.ecchronos.connection.DataCenterAwarePolicy contactPoints: - host: 127.0.0.1 port: 9042 diff --git a/application/src/test/resources/security/enabled_keystore_jmxandcql.yml b/application/src/test/resources/security/enabled_keystore_jmxandcql.yml new file mode 100644 index 000000000..4f934d67c --- /dev/null +++ b/application/src/test/resources/security/enabled_keystore_jmxandcql.yml @@ -0,0 +1,45 @@ +# +# Copyright 2024 Telefonaktiebolaget LM Ericsson +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +cql: + credentials: + enabled: true + username: cqluser + password: cqlpassword + tls: + enabled: true + keystore: /path/to/cql/keystore + keystore_password: cqlkeystorepassword + truststore: /path/to/cql/truststore + truststore_password: cqltruststorepassword + protocol: TLSv1.2 + algorithm: SunX509 + store_type: JKS + cipher_suites: VALID_CIPHER_SUITE,VALID_CIPHER_SUITE2 + require_endpoint_verification: true + +jmx: + credentials: + enabled: true + username: jmxuser + password: jmxpassword + tls: + enabled: true + keystore: /path/to/jmx/keystore + keystore_password: jmxkeystorepassword + truststore: /path/to/jmx/truststore + truststore_password: jmxtruststorepassword + protocol: TLSv1.2 + cipher_suites: VALID_CIPHER_SUITE,VALID_CIPHER_SUITE2 diff --git a/application/src/test/resources/security/enabled_nokeystore_nopem_cql.yml b/application/src/test/resources/security/enabled_nokeystore_nopem_cql.yml new file mode 100644 index 000000000..8cc1d8d4d --- /dev/null +++ b/application/src/test/resources/security/enabled_nokeystore_nopem_cql.yml @@ -0,0 +1,25 @@ +# +# Copyright 2024 Telefonaktiebolaget LM Ericsson +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +cql: + credentials: + enabled: true + username: cqluser + password: cqlpassword + tls: + enabled: true + protocol: TLSv1.2 + cipher_suites: VALID_CIPHER_SUITE,VALID_CIPHER_SUITE2 + require_endpoint_verification: true diff --git a/application/src/test/resources/security/enabled_nokeystore_nopem_jmx.yml b/application/src/test/resources/security/enabled_nokeystore_nopem_jmx.yml new file mode 100644 index 000000000..ff1f299d7 --- /dev/null +++ b/application/src/test/resources/security/enabled_nokeystore_nopem_jmx.yml @@ -0,0 +1,24 @@ +# +# Copyright 2024 Telefonaktiebolaget LM Ericsson +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +jmx: + credentials: + enabled: true + username: jmxuser + password: jmxpassword + tls: + enabled: true + protocol: TLSv1.2 + cipher_suites: VALID_CIPHER_SUITE,VALID_CIPHER_SUITE2 diff --git a/application/src/test/resources/security/enabled_pem_cql.yml b/application/src/test/resources/security/enabled_pem_cql.yml new file mode 100644 index 000000000..9a959c099 --- /dev/null +++ b/application/src/test/resources/security/enabled_pem_cql.yml @@ -0,0 +1,28 @@ +# +# Copyright 2024 Telefonaktiebolaget LM Ericsson +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +cql: + credentials: + enabled: true + username: cqluser + password: cqlpassword + tls: + enabled: true + certificate: /path/to/cql/certificate + certificate_private_key: /path/to/cql/certificate_key + trust_certificate: /path/to/cql/certificate_authorities + protocol: TLSv1.2 + cipher_suites: VALID_CIPHER_SUITE,VALID_CIPHER_SUITE2 + require_endpoint_verification: true diff --git a/application/src/test/resources/security/enabled_pem_jmx.yml b/application/src/test/resources/security/enabled_pem_jmx.yml new file mode 100644 index 000000000..22056b7ff --- /dev/null +++ b/application/src/test/resources/security/enabled_pem_jmx.yml @@ -0,0 +1,28 @@ +# +# Copyright 2024 Telefonaktiebolaget LM Ericsson +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +jmx: + credentials: + enabled: true + username: jmxuser + password: jmxpassword + tls: + enabled: true + certificate: /path/to/jmx/certificate + certificate_private_key: /path/to/jmx/certificate_key + trust_certificate: /path/to/jmx/certificate_authorities + protocol: TLSv1.2 + cipher_suites: VALID_CIPHER_SUITE,VALID_CIPHER_SUITE2 + require_endpoint_verification: true diff --git a/connection.impl/src/main/java/com/ericsson/bss/cassandra/ecchronos/connection/impl/builders/DistributedJmxBuilder.java b/connection.impl/src/main/java/com/ericsson/bss/cassandra/ecchronos/connection/impl/builders/DistributedJmxBuilder.java index f212d625d..e8879bf7d 100644 --- a/connection.impl/src/main/java/com/ericsson/bss/cassandra/ecchronos/connection/impl/builders/DistributedJmxBuilder.java +++ b/connection.impl/src/main/java/com/ericsson/bss/cassandra/ecchronos/connection/impl/builders/DistributedJmxBuilder.java @@ -14,10 +14,12 @@ */ package com.ericsson.bss.cassandra.ecchronos.connection.impl.builders; +import com.datastax.oss.driver.api.core.AllNodesFailedException; import com.datastax.oss.driver.api.core.CqlSession; import com.datastax.oss.driver.api.core.cql.Row; import com.datastax.oss.driver.api.core.cql.SimpleStatement; import com.datastax.oss.driver.api.core.metadata.Node; +import com.datastax.oss.driver.api.core.servererrors.QueryExecutionException; import com.ericsson.bss.cassandra.ecchronos.connection.impl.providers.DistributedJmxConnectionProviderImpl; import com.ericsson.bss.cassandra.ecchronos.data.enums.NodeStatus; import com.ericsson.bss.cassandra.ecchronos.data.exceptions.EcChronosException; @@ -152,26 +154,36 @@ private void createConnections() throws IOException private void reconnect(final Node node) throws IOException, EcChronosException { - String host = node.getBroadcastRpcAddress().get().getHostString(); - Integer port = getJMXPort(node); - if (host.contains(":")) + try { - // Use square brackets to surround IPv6 addresses - host = "[" + host + "]"; - } + String host = node.getBroadcastRpcAddress().get().getHostString(); + Integer port = getJMXPort(node); + if (host.contains(":")) + { + // Use square brackets to surround IPv6 addresses + host = "[" + host + "]"; + } - LOG.info("Starting to instantiate JMXService with host: {} and port: {}", host, port); - JMXServiceURL jmxUrl = new JMXServiceURL(String.format(JMX_FORMAT_URL, host, port)); - LOG.debug("Connecting JMX through {}, credentials: {}, tls: {}", jmxUrl, isAuthEnabled(), isTLSEnabled()); - JMXConnector jmxConnector = JMXConnectorFactory.connect(jmxUrl, createJMXEnv()); - if (isConnected(jmxConnector)) - { - LOG.info("Connected JMX for {}", jmxUrl); - myEccNodesSync.updateNodeStatus(NodeStatus.AVAILABLE, node.getDatacenter(), node.getHostId()); - myJMXConnections.put(Objects.requireNonNull(node.getHostId()), jmxConnector); + LOG.info("Starting to instantiate JMXService with host: {} and port: {}", host, port); + JMXServiceURL jmxUrl = new JMXServiceURL(String.format(JMX_FORMAT_URL, host, port)); + LOG.debug("Connecting JMX through {}, credentials: {}, tls: {}", jmxUrl, isAuthEnabled(), isTLSEnabled()); + JMXConnector jmxConnector = JMXConnectorFactory.connect(jmxUrl, createJMXEnv()); + if (isConnected(jmxConnector)) + { + LOG.info("Connected JMX for {}", jmxUrl); + myEccNodesSync.updateNodeStatus(NodeStatus.AVAILABLE, node.getDatacenter(), node.getHostId()); + myJMXConnections.put(Objects.requireNonNull(node.getHostId()), jmxConnector); + } + else + { + myEccNodesSync.updateNodeStatus(NodeStatus.UNAVAILABLE, node.getDatacenter(), node.getHostId()); } - else + } + catch + ( + AllNodesFailedException|QueryExecutionException|IOException|SecurityException e) { + LOG.error("Failed to create JMX connection with node {} because of {}", node.getHostId(), e.getMessage()); myEccNodesSync.updateNodeStatus(NodeStatus.UNAVAILABLE, node.getDatacenter(), node.getHostId()); } } diff --git a/connection.impl/src/main/java/com/ericsson/bss/cassandra/ecchronos/connection/impl/builders/DistributedNativeBuilder.java b/connection.impl/src/main/java/com/ericsson/bss/cassandra/ecchronos/connection/impl/builders/DistributedNativeBuilder.java index 2eb23b7e2..cc748a225 100644 --- a/connection.impl/src/main/java/com/ericsson/bss/cassandra/ecchronos/connection/impl/builders/DistributedNativeBuilder.java +++ b/connection.impl/src/main/java/com/ericsson/bss/cassandra/ecchronos/connection/impl/builders/DistributedNativeBuilder.java @@ -27,6 +27,7 @@ import com.datastax.oss.driver.api.core.metadata.NodeStateListener; import com.datastax.oss.driver.api.core.metadata.schema.SchemaChangeListener; import com.datastax.oss.driver.api.core.ssl.SslEngineFactory; +import com.datastax.oss.driver.internal.core.loadbalancing.DefaultLoadBalancingPolicy; import com.datastax.oss.driver.shaded.guava.common.annotations.VisibleForTesting; import com.datastax.oss.driver.api.core.metrics.DefaultSessionMetric; import com.ericsson.bss.cassandra.ecchronos.connection.impl.providers.DistributedNativeConnectionProviderImpl; @@ -62,6 +63,7 @@ public class DistributedNativeBuilder private ConnectionType myType = ConnectionType.datacenterAware; private List myInitialContactPoints = new ArrayList<>(); private String myLocalDatacenter = "datacenter1"; + private Class myDatacenterAwarePolicy = DataCenterAwarePolicy.class; private List myDatacenterAware = new ArrayList<>(); private List> myRackAware = new ArrayList<>(); @@ -112,6 +114,20 @@ public final DistributedNativeBuilder withLocalDatacenter(final String localData return this; } + /** + * Sets the DataCenterAwarePolicy used for load-balancing policy. + * + * @param datacenterAwarePolicy + * the custom class of the load-balancing policy. + * @return the current instance of {@link DistributedNativeBuilder}. + */ + public final DistributedNativeBuilder withDatacenterAwarePolicy( + final Class datacenterAwarePolicy) + { + myDatacenterAwarePolicy = datacenterAwarePolicy; + return this; + } + /** * Sets the datacenter awareness for the distributed native connection. * @@ -321,10 +337,13 @@ private static ProgrammaticDriverConfigLoaderBuilder loaderBuilder( ProgrammaticDriverConfigLoaderBuilder loaderBuilder = DriverConfigLoader.programmaticBuilder() .withStringList(DefaultDriverOption.METADATA_SCHEMA_REFRESHED_KEYSPACES, SCHEMA_REFRESHED_KEYSPACES); - loaderBuilder.withString(DefaultDriverOption.LOAD_BALANCING_POLICY_CLASS, - DataCenterAwarePolicy.class.getCanonicalName()); - loaderBuilder.withInt(DefaultDriverOption.LOAD_BALANCING_DC_FAILOVER_MAX_NODES_PER_REMOTE_DC, - MAX_NODES_PER_DC); + if (builder.myType.equals(ConnectionType.datacenterAware)) + { + loaderBuilder.withString(DefaultDriverOption.LOAD_BALANCING_POLICY_CLASS, + builder.myDatacenterAwarePolicy.getCanonicalName()); + loaderBuilder.withInt(DefaultDriverOption.LOAD_BALANCING_DC_FAILOVER_MAX_NODES_PER_REMOTE_DC, + MAX_NODES_PER_DC); + } if (builder.myIsMetricsEnabled) { loaderBuilder.withStringList(DefaultDriverOption.METRICS_SESSION_ENABLED, SESSION_METRICS); diff --git a/connection/src/test/java/com/ericsson/bss/cassandra/ecchronos/connection/TestDataCenterAwarePolicy.java b/connection/src/test/java/com/ericsson/bss/cassandra/ecchronos/connection/TestDataCenterAwarePolicy.java new file mode 100644 index 000000000..646664b35 --- /dev/null +++ b/connection/src/test/java/com/ericsson/bss/cassandra/ecchronos/connection/TestDataCenterAwarePolicy.java @@ -0,0 +1,322 @@ +/* + * Copyright 2024 Telefonaktiebolaget LM Ericsson + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.ericsson.bss.cassandra.ecchronos.connection; + +import com.datastax.oss.driver.api.core.ConsistencyLevel; +import com.datastax.oss.driver.api.core.CqlIdentifier; +import com.datastax.oss.driver.api.core.config.DefaultDriverOption; +import com.datastax.oss.driver.api.core.config.DriverConfig; +import com.datastax.oss.driver.api.core.config.DriverExecutionProfile; +import com.datastax.oss.driver.api.core.cql.BoundStatement; +import com.datastax.oss.driver.api.core.cql.SimpleStatement; +import com.datastax.oss.driver.api.core.loadbalancing.LoadBalancingPolicy; +import com.datastax.oss.driver.api.core.loadbalancing.NodeDistance; +import com.datastax.oss.driver.api.core.metadata.Metadata; +import com.datastax.oss.driver.api.core.metadata.Node; +import com.datastax.oss.driver.api.core.metadata.NodeState; +import com.datastax.oss.driver.api.core.metadata.TokenMap; +import com.datastax.oss.driver.api.core.session.Session; +import com.datastax.oss.driver.internal.core.ConsistencyLevelRegistry; +import com.datastax.oss.driver.internal.core.context.InternalDriverContext; +import com.datastax.oss.driver.internal.core.metadata.MetadataManager; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +import java.nio.ByteBuffer; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Optional; +import java.util.Queue; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.CopyOnWriteArrayList; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.Silent.class) +public class TestDataCenterAwarePolicy +{ + private final String myLocalDc = "DC1"; + private final String myRemoteDc = "DC2"; + + @Mock + private Session mySessionMock; + + @Mock + private Metadata myMetadataMock; + + @Mock + private TokenMap myTokenMapMock; + + @Mock + private InternalDriverContext myDriverContextMock; + + @Mock + private DriverConfig myDriverConfigMock; + + @Mock + private DriverExecutionProfile myDriverExecutionProfileMock; + + @Mock + private ConsistencyLevelRegistry myConsistencyLevelRegistryMock; + + @Mock + private MetadataManager myMetadataManagerMock; + + @Mock + private LoadBalancingPolicy.DistanceReporter myDistanceReporterMock; + + @Mock + private Node myNodeDC1Mock; + + @Mock + private Node myNodeDC2Mock; + + @Mock + private Node myNodeDC3Mock; + + @Mock + private Node myNodeNoDCMock; + + @Mock + private Node myNodeNotDC3Mock; + + private Map myNodes = new HashMap<>(); + + @Before + public void setup() + { + when(mySessionMock.getMetadata()).thenReturn(myMetadataMock); + when(myMetadataMock.getTokenMap()).thenReturn(Optional.of(myTokenMapMock)); + + when(myNodeDC1Mock.getDatacenter()).thenReturn("DC1"); + when(myNodeDC1Mock.getState()).thenReturn(NodeState.UP); + when(myNodeDC2Mock.getDatacenter()).thenReturn("DC2"); + when(myNodeDC2Mock.getState()).thenReturn(NodeState.UP); + when(myNodeDC3Mock.getDatacenter()).thenReturn("DC3"); + when(myNodeDC3Mock.getState()).thenReturn(NodeState.UP); + when(myNodeNoDCMock.getDatacenter()).thenReturn("no DC"); + when(myNodeNoDCMock.getState()).thenReturn(NodeState.UP); + when(myNodeNotDC3Mock.getDatacenter()).thenReturn("DC3"); + when(myNodeNotDC3Mock.getState()).thenReturn(NodeState.UP); + + myNodes.put(UUID.randomUUID(), myNodeDC1Mock); + myNodes.put(UUID.randomUUID(), myNodeDC2Mock); + myNodes.put(UUID.randomUUID(), myNodeDC3Mock); + when(myDriverContextMock.getConfig()).thenReturn(myDriverConfigMock); + when(myDriverContextMock.getLocalDatacenter(any())).thenReturn(myLocalDc); + when(myDriverContextMock.getMetadataManager()).thenReturn(myMetadataManagerMock); + when(myMetadataManagerMock.getMetadata()).thenReturn(myMetadataMock); + when(myDriverConfigMock.getProfile(any(String.class))).thenReturn(myDriverExecutionProfileMock); + when(myDriverExecutionProfileMock.getName()).thenReturn("unittest"); + when(myDriverExecutionProfileMock.getInt(DefaultDriverOption.LOAD_BALANCING_DC_FAILOVER_MAX_NODES_PER_REMOTE_DC)).thenReturn(999); + when(myDriverExecutionProfileMock.getBoolean(DefaultDriverOption.LOAD_BALANCING_DC_FAILOVER_ALLOW_FOR_LOCAL_CONSISTENCY_LEVELS)).thenReturn(false); + when(myDriverExecutionProfileMock.getString(DefaultDriverOption.REQUEST_CONSISTENCY)).thenReturn("LOCAL_QUORUM"); + when(myDriverContextMock.getConsistencyLevelRegistry()).thenReturn(myConsistencyLevelRegistryMock); + when(myConsistencyLevelRegistryMock.nameToLevel(any(String.class))).thenReturn(ConsistencyLevel.LOCAL_QUORUM); + } + + @Test + public void testDistanceHost() + { + DataCenterAwarePolicy policy = new DataCenterAwarePolicy(myDriverContextMock, ""); + policy.init(myNodes, myDistanceReporterMock); + + NodeDistance distance1 = policy.distance(myNodeDC1Mock, myLocalDc); + NodeDistance distance2 = policy.distance(myNodeDC2Mock, myLocalDc); + NodeDistance distance3 = policy.distance(myNodeDC3Mock, myLocalDc); + NodeDistance distance4 = policy.distance(myNodeNoDCMock, myLocalDc); + NodeDistance distance5 = policy.distance(myNodeNotDC3Mock, myLocalDc); + + assertThat(distance1).isEqualTo(NodeDistance.LOCAL); + assertThat(distance2).isEqualTo(NodeDistance.REMOTE); + assertThat(distance3).isEqualTo(NodeDistance.REMOTE); + assertThat(distance4).isEqualTo(NodeDistance.IGNORED); + assertThat(distance5).isEqualTo(NodeDistance.IGNORED); + } + + @Test + public void testNewQueryPlanWithNotPartitionAwareStatement() + { + SimpleStatement simpleStatement = SimpleStatement.newInstance("SELECT *"); + + DataCenterAwarePolicy policy = new DataCenterAwarePolicy(myDriverContextMock, ""); + + policy.init(myNodes, myDistanceReporterMock); + + Set nodes = new HashSet<>(); + nodes.add(myNodeDC1Mock); + when(myTokenMapMock.getReplicas(any(CqlIdentifier.class), any(ByteBuffer.class))).thenReturn(nodes); + Queue queue = policy.newQueryPlan(simpleStatement, mySessionMock); + + assertThat(queue.isEmpty()).isFalse(); + assertThat(queue.poll()).isEqualTo(myNodeDC1Mock); + assertThat(queue.isEmpty()).isTrue(); + } + + @Test + public void testNewQueryPlanWithPartitionAwareStatementLocalDc() + { + BoundStatement boundStatement = mock(BoundStatement.class); + when(boundStatement.getRoutingKeyspace()).thenReturn(CqlIdentifier.fromInternal("foo")); + when(boundStatement.getRoutingKey()).thenReturn(ByteBuffer.wrap("foo".getBytes())); + DataCenterAwareStatement partitionAwareStatement = new DataCenterAwareStatement(boundStatement, myLocalDc); + + DataCenterAwarePolicy policy = new DataCenterAwarePolicy(myDriverContextMock, ""); + + policy.init(myNodes, myDistanceReporterMock); + + Set nodes = new HashSet<>(); + nodes.add(myNodeDC1Mock); + when(myTokenMapMock.getReplicas(any(CqlIdentifier.class), any(ByteBuffer.class))).thenReturn(nodes); + + Queue queue = policy.newQueryPlan(partitionAwareStatement, mySessionMock); + + assertThat(queue.isEmpty()).isFalse(); + assertThat(queue.poll()).isEqualTo(myNodeDC1Mock); + assertThat(queue.isEmpty()).isTrue(); + } + + @Test + public void testNewQueryPlanWithPartitionAwareStatementRemoteDc() + { + BoundStatement boundStatement = mock(BoundStatement.class); + when(boundStatement.getRoutingKeyspace()).thenReturn(CqlIdentifier.fromInternal("foo")); + when(boundStatement.getRoutingKey()).thenReturn(ByteBuffer.wrap("foo".getBytes())); + DataCenterAwareStatement partitionAwareStatement = new DataCenterAwareStatement(boundStatement, myRemoteDc); + + DataCenterAwarePolicy policy = new DataCenterAwarePolicy(myDriverContextMock, ""); + + policy.init(myNodes, myDistanceReporterMock); + + Set nodes = new HashSet<>(); + nodes.add(myNodeDC1Mock); + when(myTokenMapMock.getReplicas(any(CqlIdentifier.class), any(ByteBuffer.class))).thenReturn(nodes); + + Queue queue = policy.newQueryPlan(partitionAwareStatement, mySessionMock); + + assertThat(queue.isEmpty()).isFalse(); + assertThat(queue.poll()).isEqualTo(myNodeDC2Mock); + assertThat(queue.isEmpty()).isTrue(); + } + + @Test + public void testOnUp() + { + DataCenterAwarePolicy policy = new DataCenterAwarePolicy(myDriverContextMock, ""); + policy.init(myNodes, myDistanceReporterMock); + + CopyOnWriteArrayList nodesInDC = policy.getPerDcLiveNodes().get("DC3"); + assertThat(nodesInDC.contains(myNodeDC3Mock)); + assertThat(nodesInDC.size()).isEqualTo(1); + + policy.onUp(myNodeNotDC3Mock); + + nodesInDC = policy.getPerDcLiveNodes().get("DC3"); + assertThat(nodesInDC.contains(myNodeDC3Mock)); + assertThat(nodesInDC.contains(myNodeNotDC3Mock)); + assertThat(nodesInDC.size()).isEqualTo(2); + } + + @Test + public void testOnUpTwice() + { + DataCenterAwarePolicy policy = new DataCenterAwarePolicy(myDriverContextMock, ""); + policy.init(myNodes, myDistanceReporterMock); + + CopyOnWriteArrayList nodesInDC = policy.getPerDcLiveNodes().get("DC3"); + assertThat(nodesInDC.contains(myNodeDC3Mock)); + assertThat(nodesInDC.size()).isEqualTo(1); + + policy.onUp(myNodeNotDC3Mock); + policy.onUp(myNodeNotDC3Mock); + + nodesInDC = policy.getPerDcLiveNodes().get("DC3"); + assertThat(nodesInDC.contains(myNodeDC3Mock)); + assertThat(nodesInDC.contains(myNodeNotDC3Mock)); + assertThat(nodesInDC.size()).isEqualTo(2); + } + + @Test + public void testOnDown() + { + DataCenterAwarePolicy policy = new DataCenterAwarePolicy(myDriverContextMock, ""); + policy.init(myNodes, myDistanceReporterMock); + CopyOnWriteArrayList nodesInDC = policy.getPerDcLiveNodes().get("DC3"); + assertThat(nodesInDC.contains(myNodeDC3Mock)); + assertThat(nodesInDC.size()).isEqualTo(1); + + policy.onDown(myNodeDC3Mock); + + nodesInDC = policy.getPerDcLiveNodes().get("DC3"); + assertThat(nodesInDC).isEmpty(); + } + + @Test + public void testOnDownTwice() + { + DataCenterAwarePolicy policy = new DataCenterAwarePolicy(myDriverContextMock, ""); + policy.init(myNodes, myDistanceReporterMock); + CopyOnWriteArrayList nodesInDC = policy.getPerDcLiveNodes().get("DC3"); + assertThat(nodesInDC.contains(myNodeDC3Mock)); + assertThat(nodesInDC.size()).isEqualTo(1); + + policy.onDown(myNodeDC3Mock); + policy.onDown(myNodeDC3Mock); + + nodesInDC = policy.getPerDcLiveNodes().get("DC3"); + assertThat(nodesInDC).isEmpty(); + } + + @Test + public void testOnAdd() + { + DataCenterAwarePolicy policy = new DataCenterAwarePolicy(myDriverContextMock, ""); + policy.init(myNodes, myDistanceReporterMock); + + CopyOnWriteArrayList nodesInDC = policy.getPerDcLiveNodes().get("DC3"); + assertThat(nodesInDC.contains(myNodeDC3Mock)); + assertThat(nodesInDC.size()).isEqualTo(1); + + policy.onAdd(myNodeNotDC3Mock); + + nodesInDC = policy.getPerDcLiveNodes().get("DC3"); + assertThat(nodesInDC.contains(myNodeDC3Mock)); + assertThat(nodesInDC.contains(myNodeNotDC3Mock)); + assertThat(nodesInDC.size()).isEqualTo(2); + } + + @Test + public void testOnRemove() + { + DataCenterAwarePolicy policy = new DataCenterAwarePolicy(myDriverContextMock, ""); + policy.init(myNodes, myDistanceReporterMock); + CopyOnWriteArrayList nodesInDC = policy.getPerDcLiveNodes().get("DC3"); + assertThat(nodesInDC.contains(myNodeDC3Mock)); + assertThat(nodesInDC.size()).isEqualTo(1); + + policy.onRemove(myNodeDC3Mock); + + nodesInDC = policy.getPerDcLiveNodes().get("DC3"); + assertThat(nodesInDC).isEmpty(); + } +} diff --git a/connection/src/test/java/com/ericsson/bss/cassandra/ecchronos/connection/TestDataCenterAwareStatement.java b/connection/src/test/java/com/ericsson/bss/cassandra/ecchronos/connection/TestDataCenterAwareStatement.java new file mode 100644 index 000000000..292234c4f --- /dev/null +++ b/connection/src/test/java/com/ericsson/bss/cassandra/ecchronos/connection/TestDataCenterAwareStatement.java @@ -0,0 +1,32 @@ +/* + * Copyright 2024 Telefonaktiebolaget LM Ericsson + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.ericsson.bss.cassandra.ecchronos.connection; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.Test; + +public class TestDataCenterAwareStatement +{ + @Test + public void testGetDataCenter() + { + String dataCenter = "DC1"; + + DataCenterAwareStatement statement = new DataCenterAwareStatement(null, dataCenter); + + assertThat(statement.getDataCenter()).isEqualTo(dataCenter); + } +} From ba161413e35bcdb7729c63ab535e8b2b54b49d2d Mon Sep 17 00:00:00 2001 From: VictorCavichioli Date: Wed, 14 Aug 2024 11:10:41 -0300 Subject: [PATCH 3/6] Fix PMD Violation --- .../config/connection/AgentConnectionConfig.java | 6 ++++-- .../connection/impl/builders/DistributedJmxBuilder.java | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/application/src/main/java/com/ericsson/bss/cassandra/ecchronos/application/config/connection/AgentConnectionConfig.java b/application/src/main/java/com/ericsson/bss/cassandra/ecchronos/application/config/connection/AgentConnectionConfig.java index 3be179727..2177ca5cd 100644 --- a/application/src/main/java/com/ericsson/bss/cassandra/ecchronos/application/config/connection/AgentConnectionConfig.java +++ b/application/src/main/java/com/ericsson/bss/cassandra/ecchronos/application/config/connection/AgentConnectionConfig.java @@ -111,7 +111,7 @@ public String getLocalDatacenter() * @return the DataCenterAwarePolicy. */ @JsonProperty("datacenterAwarePolicy") - public final Class getDatacenterAwarePolicy() + public Class getDatacenterAwarePolicy() { return myDatacenterAwarePolicy; } @@ -123,7 +123,9 @@ public final Class getDatacenterAwarePolic * the DataCenterAwarePolicy to set. */ @JsonProperty("datacenterAwarePolicy") - public final void setDatacenterAwarePolicy(final Class datacenterAwarePolicy) throws NoSuchMethodException + public void setDatacenterAwarePolicy( + final Class datacenterAwarePolicy + ) throws NoSuchMethodException { myDatacenterAwarePolicy = datacenterAwarePolicy; } diff --git a/connection.impl/src/main/java/com/ericsson/bss/cassandra/ecchronos/connection/impl/builders/DistributedJmxBuilder.java b/connection.impl/src/main/java/com/ericsson/bss/cassandra/ecchronos/connection/impl/builders/DistributedJmxBuilder.java index e8879bf7d..948d2395d 100644 --- a/connection.impl/src/main/java/com/ericsson/bss/cassandra/ecchronos/connection/impl/builders/DistributedJmxBuilder.java +++ b/connection.impl/src/main/java/com/ericsson/bss/cassandra/ecchronos/connection/impl/builders/DistributedJmxBuilder.java @@ -181,7 +181,7 @@ private void reconnect(final Node node) throws IOException, EcChronosException } catch ( - AllNodesFailedException|QueryExecutionException|IOException|SecurityException e) + AllNodesFailedException | QueryExecutionException | IOException | SecurityException e) { LOG.error("Failed to create JMX connection with node {} because of {}", node.getHostId(), e.getMessage()); myEccNodesSync.updateNodeStatus(NodeStatus.UNAVAILABLE, node.getDatacenter(), node.getHostId()); From 645d2caa3cc6ea7265e01a04af10eca6a54c2b75 Mon Sep 17 00:00:00 2001 From: VictorCavichioli Date: Fri, 16 Aug 2024 11:12:41 -0300 Subject: [PATCH 4/6] Adding AllowedDC List to DatacenterAwarePolicy --- .../builders/DistributedNativeBuilder.java | 1 + .../connection/DataCenterAwarePolicy.java | 19 ++++++++++++- .../connection/TestDataCenterAwarePolicy.java | 27 ++++++++++++++----- 3 files changed, 39 insertions(+), 8 deletions(-) diff --git a/connection.impl/src/main/java/com/ericsson/bss/cassandra/ecchronos/connection/impl/builders/DistributedNativeBuilder.java b/connection.impl/src/main/java/com/ericsson/bss/cassandra/ecchronos/connection/impl/builders/DistributedNativeBuilder.java index cc748a225..39d57163f 100644 --- a/connection.impl/src/main/java/com/ericsson/bss/cassandra/ecchronos/connection/impl/builders/DistributedNativeBuilder.java +++ b/connection.impl/src/main/java/com/ericsson/bss/cassandra/ecchronos/connection/impl/builders/DistributedNativeBuilder.java @@ -339,6 +339,7 @@ private static ProgrammaticDriverConfigLoaderBuilder loaderBuilder( SCHEMA_REFRESHED_KEYSPACES); if (builder.myType.equals(ConnectionType.datacenterAware)) { + DataCenterAwarePolicy.setAllowedDcs(builder.myDatacenterAware); loaderBuilder.withString(DefaultDriverOption.LOAD_BALANCING_POLICY_CLASS, builder.myDatacenterAwarePolicy.getCanonicalName()); loaderBuilder.withInt(DefaultDriverOption.LOAD_BALANCING_DC_FAILOVER_MAX_NODES_PER_REMOTE_DC, diff --git a/connection/src/main/java/com/ericsson/bss/cassandra/ecchronos/connection/DataCenterAwarePolicy.java b/connection/src/main/java/com/ericsson/bss/cassandra/ecchronos/connection/DataCenterAwarePolicy.java index d69755132..e8e26fef0 100644 --- a/connection/src/main/java/com/ericsson/bss/cassandra/ecchronos/connection/DataCenterAwarePolicy.java +++ b/connection/src/main/java/com/ericsson/bss/cassandra/ecchronos/connection/DataCenterAwarePolicy.java @@ -52,12 +52,21 @@ public class DataCenterAwarePolicy extends DefaultLoadBalancingPolicy private final ConcurrentMap> myPerDcLiveNodes = new ConcurrentHashMap<>(); private final AtomicInteger myIndex = new AtomicInteger(); + private static List myAllowedDcs; public DataCenterAwarePolicy(final DriverContext context, final String profileName) { super(context, profileName); } + public static void setAllowedDcs(final List allowedDcs) + { + if (allowedDcs != null) + { + myAllowedDcs = allowedDcs; + } + } + @Override public final void init(final Map nodes, final DistanceReporter distanceReporter) { @@ -168,6 +177,10 @@ private Queue getQueryPlan(final String datacenter, final Set replic public NodeDistance distance(final Node node, final String dataCenter) { String dc = getDc(node); + if (!getLocalDatacenter().equals(dc) && myAllowedDcs != null && !myAllowedDcs.contains(dc)) + { + return NodeDistance.IGNORED; + } if (dc.equals(dataCenter)) { return NodeDistance.LOCAL; @@ -184,7 +197,11 @@ public NodeDistance distance(final Node node, final String dataCenter) private Queue getFallbackQueryPlan(final String dataCenter) { - CopyOnWriteArrayList localLiveNodes = myPerDcLiveNodes.get(dataCenter); + CopyOnWriteArrayList localLiveNodes = null; + if (getLocalDatacenter().equals(dataCenter) || myAllowedDcs == null || myAllowedDcs.contains(dataCenter)) + { + localLiveNodes = myPerDcLiveNodes.get(dataCenter); + } final List nodes = localLiveNodes == null ? Collections.emptyList() : cloneList(localLiveNodes); final int startIndex = myIndex.getAndIncrement(); int index = startIndex; diff --git a/connection/src/test/java/com/ericsson/bss/cassandra/ecchronos/connection/TestDataCenterAwarePolicy.java b/connection/src/test/java/com/ericsson/bss/cassandra/ecchronos/connection/TestDataCenterAwarePolicy.java index 646664b35..b5cac68be 100644 --- a/connection/src/test/java/com/ericsson/bss/cassandra/ecchronos/connection/TestDataCenterAwarePolicy.java +++ b/connection/src/test/java/com/ericsson/bss/cassandra/ecchronos/connection/TestDataCenterAwarePolicy.java @@ -31,6 +31,7 @@ import com.datastax.oss.driver.internal.core.ConsistencyLevelRegistry; import com.datastax.oss.driver.internal.core.context.InternalDriverContext; import com.datastax.oss.driver.internal.core.metadata.MetadataManager; +import java.util.*; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -38,13 +39,6 @@ import org.mockito.junit.MockitoJUnitRunner; import java.nio.ByteBuffer; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Map; -import java.util.Optional; -import java.util.Queue; -import java.util.Set; -import java.util.UUID; import java.util.concurrent.CopyOnWriteArrayList; import static org.assertj.core.api.Assertions.assertThat; @@ -57,6 +51,7 @@ public class TestDataCenterAwarePolicy { private final String myLocalDc = "DC1"; private final String myRemoteDc = "DC2"; + private final String[] myAllowedDcs = {"DC1", "DC2"}; @Mock private Session mySessionMock; @@ -138,6 +133,7 @@ public void setup() @Test public void testDistanceHost() { + DataCenterAwarePolicy.setAllowedDcs(null); DataCenterAwarePolicy policy = new DataCenterAwarePolicy(myDriverContextMock, ""); policy.init(myNodes, myDistanceReporterMock); @@ -154,6 +150,23 @@ public void testDistanceHost() assertThat(distance5).isEqualTo(NodeDistance.IGNORED); } + @Test + public void testDistanceHostWithAllowedDcs() + { + DataCenterAwarePolicy.setAllowedDcs(List.of(myAllowedDcs)); + + DataCenterAwarePolicy policy = new DataCenterAwarePolicy(myDriverContextMock, ""); + policy.init(myNodes, myDistanceReporterMock); + + NodeDistance distance1 = policy.distance(myNodeDC3Mock, myLocalDc); + NodeDistance distance2 = policy.distance(myNodeDC1Mock, myLocalDc); + NodeDistance distance3 = policy.distance(myNodeDC2Mock, myLocalDc); + + assertThat(distance1).isEqualTo(NodeDistance.IGNORED); + assertThat(distance2).isEqualTo(NodeDistance.LOCAL); + assertThat(distance3).isEqualTo(NodeDistance.REMOTE); + } + @Test public void testNewQueryPlanWithNotPartitionAwareStatement() { From f1687bdacacf00a07ebbe9157007741d44017965 Mon Sep 17 00:00:00 2001 From: VictorCavichioli Date: Fri, 16 Aug 2024 12:10:52 -0300 Subject: [PATCH 5/6] Create CustomDriverOption for PartitionAwarePolicyAllowedDCs --- .../builders/DistributedNativeBuilder.java | 4 +- .../connection/CustomDriverOption.java | 37 +++++++++++++++++++ .../connection/DataCenterAwarePolicy.java | 13 ++----- .../connection/TestDataCenterAwarePolicy.java | 6 ++- 4 files changed, 47 insertions(+), 13 deletions(-) create mode 100644 connection/src/main/java/com/ericsson/bss/cassandra/ecchronos/connection/CustomDriverOption.java diff --git a/connection.impl/src/main/java/com/ericsson/bss/cassandra/ecchronos/connection/impl/builders/DistributedNativeBuilder.java b/connection.impl/src/main/java/com/ericsson/bss/cassandra/ecchronos/connection/impl/builders/DistributedNativeBuilder.java index 39d57163f..6e7cf8e32 100644 --- a/connection.impl/src/main/java/com/ericsson/bss/cassandra/ecchronos/connection/impl/builders/DistributedNativeBuilder.java +++ b/connection.impl/src/main/java/com/ericsson/bss/cassandra/ecchronos/connection/impl/builders/DistributedNativeBuilder.java @@ -14,9 +14,9 @@ */ package com.ericsson.bss.cassandra.ecchronos.connection.impl.builders; +import com.ericsson.bss.cassandra.ecchronos.connection.CustomDriverOption; import com.ericsson.bss.cassandra.ecchronos.connection.DataCenterAwarePolicy; import com.ericsson.bss.cassandra.ecchronos.connection.impl.enums.ConnectionType; - import com.datastax.oss.driver.api.core.CqlSession; import com.datastax.oss.driver.api.core.CqlSessionBuilder; import com.datastax.oss.driver.api.core.auth.AuthProvider; @@ -339,7 +339,7 @@ private static ProgrammaticDriverConfigLoaderBuilder loaderBuilder( SCHEMA_REFRESHED_KEYSPACES); if (builder.myType.equals(ConnectionType.datacenterAware)) { - DataCenterAwarePolicy.setAllowedDcs(builder.myDatacenterAware); + loaderBuilder.withStringList(CustomDriverOption.PartitionAwarePolicyAllowedDCs, builder.myDatacenterAware); loaderBuilder.withString(DefaultDriverOption.LOAD_BALANCING_POLICY_CLASS, builder.myDatacenterAwarePolicy.getCanonicalName()); loaderBuilder.withInt(DefaultDriverOption.LOAD_BALANCING_DC_FAILOVER_MAX_NODES_PER_REMOTE_DC, diff --git a/connection/src/main/java/com/ericsson/bss/cassandra/ecchronos/connection/CustomDriverOption.java b/connection/src/main/java/com/ericsson/bss/cassandra/ecchronos/connection/CustomDriverOption.java new file mode 100644 index 000000000..276a76151 --- /dev/null +++ b/connection/src/main/java/com/ericsson/bss/cassandra/ecchronos/connection/CustomDriverOption.java @@ -0,0 +1,37 @@ +/* + * Copyright 2024 Telefonaktiebolaget LM Ericsson + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.ericsson.bss.cassandra.ecchronos.connection; + +import com.datastax.oss.driver.api.core.config.DriverOption; + +public enum CustomDriverOption implements DriverOption +{ + + PartitionAwarePolicyAllowedDCs("basic.allowed-dcs"); + + private final String myPath; + + CustomDriverOption(final String path) + { + myPath = path; + } + + @Override + public String getPath() + { + return myPath; + } + +} diff --git a/connection/src/main/java/com/ericsson/bss/cassandra/ecchronos/connection/DataCenterAwarePolicy.java b/connection/src/main/java/com/ericsson/bss/cassandra/ecchronos/connection/DataCenterAwarePolicy.java index e8e26fef0..34e2001ca 100644 --- a/connection/src/main/java/com/ericsson/bss/cassandra/ecchronos/connection/DataCenterAwarePolicy.java +++ b/connection/src/main/java/com/ericsson/bss/cassandra/ecchronos/connection/DataCenterAwarePolicy.java @@ -52,19 +52,14 @@ public class DataCenterAwarePolicy extends DefaultLoadBalancingPolicy private final ConcurrentMap> myPerDcLiveNodes = new ConcurrentHashMap<>(); private final AtomicInteger myIndex = new AtomicInteger(); - private static List myAllowedDcs; + private final List myAllowedDcs; public DataCenterAwarePolicy(final DriverContext context, final String profileName) { super(context, profileName); - } - - public static void setAllowedDcs(final List allowedDcs) - { - if (allowedDcs != null) - { - myAllowedDcs = allowedDcs; - } + myAllowedDcs = context.getConfig() + .getDefaultProfile() + .getStringList(CustomDriverOption.PartitionAwarePolicyAllowedDCs); } @Override diff --git a/connection/src/test/java/com/ericsson/bss/cassandra/ecchronos/connection/TestDataCenterAwarePolicy.java b/connection/src/test/java/com/ericsson/bss/cassandra/ecchronos/connection/TestDataCenterAwarePolicy.java index b5cac68be..890d2be69 100644 --- a/connection/src/test/java/com/ericsson/bss/cassandra/ecchronos/connection/TestDataCenterAwarePolicy.java +++ b/connection/src/test/java/com/ericsson/bss/cassandra/ecchronos/connection/TestDataCenterAwarePolicy.java @@ -122,18 +122,20 @@ public void setup() when(myDriverContextMock.getMetadataManager()).thenReturn(myMetadataManagerMock); when(myMetadataManagerMock.getMetadata()).thenReturn(myMetadataMock); when(myDriverConfigMock.getProfile(any(String.class))).thenReturn(myDriverExecutionProfileMock); + when(myDriverConfigMock.getDefaultProfile()).thenReturn(myDriverExecutionProfileMock); when(myDriverExecutionProfileMock.getName()).thenReturn("unittest"); when(myDriverExecutionProfileMock.getInt(DefaultDriverOption.LOAD_BALANCING_DC_FAILOVER_MAX_NODES_PER_REMOTE_DC)).thenReturn(999); when(myDriverExecutionProfileMock.getBoolean(DefaultDriverOption.LOAD_BALANCING_DC_FAILOVER_ALLOW_FOR_LOCAL_CONSISTENCY_LEVELS)).thenReturn(false); when(myDriverExecutionProfileMock.getString(DefaultDriverOption.REQUEST_CONSISTENCY)).thenReturn("LOCAL_QUORUM"); when(myDriverContextMock.getConsistencyLevelRegistry()).thenReturn(myConsistencyLevelRegistryMock); when(myConsistencyLevelRegistryMock.nameToLevel(any(String.class))).thenReturn(ConsistencyLevel.LOCAL_QUORUM); + when(myDriverConfigMock.getDefaultProfile().getStringList(CustomDriverOption.PartitionAwarePolicyAllowedDCs)).thenReturn(null); } @Test public void testDistanceHost() { - DataCenterAwarePolicy.setAllowedDcs(null); +// DataCenterAwarePolicy.setAllowedDcs(null); DataCenterAwarePolicy policy = new DataCenterAwarePolicy(myDriverContextMock, ""); policy.init(myNodes, myDistanceReporterMock); @@ -153,7 +155,7 @@ public void testDistanceHost() @Test public void testDistanceHostWithAllowedDcs() { - DataCenterAwarePolicy.setAllowedDcs(List.of(myAllowedDcs)); + when(myDriverConfigMock.getDefaultProfile().getStringList(CustomDriverOption.PartitionAwarePolicyAllowedDCs)).thenReturn(List.of(myAllowedDcs)); DataCenterAwarePolicy policy = new DataCenterAwarePolicy(myDriverContextMock, ""); policy.init(myNodes, myDistanceReporterMock); From 7f7b44b87a12afd68040b9ef333a3ad31e567fff Mon Sep 17 00:00:00 2001 From: VictorCavichioli Date: Tue, 20 Aug 2024 08:59:17 -0300 Subject: [PATCH 6/6] Fix Wrong Year on Headers and Add Clustering Columns on TestEccNodesSync --- .../ecchronos/application/config/security/Security.java | 2 +- .../ecchronos/application/providers/package-info.java | 2 +- .../ecchronos/application/spring/BeanConfigurator.java | 3 --- .../bss/cassandra/ecchronos/data/sync/EccNodesSync.java | 2 +- .../bss/cassandra/ecchronos/data/sync/TestEccNodesSync.java | 3 ++- 5 files changed, 5 insertions(+), 7 deletions(-) diff --git a/application/src/main/java/com/ericsson/bss/cassandra/ecchronos/application/config/security/Security.java b/application/src/main/java/com/ericsson/bss/cassandra/ecchronos/application/config/security/Security.java index 1d976ab54..bbc16c258 100644 --- a/application/src/main/java/com/ericsson/bss/cassandra/ecchronos/application/config/security/Security.java +++ b/application/src/main/java/com/ericsson/bss/cassandra/ecchronos/application/config/security/Security.java @@ -1,5 +1,5 @@ /* - * Copyright 2023 Telefonaktiebolaget LM Ericsson + * Copyright 2024 Telefonaktiebolaget LM Ericsson * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/com/ericsson/bss/cassandra/ecchronos/application/providers/package-info.java b/application/src/main/java/com/ericsson/bss/cassandra/ecchronos/application/providers/package-info.java index 02cf97b4e..e8f9abf52 100644 --- a/application/src/main/java/com/ericsson/bss/cassandra/ecchronos/application/providers/package-info.java +++ b/application/src/main/java/com/ericsson/bss/cassandra/ecchronos/application/providers/package-info.java @@ -1,5 +1,5 @@ /* - * Copyright 2023 Telefonaktiebolaget LM Ericsson + * Copyright 2024 Telefonaktiebolaget LM Ericsson * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/application/src/main/java/com/ericsson/bss/cassandra/ecchronos/application/spring/BeanConfigurator.java b/application/src/main/java/com/ericsson/bss/cassandra/ecchronos/application/spring/BeanConfigurator.java index dc769cc52..57e449bfb 100644 --- a/application/src/main/java/com/ericsson/bss/cassandra/ecchronos/application/spring/BeanConfigurator.java +++ b/application/src/main/java/com/ericsson/bss/cassandra/ecchronos/application/spring/BeanConfigurator.java @@ -266,14 +266,11 @@ private EccNodesSync getEccNodesSync( final DistributedNativeConnectionProvider distributedNativeConnectionProvider ) throws UnknownHostException, EcChronosException { - LOG.info("Creating ecChronos nodes_sync bean"); EccNodesSync myEccNodesSync = EccNodesSync.newBuilder() .withInitialNodesList(distributedNativeConnectionProvider.getNodes()) .withSession(distributedNativeConnectionProvider.getCqlSession()) .withEcchronosID(ecChronosID) .build(); - LOG.info("ecChronos nodes_sync bean created with success"); - LOG.info("Starting to acquire nodes"); myEccNodesSync.acquireNodes(); LOG.info("Nodes acquired with success"); return myEccNodesSync; diff --git a/data/src/main/java/com/ericsson/bss/cassandra/ecchronos/data/sync/EccNodesSync.java b/data/src/main/java/com/ericsson/bss/cassandra/ecchronos/data/sync/EccNodesSync.java index 976828805..4b70b0878 100644 --- a/data/src/main/java/com/ericsson/bss/cassandra/ecchronos/data/sync/EccNodesSync.java +++ b/data/src/main/java/com/ericsson/bss/cassandra/ecchronos/data/sync/EccNodesSync.java @@ -159,7 +159,7 @@ public ResultSet updateNodeStatus( ResultSet tmpResultSet = updateNodeStateStatement(nodeStatus, datacenterName, nodeID); if (tmpResultSet.wasApplied()) { - LOG.info("Node {} successfully upgraded", nodeID); + LOG.info("Node {} successfully updated", nodeID); } else { diff --git a/data/src/test/java/com/ericsson/bss/cassandra/ecchronos/data/sync/TestEccNodesSync.java b/data/src/test/java/com/ericsson/bss/cassandra/ecchronos/data/sync/TestEccNodesSync.java index df0337a51..daf0ea662 100644 --- a/data/src/test/java/com/ericsson/bss/cassandra/ecchronos/data/sync/TestEccNodesSync.java +++ b/data/src/test/java/com/ericsson/bss/cassandra/ecchronos/data/sync/TestEccNodesSync.java @@ -62,7 +62,8 @@ public void setup() throws IOException "node_status TEXT, " + "last_connection TIMESTAMP, " + "next_connection TIMESTAMP, " + - "PRIMARY KEY(ecchronos_id, datacenter_name, node_id));", + "PRIMARY KEY(ecchronos_id, datacenter_name, node_id)) " + + "WITH CLUSTERING ORDER BY( datacenter_name DESC, node_id DESC);", ECCHRONOS_KEYSPACE );