diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml index fd3e12d91..6c8ea19d1 100644 --- a/.github/workflows/docker-build.yml +++ b/.github/workflows/docker-build.yml @@ -99,7 +99,7 @@ jobs: - name: Build Docker image (Lightweight) run: | - docker build . --file sink-connector-lightweight/Dockerfile --tag altinityinfra/clickhouse-sink-connector:${{ env.IMAGE_TAG }}-lt + docker build . --file sink-connector-lightweight/Dockerfile --build-arg DOCKER_TAG=${{ env.IMAGE_TAG }} --tag altinityinfra/clickhouse-sink-connector:${{ env.IMAGE_TAG }}-lt docker save altinityinfra/clickhouse-sink-connector:${{ env.IMAGE_TAG }}-lt | gzip > clickhouse-sink-connector_${{ env.IMAGE_TAG }}-lt.tar.gz - name: Upload Docker tar (Lightweight) diff --git a/.github/workflows/testflows-sink-connector-kafka.yml b/.github/workflows/testflows-sink-connector-kafka.yml index ac2963adf..bcea19c6d 100644 --- a/.github/workflows/testflows-sink-connector-kafka.yml +++ b/.github/workflows/testflows-sink-connector-kafka.yml @@ -1,4 +1,5 @@ name: Kafka - TestFlows Tests +run-name: ${{ inputs.custom_run_name || 'Kafka - TestFlows Tests' }} on: workflow_call: @@ -7,6 +8,14 @@ on: description: "Kafka connector docker image" required: true type: string + package: + description: "Package either 'docker://' or 'https://'. Example: 'https://s3.amazonaws.com/clickhouse-builds/23.3/.../package_release/clickhouse-common-static_23.3.1.64_amd64.deb', or 'docker://altinity/clickhouse-server:23.8.8'" + type: string + default: docker://clickhouse/clickhouse-server:23.3 + output_format: + description: "Testflows output style." + type: string + default: new-fails secrets: DOCKERHUB_USERNAME: required: false @@ -22,7 +31,37 @@ on: description: "Kafka connector docker image" required: true type: string - + package: + description: "Package either 'docker://' or 'https://'. Example: 'https://s3.amazonaws.com/clickhouse-builds/23.3/.../package_release/clickhouse-common-static_23.3.1.64_amd64.deb', or 'docker://altinity/clickhouse-server:23.8.8'" + type: string + default: docker://clickhouse/clickhouse-server:23.3 + extra_args: + description: "Specific Suite To Run (Default * to run everything)." + required: false + type: string + custom_run_name: + description: 'Custom run name (optional)' + required: false + output_format: + description: "Testflows output style." + type: choice + options: + - new-fails + - nice-new-fails + - brisk-new-fails + - plain-new-fails + - pnice-new-fails + - classic + - nice + - fails + - slick + - brisk + - quiet + - short + - manual + - dots + - progress + - raw env: SINK_CONNECTOR_IMAGE: ${{ inputs.SINK_CONNECTOR_IMAGE }} @@ -75,7 +114,7 @@ jobs: - name: Run testflows tests working-directory: sink-connector/tests/integration - run: python3 -u regression.py --only "/mysql to clickhouse replication/*" --clickhouse-binary-path=docker://clickhouse/clickhouse-server:22.8 --test-to-end -o classic --collect-service-logs --attr project="${GITHUB_REPOSITORY}" project.id="$GITHUB_RUN_NUMBER" user.name="$GITHUB_ACTOR" github_actions_run="$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID" sink_version="altinity/clickhouse-sink-connector:${SINK_CONNECTOR_VERSION}" s3_url="https://altinity-test-reports.s3.amazonaws.com/index.html#altinity-sink-connector/testflows/${{ steps.date.outputs.date }}_${{github.run.number}}/" --log logs/raw.log + run: python3 -u regression.py --only "/regression/${{ inputs.extra_args != '' && inputs.extra_args || '*' }}" --clickhouse-binary-path="${{inputs.package}}" --test-to-end --output ${{ inputs.output_format }} --collect-service-logs --attr project="${GITHUB_REPOSITORY}" project.id="$GITHUB_RUN_NUMBER" user.name="$GITHUB_ACTOR" github_actions_run="$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID" sink_version="altinity/clickhouse-sink-connector:${SINK_CONNECTOR_VERSION}" s3_url="https://altinity-test-reports.s3.amazonaws.com/index.html#altinity-sink-connector/testflows/${{ steps.date.outputs.date }}_${{github.run.number}}/" --log logs/raw.log - name: Create tfs results report if: always() diff --git a/.github/workflows/testflows-sink-connector-lightweight.yml b/.github/workflows/testflows-sink-connector-lightweight.yml index db7e320df..c0da3df16 100644 --- a/.github/workflows/testflows-sink-connector-lightweight.yml +++ b/.github/workflows/testflows-sink-connector-lightweight.yml @@ -12,6 +12,10 @@ on: description: "Package either 'docker://' or 'https://'. Example: 'https://s3.amazonaws.com/clickhouse-builds/23.3/.../package_release/clickhouse-common-static_23.3.1.64_amd64.deb', or 'docker://altinity/clickhouse-server:23.8.8'" type: string default: docker://clickhouse/clickhouse-server:23.3 + output_format: + description: "Testflows output style." + type: string + default: new-fails secrets: DOCKERHUB_USERNAME: required: false @@ -38,6 +42,26 @@ on: custom_run_name: description: 'Custom run name (optional)' required: false + output_format: + description: "Testflows output style." + type: choice + options: + - new-fails + - nice-new-fails + - brisk-new-fails + - plain-new-fails + - pnice-new-fails + - classic + - nice + - fails + - slick + - brisk + - quiet + - short + - manual + - dots + - progress + - raw env: SINK_CONNECTOR_IMAGE: ${{ inputs.SINK_CONNECTOR_IMAGE }} @@ -91,7 +115,7 @@ jobs: - name: Run testflows tests working-directory: sink-connector-lightweight/tests/integration - run: python3 -u regression.py --only "/mysql to clickhouse replication/auto table creation/${{ inputs.extra_args != '' && inputs.extra_args || '*' }}" --clickhouse-binary-path="${{inputs.package}}" --test-to-end -o classic --collect-service-logs --attr project="${GITHUB_REPOSITORY}" project.id="$GITHUB_RUN_NUMBER" user.name="$GITHUB_ACTOR" github_actions_run="$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID" sink_version="registry.gitlab.com/altinity-public/container-images/clickhouse_debezium_embedded:latest" s3_url="https://altinity-test-reports.s3.amazonaws.com/index.html#altinity-sink-connector/testflows/${{ steps.date.outputs.date }}_${{github.run.number}}/" --log logs/raw.log + run: python3 -u regression.py --only "/mysql to clickhouse replication/auto table creation/${{ inputs.extra_args != '' && inputs.extra_args || '*' }}" --clickhouse-binary-path="${{inputs.package}}" --test-to-end --output ${{ inputs.output_format }} --collect-service-logs --attr project="${GITHUB_REPOSITORY}" project.id="$GITHUB_RUN_NUMBER" user.name="$GITHUB_ACTOR" github_actions_run="$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID" sink_version="registry.gitlab.com/altinity-public/container-images/clickhouse_debezium_embedded:latest" s3_url="https://altinity-test-reports.s3.amazonaws.com/index.html#altinity-sink-connector/testflows/${{ steps.date.outputs.date }}_${{github.run.number}}/" --log logs/raw.log - name: Create tfs results report if: always() @@ -169,7 +193,7 @@ jobs: - name: Run testflows tests working-directory: sink-connector-lightweight/tests/integration - run: python3 -u regression.py --only "/mysql to clickhouse replication/auto replicated table creation/${{ inputs.extra_args != '' && inputs.extra_args || '*' }}" --clickhouse-binary-path="${{inputs.package}}" --test-to-end -o classic --collect-service-logs --attr project="${GITHUB_REPOSITORY}" project.id="$GITHUB_RUN_NUMBER" user.name="$GITHUB_ACTOR" github_actions_run="$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID" sink_version="registry.gitlab.com/altinity-public/container-images/clickhouse_debezium_embedded:latest" s3_url="https://altinity-test-reports.s3.amazonaws.com/index.html#altinity-sink-connector/testflows/${{ steps.date.outputs.date }}_${{github.run.number}}/" --log logs/raw.log + run: python3 -u regression.py --only "/mysql to clickhouse replication/auto replicated table creation/${{ inputs.extra_args != '' && inputs.extra_args || '*' }}" --clickhouse-binary-path="${{inputs.package}}" --test-to-end --output ${{ inputs.output_format }} --collect-service-logs --attr project="${GITHUB_REPOSITORY}" project.id="$GITHUB_RUN_NUMBER" user.name="$GITHUB_ACTOR" github_actions_run="$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID" sink_version="registry.gitlab.com/altinity-public/container-images/clickhouse_debezium_embedded:latest" s3_url="https://altinity-test-reports.s3.amazonaws.com/index.html#altinity-sink-connector/testflows/${{ steps.date.outputs.date }}_${{github.run.number}}/" --log logs/raw.log - name: Create tfs results report if: always() diff --git a/README.md b/README.md index 987fff9b3..031ae7bce 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ [![License](http://img.shields.io/:license-apache%202.0-brightgreen.svg)](http://www.apache.org/licenses/LICENSE-2.0.html) [![Sink Connector(Kafka version) tests](https://github.com/Altinity/clickhouse-sink-connector/actions/workflows/sink-connector-kafka-tests.yml/badge.svg)](https://github.com/Altinity/clickhouse-sink-connector/actions/workflows/sink-connector-kafka-tests.yml) [![Sink Connector(Light-weight) Tests](https://github.com/Altinity/clickhouse-sink-connector/actions/workflows/sink-connector-lightweight-tests.yml/badge.svg)](https://github.com/Altinity/clickhouse-sink-connector/actions/workflows/sink-connector-lightweight-tests.yml) - + AltinityDB Slack Docker Pulls @@ -16,7 +16,7 @@ for analysis. ## Features -* Initial data dump and load +* [Initial data dump and load(MySQL)](sink-connector/python/README.md) * Change data capture of new transactions using [Debezium](https://debezium.io/) * Automatic loading into ClickHouse * Sources: Support for MySQL, PostgreSQL (other databases experimental) @@ -55,9 +55,12 @@ First two are good tutorials on MySQL and PostgreSQL respectively. * [ClickHouse Table Engine Types](doc/clickhouse_engines.md) * [Troubleshooting](doc/Troubleshooting.md) * [TimeZone and DATETIME/TIMESTAMP](doc/timezone.md) +* [Replication Start Position](doc/replication_start_position.md) * [Logging](doc/logging.md) * [Production Setup](doc/production_setup.md) * [Adding new tables(Incremental Snapshot)](doc/incremental_snapshot.md) +* [Configuration](doc/configuration.md) +* [State Storage](doc/state_storage.md) ### Operations @@ -100,6 +103,6 @@ to ClickHouse and analytic applications built on ClickHouse. - [Official website](https://altinity.com/) - Get a high level overview of Altinity and our offerings. - [Altinity.Cloud](https://altinity.com/cloud-database/) - Run ClickHouse in our cloud or yours. - [Altinity Support](https://altinity.com/support/) - Get Enterprise-class support for ClickHouse and Sink Connector. -- [Slack](https://altinitydbworkspace.slack.com/join/shared_invite/zt-1togw9b4g-N0ZOXQyEyPCBh_7IEHUjdw#/shared-invite/email) - Talk directly with ClickHouse users and Altinity devs. +- [Slack](https://altinity.com/slack) - Talk directly with ClickHouse users and Altinity devs. - [Contact us](https://hubs.la/Q020sH3Z0) - Contact Altinity with your questions or issues. - [Free consultation](https://hubs.la/Q020sHkv0) - Get a free consultation with a ClickHouse expert today. diff --git a/doc/Monitoring.md b/doc/Monitoring.md index acf4ef1f7..d75ea213e 100644 --- a/doc/Monitoring.md +++ b/doc/Monitoring.md @@ -77,7 +77,14 @@ record_insert_seq: ```select event_time, database, table, rows, duration_ms,size_in_bytes from system.part_log where table='table' and event_type='NewPart' and event_time > now () - interval 30 minute and database='db' ;``` ## Grafana Dashboard -JMX metrics of sink connector are exposed through the port +JMX metrics of sink connector are exposed through the port.(Default: 8084) +If you need to override the port, add `metrics.port` in config.yml + +``` +metrics.enable: "true" +metrics.port: 8085 +``` + The JMX_exporter docker image scrapes the JMX metrics from the sink connector The metrics can be read through the following URL diff --git a/doc/Troubleshooting.md b/doc/Troubleshooting.md index e839c06bd..6a38d4e6c 100644 --- a/doc/Troubleshooting.md +++ b/doc/Troubleshooting.md @@ -55,3 +55,6 @@ https://stackoverflow.com/questions/63523998/multiple-debezium-connector-for-one ### PostgreSQL - ERROR - Error starting connectorio.debezium.DebeziumException: Creation of replication slot failed; when setting up multiple connectors for the same database host, please make sure to use a distinct replication slot name for each. Make sure to add `slot.name` to the configuration(config.yml) and change it to a unique name. + +### PostgreSQL (WAL size growing) +[Handling PostgreSQL WAL Growth with Debezium Connectors](doc/postgres_wal_growth.md) \ No newline at end of file diff --git a/doc/configuration.md b/doc/configuration.md index 53a218ca0..c333e642f 100644 --- a/doc/configuration.md +++ b/doc/configuration.md @@ -1,34 +1,35 @@ ### Configuration Reference - Configuration | Description | -|----------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| database.hostname | Source Database HostName | -| database.port | Source Database Port number | -| database.user | Source Database Username(user needs to have replication permission, Refer https://debezium.io/documentation/reference/stable/connectors/mysql.html) GRANT SELECT, RELOAD, SHOW DATABASES, REPLICATION SLAVE, REPLICATION CLIENT ON *.* TO 'user' IDENTIFIED BY 'password'; | -| database.password | Source Database Password | -| database.include.list | List of databases to be included in replication. | -| table.include.list | List of tables to be included in replication. MySQL(db_name.table_name), PostgreSQL(schema_name.table_name) | -| clickhouse.server.url | ClickHouse URL, For TLS(use `https` and set port to `8443`) | -| clickhouse.server.user | ClickHouse username | -| clickhouse.server.password | ClickHouse password | -| clickhouse.server.port | ClickHouse port, For TLS(use the correct port `8443` or `443` | -| snapshot.mode | "initial" -> Data that already exists in source database will be replicated. "schema_only" -> Replicate data that is added/modified after the connector is started.\
MySQL: https://debezium.io/documentation/reference/stable/connectors/mysql.html#mysql-property-snapshot-mode \
PostgreSQL: https://debezium.io/documentation/reference/stable/connectors/postgresql.html#postgresql-property-snapshot-mode
MongoDB: initial, never. https://debezium.io/documentation/reference/stable/connectors/mongodb.html | -| connector.class | MySQL -> "io.debezium.connector.mysql.MySqlConnector"
PostgreSQL ->
Mongo ->
| -| offset.storage.file.filename | Offset storage file(This stores the offsets of the source database) MySQL: mysql binlog file and position, gtid set. Make sure this file is durable and its not persisted in temp directories. | -| database.history.file.filename | Database History: Make sure this file is durable and its not persisted in temp directories. | -| schema.history.internal.file.filename | Schema History: Make sure this file is durable and its not persisted in temp directories. | -| disable.ddl | **Optional**, Default: false, if DDL execution needs to be disabled | -| enable.ddl.snapshot | **Optional**, Default: false, If set to true, the DDL that is passed as part of snapshot process will be executed. Default behavior is DROP/TRUNCATE as part of snapshot is disabled. | -| database.allowPublicKeyRetrieval | **Optional**, MySQL specific: true/false | -| auto.create.tables | When True, connector will create tables(transformed DDL from source) | -| persist.raw.bytes | Debezium.BYTES data(usually UUID) is persisted as raw bytes(CH String) if set to true. | -| database.connectionTimeZone | Example: "US/Samoa, Specify MySQL timezone for DATETIME conversions.https://debezium.io/documentation/reference/stable/connectors/mysql.html#mysql-temporal-types | -| enable.snapshot.ddl | When true, pre-existing DDL statements from source(MySQL) will be executed. Warning: This might run DROP TABLE commands. | -| clickhouse.datetime.timezone | Override timezone for DateTime columns in ClickHouse server. | -| skip_replica_start | If set to true, replication is not started, the user is expected to start replication with the sink-connector-client program. | -| restart.event.loop | If set to true, replication will be re-started based on the restart.event.loop.timeout.period parameter(which is defined in seconds) | -| restart.event.loop.timeout.period.secs | If the last change record(CDC) received from source database exceeds this threshold period defined in seconds, then replication is restarted. | -| batch.max.records | Size of the batch that is persisted to ClickHouse.(Default 100000) | -| sink.connector.max.queue.size | Size of the Queue(in Memory) that holds the CDC records(Use a lower number for Out of Memory exceptions) | -| schema.history.internal.store.only.captured.tables.ddl | Works with table.include.list/table.exclude.list , set this to true to avoid debezium capturing schemas of all tables. | -| schema.history.internal.store.only.captured.databases.ddl | Works with table.include.list/table.exclude.list , set this to true to avoid debezium capturing schemas of all databases. | + Configuration | Description | +|-----------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| database.hostname | Source Database HostName | +| database.port | Source Database Port number | +| database.user | Source Database Username(user needs to have replication permission, Refer https://debezium.io/documentation/reference/stable/connectors/mysql.html) GRANT SELECT, RELOAD, SHOW DATABASES, REPLICATION SLAVE, REPLICATION CLIENT ON *.* TO 'user' IDENTIFIED BY 'password'; | +| database.password | Source Database Password | +| database.include.list | List of databases to be included in replication. | +| table.include.list | List of tables to be included in replication. MySQL(db_name.table_name), PostgreSQL(schema_name.table_name) | +| clickhouse.server.url | ClickHouse URL, For TLS(use `https` and set port to `8443`) | +| clickhouse.server.user | ClickHouse username | +| clickhouse.server.password | ClickHouse password | +| clickhouse.server.port | ClickHouse port, For TLS(use the correct port `8443` or `443` | +| snapshot.mode | "initial" -> Data that already exists in source database will be replicated. "schema_only" -> Replicate data that is added/modified after the connector is started.\
MySQL: https://debezium.io/documentation/reference/stable/connectors/mysql.html#mysql-property-snapshot-mode \
PostgreSQL: https://debezium.io/documentation/reference/stable/connectors/postgresql.html#postgresql-property-snapshot-mode
MongoDB: initial, never. https://debezium.io/documentation/reference/stable/connectors/mongodb.html | +| connector.class | MySQL -> "io.debezium.connector.mysql.MySqlConnector"
PostgreSQL ->
Mongo ->
| +| offset.storage.file.filename | Offset storage file(This stores the offsets of the source database) MySQL: mysql binlog file and position, gtid set. Make sure this file is durable and its not persisted in temp directories. | +| database.history.file.filename | Database History: Make sure this file is durable and its not persisted in temp directories. | +| schema.history.internal.file.filename | Schema History: Make sure this file is durable and its not persisted in temp directories. | +| disable.ddl | **Optional**, Default: false, if DDL execution needs to be disabled | +| enable.ddl.snapshot | **Optional**, Default: false, If set to true, the DDL that is passed as part of snapshot process will be executed. Default behavior is DROP/TRUNCATE as part of snapshot is disabled. | +| database.allowPublicKeyRetrieval | **Optional**, MySQL specific: true/false | +| auto.create.tables | When True, connector will create tables(transformed DDL from source) | +| persist.raw.bytes | Debezium.BYTES data(usually UUID) is persisted as raw bytes(CH String) if set to true. | +| database.connectionTimeZone | Example: "US/Samoa, Specify MySQL timezone for DATETIME conversions.https://debezium.io/documentation/reference/stable/connectors/mysql.html#mysql-temporal-types | +| enable.snapshot.ddl | When true, pre-existing DDL statements from source(MySQL) will be executed. Warning: This might run DROP TABLE commands. | +| clickhouse.datetime.timezone | Override timezone for DateTime columns in ClickHouse server. | +| skip_replica_start | If set to true, replication is not started, the user is expected to start replication with the sink-connector-client program. | +| restart.event.loop | If set to true, replication will be re-started based on the restart.event.loop.timeout.period parameter(which is defined in seconds) | +| restart.event.loop.timeout.period.secs | If the last change record(CDC) received from source database exceeds this threshold period defined in seconds, then replication is restarted. | +| batch.max.records | Size of the batch that is persisted to ClickHouse.(Default 100000) | +| sink.connector.max.queue.size | Size of the Queue(in Memory) that holds the CDC records(Use a lower number for Out of Memory exceptions) | +| schema.history.internal.store.only.captured.tables.ddl | Works with table.include.list/table.exclude.list , set this to true to avoid debezium capturing schemas of all tables. | +| schema.history.internal.store.only.captured.databases.ddl | Works with table.include.list/table.exclude.list , set this to true to avoid debezium capturing schemas of all databases. | +| single.threaded | False(Default), This mode skips the entire sink connector thread pool/queue and inserts records in batches on a single thread(Refer doc/production_setup.md) | diff --git a/doc/getting_started_jar.md b/doc/getting_started_jar.md new file mode 100644 index 000000000..a6dce37b0 --- /dev/null +++ b/doc/getting_started_jar.md @@ -0,0 +1,30 @@ +# QuickStart Installation for Lightweight Sink Connector(JAR file) + +This is the recommended path for initial use. It uses PostgreSQL as the source database and brings up a full stack with +PostgreSQL, ClickHouse, Lightweight Sink Connector, and Grafana. Example shown here is for Ubuntu but will work for any Linux or MacOS provided prerequisites are installed. + + +### Start PostgreSQL and ClickHouse in docker-compose. +``` +cd sink-connector-lightweight/docker +docker-compose -f docker-compose-postgres-standalone.yml up +``` +This will start PostgreSQL and ClickHouse in docker-compose. + +For simplicity, the configuration file for sink connector (`docker/config_postgres_local.yml`) +is setup to connect to the PostgreSQL and ClickHouse services started by docker compose. + +### Start the Lightweight Sink Connector(JAR file) +``` +java -jar target/clickhouse-debezium-embedded-0.0.4.jar docker/config_postgres_local.yml +``` + +By default the replication is setup to replicate two tables, +in the sink connector logs you will notice the following lines that +highlight the newer records that are inserted to ClickHouse. + +``` +2024-08-14 18:44:40.613 INFO - *** INSERT QUERY for Database(public) ***: insert into `tm`(`id`,`secid`,`acc_id`,`ccatz`,`tcred`,`am`,`set_date`,`created`,`updated`,`events_id`,`events_transaction_id`,`events_status`,`events_payment_snapshot`,`events_created`,`vid`,`vtid`,`vstatus`,`vamount`,`vcreated`,`vbilling_currency`,`_version`,`_sign`) select `id`,`secid`,`acc_id`,`ccatz`,`tcred`,`am`,`set_date`,`created`,`updated`,`events_id`,`events_transaction_id`,`events_status`,`events_payment_snapshot`,`events_created`,`vid`,`vtid`,`vstatus`,`vamount`,`vcreated`,`vbilling_currency`,`_version`,`_sign` from input('`id` UUID,`secid` Nullable(UUID),`acc_id` Nullable(UUID),`ccatz` Nullable(String),`tcred` Nullable(Bool),`am` Nullable(Decimal(21, 5)),`set_date` Nullable(String),`created` Nullable(String),`updated` Nullable(String),`events_id` Nullable(UUID),`events_transaction_id` Nullable(UUID),`events_status` Nullable(String),`events_payment_snapshot` Nullable(String),`events_created` Nullable(String),`vid` Nullable(UUID),`vtid` Nullable(UUID),`vstatus` Nullable(String),`vamount` Nullable(Decimal(21, 5)),`vcreated` Nullable(String),`vbilling_currency` Nullable(String),`_version` UInt64,`_sign` UInt8') +2024-08-14 18:44:40.625 INFO - *************** EXECUTED BATCH Successfully Records: 2************** task(0) Thread ID: Sink Connector thread-pool-5 Result: [I@14e8dc94 Database: public Table: tm + +``` \ No newline at end of file diff --git a/doc/img/lightweight_main.drawio b/doc/img/lightweight_main.drawio new file mode 100644 index 000000000..b025fbca6 --- /dev/null +++ b/doc/img/lightweight_main.drawio @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/doc/img/single_threaded.drawio b/doc/img/single_threaded.drawio new file mode 100644 index 000000000..9a2dca378 --- /dev/null +++ b/doc/img/single_threaded.drawio @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/doc/img/single_threaded.jpg b/doc/img/single_threaded.jpg new file mode 100644 index 000000000..cb6372e20 Binary files /dev/null and b/doc/img/single_threaded.jpg differ diff --git a/doc/img/state_storage.jpg b/doc/img/state_storage.jpg new file mode 100644 index 000000000..08d723a83 Binary files /dev/null and b/doc/img/state_storage.jpg differ diff --git a/doc/postgres_wal_growth.md b/doc/postgres_wal_growth.md new file mode 100644 index 000000000..5757b1580 --- /dev/null +++ b/doc/postgres_wal_growth.md @@ -0,0 +1,41 @@ +## Handling PostgreSQL WAL Growth with Debezium Connectors +Credits: https://medium.com/@pawanpg0963/postgres-replication-lag-using-debezium-connector-4ba50e330cd6 + +One of the common problems with PostgreSQL is the WAL size increasing. This issue can be observed when using Debezium connectors for change data capture. +The WAL size increases due to the connector not sending any data to the replication slot. +This can be observed by checking the replication slot lag using the following query: +```sql +postgres=# SELECT slot_name, pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn)) AS replicationSlotLag, +pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(), confirmed_flush_lsn)) AS confirmedLag, active FROM pg_replication_slots; + slot_name | replicationslotlag | confirmedlag | active +-----------+--------------------+--------------+-------- + db1_slot | 20 GB | 16 GB | t + db2_slot | 62 MB | 42 MB | t +(2 rows) +``` + +This issue can be addressed using the `heartbeat.interval.ms` configuration + +### Solution +Create a new table in postgres (Heartbeat) table. +```sql +CREATE TABLE heartbeat.pg_heartbeat ( +random_text TEXT, +last_update TIMESTAMP +); +``` +Add the table to the existing publisher used by the connector: +```sql +INSERT INTO heartbeat.pg_heartbeat (random_text, last_update) VALUES ('test_heartbeat', NOW()); +``` +Add the table to the existing publisher used by the connector: +``` +ALTER PUBLICATION db1_pub ADD TABLE heartbeat.pg_heartbeat; +ALTER PUBLICATION db2_pub ADD TABLE heartbeat.pg_heartbeat; +``` +Grant privileges to the schema heartbeat and table pg_heartbeat to the replication user used by the connector. +Add the following configuration to `config.yml` +``` +heartbeat.interval.ms=10000 +heartbeat.action.query="UPDATE heartbeat.pg_heartbeat SET last_update=NOW();" +``` \ No newline at end of file diff --git a/doc/production_setup.md b/doc/production_setup.md index 5b97ba016..b15db2112 100644 --- a/doc/production_setup.md +++ b/doc/production_setup.md @@ -1,9 +1,15 @@ ## Production setup -![](img/production_setup.jpg) -### Improving throughput and/or Memory usage. +[Throughput & Memory Usage](#improving-throughput-and/or-memory-usage.) \ +[Low Memory environments(5GB)](#low-memory-environments5gb) \ +[Initial Load](#initial-load) \ +[MySQL Setup](#mysql-production-setup) \ +[PostgreSQL Setup](#postgresql-production-setup) \ +[ClickHouse Setup](#clickhouse-setup) +### Improving throughput and/or Memory usage. +![](img/production_setup.jpg) As detailed in the diagram above, there are components that store the messages and can be configured to improve throughput and/or memory usage. @@ -43,7 +49,17 @@ in terms of number of elements the queue can hold and the maximum size of the qu buffer.flush.time.ms: "1000" ``` -## Snapshots (Out of Memory) +## Low Memory environments(5GB) +The suggested configuration for a low memory environment is as follows to use a single threaded configuration. +Single threaded configuration can be enabled in `config.yml` +``` +single.threaded: "true" +``` +As shown in the diagram below, the Single threaded configuration will skip the sink connector queue and threadpool +and will insert batches directly from the debezium queue. +![](img/single_threaded.jpg) + +## Initial Load The following parameters might be useful to reduce the memory usage of the connector during the snapshotting phase. @@ -62,3 +78,66 @@ The maximum number of rows that the connector fetches and reads into memory when **snapshot.max.threads**: Increase this number from 1 to a higher value to enable parallel snapshotting. + +## MySQL Production Setup +# How to Reproduce + +1. Replicate a table only in `config.yml`: + + ```yaml + table.include.list: "mydb.mytable" + ``` + +2. Do not write to the table on the source database side. +3. Monitor the lag: + + ```sql + select * from altinity_sink_connector.show_replica_status\G + ``` + +4. The lag increases if this table does not get written and the binary log position does not move. It should be synced periodically to show the binary log progress. + +# Workaround + +Include a heartbeat table (see [Percona Toolkit - pt-heartbeat](https://docs.percona.com/percona-toolkit/pt-heartbeat.html)): + +### Example + +```sql +CREATE TABLE pt_heartbeat_db.heartbeat ( + id int NOT NULL PRIMARY KEY, + ts datetime NOT NULL +); +======= +**Single Threaded (Low Memory/Slow replication)**: +By setting the `single.threaded: true` configuration variable in `config.yml`, the replication will skip the sink connector queue and threadpool +and will insert batches directly from the debezium queue. +This mode will work on lower memory setup but will increase the replication speed. + +## ClickHouse Setup +The clickhouse user needs to have the following GRANTS to the +offset storage/schema history database(database provided in `offset.storage.jdbc.` configuration variable. +and the database that is replicated(database provided in `database.include.list` and `table.include.list`) + +The following example creates user `sink` with necessary GRANTS +to the offset storage/schema history database and replicated databases. +``` +ALTER SETTINGS PROFILE 'ingest' SETTINGS + deduplicate_blocks_in_dependent_materialized_views=1, + min_insert_block_size_rows_for_materialized_views=10000, + throw_on_max_partitions_per_insert_block=0, + max_partitions_per_insert_block=1000, + date_time_input_format='best_effort'; + +CREATE USER OR REPLACE 'sink' IDENTIFIED WITH sha256_hash BY '' HOST IP '::/8' SETTINGS PROFILE 'ingest'; +grant SELECT, INSERT, CREATE TABLE, CREATE DATABASE on altinity.* to sink; +grant CLUSTER ON *.* to sink; +grant SELECT, INSERT, CREATE TABLE, TRUNCATE on replicated_db.* to sink; +``` +**User Profile** +`max_partitions_per_insert_block` - The default value is 100, its recommended to set this value **1000** + +## PostgreSQL Production Setup + +One of the common problems with PostgreSQL is the WAL size increasing. +[Handling PostgreSQL WAL Growth with Debezium Connectors](postgres_wal_growth.md) diff --git a/doc/quickstart.md b/doc/quickstart.md index d699023ae..728b28bc0 100644 --- a/doc/quickstart.md +++ b/doc/quickstart.md @@ -28,10 +28,16 @@ sudo apt install clickhouse-client ## Start the stack -Use Docker Compose to start containers. +Use Docker Compose to start containers. +Set the `CLICKHOUSE_SINK_CONNECTOR_LT_IMAGE` to the latest release from the Releases page. +or run `./getLatestTag.sh` which will set the environment variable + ``` cd sink-connector-lightweight/docker -export CLICKHOUSE_SINK_CONNECTOR_LT_IMAGE=altinity/clickhouse-sink-connector:2.1.0-lt +./getLatestTag.sh +``` + +``` docker compose -f docker-compose-mysql.yml up --renew-anon-volumes ``` @@ -81,6 +87,9 @@ docker compose -f docker-compose-mysql.yml down ``` ### Connecting to External MySQL/ClickHouse +##### Pre-requisites +Make sure MySQL is setup with binlogs enabled and the MySQL replication user has the required grants. +https://debezium.io/documentation/reference/stable/connectors/mysql.html#setting-up-mysql **Step 1:** Update **MySQL** information in config.yaml(https://github.com/Altinity/clickhouse-sink-connector/blob/develop/sink-connector-lightweight/docker/config.yml ): @@ -136,6 +145,14 @@ offset.storage.jdbc.url: "jdbc:clickhouse://cloud_url:8443/altinity_sink_connect schema.history.internal.jdbc.url: "jdbc:clickhouse://cloud_url:8443/altinity_sink_connector?ssl=true" ``` +**Step 7:** Start sink connector. +After https://github.com/Altinity/clickhouse-sink-connector/blob/develop/sink-connector-lightweight/docker/config.yml is updated with both MySQL and ClickHouse information, start the sink connector service + +``` +docker-compose -f docker-compose-mysql-standalone.yml up + +``` + ## References: 1. [Sink Connector Configuration ](configuration.md) 2. [MySQL Topologies supported](https://debezium.io/documentation/reference/2.5/connectors/mysql.html#setting-up-mysql) diff --git a/doc/quickstart_ms_sql_server.md b/doc/quickstart_ms_sql_server.md new file mode 100644 index 000000000..e69de29bb diff --git a/doc/quickstart_postgres.md b/doc/quickstart_postgres.md index 59ce41393..f2a7f61e7 100644 --- a/doc/quickstart_postgres.md +++ b/doc/quickstart_postgres.md @@ -4,6 +4,10 @@ Use [Docker Compose](https://docs.docker.com/compose/) to bring up a complete configuration that illustrates operation of Altinity Sink Connector. +If you are not able to use Docker, you +can use the JAR file from releases. +Documentation to use the JAR file is [here](getting_started_jar.md). + # QuickStart Installation for Lightweight Sink Connector This is the recommended path for initial use. It uses PostgreSQL as the @@ -22,7 +26,18 @@ Install Docker and Docker Compose. ## Start the stack -Use Docker Compose to start containers. Pick the latest released tag. +Use Docker Compose to start containers. Set the CLICKHOUSE_SINK_CONNECTOR_LT_IMAGE to the latest release from the Releases page. or run ./getLatestTag.sh which will set the environment variable + +``` +export CLICKHOUSE_SINK_CONNECTOR_LT_IMAGE=altinity/clickhouse-sink-connector:2.3.0-lt +``` + +or +``` +cd sink-connector-lightweight/docker +./getLatestTag.sh +``` + ``` cd sink-connector-lightweight/docker export CLICKHOUSE_SINK_CONNECTOR_LT_IMAGE=altinity/clickhouse-sink-connector:2.0.0-lt @@ -147,4 +162,4 @@ schema.history.internal.jdbc.url: "jdbc:clickhouse://cloud_url:8443/altinity_sin 2. [PostgreSQL Setup](https://debezium.io/documentation/reference/2.5/connectors/postgresql.html#setting-up-postgresql) 3. For AWS RDS users, you might need to add heartbeat interval and query to avoid WAL logs constantly growing in size. https://stackoverflow.com/questions/76415644/postgresql-wal-log-limiting-rds - https://debezium.io/documentation/reference/stable/connectors/postgresql.html#postgresql-wal-disk-space \ No newline at end of file + https://debezium.io/documentation/reference/stable/connectors/postgresql.html#postgresql-wal-disk-space diff --git a/doc/replication_start_position.md b/doc/replication_start_position.md new file mode 100644 index 000000000..7e283fd72 --- /dev/null +++ b/doc/replication_start_position.md @@ -0,0 +1,23 @@ +## Replication Start Position + + + +#### Setting MySQL binlog position/file. + +Using the sink connector client(`sink-connector-cli`) you can specify the replication start position. +``` +./sink-connector-client change_replication_source --binlog_file binary.074080 +--binlog_position 4 --source_host localhost --source_port 3306 --source_username root --source_password root +``` + +#### Setting Postgres(LSN) position. +The lsn needs to be converted from hex to number, the part after the slash. + +Example: 0/**1A371A0** to **27488672** + +``` +sink-connector-client lsn -- lsn 27488672 +``` + +Running the above commands will set the replication start position for the sink connector +in the `source_replica_info` Debezium will use this information to start the replication from the specified position. \ No newline at end of file diff --git a/doc/state_storage.md b/doc/state_storage.md new file mode 100644 index 000000000..6a54164cb --- /dev/null +++ b/doc/state_storage.md @@ -0,0 +1,54 @@ +## State Storage + +Sink connector is designed to store data about the replication state. +Currently the only supported state storage is by persisting the information in ClickHouse tables. + +### ClickHouse State Storage + +To use ClickHouse as a state storage, you need to specify the following configuration properties: + +![State Storage](img/state_storage.jpg) + +# Offsets table(MySQL) +The offsets table defined by the `offset.storage.jdbc.offset.table.name` +Default: **"altinity_sink_connector.replica_source_info"** +This table is used to store the binlog file, position and gtids. +| Column Name | Description | Example | +|-------------|----------------------------------------------------------------------|---------| +| id | UUID | | +| offset_key | This is the Unique key for every connector. Its a combination of `name` configuration variable and the `topic.prefix` configuration variable | [\"debezium-embedded-postgres\",{\"server\":\"embeddedconnector\"}]"| +| offset_val | This column stores the offset information for MySQL, **file**- binlog file, **pos**- binlog position, **gtids**- GTID | {"ts_sec":1724849901,"file":"mysql-bin.000003","pos":197,"gtids":"03d24fcc-6567-11ef-9978-0242ac130003:1-56"} +| record_insert_seq | Timestamp when record is inserted. | 2024-08-28 12:58:22 | +| record_insert_ts | Monotonically increasing number | 174 | + +# Schema History table(MySQL) +The schema history table defined by the `schema.history.internal.jdbc.schema.history.table.name` +Default: **"altinity_sink_connector.replicate_schema_history"** +This table is used by Debezium to store historical DDL statements so the DDL statements can be parsed. +| Column Name | Description | Example | +|-------------|----------------------------------------------------------------------|---------| +| id | UUID | | +| history_data | binlog information and DDL | {"source":{"server":"embeddedconnector"},"position":{"ts_sec":1724867891,"file":"mysql-bin.000003","pos":197,"gtids":"03d24fcc-6567-11ef-9978-0242ac130003:1-56","snapshot":true},"ts_ms":1724867891697,"databaseName":"test","ddl":"DROP TABLE IF EXISTS `test`.`orders`","tableChanges":[{"type":"DROP","id":"\"test\".\"orders\""}]} | +| history_data_seq | Monotonically increasing sequence number | | +| record_insert_seq | Timestamp when record is inserted. | 2024-08-28 12:58:22 | +| record_insert_ts | Monotonically increasing number | 174 | + + + +# Offsets table.(PostgreSQL) +The offsets table defined by the `offset.storage.jdbc.offset.table.name` +Default: **"altinity_sink_connector.replica_source_info"** +This table is used to store the binlog file, position and gtids. +| Column Name | Description | Example | +|-------------|----------------------------------------------------------------------|---------| +| id | UUID | | +| offset_key | This is the Unique key for every connector. Its a combination of `name` configuration variable and the `topic.prefix` configuration variable | [\"debezium-embedded-postgres\",{\"server\":\"embeddedconnector\"}]"| +| offset_val | This column stores the LSN information for PostgreSQL | {"last_snapshot_record":true,"lsn":27476744,"txId":744,"ts_usec":1724875350871964,"snapshot":true} +| record_insert_seq | Timestamp when record is inserted. | 2024-08-28 12:58:22 | +| record_insert_ts | Monotonically increasing number | + +### offsets_value +- **lsn_proc** - Last processed LSN +- **lsn_commit** - Last committed LSN +- **messageType** - Type of message(INSERT, UPDATE, DELETE) +- diff --git a/pom.xml b/pom.xml index b122cfd0e..9a78be19d 100644 --- a/pom.xml +++ b/pom.xml @@ -14,7 +14,7 @@ 17 UTF-8 UTF-8 - 2.5.0.Beta1 + 2.7.2.Final 5.9.1 3.1.1 UTF-8 diff --git a/release-notes/2.3.0.md b/release-notes/2.3.0.md new file mode 100644 index 000000000..d9fd1b07b --- /dev/null +++ b/release-notes/2.3.0.md @@ -0,0 +1,22 @@ +## What's Changed +* Update Monitoring.md to include metrics.port by @subkanthi in https://github.com/Altinity/clickhouse-sink-connector/pull/735 +* Added integration test for PostgreSQL keepermap by @subkanthi in https://github.com/Altinity/clickhouse-sink-connector/pull/728 +* Enable metrics by default for MySQL and postgres by @subkanthi in https://github.com/Altinity/clickhouse-sink-connector/pull/750 +* Added documentation to set the replication start position by @subkanthi in https://github.com/Altinity/clickhouse-sink-connector/pull/753 +* Added documentation for using JAR file for postgres replication by @subkanthi in https://github.com/Altinity/clickhouse-sink-connector/pull/754 +* Update quickstart.md by @subkanthi in https://github.com/Altinity/clickhouse-sink-connector/pull/763 +* Update quickstart.md update docker tag. by @subkanthi in https://github.com/Altinity/clickhouse-sink-connector/pull/764 +* Removed schema history configuration settings for postgres by @subkanthi in https://github.com/Altinity/clickhouse-sink-connector/pull/755 +* 725 we cant start grafana in sink connector we have cert issue by @subkanthi in https://github.com/Altinity/clickhouse-sink-connector/pull/760 +* Added JMX exporter to export JMX metrics from debezium. by @subkanthi in https://github.com/Altinity/clickhouse-sink-connector/pull/757 +* Disable validation of source database by @subkanthi in https://github.com/Altinity/clickhouse-sink-connector/pull/716 +* Added Integration test to validate truncate event replication in post…gresql by @subkanthi in https://github.com/Altinity/clickhouse-sink-connector/pull/759 +* Fix set lsn to accept string and not long by @subkanthi in https://github.com/Altinity/clickhouse-sink-connector/pull/752 +* Added logic in retrying database in case of failure by @subkanthi in https://github.com/Altinity/clickhouse-sink-connector/pull/761 +* 630 postgres heartbeat setup documentation by @subkanthi in https://github.com/Altinity/clickhouse-sink-connector/pull/765 +* 628 add integration test for mariadb by @subkanthi in https://github.com/Altinity/clickhouse-sink-connector/pull/673 +* Added functionality to replicate in single threaded mode based on configuration without using a Queue by @subkanthi in https://github.com/Altinity/clickhouse-sink-connector/pull/756 +* Convert localDateTime to String in show_replica_status API call by @subkanthi in https://github.com/Altinity/clickhouse-sink-connector/pull/736 + + +**Full Changelog**: https://github.com/Altinity/clickhouse-sink-connector/compare/2.2.1...2.3.0 diff --git a/release-notes/2.3.1.md b/release-notes/2.3.1.md new file mode 100644 index 000000000..dc0d2fa93 --- /dev/null +++ b/release-notes/2.3.1.md @@ -0,0 +1,23 @@ +## What's Changed +* Update state_storage.md by @subkanthi in https://github.com/Altinity/clickhouse-sink-connector/pull/771 +* Update README.md to include state storage link by @subkanthi in https://github.com/Altinity/clickhouse-sink-connector/pull/772 +* Updated offset state storage documentation. by @subkanthi in https://github.com/Altinity/clickhouse-sink-connector/pull/774 +* Update state_storage.md to include schema storage. by @subkanthi in https://github.com/Altinity/clickhouse-sink-connector/pull/775 +* Update state_storage.md to include postgresql offsets by @subkanthi in https://github.com/Altinity/clickhouse-sink-connector/pull/776 +* Update quickstart.md to start sink connector service by @subkanthi in https://github.com/Altinity/clickhouse-sink-connector/pull/785 +* Added logic to get latest release from github by @subkanthi in https://github.com/Altinity/clickhouse-sink-connector/pull/786 +* Update quickstart.md with script to set environment variable. by @subkanthi in https://github.com/Altinity/clickhouse-sink-connector/pull/787 +* Update quickstart_postgres.md by @subkanthi in https://github.com/Altinity/clickhouse-sink-connector/pull/788 +* Added updates to script by @subkanthi in https://github.com/Altinity/clickhouse-sink-connector/pull/789 +* Added single.threaded flag to Mariadb test to validate replication in… by @subkanthi in https://github.com/Altinity/clickhouse-sink-connector/pull/781 +* Update production_setup.md by @subkanthi in https://github.com/Altinity/clickhouse-sink-connector/pull/796 +* Changed logging level to info for STRUCT EMPTY not a valid CDC record by @subkanthi in https://github.com/Altinity/clickhouse-sink-connector/pull/795 +* Update production_setup.md to include max_paritions_per_insert by @subkanthi in https://github.com/Altinity/clickhouse-sink-connector/pull/804 +* Update production_setup.md , fixed broken link by @subkanthi in https://github.com/Altinity/clickhouse-sink-connector/pull/809 +* Update config.ym by @subkanthi in https://github.com/Altinity/clickhouse-sink-connector/pull/813 +* Update README.md to include initial data dump/load by @subkanthi in https://github.com/Altinity/clickhouse-sink-connector/pull/821 +* 794 change logging level of struct empty not a valid cdc record + record to info by @subkanthi in https://github.com/Altinity/clickhouse-sink-connector/pull/825 +* 801 records are not acknowledged or the offsets are not updated in singlethreaded mode by @subkanthi in https://github.com/Altinity/clickhouse-sink-connector/pull/827 + + +**Full Changelog**: https://github.com/Altinity/clickhouse-sink-connector/compare/2.3.0...2.3.1 diff --git a/release-notes/2.4.0.md b/release-notes/2.4.0.md new file mode 100644 index 000000000..62f5c1884 --- /dev/null +++ b/release-notes/2.4.0.md @@ -0,0 +1,42 @@ +## What's Changed +* Update state_storage.md by @subkanthi in https://github.com/Altinity/clickhouse-sink-connector/pull/771 +* Update README.md to include state storage link by @subkanthi in https://github.com/Altinity/clickhouse-sink-connector/pull/772 +* Updated offset state storage documentation. by @subkanthi in https://github.com/Altinity/clickhouse-sink-connector/pull/774 +* Update state_storage.md to include schema storage. by @subkanthi in https://github.com/Altinity/clickhouse-sink-connector/pull/775 +* Update state_storage.md to include postgresql offsets by @subkanthi in https://github.com/Altinity/clickhouse-sink-connector/pull/776 +* Testflows new kafka tests by @Selfeer in https://github.com/Altinity/clickhouse-sink-connector/pull/738 +* Update quickstart.md to start sink connector service by @subkanthi in https://github.com/Altinity/clickhouse-sink-connector/pull/785 +* Added logic to get latest release from github by @subkanthi in https://github.com/Altinity/clickhouse-sink-connector/pull/786 +* Update quickstart.md with script to set environment variable. by @subkanthi in https://github.com/Altinity/clickhouse-sink-connector/pull/787 +* Update quickstart_postgres.md by @subkanthi in https://github.com/Altinity/clickhouse-sink-connector/pull/788 +* Added updates to script by @subkanthi in https://github.com/Altinity/clickhouse-sink-connector/pull/789 +* Added single.threaded flag to Mariadb test to validate replication in… by @subkanthi in https://github.com/Altinity/clickhouse-sink-connector/pull/781 +* Update production_setup.md by @subkanthi in https://github.com/Altinity/clickhouse-sink-connector/pull/796 +* Changed logging level to info for STRUCT EMPTY not a valid CDC record by @subkanthi in https://github.com/Altinity/clickhouse-sink-connector/pull/795 +* Update production_setup.md to include max_paritions_per_insert by @subkanthi in https://github.com/Altinity/clickhouse-sink-connector/pull/804 +* Added logic to commit batch in single threaded mode by @subkanthi in https://github.com/Altinity/clickhouse-sink-connector/pull/802 +* Update production_setup.md , fixed broken link by @subkanthi in https://github.com/Altinity/clickhouse-sink-connector/pull/809 +* Added support for DROP CONSTRAINT by @subkanthi in https://github.com/Altinity/clickhouse-sink-connector/pull/807 +* Update config.ym by @subkanthi in https://github.com/Altinity/clickhouse-sink-connector/pull/813 +* Removed logic of duplicating instances of DebeziumOffsetStorage by @subkanthi in https://github.com/Altinity/clickhouse-sink-connector/pull/833 +* Support MySQL point by @poweroftrue in https://github.com/Altinity/clickhouse-sink-connector/pull/815 +* Support MySQL Polygons by @poweroftrue in https://github.com/Altinity/clickhouse-sink-connector/pull/853 +* Added Integration test for testing POINT data type in MySQL by @subkanthi in https://github.com/Altinity/clickhouse-sink-connector/pull/851 +* Upgrade debezium to 2.7.1.Final by @subkanthi in https://github.com/Altinity/clickhouse-sink-connector/pull/792 +* Set log retention max size to 100M by @subkanthi in https://github.com/Altinity/clickhouse-sink-connector/pull/805 +* Added integration test to validate database.override.map in snapshot.mode=initial mode by @subkanthi in https://github.com/Altinity/clickhouse-sink-connector/pull/818 +* Log docker tag in startup by @subkanthi in https://github.com/Altinity/clickhouse-sink-connector/pull/841 +* Upgrade clickhouse.jdbc.version to 0.6.5 by @subkanthi in https://github.com/Altinity/clickhouse-sink-connector/pull/832 +* Single threaded documentation by @subkanthi in https://github.com/Altinity/clickhouse-sink-connector/pull/822 +* add ddl.retry by @Selfeer in https://github.com/Altinity/clickhouse-sink-connector/pull/865 +* Refactored DDL processing logic by @subkanthi in https://github.com/Altinity/clickhouse-sink-connector/pull/839 +* Testflows updates by @Selfeer in https://github.com/Altinity/clickhouse-sink-connector/pull/873 +* Added test to replicate generated columns in postgreSQL by @subkanthi in https://github.com/Altinity/clickhouse-sink-connector/pull/777 +* Added function to retrieve a map of column name and is_nullable as bo… by @subkanthi in https://github.com/Altinity/clickhouse-sink-connector/pull/852 +* Added test for MySQL DDL with partitions by @subkanthi in https://github.com/Altinity/clickhouse-sink-connector/pull/784 +* 773 add sink connector client step to purge schema history so that newer tables can be added by @subkanthi in https://github.com/Altinity/clickhouse-sink-connector/pull/779 + +## New Contributors +* @poweroftrue made their first contribution in https://github.com/Altinity/clickhouse-sink-connector/pull/815 + +**Full Changelog**: https://github.com/Altinity/clickhouse-sink-connector/compare/2.3.0...2 diff --git a/sink-connector-client/go.mod b/sink-connector-client/go.mod index 85eb048c1..486b05a22 100644 --- a/sink-connector-client/go.mod +++ b/sink-connector-client/go.mod @@ -38,6 +38,10 @@ require ( github.com/go-sql-driver/mysql v1.8.1 github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/protobuf v1.5.4 // indirect + filippo.io/edwards25519 v1.1.0 // indirect + github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/go-sql-driver/mysql v1.8.1 // indirect github.com/google/go-querystring v1.0.0 // indirect github.com/google/uuid v1.6.0 // indirect github.com/klauspost/compress v1.17.4 // indirect diff --git a/sink-connector-client/go.sum b/sink-connector-client/go.sum index 7da295e95..235da1090 100644 --- a/sink-connector-client/go.sum +++ b/sink-connector-client/go.sum @@ -6,6 +6,8 @@ github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 h1:bvDV9 github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8= github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8= github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= +filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= +filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= @@ -56,6 +58,8 @@ github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= +github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk= github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= diff --git a/sink-connector-client/main.go b/sink-connector-client/main.go index 0585664cd..b82c40b6f 100644 --- a/sink-connector-client/main.go +++ b/sink-connector-client/main.go @@ -33,7 +33,10 @@ const ( STATUS_COMMAND = "show_replica_status" UPDATE_BINLOG_COMMAND = "change_replication_source" UPDATE_LSN_COMMAND = "lsn" + DELETE_OFFSETS_COMMAND = "delete_offsets" + DELETE_SCHEMA_HISTORY_COMMAND = "delete_schema_history" ) + const ( START_REPLICATION = "start" STOP_REPLICATION = "stop" @@ -41,6 +44,8 @@ const ( STATUS = "status" UPDATE_BINLOG = "binlog" UPDATE_LSN = "lsn" + DELETE_OFFSETS = "offsets" + DELETE_SCHEMA_HISTORY = "schema-history" ) // Fetches the repos for the given Github users @@ -53,6 +58,14 @@ func getHTTPCall(url string) *grequests.Response { return resp } +func getHTTPDeleteCall(url string) *grequests.Response { + resp, err := grequests.Delete(url, requestOptions) + // you can modify the request by passing an optional RequestOptions struct + if err != nil { + log.Fatalln("Unable to make request: ", err) + } + return resp +} /** Function to get server url based on the parameters passed */ @@ -215,12 +228,79 @@ func main() { return nil }, }, - } - + { + Name: DELETE_OFFSETS_COMMAND, + Usage: "Delete offsets from the sink connector", + Action: func(c *cli.Context) error { + handleDeleteOffsets(c) + return nil + }, + }, + { + Name: DELETE_SCHEMA_HISTORY_COMMAND, + Usage: "Delete schema history from the sink connector", + Action: func(c *cli.Context) error { + handleDeleteSchemaHistory(c) + return nil + }, + }, + } app.Version = "1.0" app.Run(os.Args) } +func handleDeleteOffsets(c *cli.Context) bool { + log.Println("***** Delete offsets from the sink connector *****") + log.Println("Are you sure you want to continue? (y/n): ") + var userInput string + fmt.Scanln(&userInput) + if userInput != "y" { + log.Println("Exiting...") + return false + } else { + log.Println("Continuing...") + } + // Call a REST DELETE API to delete offsets from the sink connector + var deleteOffsetsUrl = getServerUrl(DELETE_OFFSETS, c) + log.Println("Sending request to URL: " + deleteOffsetsUrl) + resp := getHTTPDeleteCall(deleteOffsetsUrl) + time.Sleep(5 * time.Second) + if resp.StatusCode == 200 { + log.Println("Offsets deleted successfully") + return true + } else { + log.Println("Response Status Code:", resp.StatusCode) + log.Println("Error deleting offsets") + return false + } +} + +func handleDeleteSchemaHistory(c *cli.Context) bool { + log.Println("***** Delete schema history from the sink connector *****") + log.Println("Are you sure you want to continue? (y/n): ") + var userInput string + fmt.Scanln(&userInput) + if userInput != "y" { + log.Println("Exiting...") + return false + } else { + log.Println("Continuing...") + } + // Call a REST DELETE API to delete offsets from the sink connector + var deleteOffsetsUrl = getServerUrl(DELETE_SCHEMA_HISTORY, c) + log.Println("Sending request to URL: " + deleteOffsetsUrl) + resp := getHTTPDeleteCall(deleteOffsetsUrl) + time.Sleep(5 * time.Second) + if resp.StatusCode == 200 { + log.Println("Schema history deleted successfully") + return true + } else { + log.Println("Response Status Code:", resp.StatusCode) + log.Println("Error deleting schema history") + return false + } +} + func handleUpdateLsn(c *cli.Context) bool { var lsnPosition = c.String("lsn") log.Println("***** lsn position:", lsnPosition+" *****") diff --git a/sink-connector-client/sink-connector-client b/sink-connector-client/sink-connector-client index f6bec4bb8..12a0abf1e 100755 Binary files a/sink-connector-client/sink-connector-client and b/sink-connector-client/sink-connector-client differ diff --git a/sink-connector-lightweight/Dockerfile b/sink-connector-lightweight/Dockerfile index c6f0f13ab..77d76e405 100644 --- a/sink-connector-lightweight/Dockerfile +++ b/sink-connector-lightweight/Dockerfile @@ -1,4 +1,6 @@ FROM openjdk:17 +ARG DOCKER_TAG +ENV DOCKER_TAG=${DOCKER_TAG} COPY sink-connector-client/sink-connector-client /sink-connector-client COPY sink-connector-lightweight/target/clickhouse-debezium-embedded-*.jar /app.jar ENV JAVA_OPTS="-Dlog4jDebug=true" diff --git a/sink-connector-lightweight/dependency-reduced-pom.xml b/sink-connector-lightweight/dependency-reduced-pom.xml index ae2ba9236..37d175050 100644 --- a/sink-connector-lightweight/dependency-reduced-pom.xml +++ b/sink-connector-lightweight/dependency-reduced-pom.xml @@ -1,312 +1,332 @@ - - - 4.0.0 - com.altinity - clickhouse-debezium-embedded - 0.0.4 - - - - kr.motd.maven - os-maven-plugin - 1.6.2 - - - - - org.codehaus.mojo - build-helper-maven-plugin - 3.4.0 - - - generate-sources - - add-source - - - - ../sink-connector/src/main - - - - - - - maven-shade-plugin - 3.4.1 - - - package - - shade - - - - - com.altinity.clickhouse.debezium.embedded.ClickHouseDebeziumEmbeddedApplication - - - - - *:* - - org/apache/log4j/** - - - - - - - - - maven-checkstyle-plugin - ${version.checkstyle.plugin} - - - maven-surefire-plugin - 2.22.0 - - - **/*Test.java - **/*IT.java - - - - filesystem - listener - com.altinity.clickhouse.debezium.embedded.FailFastListener - - - true - true - true - ${surefire.test.runOrder} - - - - org.antlr - antlr4-maven-plugin - 4.10.1 - - - - antlr4 - - - - - - - - - native - - - - maven-surefire-report-plugin - 2.22.2 - - - - - false - native - - - - - - io.debezium - debezium-connector-mongodb - 2.7.0.Alpha2 - test - - - mongodb-driver-sync - org.mongodb - - - - - org.mongodb - mongo-java-driver - 3.12.12 - test - - - org.projectlombok - lombok - 1.18.22 - provided - - - com.github.docker-java - docker-java-api - 3.3.3 - test - - - slf4j-api - org.slf4j - - - - - org.testcontainers - testcontainers - 1.19.1 - test - - - slf4j-api - org.slf4j - - - commons-compress - org.apache.commons - - - duct-tape - org.rnorth.duct-tape - - - docker-java-transport-zerodep - com.github.docker-java - - - - - org.testcontainers - jdbc - 1.19.1 - test - - - database-commons - org.testcontainers - - - - - org.testcontainers - junit-jupiter - 1.19.1 - test - - - org.testcontainers - clickhouse - 1.19.1 - test - - - org.testcontainers - mysql - 1.19.1 - test - - - org.testcontainers - postgresql - 1.19.1 - test - - - org.testcontainers - mongodb - 1.19.1 - test - - - org.junit - junit-bom - 5.9.1 - pom - import - - - org.junit.jupiter - junit-jupiter-engine - 5.8.1 - test - - - junit-platform-engine - org.junit.platform - - - junit-jupiter-api - org.junit.jupiter - - - apiguardian-api - org.apiguardian - - - - - org.junit-pioneer - junit-pioneer - 2.0.0-RC1 - test - - - junit-jupiter-params - org.junit.jupiter - - - junit-platform-launcher - org.junit.platform - - - junit-jupiter-api - org.junit.jupiter - - - - - org.apache.tomcat - annotations-api - 6.0.53 - provided - - - org.bouncycastle - bcpkix-fips - 1.0.3 - provided - - - com.github.stefanbirkner - system-rules - 1.19.0 - test - - - - - - ${quarkus.platform.group-id} - ${quarkus.platform.artifact-id} - ${quarkus.platform.version} - pom - import - - - - - 2.14.0.Final - quarkus-bom - 1.19.1 - 3.0.0-M7 - UTF-8 - 0.0.8 - 5.9.1 - 17 - UTF-8 - 3.1.1 - 17 - 2.7.0.Alpha2 - io.quarkus.platform - - + + + 4.0.0 + com.altinity + clickhouse-debezium-embedded + 0.0.4 + + + + kr.motd.maven + os-maven-plugin + 1.6.2 + + + + + org.codehaus.mojo + build-helper-maven-plugin + 3.4.0 + + + generate-sources + + add-source + + + + ../sink-connector/src/main + + + + + + + maven-shade-plugin + 3.4.1 + + + package + + shade + + + + + + com.altinity.clickhouse.debezium.embedded.ClickHouseDebeziumEmbeddedApplication + + + + + *:* + + org/apache/log4j/** + + + + + + + + + maven-checkstyle-plugin + ${version.checkstyle.plugin} + + + maven-surefire-plugin + 2.22.0 + + + **/*Test.java + **/*IT.java + + + + filesystem + listener + com.altinity.clickhouse.debezium.embedded.FailFastListener + + + true + true + true + ${surefire.test.runOrder} + + + + org.antlr + antlr4-maven-plugin + 4.10.1 + + + + antlr4 + + + + + + + + + native + + + + maven-surefire-report-plugin + 2.22.2 + + + + + false + native + + + + + + io.javalin + javalin-testtools + 5.5.0 + test + + + okhttp + com.squareup.okhttp3 + + + + + io.debezium + debezium-connector-mongodb + 2.7.2.Final + test + + + mongodb-driver-sync + org.mongodb + + + + + org.mongodb + mongo-java-driver + 3.12.12 + test + + + org.projectlombok + lombok + 1.18.22 + provided + + + com.github.docker-java + docker-java-api + 3.3.3 + test + + + slf4j-api + org.slf4j + + + + + org.testcontainers + testcontainers + 1.19.1 + test + + + slf4j-api + org.slf4j + + + commons-compress + org.apache.commons + + + duct-tape + org.rnorth.duct-tape + + + docker-java-transport-zerodep + com.github.docker-java + + + + + org.testcontainers + jdbc + 1.19.1 + test + + + database-commons + org.testcontainers + + + + + org.testcontainers + junit-jupiter + 1.19.1 + test + + + org.testcontainers + clickhouse + 1.19.1 + test + + + org.testcontainers + mysql + 1.19.1 + test + + + org.testcontainers + postgresql + 1.19.1 + test + + + org.testcontainers + mariadb + 1.19.1 + test + + + org.testcontainers + mongodb + 1.19.1 + test + + + org.junit + junit-bom + 5.9.1 + pom + import + + + org.junit.jupiter + junit-jupiter-engine + 5.8.1 + test + + + junit-platform-engine + org.junit.platform + + + junit-jupiter-api + org.junit.jupiter + + + apiguardian-api + org.apiguardian + + + + + org.junit-pioneer + junit-pioneer + 2.0.0-RC1 + test + + + junit-jupiter-params + org.junit.jupiter + + + junit-platform-launcher + org.junit.platform + + + junit-jupiter-api + org.junit.jupiter + + + + + org.apache.tomcat + annotations-api + 6.0.53 + provided + + + org.bouncycastle + bcpkix-fips + 1.0.3 + provided + + + com.github.stefanbirkner + system-rules + 1.19.0 + test + + + + + + ${quarkus.platform.group-id} + ${quarkus.platform.artifact-id} + ${quarkus.platform.version} + pom + import + + + + + 2.14.0.Final + quarkus-bom + 1.19.1 + 3.0.0-M7 + UTF-8 + 0.0.9 + 5.5.0 + 5.9.1 + 17 + UTF-8 + 3.1.1 + 17 + 2.7.2.Final + io.quarkus.platform + + diff --git a/sink-connector-lightweight/docker/Dockerfile_grafana b/sink-connector-lightweight/docker/Dockerfile_grafana new file mode 100644 index 000000000..ff116630f --- /dev/null +++ b/sink-connector-lightweight/docker/Dockerfile_grafana @@ -0,0 +1,14 @@ +# Start from the official Grafana image +FROM grafana/grafana:latest +USER root +# Add your CA certificate to the system's trusted certificates +# If you have multiple certificates, you can copy them all +COPY ca-certificates.crt /usr/local/share/ca-certificates/ca-certificates.crt +RUN apk add --no-cache ca-certificates && \ + update-ca-certificates +# Install the Grafana plugin +# Replace 'your-plugin-id' with the actual plugin ID +#RUN grafana-cli --pluginUrl https://your-plugin-repository.com/plugins/your-plugin-id install your-plugin-id + +# Restart Grafana to pick up the changes +CMD ["/run.sh"] \ No newline at end of file diff --git a/sink-connector-lightweight/docker/ca-certificates.crt b/sink-connector-lightweight/docker/ca-certificates.crt new file mode 100644 index 000000000..cea7608a6 --- /dev/null +++ b/sink-connector-lightweight/docker/ca-certificates.crt @@ -0,0 +1,3490 @@ +-----BEGIN CERTIFICATE----- +MIIH0zCCBbugAwIBAgIIXsO3pkN/pOAwDQYJKoZIhvcNAQEFBQAwQjESMBAGA1UE +AwwJQUNDVlJBSVoxMRAwDgYDVQQLDAdQS0lBQ0NWMQ0wCwYDVQQKDARBQ0NWMQsw +CQYDVQQGEwJFUzAeFw0xMTA1MDUwOTM3MzdaFw0zMDEyMzEwOTM3MzdaMEIxEjAQ +BgNVBAMMCUFDQ1ZSQUlaMTEQMA4GA1UECwwHUEtJQUNDVjENMAsGA1UECgwEQUND +VjELMAkGA1UEBhMCRVMwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCb +qau/YUqXry+XZpp0X9DZlv3P4uRm7x8fRzPCRKPfmt4ftVTdFXxpNRFvu8gMjmoY +HtiP2Ra8EEg2XPBjs5BaXCQ316PWywlxufEBcoSwfdtNgM3802/J+Nq2DoLSRYWo +G2ioPej0RGy9ocLLA76MPhMAhN9KSMDjIgro6TenGEyxCQ0jVn8ETdkXhBilyNpA +lHPrzg5XPAOBOp0KoVdDaaxXbXmQeOW1tDvYvEyNKKGno6e6Ak4l0Squ7a4DIrhr +IA8wKFSVf+DuzgpmndFALW4ir50awQUZ0m/A8p/4e7MCQvtQqR0tkw8jq8bBD5L/ +0KIV9VMJcRz/RROE5iZe+OCIHAr8Fraocwa48GOEAqDGWuzndN9wrqODJerWx5eH +k6fGioozl2A3ED6XPm4pFdahD9GILBKfb6qkxkLrQaLjlUPTAYVtjrs78yM2x/47 +4KElB0iryYl0/wiPgL/AlmXz7uxLaL2diMMxs0Dx6M/2OLuc5NF/1OVYm3z61PMO +m3WR5LpSLhl+0fXNWhn8ugb2+1KoS5kE3fj5tItQo05iifCHJPqDQsGH+tUtKSpa +cXpkatcnYGMN285J9Y0fkIkyF/hzQ7jSWpOGYdbhdQrqeWZ2iE9x6wQl1gpaepPl +uUsXQA+xtrn13k/c4LOsOxFwYIRKQ26ZIMApcQrAZQIDAQABo4ICyzCCAscwfQYI +KwYBBQUHAQEEcTBvMEwGCCsGAQUFBzAChkBodHRwOi8vd3d3LmFjY3YuZXMvZmls +ZWFkbWluL0FyY2hpdm9zL2NlcnRpZmljYWRvcy9yYWl6YWNjdjEuY3J0MB8GCCsG +AQUFBzABhhNodHRwOi8vb2NzcC5hY2N2LmVzMB0GA1UdDgQWBBTSh7Tj3zcnk1X2 +VuqB5TbMjB4/vTAPBgNVHRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFNKHtOPfNyeT +VfZW6oHlNsyMHj+9MIIBcwYDVR0gBIIBajCCAWYwggFiBgRVHSAAMIIBWDCCASIG +CCsGAQUFBwICMIIBFB6CARAAQQB1AHQAbwByAGkAZABhAGQAIABkAGUAIABDAGUA +cgB0AGkAZgBpAGMAYQBjAGkA8wBuACAAUgBhAO0AegAgAGQAZQAgAGwAYQAgAEEA +QwBDAFYAIAAoAEEAZwBlAG4AYwBpAGEAIABkAGUAIABUAGUAYwBuAG8AbABvAGcA +7QBhACAAeQAgAEMAZQByAHQAaQBmAGkAYwBhAGMAaQDzAG4AIABFAGwAZQBjAHQA +cgDzAG4AaQBjAGEALAAgAEMASQBGACAAUQA0ADYAMAAxADEANQA2AEUAKQAuACAA +QwBQAFMAIABlAG4AIABoAHQAdABwADoALwAvAHcAdwB3AC4AYQBjAGMAdgAuAGUA +czAwBggrBgEFBQcCARYkaHR0cDovL3d3dy5hY2N2LmVzL2xlZ2lzbGFjaW9uX2Mu +aHRtMFUGA1UdHwROMEwwSqBIoEaGRGh0dHA6Ly93d3cuYWNjdi5lcy9maWxlYWRt +aW4vQXJjaGl2b3MvY2VydGlmaWNhZG9zL3JhaXphY2N2MV9kZXIuY3JsMA4GA1Ud +DwEB/wQEAwIBBjAXBgNVHREEEDAOgQxhY2N2QGFjY3YuZXMwDQYJKoZIhvcNAQEF +BQADggIBAJcxAp/n/UNnSEQU5CmH7UwoZtCPNdpNYbdKl02125DgBS4OxnnQ8pdp +D70ER9m+27Up2pvZrqmZ1dM8MJP1jaGo/AaNRPTKFpV8M9xii6g3+CfYCS0b78gU +JyCpZET/LtZ1qmxNYEAZSUNUY9rizLpm5U9EelvZaoErQNV/+QEnWCzI7UiRfD+m +AM/EKXMRNt6GGT6d7hmKG9Ww7Y49nCrADdg9ZuM8Db3VlFzi4qc1GwQA9j9ajepD +vV+JHanBsMyZ4k0ACtrJJ1vnE5Bc5PUzolVt3OAJTS+xJlsndQAJxGJ3KQhfnlms +tn6tn1QwIgPBHnFk/vk4CpYY3QIUrCPLBhwepH2NDd4nQeit2hW3sCPdK6jT2iWH +7ehVRE2I9DZ+hJp4rPcOVkkO1jMl1oRQQmwgEh0q1b688nCBpHBgvgW1m54ERL5h +I6zppSSMEYCUWqKiuUnSwdzRp+0xESyeGabu4VXhwOrPDYTkF7eifKXeVSUG7szA +h1xA2syVP1XgNce4hL60Xc16gwFy7ofmXx2utYXGJt/mwZrpHgJHnyqobalbz+xF +d3+YJ5oyXSrjhO7FmGYvliAd3djDJ9ew+f7Zfc3Qn48LFFhRny+Lwzgt3uiP1o2H +pPVWQxaZLPSkVrQ0uGE3ycJYgBugl6H8WY3pEfbRD0tVNEYqi4Y7 +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFgzCCA2ugAwIBAgIPXZONMGc2yAYdGsdUhGkHMA0GCSqGSIb3DQEBCwUAMDsx +CzAJBgNVBAYTAkVTMREwDwYDVQQKDAhGTk1ULVJDTTEZMBcGA1UECwwQQUMgUkFJ +WiBGTk1ULVJDTTAeFw0wODEwMjkxNTU5NTZaFw0zMDAxMDEwMDAwMDBaMDsxCzAJ +BgNVBAYTAkVTMREwDwYDVQQKDAhGTk1ULVJDTTEZMBcGA1UECwwQQUMgUkFJWiBG +Tk1ULVJDTTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBALpxgHpMhm5/ +yBNtwMZ9HACXjywMI7sQmkCpGreHiPibVmr75nuOi5KOpyVdWRHbNi63URcfqQgf +BBckWKo3Shjf5TnUV/3XwSyRAZHiItQDwFj8d0fsjz50Q7qsNI1NOHZnjrDIbzAz +WHFctPVrbtQBULgTfmxKo0nRIBnuvMApGGWn3v7v3QqQIecaZ5JCEJhfTzC8PhxF +tBDXaEAUwED653cXeuYLj2VbPNmaUtu1vZ5Gzz3rkQUCwJaydkxNEJY7kvqcfw+Z +374jNUUeAlz+taibmSXaXvMiwzn15Cou08YfxGyqxRxqAQVKL9LFwag0Jl1mpdIC +IfkYtwb1TplvqKtMUejPUBjFd8g5CSxJkjKZqLsXF3mwWsXmo8RZZUc1g16p6DUL +mbvkzSDGm0oGObVo/CK67lWMK07q87Hj/LaZmtVC+nFNCM+HHmpxffnTtOmlcYF7 +wk5HlqX2doWjKI/pgG6BU6VtX7hI+cL5NqYuSf+4lsKMB7ObiFj86xsc3i1w4peS +MKGJ47xVqCfWS+2QrYv6YyVZLag13cqXM7zlzced0ezvXg5KkAYmY6252TUtB7p2 +ZSysV4999AeU14ECll2jB0nVetBX+RvnU0Z1qrB5QstocQjpYL05ac70r8NWQMet +UqIJ5G+GR4of6ygnXYMgrwTJbFaai0b1AgMBAAGjgYMwgYAwDwYDVR0TAQH/BAUw +AwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFPd9xf3E6Jobd2Sn9R2gzL+H +YJptMD4GA1UdIAQ3MDUwMwYEVR0gADArMCkGCCsGAQUFBwIBFh1odHRwOi8vd3d3 +LmNlcnQuZm5tdC5lcy9kcGNzLzANBgkqhkiG9w0BAQsFAAOCAgEAB5BK3/MjTvDD +nFFlm5wioooMhfNzKWtN/gHiqQxjAb8EZ6WdmF/9ARP67Jpi6Yb+tmLSbkyU+8B1 +RXxlDPiyN8+sD8+Nb/kZ94/sHvJwnvDKuO+3/3Y3dlv2bojzr2IyIpMNOmqOFGYM +LVN0V2Ue1bLdI4E7pWYjJ2cJj+F3qkPNZVEI7VFY/uY5+ctHhKQV8Xa7pO6kO8Rf +77IzlhEYt8llvhjho6Tc+hj507wTmzl6NLrTQfv6MooqtyuGC2mDOL7Nii4LcK2N +JpLuHvUBKwrZ1pebbuCoGRw6IYsMHkCtA+fdZn71uSANA+iW+YJF1DngoABd15jm +fZ5nc8OaKveri6E6FO80vFIOiZiaBECEHX5FaZNXzuvO+FB8TxxuBEOb+dY7Ixjp +6o7RTUaN8Tvkasq6+yO3m/qZASlaWFot4/nUbQ4mrcFuNLwy+AwF+mWj2zs3gyLp +1txyM/1d8iC9djwj2ij3+RvrWWTV3F9yfiD8zYm1kGdNYno/Tq0dwzn+evQoFt9B +9kiABdcPUXmsEKvU7ANm5mqwujGSQkBqvjrTcuFqN1W8rB2Vt2lh8kORdOag0wok +RqEIr9baRRmW1FMdW4R58MD3R++Lj8UGrp1MYp3/RgT408m2ECVAdf4WqslKYIYv +uu8wd+RU4riEmViAqhOLUTpPSPaLtrM= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFuzCCA6OgAwIBAgIIVwoRl0LE48wwDQYJKoZIhvcNAQELBQAwazELMAkGA1UE +BhMCSVQxDjAMBgNVBAcMBU1pbGFuMSMwIQYDVQQKDBpBY3RhbGlzIFMucC5BLi8w +MzM1ODUyMDk2NzEnMCUGA1UEAwweQWN0YWxpcyBBdXRoZW50aWNhdGlvbiBSb290 +IENBMB4XDTExMDkyMjExMjIwMloXDTMwMDkyMjExMjIwMlowazELMAkGA1UEBhMC +SVQxDjAMBgNVBAcMBU1pbGFuMSMwIQYDVQQKDBpBY3RhbGlzIFMucC5BLi8wMzM1 +ODUyMDk2NzEnMCUGA1UEAwweQWN0YWxpcyBBdXRoZW50aWNhdGlvbiBSb290IENB +MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAp8bEpSmkLO/lGMWwUKNv +UTufClrJwkg4CsIcoBh/kbWHuUA/3R1oHwiD1S0eiKD4j1aPbZkCkpAW1V8IbInX +4ay8IMKx4INRimlNAJZaby/ARH6jDuSRzVju3PvHHkVH3Se5CAGfpiEd9UEtL0z9 +KK3giq0itFZljoZUj5NDKd45RnijMCO6zfB9E1fAXdKDa0hMxKufgFpbOr3JpyI/ +gCczWw63igxdBzcIy2zSekciRDXFzMwujt0q7bd9Zg1fYVEiVRvjRuPjPdA1Yprb +rxTIW6HMiRvhMCb8oJsfgadHHwTrozmSBp+Z07/T6k9QnBn+locePGX2oxgkg4YQ +51Q+qDp2JE+BIcXjDwL4k5RHILv+1A7TaLndxHqEguNTVHnd25zS8gebLra8Pu2F +be8lEfKXGkJh90qX6IuxEAf6ZYGyojnP9zz/GPvG8VqLWeICrHuS0E4UT1lF9gxe +KF+w6D9Fz8+vm2/7hNN3WpVvrJSEnu68wEqPSpP4RCHiMUVhUE4Q2OM1fEwZtN4F +v6MGn8i1zeQf1xcGDXqVdFUNaBr8EBtiZJ1t4JWgw5QHVw0U5r0F+7if5t+L4sbn +fpb2U8WANFAoWPASUHEXMLrmeGO89LKtmyuy/uE5jF66CyCU3nuDuP/jVo23Eek7 +jPKxwV2dpAtMK9myGPW1n0sCAwEAAaNjMGEwHQYDVR0OBBYEFFLYiDrIn3hm7Ynz +ezhwlMkCAjbQMA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAUUtiIOsifeGbt +ifN7OHCUyQICNtAwDgYDVR0PAQH/BAQDAgEGMA0GCSqGSIb3DQEBCwUAA4ICAQAL +e3KHwGCmSUyIWOYdiPcUZEim2FgKDk8TNd81HdTtBjHIgT5q1d07GjLukD0R0i70 +jsNjLiNmsGe+b7bAEzlgqqI0JZN1Ut6nna0Oh4lScWoWPBkdg/iaKWW+9D+a2fDz +WochcYBNy+A4mz+7+uAwTc+G02UQGRjRlwKxK3JCaKygvU5a2hi/a5iB0P2avl4V +SM0RFbnAKVy06Ij3Pjaut2L9HmLecHgQHEhb2rykOLpn7VU+Xlff1ANATIGk0k9j +pwlCCRT8AKnCgHNPLsBA2RF7SOp6AsDT6ygBJlh0wcBzIm2Tlf05fbsq4/aC4yyX +X04fkZT6/iyj2HYauE2yOE+b+h1IYHkm4vP9qdCa6HCPSXrW5b0KDtst842/6+Ok +fcvHlXHo2qN8xcL4dJIEG4aspCJTQLas/kx2z/uUMsA1n3Y/buWQbqCmJqK4LL7R +K4X9p2jIugErsWx0Hbhzlefut8cl8ABMALJ+tguLHPPAUJ4lueAI3jZm/zel0btU +ZCzJJ7VLkn5l/9Mt4blOvH+kQSGQQXemOR/qnuOf0GZvBeyqdn6/axag67XH/JJU +LysRJyU3eExRarDzzFhdFPFqSBX/wge2sY0PjlxQRrM9vwGYT7JZVEc+NHt4bVaT +LnPqZih4zR0Uv6CPLy64Lo7yFIrM6bV8+2ydDKXhlg== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIDTDCCAjSgAwIBAgIId3cGJyapsXwwDQYJKoZIhvcNAQELBQAwRDELMAkGA1UE +BhMCVVMxFDASBgNVBAoMC0FmZmlybVRydXN0MR8wHQYDVQQDDBZBZmZpcm1UcnVz +dCBDb21tZXJjaWFsMB4XDTEwMDEyOTE0MDYwNloXDTMwMTIzMTE0MDYwNlowRDEL +MAkGA1UEBhMCVVMxFDASBgNVBAoMC0FmZmlybVRydXN0MR8wHQYDVQQDDBZBZmZp +cm1UcnVzdCBDb21tZXJjaWFsMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC +AQEA9htPZwcroRX1BiLLHwGy43NFBkRJLLtJJRTWzsO3qyxPxkEylFf6EqdbDuKP +Hx6GGaeqtS25Xw2Kwq+FNXkyLbscYjfysVtKPcrNcV/pQr6U6Mje+SJIZMblq8Yr +ba0F8PrVC8+a5fBQpIs7R6UjW3p6+DM/uO+Zl+MgwdYoic+U+7lF7eNAFxHUdPAL +MeIrJmqbTFeurCA+ukV6BfO9m2kVrn1OIGPENXY6BwLJN/3HR+7o8XYdcxXyl6S1 +yHp52UKqK39c/s4mT6NmgTWvRLpUHhwwMmWd5jyTXlBOeuM61G7MGvv50jeuJCqr +VwMiKA1JdX+3KNp1v47j3A55MQIDAQABo0IwQDAdBgNVHQ4EFgQUnZPGU4teyq8/ +nx4P5ZmVvCT2lI8wDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwDQYJ +KoZIhvcNAQELBQADggEBAFis9AQOzcAN/wr91LoWXym9e2iZWEnStB03TX8nfUYG +XUPGhi4+c7ImfU+TqbbEKpqrIZcUsd6M06uJFdhrJNTxFq7YpFzUf1GO7RgBsZNj +vbz4YYCanrHOQnDiqX0GJX0nof5v7LMeJNrjS1UaADs1tDvZ110w/YETifLCBivt +Z8SOyUOyXGsViQK8YvxO8rUzqrJv0wqiUOP2O+guRMLbZjipM1ZI8W0bM40NjD9g +N53Tym1+NH4Nn3J2ixufcv1SNUFFApYvHLKac0khsUlHRUe072o0EclNmsxZt9YC +nlpOZbWUrhvfKbAW8b8Angc6F2S1BLUjIZkKlTuXfO8= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIDTDCCAjSgAwIBAgIIfE8EORzUmS0wDQYJKoZIhvcNAQEFBQAwRDELMAkGA1UE +BhMCVVMxFDASBgNVBAoMC0FmZmlybVRydXN0MR8wHQYDVQQDDBZBZmZpcm1UcnVz +dCBOZXR3b3JraW5nMB4XDTEwMDEyOTE0MDgyNFoXDTMwMTIzMTE0MDgyNFowRDEL +MAkGA1UEBhMCVVMxFDASBgNVBAoMC0FmZmlybVRydXN0MR8wHQYDVQQDDBZBZmZp +cm1UcnVzdCBOZXR3b3JraW5nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC +AQEAtITMMxcua5Rsa2FSoOujz3mUTOWUgJnLVWREZY9nZOIG41w3SfYvm4SEHi3y +YJ0wTsyEheIszx6e/jarM3c1RNg1lho9Nuh6DtjVR6FqaYvZ/Ls6rnla1fTWcbua +kCNrmreIdIcMHl+5ni36q1Mr3Lt2PpNMCAiMHqIjHNRqrSK6mQEubWXLviRmVSRL +QESxG9fhwoXA3hA/Pe24/PHxI1Pcv2WXb9n5QHGNfb2V1M6+oF4nI979ptAmDgAp +6zxG8D1gvz9Q0twmQVGeFDdCBKNwV6gbh+0t+nvujArjqWaJGctB+d1ENmHP4ndG +yH329JKBNv3bNPFyfvMMFr20FQIDAQABo0IwQDAdBgNVHQ4EFgQUBx/S55zawm6i +QLSwelAQUHTEyL0wDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwDQYJ +KoZIhvcNAQEFBQADggEBAIlXshZ6qML91tmbmzTCnLQyFE2npN/svqe++EPbkTfO +tDIuUFUaNU52Q3Eg75N3ThVwLofDwR1t3Mu1J9QsVtFSUzpE0nPIxBsFZVpikpzu +QY0x2+c06lkh1QF612S4ZDnNye2v7UsDSKegmQGA3GWjNq5lWUhPgkvIZfFXHeVZ +Lgo/bNjR9eUJtGxUAArgFU2HdW23WJZa3W3SAKD0m0i+wzekujbgfIeFlxoVot4u +olu9rxj5kFDNcFn4J2dHy8egBzp90SxdbBk6ZrV9/ZFvgrG+CJPbFEfxojfHRZ48 +x3evZKiT3/Zpg4Jg8klCNO1aAFSFHBY2kgxc+qatv9s= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFRjCCAy6gAwIBAgIIbYwURrGmCu4wDQYJKoZIhvcNAQEMBQAwQTELMAkGA1UE +BhMCVVMxFDASBgNVBAoMC0FmZmlybVRydXN0MRwwGgYDVQQDDBNBZmZpcm1UcnVz +dCBQcmVtaXVtMB4XDTEwMDEyOTE0MTAzNloXDTQwMTIzMTE0MTAzNlowQTELMAkG +A1UEBhMCVVMxFDASBgNVBAoMC0FmZmlybVRydXN0MRwwGgYDVQQDDBNBZmZpcm1U +cnVzdCBQcmVtaXVtMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAxBLf +qV/+Qd3d9Z+K4/as4Tx4mrzY8H96oDMq3I0gW64tb+eT2TZwamjPjlGjhVtnBKAQ +JG9dKILBl1fYSCkTtuG+kU3fhQxTGJoeJKJPj/CihQvL9Cl/0qRY7iZNyaqoe5rZ ++jjeRFcV5fiMyNlI4g0WJx0eyIOFJbe6qlVBzAMiSy2RjYvmia9mx+n/K+k8rNrS +s8PhaJyJ+HoAVt70VZVs+7pk3WKL3wt3MutizCaam7uqYoNMtAZ6MMgpv+0GTZe5 +HMQxK9VfvFMSF5yZVylmd2EhMQcuJUmdGPLu8ytxjLW6OQdJd/zvLpKQBY0tL3d7 +70O/Nbua2Plzpyzy0FfuKE4mX4+QaAkvuPjcBukumj5Rp9EixAqnOEhss/n/fauG +V+O61oV4d7pD6kh/9ti+I20ev9E2bFhc8e6kGVQa9QPSdubhjL08s9NIS+LI+H+S +qHZGnEJlPqQewQcDWkYtuJfzt9WyVSHvutxMAJf7FJUnM7/oQ0dG0giZFmA7mn7S +5u046uwBHjxIVkkJx0w3AJ6IDsBz4W9m6XJHMD4Q5QsDyZpCAGzFlH5hxIrff4Ia +C1nEWTJ3s7xgaVY5/bQGeyzWZDbZvUjthB9+pSKPKrhC9IK31FOQeE4tGv2Bb0TX +OwF0lkLgAOIua+rF7nKsu7/+6qqo+Nz2snmKtmcCAwEAAaNCMEAwHQYDVR0OBBYE +FJ3AZ6YMItkm9UWrpmVSESfYRaxjMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/ +BAQDAgEGMA0GCSqGSIb3DQEBDAUAA4ICAQCzV00QYk465KzquByvMiPIs0laUZx2 +KI15qldGF9X1Uva3ROgIRL8YhNILgM3FEv0AVQVhh0HctSSePMTYyPtwni94loMg +Nt58D2kTiKV1NpgIpsbfrM7jWNa3Pt668+s0QNiigfV4Py/VpfzZotReBA4Xrf5B +8OWycvpEgjNC6C1Y91aMYj+6QrCcDFx+LmUmXFNPALJ4fqENmS2NuB2OosSw/WDQ +MKSOyARiqcTtNd56l+0OOF6SL5Nwpamcb6d9Ex1+xghIsV5n61EIJenmJWtSKZGc +0jlzCFfemQa0W50QBuHCAKi4HEoCChTQwUHK+4w1IX2COPKpVJEZNZOUbWo6xbLQ +u4mGk+ibyQ86p3q4ofB4Rvr8Ny/lioTz3/4E2aFooC8k4gmVBtWVyuEklut89pMF +u+1z6S3RdTnX5yTb2E5fQ4+e0BQ5v1VwSJlXMbSc7kqYA5YwH2AG7hsj/oFgIxpH +YoWlzBk0gG+zrBrjn/B7SK3VAdlntqlyk+otZrWyuOQ9PLLvTIzq6we/qzWaVYa8 +GKa1qF60g2xraUDTn9zxw2lrueFtCfTxqlB2Cnp9ehehVZZCmTEJ3WARjQUwfuaO +RtGdFNrHF+QFlozEJLUbzxQHskD4o55BhrwE0GuWyCqANP2/7waj3VjFhT0+j/6e +KeC2uAloGRwYQw== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIB/jCCAYWgAwIBAgIIdJclisc/elQwCgYIKoZIzj0EAwMwRTELMAkGA1UEBhMC +VVMxFDASBgNVBAoMC0FmZmlybVRydXN0MSAwHgYDVQQDDBdBZmZpcm1UcnVzdCBQ +cmVtaXVtIEVDQzAeFw0xMDAxMjkxNDIwMjRaFw00MDEyMzExNDIwMjRaMEUxCzAJ +BgNVBAYTAlVTMRQwEgYDVQQKDAtBZmZpcm1UcnVzdDEgMB4GA1UEAwwXQWZmaXJt +VHJ1c3QgUHJlbWl1bSBFQ0MwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQNMF4bFZ0D +0KF5Nbc6PJJ6yhUczWLznCZcBz3lVPqj1swS6vQUX+iOGasvLkjmrBhDeKzQN8O9 +ss0s5kfiGuZjuD0uL3jET9v0D6RoTFVya5UdThhClXjMNzyR4ptlKymjQjBAMB0G +A1UdDgQWBBSaryl6wBE1NSZRMADDav5A1a7WPDAPBgNVHRMBAf8EBTADAQH/MA4G +A1UdDwEB/wQEAwIBBjAKBggqhkjOPQQDAwNnADBkAjAXCfOHiFBar8jAQr9HX/Vs +aobgxCd05DhT1wV/GzTjxi+zygk8N53X57hG8f2h4nECMEJZh0PUUd+60wkyWs6I +flc9nF9Ca/UHLbXwgpP5WW+uZPpY5Yse42O+tYHNbwKMeQ== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIDQTCCAimgAwIBAgITBmyfz5m/jAo54vB4ikPmljZbyjANBgkqhkiG9w0BAQsF +ADA5MQswCQYDVQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6 +b24gUm9vdCBDQSAxMB4XDTE1MDUyNjAwMDAwMFoXDTM4MDExNzAwMDAwMFowOTEL +MAkGA1UEBhMCVVMxDzANBgNVBAoTBkFtYXpvbjEZMBcGA1UEAxMQQW1hem9uIFJv +b3QgQ0EgMTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALJ4gHHKeNXj +ca9HgFB0fW7Y14h29Jlo91ghYPl0hAEvrAIthtOgQ3pOsqTQNroBvo3bSMgHFzZM +9O6II8c+6zf1tRn4SWiw3te5djgdYZ6k/oI2peVKVuRF4fn9tBb6dNqcmzU5L/qw +IFAGbHrQgLKm+a/sRxmPUDgH3KKHOVj4utWp+UhnMJbulHheb4mjUcAwhmahRWa6 +VOujw5H5SNz/0egwLX0tdHA114gk957EWW67c4cX8jJGKLhD+rcdqsq08p8kDi1L +93FcXmn/6pUCyziKrlA4b9v7LWIbxcceVOF34GfID5yHI9Y/QCB/IIDEgEw+OyQm +jgSubJrIqg0CAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMC +AYYwHQYDVR0OBBYEFIQYzIU07LwMlJQuCFmcx7IQTgoIMA0GCSqGSIb3DQEBCwUA +A4IBAQCY8jdaQZChGsV2USggNiMOruYou6r4lK5IpDB/G/wkjUu0yKGX9rbxenDI +U5PMCCjjmCXPI6T53iHTfIUJrU6adTrCC2qJeHZERxhlbI1Bjjt/msv0tadQ1wUs +N+gDS63pYaACbvXy8MWy7Vu33PqUXHeeE6V/Uq2V8viTO96LXFvKWlJbYK8U90vv +o/ufQJVtMVT8QtPHRh8jrdkPSHCa2XV4cdFyQzR1bldZwgJcJmApzyMZFo6IQ6XU +5MsI+yMRQ+hDKXJioaldXgjUkK642M4UwtBV8ob2xJNDd2ZhwLnoQdeXeGADbkpy +rqXRfboQnoZsG4q5WTP468SQvvG5 +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFQTCCAymgAwIBAgITBmyf0pY1hp8KD+WGePhbJruKNzANBgkqhkiG9w0BAQwF +ADA5MQswCQYDVQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6 +b24gUm9vdCBDQSAyMB4XDTE1MDUyNjAwMDAwMFoXDTQwMDUyNjAwMDAwMFowOTEL +MAkGA1UEBhMCVVMxDzANBgNVBAoTBkFtYXpvbjEZMBcGA1UEAxMQQW1hem9uIFJv +b3QgQ0EgMjCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK2Wny2cSkxK +gXlRmeyKy2tgURO8TW0G/LAIjd0ZEGrHJgw12MBvIITplLGbhQPDW9tK6Mj4kHbZ +W0/jTOgGNk3Mmqw9DJArktQGGWCsN0R5hYGCrVo34A3MnaZMUnbqQ523BNFQ9lXg +1dKmSYXpN+nKfq5clU1Imj+uIFptiJXZNLhSGkOQsL9sBbm2eLfq0OQ6PBJTYv9K +8nu+NQWpEjTj82R0Yiw9AElaKP4yRLuH3WUnAnE72kr3H9rN9yFVkE8P7K6C4Z9r +2UXTu/Bfh+08LDmG2j/e7HJV63mjrdvdfLC6HM783k81ds8P+HgfajZRRidhW+me +z/CiVX18JYpvL7TFz4QuK/0NURBs+18bvBt+xa47mAExkv8LV/SasrlX6avvDXbR +8O70zoan4G7ptGmh32n2M8ZpLpcTnqWHsFcQgTfJU7O7f/aS0ZzQGPSSbtqDT6Zj +mUyl+17vIWR6IF9sZIUVyzfpYgwLKhbcAS4y2j5L9Z469hdAlO+ekQiG+r5jqFoz +7Mt0Q5X5bGlSNscpb/xVA1wf+5+9R+vnSUeVC06JIglJ4PVhHvG/LopyboBZ/1c6 ++XUyo05f7O0oYtlNc/LMgRdg7c3r3NunysV+Ar3yVAhU/bQtCSwXVEqY0VThUWcI +0u1ufm8/0i2BWSlmy5A5lREedCf+3euvAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMB +Af8wDgYDVR0PAQH/BAQDAgGGMB0GA1UdDgQWBBSwDPBMMPQFWAJI/TPlUq9LhONm +UjANBgkqhkiG9w0BAQwFAAOCAgEAqqiAjw54o+Ci1M3m9Zh6O+oAA7CXDpO8Wqj2 +LIxyh6mx/H9z/WNxeKWHWc8w4Q0QshNabYL1auaAn6AFC2jkR2vHat+2/XcycuUY ++gn0oJMsXdKMdYV2ZZAMA3m3MSNjrXiDCYZohMr/+c8mmpJ5581LxedhpxfL86kS +k5Nrp+gvU5LEYFiwzAJRGFuFjWJZY7attN6a+yb3ACfAXVU3dJnJUH/jWS5E4ywl +7uxMMne0nxrpS10gxdr9HIcWxkPo1LsmmkVwXqkLN1PiRnsn/eBG8om3zEK2yygm +btmlyTrIQRNg91CMFa6ybRoVGld45pIq2WWQgj9sAq+uEjonljYE1x2igGOpm/Hl +urR8FLBOybEfdF849lHqm/osohHUqS0nGkWxr7JOcQ3AWEbWaQbLU8uz/mtBzUF+ +fUwPfHJ5elnNXkoOrJupmHN5fLT0zLm4BwyydFy4x2+IoZCn9Kr5v2c69BoVYh63 +n749sSmvZ6ES8lgQGVMDMBu4Gon2nL2XA46jCfMdiyHxtN/kHNGfZQIG6lzWE7OE +76KlXIx3KadowGuuQNKotOrN8I1LOJwZmhsoVLiJkO/KdYE+HvJkJMcYr07/R54H +9jVlpNMKVv/1F2Rs76giJUmTtt8AF9pYfl3uxRuw0dFfIRDH+fO6AgonB8Xx1sfT +4PsJYGw= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIBtjCCAVugAwIBAgITBmyf1XSXNmY/Owua2eiedgPySjAKBggqhkjOPQQDAjA5 +MQswCQYDVQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6b24g +Um9vdCBDQSAzMB4XDTE1MDUyNjAwMDAwMFoXDTQwMDUyNjAwMDAwMFowOTELMAkG +A1UEBhMCVVMxDzANBgNVBAoTBkFtYXpvbjEZMBcGA1UEAxMQQW1hem9uIFJvb3Qg +Q0EgMzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABCmXp8ZBf8ANm+gBG1bG8lKl +ui2yEujSLtf6ycXYqm0fc4E7O5hrOXwzpcVOho6AF2hiRVd9RFgdszflZwjrZt6j +QjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGGMB0GA1UdDgQWBBSr +ttvXBp43rDCGB5Fwx5zEGbF4wDAKBggqhkjOPQQDAgNJADBGAiEA4IWSoxe3jfkr +BqWTrBqYaGFy+uGh0PsceGCmQ5nFuMQCIQCcAu/xlJyzlvnrxir4tiz+OpAUFteM +YyRIHN8wfdVoOw== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIB8jCCAXigAwIBAgITBmyf18G7EEwpQ+Vxe3ssyBrBDjAKBggqhkjOPQQDAzA5 +MQswCQYDVQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6b24g +Um9vdCBDQSA0MB4XDTE1MDUyNjAwMDAwMFoXDTQwMDUyNjAwMDAwMFowOTELMAkG +A1UEBhMCVVMxDzANBgNVBAoTBkFtYXpvbjEZMBcGA1UEAxMQQW1hem9uIFJvb3Qg +Q0EgNDB2MBAGByqGSM49AgEGBSuBBAAiA2IABNKrijdPo1MN/sGKe0uoe0ZLY7Bi +9i0b2whxIdIA6GO9mif78DluXeo9pcmBqqNbIJhFXRbb/egQbeOc4OO9X4Ri83Bk +M6DLJC9wuoihKqB1+IGuYgbEgds5bimwHvouXKNCMEAwDwYDVR0TAQH/BAUwAwEB +/zAOBgNVHQ8BAf8EBAMCAYYwHQYDVR0OBBYEFNPsxzplbszh2naaVvuc84ZtV+WB +MAoGCCqGSM49BAMDA2gAMGUCMDqLIfG9fhGt0O9Yli/W651+kI0rz2ZVwyzjKKlw +CkcO8DdZEv8tmZQoTipPNU0zWgIxAOp1AE47xDqUEpHJWEadIRNyp4iciuRMStuW +1KyLa2tJElMzrdfkviT8tQp21KW8EA== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIDdzCCAl+gAwIBAgIIXDPLYixfszIwDQYJKoZIhvcNAQELBQAwPDEeMBwGA1UE +AwwVQXRvcyBUcnVzdGVkUm9vdCAyMDExMQ0wCwYDVQQKDARBdG9zMQswCQYDVQQG +EwJERTAeFw0xMTA3MDcxNDU4MzBaFw0zMDEyMzEyMzU5NTlaMDwxHjAcBgNVBAMM +FUF0b3MgVHJ1c3RlZFJvb3QgMjAxMTENMAsGA1UECgwEQXRvczELMAkGA1UEBhMC +REUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCVhTuXbyo7LjvPpvMp +Nb7PGKw+qtn4TaA+Gke5vJrf8v7MPkfoepbCJI419KkM/IL9bcFyYie96mvr54rM +VD6QUM+A1JX76LWC1BTFtqlVJVfbsVD2sGBkWXppzwO3bw2+yj5vdHLqqjAqc2K+ +SZFhyBH+DgMq92og3AIVDV4VavzjgsG1xZ1kCWyjWZgHJ8cblithdHFsQ/H3NYkQ +4J7sVaE3IqKHBAUsR320HLliKWYoyrfhk/WklAOZuXCFteZI6o1Q/NnezG8HDt0L +cp2AMBYHlT8oDv3FdU9T1nSatCQujgKRz3bFmx5VdJx4IbHwLfELn8LVlhgf8FQi +eowHAgMBAAGjfTB7MB0GA1UdDgQWBBSnpQaxLKYJYO7Rl+lwrrw7GWzbITAPBgNV +HRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFKelBrEspglg7tGX6XCuvDsZbNshMBgG +A1UdIAQRMA8wDQYLKwYBBAGwLQMEAQEwDgYDVR0PAQH/BAQDAgGGMA0GCSqGSIb3 +DQEBCwUAA4IBAQAmdzTblEiGKkGdLD4GkGDEjKwLVLgfuXvTBznk+j57sj1O7Z8j +vZfza1zv7v1Apt+hk6EKhqzvINB5Ab149xnYJDE0BAGmuhWawyfc2E8PzBhj/5kP +DpFrdRbhIfzYJsdHt6bPWHJxfrrhTZVHO8mvbaG0weyJ9rQPOLXiZNwlz6bb65pc +maHFCN795trV1lpFDMS3wrUU77QR/w4VtfX128a961qn8FYiqTxlVMYVqL2Gns2D +lmh6cYGJ4Qvh6hEbaAjMaZ7snkGeRDImeuKHCnE96+RapNLbxc3G3mB/ufNPRJLv +KrcYPqcZ2Qt9sTdBQrC6YB3y/gkRsPCHe6ed +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIGFDCCA/ygAwIBAgIIU+w77vuySF8wDQYJKoZIhvcNAQEFBQAwUTELMAkGA1UE +BhMCRVMxQjBABgNVBAMMOUF1dG9yaWRhZCBkZSBDZXJ0aWZpY2FjaW9uIEZpcm1h +cHJvZmVzaW9uYWwgQ0lGIEE2MjYzNDA2ODAeFw0wOTA1MjAwODM4MTVaFw0zMDEy +MzEwODM4MTVaMFExCzAJBgNVBAYTAkVTMUIwQAYDVQQDDDlBdXRvcmlkYWQgZGUg +Q2VydGlmaWNhY2lvbiBGaXJtYXByb2Zlc2lvbmFsIENJRiBBNjI2MzQwNjgwggIi +MA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDKlmuO6vj78aI14H9M2uDDUtd9 +thDIAl6zQyrET2qyyhxdKJp4ERppWVevtSBC5IsP5t9bpgOSL/UR5GLXMnE42QQM +cas9UX4PB99jBVzpv5RvwSmCwLTaUbDBPLutN0pcyvFLNg4kq7/DhHf9qFD0sefG +L9ItWY16Ck6WaVICqjaY7Pz6FIMMNx/Jkjd/14Et5cS54D40/mf0PmbR0/RAz15i +NA9wBj4gGFrO93IbJWyTdBSTo3OxDqqHECNZXyAFGUftaI6SEspd/NYrspI8IM/h +X68gvqB2f3bl7BqGYTM+53u0P6APjqK5am+5hyZvQWyIplD9amML9ZMWGxmPsu2b +m8mQ9QEM3xk9Dz44I8kvjwzRAv4bVdZO0I08r0+k8/6vKtMFnXkIoctXMbScyJCy +Z/QYFpM6/EfY0XiWMR+6KwxfXZmtY4laJCB22N/9q06mIqqdXuYnin1oKaPnirja +EbsXLZmdEyRG98Xi2J+Of8ePdG1asuhy9azuJBCtLxTa/y2aRnFHvkLfuwHb9H/T +KI8xWVvTyQKmtFLKbpf7Q8UIJm+K9Lv9nyiqDdVF8xM6HdjAeI9BZzwelGSuewvF +6NkBiDkal4ZkQdU7hwxu+g/GvUgUvzlN1J5Bto+WHWOWk9mVBngxaJ43BjuAiUVh +OSPHG0SjFeUc+JIwuwIDAQABo4HvMIHsMBIGA1UdEwEB/wQIMAYBAf8CAQEwDgYD +VR0PAQH/BAQDAgEGMB0GA1UdDgQWBBRlzeurNR4APn7VdMActHNHDhpkLzCBpgYD +VR0gBIGeMIGbMIGYBgRVHSAAMIGPMC8GCCsGAQUFBwIBFiNodHRwOi8vd3d3LmZp +cm1hcHJvZmVzaW9uYWwuY29tL2NwczBcBggrBgEFBQcCAjBQHk4AUABhAHMAZQBv +ACAAZABlACAAbABhACAAQgBvAG4AYQBuAG8AdgBhACAANAA3ACAAQgBhAHIAYwBl +AGwAbwBuAGEAIAAwADgAMAAxADcwDQYJKoZIhvcNAQEFBQADggIBABd9oPm03cXF +661LJLWhAqvdpYhKsg9VSytXjDvlMd3+xDLx51tkljYyGOylMnfX40S2wBEqgLk9 +am58m9Ot/MPWo+ZkKXzR4Tgegiv/J2Wv+xYVxC5xhOW1//qkR71kMrv2JYSiJ0L1 +ILDCExARzRAVukKQKtJE4ZYm6zFIEv0q2skGz3QeqUvVhyj5eTSSPi5E6PaPT481 +PyWzOdxjKpBrIF/EUhJOlywqrJ2X3kjyo2bbwtKDlaZmp54lD+kLM5FlClrD2VQS +3a/DTg4fJl4N3LON7NWBcN7STyQF82xO9UxJZo3R/9ILJUFI/lGExkKvgATP0H5k +SeTy36LssUzAKh3ntLFlosS88Zj0qnAHY7S42jtM+kAiMFsRpvAFDsYCA0irhpuF +3dvd6qJ2gHN99ZwExEWN57kci57q13XRcrHedUTnQn3iV2t93Jm8PYMo6oCTjcVM +ZcFwgbg4/EMxsvYDNEeyrPsiBsse3RdHHF9mudMaotoRsaS8I8nkvof/uZS2+F0g +StRf571oe2XyFR7SOqkt6dhrJKyXWERHrVkY8SFlcN7ONGCoQPHzPKTDKCOM/icz +Q0CgFzzr6juwcqajuUpLXhZI9LK8yIySxZ2frHI2vDSANGupi5LAuBft7HZT9SQB +jLMi6Et8Vcad+qMUu2WFbm5PEn4KPJ2V +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIDdzCCAl+gAwIBAgIEAgAAuTANBgkqhkiG9w0BAQUFADBaMQswCQYDVQQGEwJJ +RTESMBAGA1UEChMJQmFsdGltb3JlMRMwEQYDVQQLEwpDeWJlclRydXN0MSIwIAYD +VQQDExlCYWx0aW1vcmUgQ3liZXJUcnVzdCBSb290MB4XDTAwMDUxMjE4NDYwMFoX +DTI1MDUxMjIzNTkwMFowWjELMAkGA1UEBhMCSUUxEjAQBgNVBAoTCUJhbHRpbW9y +ZTETMBEGA1UECxMKQ3liZXJUcnVzdDEiMCAGA1UEAxMZQmFsdGltb3JlIEN5YmVy +VHJ1c3QgUm9vdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKMEuyKr +mD1X6CZymrV51Cni4eiVgLGw41uOKymaZN+hXe2wCQVt2yguzmKiYv60iNoS6zjr +IZ3AQSsBUnuId9Mcj8e6uYi1agnnc+gRQKfRzMpijS3ljwumUNKoUMMo6vWrJYeK +mpYcqWe4PwzV9/lSEy/CG9VwcPCPwBLKBsua4dnKM3p31vjsufFoREJIE9LAwqSu +XmD+tqYF/LTdB1kC1FkYmGP1pWPgkAx9XbIGevOF6uvUA65ehD5f/xXtabz5OTZy +dc93Uk3zyZAsuT3lySNTPx8kmCFcB5kpvcY67Oduhjprl3RjM71oGDHweI12v/ye +jl0qhqdNkNwnGjkCAwEAAaNFMEMwHQYDVR0OBBYEFOWdWTCCR1jMrPoIVDaGezq1 +BE3wMBIGA1UdEwEB/wQIMAYBAf8CAQMwDgYDVR0PAQH/BAQDAgEGMA0GCSqGSIb3 +DQEBBQUAA4IBAQCFDF2O5G9RaEIFoN27TyclhAO992T9Ldcw46QQF+vaKSm2eT92 +9hkTI7gQCvlYpNRhcL0EYWoSihfVCr3FvDB81ukMJY2GQE/szKN+OMY3EU/t3Wgx +jkzSswF07r51XgdIGn9w/xZchMB5hbgF/X++ZRGjD8ACtPhSNzkE1akxehi/oCr0 +Epn3o0WC4zxe9Z2etciefC7IpJ5OCBRLbf1wbWsaY71k5h+3zvDyny67G7fyUIhz +ksLi4xaNmjICq44Y3ekQEe5+NauQrz4wlHrQMz2nZQ/1/I6eYs9HRCwBXbsdtTLS +R9I4LtD+gdwyah617jzV/OeBHRnDJELqYzmp +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFWTCCA0GgAwIBAgIBAjANBgkqhkiG9w0BAQsFADBOMQswCQYDVQQGEwJOTzEd +MBsGA1UECgwUQnV5cGFzcyBBUy05ODMxNjMzMjcxIDAeBgNVBAMMF0J1eXBhc3Mg +Q2xhc3MgMiBSb290IENBMB4XDTEwMTAyNjA4MzgwM1oXDTQwMTAyNjA4MzgwM1ow +TjELMAkGA1UEBhMCTk8xHTAbBgNVBAoMFEJ1eXBhc3MgQVMtOTgzMTYzMzI3MSAw +HgYDVQQDDBdCdXlwYXNzIENsYXNzIDIgUm9vdCBDQTCCAiIwDQYJKoZIhvcNAQEB +BQADggIPADCCAgoCggIBANfHXvfBB9R3+0Mh9PT1aeTuMgHbo4Yf5FkNuud1g1Lr +6hxhFUi7HQfKjK6w3Jad6sNgkoaCKHOcVgb/S2TwDCo3SbXlzwx87vFKu3MwZfPV +L4O2fuPn9Z6rYPnT8Z2SdIrkHJasW4DptfQxh6NR/Md+oW+OU3fUl8FVM5I+GC91 +1K2GScuVr1QGbNgGE41b/+EmGVnAJLqBcXmQRFBoJJRfuLMR8SlBYaNByyM21cHx +MlAQTn/0hpPshNOOvEu/XAFOBz3cFIqUCqTqc/sLUegTBxj6DvEr0VQVfTzh97QZ +QmdiXnfgolXsttlpF9U6r0TtSsWe5HonfOV116rLJeffawrbD02TTqigzXsu8lkB +arcNuAeBfos4GzjmCleZPe4h6KP1DBbdi+w0jpwqHAAVF41og9JwnxgIzRFo1clr +Us3ERo/ctfPYV3Me6ZQ5BL/T3jjetFPsaRyifsSP5BtwrfKi+fv3FmRmaZ9JUaLi +FRhnBkp/1Wy1TbMz4GHrXb7pmA8y1x1LPC5aAVKRCfLf6o3YBkBjqhHk/sM3nhRS +P/TizPJhk9H9Z2vXUq6/aKtAQ6BXNVN48FP4YUIHZMbXb5tMOA1jrGKvNouicwoN +9SG9dKpN6nIDSdvHXx1iY8f93ZHsM+71bbRuMGjeyNYmsHVee7QHIJihdjK4TWxP +AgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFMmAd+BikoL1Rpzz +uvdMw964o605MA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQsFAAOCAgEAU18h +9bqwOlI5LJKwbADJ784g7wbylp7ppHR/ehb8t/W2+xUbP6umwHJdELFx7rxP462s +A20ucS6vxOOto70MEae0/0qyexAQH6dXQbLArvQsWdZHEIjzIVEpMMpghq9Gqx3t +OluwlN5E40EIosHsHdb9T7bWR9AUC8rmyrV7d35BH16Dx7aMOZawP5aBQW9gkOLo ++fsicdl9sz1Gv7SEr5AcD48Saq/v7h56rgJKihcrdv6sVIkkLE8/trKnToyokZf7 +KcZ7XC25y2a2t6hbElGFtQl+Ynhw/qlqYLYdDnkM/crqJIByw5c/8nerQyIKx+u2 +DISCLIBrQYoIwOula9+ZEsuK1V6ADJHgJgg2SMX6OBE1/yWDLfJ6v9r9jv6ly0Us +H8SIU653DtmadsWOLB2jutXsMq7Aqqz30XpN69QH4kj3Io6wpJ9qzo6ysmD0oyLQ +I+uUWnpp3Q+/QFesa1lQ2aOZ4W7+jQF5JyMV3pKdewlNWudLSDBaGOYKbeaP4NK7 +5t98biGCwWg5TbSYWGZizEqQXsP6JwSxeRV0mcy+rSDeJmAc61ZRpqPq5KM/p/9h +3PFaTWwyI0PurKju7koSCTxdccK+efrCh2gdC/1cacwG0Jp9VJkqyTkaGa9LKkPz +Y11aWOIv4x3kqdbQCtCev9eBCfHJxyYNrJgWVqA= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFWTCCA0GgAwIBAgIBAjANBgkqhkiG9w0BAQsFADBOMQswCQYDVQQGEwJOTzEd +MBsGA1UECgwUQnV5cGFzcyBBUy05ODMxNjMzMjcxIDAeBgNVBAMMF0J1eXBhc3Mg +Q2xhc3MgMyBSb290IENBMB4XDTEwMTAyNjA4Mjg1OFoXDTQwMTAyNjA4Mjg1OFow +TjELMAkGA1UEBhMCTk8xHTAbBgNVBAoMFEJ1eXBhc3MgQVMtOTgzMTYzMzI3MSAw +HgYDVQQDDBdCdXlwYXNzIENsYXNzIDMgUm9vdCBDQTCCAiIwDQYJKoZIhvcNAQEB +BQADggIPADCCAgoCggIBAKXaCpUWUOOV8l6ddjEGMnqb8RB2uACatVI2zSRHsJ8Y +ZLya9vrVediQYkwiL944PdbgqOkcLNt4EemOaFEVcsfzM4fkoF0LXOBXByow9c3E +N3coTRiR5r/VUv1xLXA+58bEiuPwKAv0dpihi4dVsjoT/Lc+JzeOIuOoTyrvYLs9 +tznDDgFHmV0ST9tD+leh7fmdvhFHJlsTmKtdFoqwNxxXnUX/iJY2v7vKB3tvh2PX +0DJq1l1sDPGzbjniazEuOQAnFN44wOwZZoYS6J1yFhNkUsepNxz9gjDthBgd9K5c +/3ATAOux9TN6S9ZV+AWNS2mw9bMoNlwUxFFzTWsL8TQH2xc519woe2v1n/MuwU8X +KhDzzMro6/1rqy6any2CbgTUUgGTLT2G/H783+9CHaZr77kgxve9oKeV/afmiSTY +zIw0bOIjL9kSGiG5VZFvC5F5GQytQIgLcOJ60g7YaEi7ghM5EFjp2CoHxhLbWNvS +O1UQRwUVZ2J+GGOmRj8JDlQyXr8NYnon74Do29lLBlo3WiXQCBJ31G8JUJc9yB3D +34xFMFbG02SrZvPAXpacw8Tvw3xrizp5f7NJzz3iiZ+gMEuFuZyUJHmPfWupRWgP +K9Dx2hzLabjKSWJtyNBjYt1gD1iqj6G8BaVmos8bdrKEZLFMOVLAMLrwjEsCsLa3 +AgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFEe4zf/lb+74suwv +Tg75JbCOPGvDMA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQsFAAOCAgEAACAj +QTUEkMJAYmDv4jVM1z+s4jSQuKFvdvoWFqRINyzpkMLyPPgKn9iB5btb2iUspKdV +cSQy9sgL8rxq+JOssgfCX5/bzMiKqr5qb+FJEMwx14C7u8jYog5kV+qi9cKpMRXS +IGrs/CIBKM+GuIAeqcwRpTzyFrNHnfzSgCHEy9BHcEGhyoMZCCxt8l13nIoUE9Q2 +HJLw5QY33KbmkJs4j1xrG0aGQ0JfPgEHU1RdZX33inOhmlRaHylDFCfChQ+1iHsa +O5S3HWCntZznKWlXWpuTekMwGwPXYshApqr8ZORK15FTAaggiG6cX0S5y2CBNOxv +033aSF/rtJC8LakcC6wc1aJoIIAE1vyxjy+7SjENSoYc6+I2KSb12tjE8nVhz36u +dmNKekBlk4f4HoCMhuWG1o8O/FMsYOgWYRqiPkN7zTlgVGr18okmAWiDSKIz6MkE +kbIRNBE+6tBDGR8Dk5AM/1E9V/RBbuHLoL7ryWPNbczk+DaqaJ3tvV2XcEQNtg41 +3OEMXbugUZTLfhbrES+jkkXITHHZvMmZUldGL1DPvTVp9D0VzgalLA8+9oG6lLvD +u79leNKGef9JOxqDDPDeeOzI8k1MGt6CKfjBWtrt7uYnXuhF0J0cUahoq0Tj0Itq +4/g7u9xN12TyUb7mqqta6THuBrxzvxNiCp/HuZc= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFaTCCA1GgAwIBAgIJAJK4iNuwisFjMA0GCSqGSIb3DQEBCwUAMFIxCzAJBgNV +BAYTAlNLMRMwEQYDVQQHEwpCcmF0aXNsYXZhMRMwEQYDVQQKEwpEaXNpZyBhLnMu +MRkwFwYDVQQDExBDQSBEaXNpZyBSb290IFIyMB4XDTEyMDcxOTA5MTUzMFoXDTQy +MDcxOTA5MTUzMFowUjELMAkGA1UEBhMCU0sxEzARBgNVBAcTCkJyYXRpc2xhdmEx +EzARBgNVBAoTCkRpc2lnIGEucy4xGTAXBgNVBAMTEENBIERpc2lnIFJvb3QgUjIw +ggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCio8QACdaFXS1tFPbCw3Oe +NcJxVX6B+6tGUODBfEl45qt5WDza/3wcn9iXAng+a0EE6UG9vgMsRfYvZNSrXaNH +PWSb6WiaxswbP7q+sos0Ai6YVRn8jG+qX9pMzk0DIaPY0jSTVpbLTAwAFjxfGs3I +x2ymrdMxp7zo5eFm1tL7A7RBZckQrg4FY8aAamkw/dLukO8NJ9+flXP04SXabBbe +QTg06ov80egEFGEtQX6sx3dOy1FU+16SGBsEWmjGycT6txOgmLcRK7fWV8x8nhfR +yyX+hk4kLlYMeE2eARKmK6cBZW58Yh2EhN/qwGu1pSqVg8NTEQxzHQuyRpDRQjrO +QG6Vrf/GlK1ul4SOfW+eioANSW1z4nuSHsPzwfPrLgVv2RvPN3YEyLRa5Beny912 +H9AZdugsBbPWnDTYltxhh5EF5EQIM8HauQhl1K6yNg3ruji6DOWbnuuNZt2Zz9aJ +QfYEkoopKW1rOhzndX0CcQ7zwOe9yxndnWCywmZgtrEE7snmhrmaZkCo5xHtgUUD +i/ZnWejBBhG93c+AAk9lQHhcR1DIm+YfgXvkRKhbhZri3lrVx/k6RGZL5DJUfORs +nLMOPReisjQS1n6yqEm70XooQL6iFh/f5DcfEXP7kAplQ6INfPgGAVUzfbANuPT1 +rqVCV3w2EYx7XsQDnYx5nQIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1Ud +DwEB/wQEAwIBBjAdBgNVHQ4EFgQUtZn4r7CU9eMg1gqtzk5WpC5uQu0wDQYJKoZI +hvcNAQELBQADggIBACYGXnDnZTPIgm7ZnBc6G3pmsgH2eDtpXi/q/075KMOYKmFM +tCQSin1tERT3nLXK5ryeJ45MGcipvXrA1zYObYVybqjGom32+nNjf7xueQgcnYqf +GopTpti72TVVsRHFqQOzVju5hJMiXn7B9hJSi+osZ7z+Nkz1uM/Rs0mSO9MpDpkb +lvdhuDvEK7Z4bLQjb/D907JedR+Zlais9trhxTF7+9FGs9K8Z7RiVLoJ92Owk6Ka ++elSLotgEqv89WBW7xBci8QaQtyDW2QOy7W81k/BfDxujRNt+3vrMNDcTa/F1bal +TFtxyegxvug4BkihGuLq0t4SOVga/4AOgnXmt8kHbA7v/zjxmHHEt38OFdAlab0i +nSvtBfZGR6ztwPDUO+Ls7pZbkBNOHlY667DvlruWIxG68kOGdGSVyCh13x01utI3 +gzhTODY7z2zp+WsO0PsE6E9312UBeIYMej4hYvF/Y3EMyZ9E26gnonW+boE+18Dr +G5gPcFw0sorMwIUY6256s/daoQe/qUKS82Ail+QUoQebTnbAjn39pCXHR+3/H3Os +zMOl6W8KjptlwlCFtaOgUxLMVYdh84GuEEZhvUQhuMI9dM9+JDX6HAcOmz0iyu8x +L4ysEr3vQCj8KWefshNPZiTEUxnpHikV7+ZtsH8tZ/3zbBt1RqPlShfppNcL +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFjTCCA3WgAwIBAgIEGErM1jANBgkqhkiG9w0BAQsFADBWMQswCQYDVQQGEwJD +TjEwMC4GA1UECgwnQ2hpbmEgRmluYW5jaWFsIENlcnRpZmljYXRpb24gQXV0aG9y +aXR5MRUwEwYDVQQDDAxDRkNBIEVWIFJPT1QwHhcNMTIwODA4MDMwNzAxWhcNMjkx +MjMxMDMwNzAxWjBWMQswCQYDVQQGEwJDTjEwMC4GA1UECgwnQ2hpbmEgRmluYW5j +aWFsIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MRUwEwYDVQQDDAxDRkNBIEVWIFJP +T1QwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDXXWvNED8fBVnVBU03 +sQ7smCuOFR36k0sXgiFxEFLXUWRwFsJVaU2OFW2fvwwbwuCjZ9YMrM8irq93VCpL +TIpTUnrD7i7es3ElweldPe6hL6P3KjzJIx1qqx2hp/Hz7KDVRM8Vz3IvHWOX6Jn5 +/ZOkVIBMUtRSqy5J35DNuF++P96hyk0g1CXohClTt7GIH//62pCfCqktQT+x8Rgp +7hZZLDRJGqgG16iI0gNyejLi6mhNbiyWZXvKWfry4t3uMCz7zEasxGPrb382KzRz +EpR/38wmnvFyXVBlWY9ps4deMm/DGIq1lY+wejfeWkU7xzbh72fROdOXW3NiGUgt +hxwG+3SYIElz8AXSG7Ggo7cbcNOIabla1jj0Ytwli3i/+Oh+uFzJlU9fpy25IGvP +a931DfSCt/SyZi4QKPaXWnuWFo8BGS1sbn85WAZkgwGDg8NNkt0yxoekN+kWzqot +aK8KgWU6cMGbrU1tVMoqLUuFG7OA5nBFDWteNfB/O7ic5ARwiRIlk9oKmSJgamNg +TnYGmE69g60dWIolhdLHZR4tjsbftsbhf4oEIRUpdPA+nJCdDC7xij5aqgwJHsfV +PKPtl8MeNPo4+QgO48BdK4PRVmrJtqhUUy54Mmc9gn900PvhtgVguXDbjgv5E1hv +cWAQUhC5wUEJ73IfZzF4/5YFjQIDAQABo2MwYTAfBgNVHSMEGDAWgBTj/i39KNAL +tbq2osS/BqoFjJP7LzAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAd +BgNVHQ4EFgQU4/4t/SjQC7W6tqLEvwaqBYyT+y8wDQYJKoZIhvcNAQELBQADggIB +ACXGumvrh8vegjmWPfBEp2uEcwPenStPuiB/vHiyz5ewG5zz13ku9Ui20vsXiObT +ej/tUxPQ4i9qecsAIyjmHjdXNYmEwnZPNDatZ8POQQaIxffu2Bq41gt/UP+TqhdL +jOztUmCypAbqTuv0axn96/Ua4CUqmtzHQTb3yHQFhDmVOdYLO6Qn+gjYXB74BGBS +ESgoA//vU2YApUo0FmZ8/Qmkrp5nGm9BC2sGE5uPhnEFtC+NiWYzKXZUmhH4J/qy +P5Hgzg0b8zAarb8iXRvTvyUFTeGSGn+ZnzxEk8rUQElsgIfXBDrDMlI1Dlb4pd19 +xIsNER9Tyx6yF7Zod1rg1MvIB671Oi6ON7fQAUtDKXeMOZePglr4UeWJoBjnaH9d +Ci77o0cOPaYjesYBx4/IXr9tgFa+iiS6M+qf4TIRnvHST4D2G0CvOJ4RUHlzEhLN +5mydLIhyPDCBBpEi6lmt2hkuIsKNuYyH4Ga8cyNfIWRjgEj1oDwYPZTISEEdQLpe +/v5WOaHIz16eGWRGENoXkbcFgKyLmZJ956LYBws2J+dIeWCKw9cTXPhyQN9Ky8+Z +AAoACxGV2lZFA4gKn2fQ1XmxqI1AbQ3CekD6819kR5LLU7m7Wc5P/dAVUwHY3+vZ +5nbv0CO7O6l5s9UCKc2Jo5YPSjXnTkLAdc0Hz+Ys63su +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIEHTCCAwWgAwIBAgIQToEtioJl4AsC7j41AkblPTANBgkqhkiG9w0BAQUFADCB +gTELMAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4G +A1UEBxMHU2FsZm9yZDEaMBgGA1UEChMRQ09NT0RPIENBIExpbWl0ZWQxJzAlBgNV +BAMTHkNPTU9ETyBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAeFw0wNjEyMDEwMDAw +MDBaFw0yOTEyMzEyMzU5NTlaMIGBMQswCQYDVQQGEwJHQjEbMBkGA1UECBMSR3Jl +YXRlciBNYW5jaGVzdGVyMRAwDgYDVQQHEwdTYWxmb3JkMRowGAYDVQQKExFDT01P +RE8gQ0EgTGltaXRlZDEnMCUGA1UEAxMeQ09NT0RPIENlcnRpZmljYXRpb24gQXV0 +aG9yaXR5MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0ECLi3LjkRv3 +UcEbVASY06m/weaKXTuH+7uIzg3jLz8GlvCiKVCZrts7oVewdFFxze1CkU1B/qnI +2GqGd0S7WWaXUF601CxwRM/aN5VCaTwwxHGzUvAhTaHYujl8HJ6jJJ3ygxaYqhZ8 +Q5sVW7euNJH+1GImGEaaP+vB+fGQV+useg2L23IwambV4EajcNxo2f8ESIl33rXp ++2dtQem8Ob0y2WIC8bGoPW43nOIv4tOiJovGuFVDiOEjPqXSJDlqR6sA1KGzqSX+ +DT+nHbrTUcELpNqsOO9VUCQFZUaTNE8tja3G1CEZ0o7KBWFxB3NH5YoZEr0ETc5O +nKVIrLsm9wIDAQABo4GOMIGLMB0GA1UdDgQWBBQLWOWLxkwVN6RAqTCpIb5HNlpW +/zAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zBJBgNVHR8EQjBAMD6g +PKA6hjhodHRwOi8vY3JsLmNvbW9kb2NhLmNvbS9DT01PRE9DZXJ0aWZpY2F0aW9u +QXV0aG9yaXR5LmNybDANBgkqhkiG9w0BAQUFAAOCAQEAPpiem/Yb6dc5t3iuHXIY +SdOH5EOC6z/JqvWote9VfCFSZfnVDeFs9D6Mk3ORLgLETgdxb8CPOGEIqB6BCsAv +IC9Bi5HcSEW88cbeunZrM8gALTFGTO3nnc+IlP8zwFboJIYmuNg4ON8qa90SzMc/ +RxdMosIGlgnW2/4/PEZB31jiVg88O8EckzXZOFKs7sjsLjBOlDW0JB9LeGna8gI4 +zJVSk/BwJVmcIGfE7vmLV2H0knZ9P4SNVbfo5azV8fUZVqZa+5Acr5Pr5RzUZ5dd +BA6+C4OmF4O5MBKgxTMVBbkN+8cFduPYSo38NBejxiEovjBFMR7HeL5YYTisO+IB +ZQ== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICiTCCAg+gAwIBAgIQH0evqmIAcFBUTAGem2OZKjAKBggqhkjOPQQDAzCBhTEL +MAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UE +BxMHU2FsZm9yZDEaMBgGA1UEChMRQ09NT0RPIENBIExpbWl0ZWQxKzApBgNVBAMT +IkNPTU9ETyBFQ0MgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMDgwMzA2MDAw +MDAwWhcNMzgwMTE4MjM1OTU5WjCBhTELMAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdy +ZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UEBxMHU2FsZm9yZDEaMBgGA1UEChMRQ09N +T0RPIENBIExpbWl0ZWQxKzApBgNVBAMTIkNPTU9ETyBFQ0MgQ2VydGlmaWNhdGlv +biBBdXRob3JpdHkwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQDR3svdcmCFYX7deSR +FtSrYpn1PlILBs5BAH+X4QokPB0BBO490o0JlwzgdeT6+3eKKvUDYEs2ixYjFq0J +cfRK9ChQtP6IHG4/bC8vCVlbpVsLM5niwz2J+Wos77LTBumjQjBAMB0GA1UdDgQW +BBR1cacZSBm8nZ3qQUfflMRId5nTeTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/ +BAUwAwEB/zAKBggqhkjOPQQDAwNoADBlAjEA7wNbeqy3eApyt4jf/7VGFAkK+qDm +fQjGGoe9GKhzvSbKYAydzpmfz1wPMOG+FDHqAjAU9JM8SaczepBGR7NjfRObTrdv +GDeAU/7dIOA1mjbRxwG55tzd8/8dLDoWV9mSOdY= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIF2DCCA8CgAwIBAgIQTKr5yttjb+Af907YWwOGnTANBgkqhkiG9w0BAQwFADCB +hTELMAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4G +A1UEBxMHU2FsZm9yZDEaMBgGA1UEChMRQ09NT0RPIENBIExpbWl0ZWQxKzApBgNV +BAMTIkNPTU9ETyBSU0EgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMTAwMTE5 +MDAwMDAwWhcNMzgwMTE4MjM1OTU5WjCBhTELMAkGA1UEBhMCR0IxGzAZBgNVBAgT +EkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UEBxMHU2FsZm9yZDEaMBgGA1UEChMR +Q09NT0RPIENBIExpbWl0ZWQxKzApBgNVBAMTIkNPTU9ETyBSU0EgQ2VydGlmaWNh +dGlvbiBBdXRob3JpdHkwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCR +6FSS0gpWsawNJN3Fz0RndJkrN6N9I3AAcbxT38T6KhKPS38QVr2fcHK3YX/JSw8X +pz3jsARh7v8Rl8f0hj4K+j5c+ZPmNHrZFGvnnLOFoIJ6dq9xkNfs/Q36nGz637CC +9BR++b7Epi9Pf5l/tfxnQ3K9DADWietrLNPtj5gcFKt+5eNu/Nio5JIk2kNrYrhV +/erBvGy2i/MOjZrkm2xpmfh4SDBF1a3hDTxFYPwyllEnvGfDyi62a+pGx8cgoLEf +Zd5ICLqkTqnyg0Y3hOvozIFIQ2dOciqbXL1MGyiKXCJ7tKuY2e7gUYPDCUZObT6Z ++pUX2nwzV0E8jVHtC7ZcryxjGt9XyD+86V3Em69FmeKjWiS0uqlWPc9vqv9JWL7w +qP/0uK3pN/u6uPQLOvnoQ0IeidiEyxPx2bvhiWC4jChWrBQdnArncevPDt09qZah +SL0896+1DSJMwBGB7FY79tOi4lu3sgQiUpWAk2nojkxl8ZEDLXB0AuqLZxUpaVIC +u9ffUGpVRr+goyhhf3DQw6KqLCGqR84onAZFdr+CGCe01a60y1Dma/RMhnEw6abf +Fobg2P9A3fvQQoh/ozM6LlweQRGBY84YcWsr7KaKtzFcOmpH4MN5WdYgGq/yapiq +crxXStJLnbsQ/LBMQeXtHT1eKJ2czL+zUdqnR+WEUwIDAQABo0IwQDAdBgNVHQ4E +FgQUu69+Aj36pvE8hI6t7jiY7NkyMtQwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB +/wQFMAMBAf8wDQYJKoZIhvcNAQEMBQADggIBAArx1UaEt65Ru2yyTUEUAJNMnMvl +wFTPoCWOAvn9sKIN9SCYPBMtrFaisNZ+EZLpLrqeLppysb0ZRGxhNaKatBYSaVqM +4dc+pBroLwP0rmEdEBsqpIt6xf4FpuHA1sj+nq6PK7o9mfjYcwlYRm6mnPTXJ9OV +2jeDchzTc+CiR5kDOF3VSXkAKRzH7JsgHAckaVd4sjn8OoSgtZx8jb8uk2Intzna +FxiuvTwJaP+EmzzV1gsD41eeFPfR60/IvYcjt7ZJQ3mFXLrrkguhxuhoqEwWsRqZ +CuhTLJK7oQkYdQxlqHvLI7cawiiFwxv/0Cti76R7CZGYZ4wUAc1oBmpjIXUDgIiK +boHGhfKppC3n9KUkEEeDys30jXlYsQab5xoq2Z0B15R97QNKyvDb6KkBPvVWmcke +jkk9u+UJueBPSZI9FoJAzMxZxuY67RIuaTxslbH9qh17f4a+Hg4yRvv7E491f0yL +S0Zj/gA0QHDBw7mh3aZw4gSzQbzpgJHqZJx64SIDqZxubw5lT2yHh17zbqD5daWb +QOhTsiedSrnAdyGN/4fy3ryM7xfft0kL0fJuMAsaDk527RH89elWsn2/x20Kk4yl +0MC2Hb46TpSi125sC8KKfPog88Tk5c0NqMuRkrF8hey1FGlmDoLnzc7ILaZRfyHB +NVOFBkpdn627G190 +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIDqDCCApCgAwIBAgIJAP7c4wEPyUj/MA0GCSqGSIb3DQEBBQUAMDQxCzAJBgNV +BAYTAkZSMRIwEAYDVQQKDAlEaGlteW90aXMxETAPBgNVBAMMCENlcnRpZ25hMB4X +DTA3MDYyOTE1MTMwNVoXDTI3MDYyOTE1MTMwNVowNDELMAkGA1UEBhMCRlIxEjAQ +BgNVBAoMCURoaW15b3RpczERMA8GA1UEAwwIQ2VydGlnbmEwggEiMA0GCSqGSIb3 +DQEBAQUAA4IBDwAwggEKAoIBAQDIaPHJ1tazNHUmgh7stL7qXOEm7RFHYeGifBZ4 +QCHkYJ5ayGPhxLGWkv8YbWkj4Sti993iNi+RB7lIzw7sebYs5zRLcAglozyHGxny +gQcPOJAZ0xH+hrTy0V4eHpbNgGzOOzGTtvKg0KmVEn2lmsxryIRWijOp5yIVUxbw +zBfsV1/pogqYCd7jX5xv3EjjhQsVWqa6n6xI4wmy9/Qy3l40vhx4XUJbzg4ij02Q +130yGLMLLGq/jj8UEYkgDncUtT2UCIf3JR7VsmAA7G8qKCVuKj4YYxclPz5EIBb2 +JsglrgVKtOdjLPOMFlN+XPsRGgjBRmKfIrjxwo1p3Po6WAbfAgMBAAGjgbwwgbkw +DwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUGu3+QTmQtCRZvgHyUtVF9lo53BEw +ZAYDVR0jBF0wW4AUGu3+QTmQtCRZvgHyUtVF9lo53BGhOKQ2MDQxCzAJBgNVBAYT +AkZSMRIwEAYDVQQKDAlEaGlteW90aXMxETAPBgNVBAMMCENlcnRpZ25hggkA/tzj +AQ/JSP8wDgYDVR0PAQH/BAQDAgEGMBEGCWCGSAGG+EIBAQQEAwIABzANBgkqhkiG +9w0BAQUFAAOCAQEAhQMeknH2Qq/ho2Ge6/PAD/Kl1NqV5ta+aDY9fm4fTIrv0Q8h +bV6lUmPOEvjvKtpv6zf+EwLHyzs+ImvaYS5/1HI93TDhHkxAGYwP15zRgzB7mFnc +fca5DClMoTOi62c6ZYTTluLtdkVwj7Ur3vkj1kluPBS1xp81HlDQwY9qcEQCYsuu +HWhBp6pX6FOqB9IG9tUUBguRA3UsbHK1YZWaDYu5Def131TN3ubY1gkIl2PlwS6w +t0QmwCbAr1UwnjvVNioZBPRcHv/PLLf/0P2HQBHVESO7SMAhqaQoLf0V+LBOK/Qw +WyH8EZE0vkHve52Xdf+XlcCWWC/qu0bXu+TZLg== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIDuzCCAqOgAwIBAgIDBETAMA0GCSqGSIb3DQEBBQUAMH4xCzAJBgNVBAYTAlBM +MSIwIAYDVQQKExlVbml6ZXRvIFRlY2hub2xvZ2llcyBTLkEuMScwJQYDVQQLEx5D +ZXJ0dW0gQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkxIjAgBgNVBAMTGUNlcnR1bSBU +cnVzdGVkIE5ldHdvcmsgQ0EwHhcNMDgxMDIyMTIwNzM3WhcNMjkxMjMxMTIwNzM3 +WjB+MQswCQYDVQQGEwJQTDEiMCAGA1UEChMZVW5pemV0byBUZWNobm9sb2dpZXMg +Uy5BLjEnMCUGA1UECxMeQ2VydHVtIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MSIw +IAYDVQQDExlDZXJ0dW0gVHJ1c3RlZCBOZXR3b3JrIENBMIIBIjANBgkqhkiG9w0B +AQEFAAOCAQ8AMIIBCgKCAQEA4/t9o3K6wvDJFIf1awFO4W5AB7ptJ11/91sts1rH +UV+rpDKmYYe2bg+G0jACl/jXaVehGDldamR5xgFZrDwxSjh80gTSSyjoIF87B6LM +TXPb865Px1bVWqeWifrzq2jUI4ZZJ88JJ7ysbnKDHDBy3+Ci6dLhdHUZvSqeexVU +BBvXQzmtVSjF4hq79MDkrjhJM8x2hZ85RdKknvISjFH4fOQtf/WsX+sWn7Et0brM +kUJ3TCXJkDhv2/DM+44el1k+1WBO5gUo7Ul5E0u6SNsv+XLTOcr+H9g0cvW0QM8x +AcPs3hEtF10fuFDRXhmnad4HMyjKUJX5p1TLVIZQRan5SQIDAQABo0IwQDAPBgNV +HRMBAf8EBTADAQH/MB0GA1UdDgQWBBQIds3LB/8k9sXN7buQvOKEN0Z19zAOBgNV +HQ8BAf8EBAMCAQYwDQYJKoZIhvcNAQEFBQADggEBAKaorSLOAT2mo/9i0Eidi15y +sHhE49wcrwn9I0j6vSrEuVUEtRCjjSfeC4Jj0O7eDDd5QVsisrCaQVymcODU0HfL +I9MA4GxWL+FpDQ3Zqr8hgVDZBqWo/5U30Kr+4rP1mS1FhIrlQgnXdAIv94nYmem8 +J9RHjboNRhx3zxSkHLmkMcScKHQDNP8zGSal6Q10tz6XxnboJ5ajZt3hrvJBW8qY +VoNzcOSGGtIxQbovvi0TWnZvTuhOgQ4/WwMioBK+ZlgRSssDxLQqKi2WF+A5VLxI +03YnnZotBqbJ7DnSq9ufmgsnAjUpsUCV5/nonFWIGUbWtzT1fs45mtk48VH3Tyw= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIF0jCCA7qgAwIBAgIQIdbQSk8lD8kyN/yqXhKN6TANBgkqhkiG9w0BAQ0FADCB +gDELMAkGA1UEBhMCUEwxIjAgBgNVBAoTGVVuaXpldG8gVGVjaG5vbG9naWVzIFMu +QS4xJzAlBgNVBAsTHkNlcnR1bSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTEkMCIG +A1UEAxMbQ2VydHVtIFRydXN0ZWQgTmV0d29yayBDQSAyMCIYDzIwMTExMDA2MDgz +OTU2WhgPMjA0NjEwMDYwODM5NTZaMIGAMQswCQYDVQQGEwJQTDEiMCAGA1UEChMZ +VW5pemV0byBUZWNobm9sb2dpZXMgUy5BLjEnMCUGA1UECxMeQ2VydHVtIENlcnRp +ZmljYXRpb24gQXV0aG9yaXR5MSQwIgYDVQQDExtDZXJ0dW0gVHJ1c3RlZCBOZXR3 +b3JrIENBIDIwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC9+Xj45tWA +DGSdhhuWZGc/IjoedQF97/tcZ4zJzFxrqZHmuULlIEub2pt7uZld2ZuAS9eEQCsn +0+i6MLs+CRqnSZXvK0AkwpfHp+6bJe+oCgCXhVqqndwpyeI1B+twTUrWwbNWuKFB +OJvR+zF/j+Bf4bE/D44WSWDXBo0Y+aomEKsq09DRZ40bRr5HMNUuctHFY9rnY3lE +fktjJImGLjQ/KUxSiyqnwOKRKIm5wFv5HdnnJ63/mgKXwcZQkpsCLL2puTRZCr+E +Sv/f/rOf69me4Jgj7KZrdxYq28ytOxykh9xGc14ZYmhFV+SQgkK7QtbwYeDBoz1m +o130GO6IyY0XRSmZMnUCMe4pJshrAua1YkV/NxVaI2iJ1D7eTiew8EAMvE0Xy02i +sx7QBlrd9pPPV3WZ9fqGGmd4s7+W/jTcvedSVuWz5XV710GRBdxdaeOVDUO5/IOW +OZV7bIBaTxNyxtd9KXpEulKkKtVBRgkg/iKgtlswjbyJDNXXcPiHUv3a76xRLgez +Tv7QCdpw75j6VuZt27VXS9zlLCUVyJ4ueE742pyehizKV/Ma5ciSixqClnrDvFAS +adgOWkaLOusm+iPJtrCBvkIApPjW/jAux9JG9uWOdf3yzLnQh1vMBhBgu4M1t15n +3kfsmUjxpKEV/q2MYo45VU85FrmxY53/twIDAQABo0IwQDAPBgNVHRMBAf8EBTAD +AQH/MB0GA1UdDgQWBBS2oVQ5AsOgP46KvPrU+Bym0ToO/TAOBgNVHQ8BAf8EBAMC +AQYwDQYJKoZIhvcNAQENBQADggIBAHGlDs7k6b8/ONWJWsQCYftMxRQXLYtPU2sQ +F/xlhMcQSZDe28cmk4gmb3DWAl45oPePq5a1pRNcgRRtDoGCERuKTsZPpd1iHkTf +CVn0W3cLN+mLIMb4Ck4uWBzrM9DPhmDJ2vuAL55MYIR4PSFk1vtBHxgP58l1cb29 +XN40hz5BsA72udY/CROWFC/emh1auVbONTqwX3BNXuMp8SMoclm2q8KMZiYcdywm +djWLKKdpoPk79SPdhRB0yZADVpHnr7pH1BKXESLjokmUbOe3lEu6LaTaM4tMpkT/ +WjzGHWTYtTHkpjx6qFcL2+1hGsvxznN3Y6SHb0xRONbkX8eftoEq5IVIeVheO/jb +AoJnwTnbw3RLPTYe+SmTiGhbqEQZIfCn6IENLOiTNrQ3ssqwGyZ6miUfmpqAnksq +P/ujmv5zMnHCnsZy4YpoJ/HkD7TETKVhk/iXEAcqMCWpuchxuO9ozC1+9eB+D4Ko +b7a6bINDd82Kkhehnlt4Fj1F4jNy3eFmypnTycUm/Q1oBEauttmbjL4ZvrHG8hnj +XALKLNhvSgfZyTXaQHXyxKcZb55CEJh15pWLYLztxRLXis7VmFxWlgPF7ncGNf/P +5O4/E2Hu29othfDNrp2yGAlFw5Khchf8R7agCyzxxN5DaAhqXzvwdmP7zAYspsbi +DrW5viSP +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIEMjCCAxqgAwIBAgIBATANBgkqhkiG9w0BAQUFADB7MQswCQYDVQQGEwJHQjEb +MBkGA1UECAwSR3JlYXRlciBNYW5jaGVzdGVyMRAwDgYDVQQHDAdTYWxmb3JkMRow +GAYDVQQKDBFDb21vZG8gQ0EgTGltaXRlZDEhMB8GA1UEAwwYQUFBIENlcnRpZmlj +YXRlIFNlcnZpY2VzMB4XDTA0MDEwMTAwMDAwMFoXDTI4MTIzMTIzNTk1OVowezEL +MAkGA1UEBhMCR0IxGzAZBgNVBAgMEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UE +BwwHU2FsZm9yZDEaMBgGA1UECgwRQ29tb2RvIENBIExpbWl0ZWQxITAfBgNVBAMM +GEFBQSBDZXJ0aWZpY2F0ZSBTZXJ2aWNlczCCASIwDQYJKoZIhvcNAQEBBQADggEP +ADCCAQoCggEBAL5AnfRu4ep2hxxNRUSOvkbIgwadwSr+GB+O5AL686tdUIoWMQua +BtDFcCLNSS1UY8y2bmhGC1Pqy0wkwLxyTurxFa70VJoSCsN6sjNg4tqJVfMiWPPe +3M/vg4aijJRPn2jymJBGhCfHdr/jzDUsi14HZGWCwEiwqJH5YZ92IFCokcdmtet4 +YgNW8IoaE+oxox6gmf049vYnMlhvB/VruPsUK6+3qszWY19zjNoFmag4qMsXeDZR +rOme9Hg6jc8P2ULimAyrL58OAd7vn5lJ8S3frHRNG5i1R8XlKdH5kBjHYpy+g8cm +ez6KJcfA3Z3mNWgQIJ2P2N7Sw4ScDV7oL8kCAwEAAaOBwDCBvTAdBgNVHQ4EFgQU +oBEKIz6W8Qfs4q8p74Klf9AwpLQwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQF +MAMBAf8wewYDVR0fBHQwcjA4oDagNIYyaHR0cDovL2NybC5jb21vZG9jYS5jb20v +QUFBQ2VydGlmaWNhdGVTZXJ2aWNlcy5jcmwwNqA0oDKGMGh0dHA6Ly9jcmwuY29t +b2RvLm5ldC9BQUFDZXJ0aWZpY2F0ZVNlcnZpY2VzLmNybDANBgkqhkiG9w0BAQUF +AAOCAQEACFb8AvCb6P+k+tZ7xkSAzk/ExfYAWMymtrwUSWgEdujm7l3sAg9g1o1Q +GE8mTgHj5rCl7r+8dFRBv/38ErjHT1r0iWAFf2C3BUrz9vHCv8S5dIa2LX1rzNLz +Rt0vxuBqw8M0Ayx9lt1awg6nCpnBBYurDC/zXDrPbDdVCYfeU0BsWO/8tqtlbgT2 +G9w84FoVxp7Z8VlIMCFlA2zs6SFz7JsDoeA3raAVGI/6ugLOpyypEBMs1OUIJqsi +l2D4kF501KKaU73yqWjgom7C12yxow+ev+to51byrvLjKzg6CYG1a4XXvi3tPxq3 +smPi9WIsgtRqAEFQ8TmDn5XpNpaYbg== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIEMzCCAxugAwIBAgIDCYPzMA0GCSqGSIb3DQEBCwUAME0xCzAJBgNVBAYTAkRF +MRUwEwYDVQQKDAxELVRydXN0IEdtYkgxJzAlBgNVBAMMHkQtVFJVU1QgUm9vdCBD +bGFzcyAzIENBIDIgMjAwOTAeFw0wOTExMDUwODM1NThaFw0yOTExMDUwODM1NTha +ME0xCzAJBgNVBAYTAkRFMRUwEwYDVQQKDAxELVRydXN0IEdtYkgxJzAlBgNVBAMM +HkQtVFJVU1QgUm9vdCBDbGFzcyAzIENBIDIgMjAwOTCCASIwDQYJKoZIhvcNAQEB +BQADggEPADCCAQoCggEBANOySs96R+91myP6Oi/WUEWJNTrGa9v+2wBoqOADER03 +UAifTUpolDWzU9GUY6cgVq/eUXjsKj3zSEhQPgrfRlWLJ23DEE0NkVJD2IfgXU42 +tSHKXzlABF9bfsyjxiupQB7ZNoTWSPOSHjRGICTBpFGOShrvUD9pXRl/RcPHAY9R +ySPocq60vFYJfxLLHLGvKZAKyVXMD9O0Gu1HNVpK7ZxzBCHQqr0ME7UAyiZsxGsM +lFqVlNpQmvH/pStmMaTJOKDfHR+4CS7zp+hnUquVH+BGPtikw8paxTGA6Eian5Rp +/hnd2HN8gcqW3o7tszIFZYQ05ub9VxC1X3a/L7AQDcUCAwEAAaOCARowggEWMA8G +A1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFP3aFMSfMN4hvR5COfyrYyNJ4PGEMA4G +A1UdDwEB/wQEAwIBBjCB0wYDVR0fBIHLMIHIMIGAoH6gfIZ6bGRhcDovL2RpcmVj +dG9yeS5kLXRydXN0Lm5ldC9DTj1ELVRSVVNUJTIwUm9vdCUyMENsYXNzJTIwMyUy +MENBJTIwMiUyMDIwMDksTz1ELVRydXN0JTIwR21iSCxDPURFP2NlcnRpZmljYXRl +cmV2b2NhdGlvbmxpc3QwQ6BBoD+GPWh0dHA6Ly93d3cuZC10cnVzdC5uZXQvY3Js +L2QtdHJ1c3Rfcm9vdF9jbGFzc18zX2NhXzJfMjAwOS5jcmwwDQYJKoZIhvcNAQEL +BQADggEBAH+X2zDI36ScfSF6gHDOFBJpiBSVYEQBrLLpME+bUMJm2H6NMLVwMeni +acfzcNsgFYbQDfC+rAF1hM5+n02/t2A7nPPKHeJeaNijnZflQGDSNiH+0LS4F9p0 +o3/U37CYAqxva2ssJSRyoWXuJVrl5jLn8t+rSfrzkGkj2wTZ51xY/GXUl77M/C4K +zCUqNQT4YJEVdT1B/yMfGchs64JTBKbkTCJNjYy6zltz7GRUUG3RnFX7acM2w4y8 +PIWmawomDeCTmGCufsYkl4phX5GOZpIJhzbNi5stPvZR1FDUWSi9g/LMKHtThm3Y +Johw1+qRzT65ysCQblrGXnRl11z+o+I= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIEQzCCAyugAwIBAgIDCYP0MA0GCSqGSIb3DQEBCwUAMFAxCzAJBgNVBAYTAkRF +MRUwEwYDVQQKDAxELVRydXN0IEdtYkgxKjAoBgNVBAMMIUQtVFJVU1QgUm9vdCBD +bGFzcyAzIENBIDIgRVYgMjAwOTAeFw0wOTExMDUwODUwNDZaFw0yOTExMDUwODUw +NDZaMFAxCzAJBgNVBAYTAkRFMRUwEwYDVQQKDAxELVRydXN0IEdtYkgxKjAoBgNV +BAMMIUQtVFJVU1QgUm9vdCBDbGFzcyAzIENBIDIgRVYgMjAwOTCCASIwDQYJKoZI +hvcNAQEBBQADggEPADCCAQoCggEBAJnxhDRwui+3MKCOvXwEz75ivJn9gpfSegpn +ljgJ9hBOlSJzmY3aFS3nBfwZcyK3jpgAvDw9rKFs+9Z5JUut8Mxk2og+KbgPCdM0 +3TP1YtHhzRnp7hhPTFiu4h7WDFsVWtg6uMQYZB7jM7K1iXdODL/ZlGsTl28So/6Z +qQTMFexgaDbtCHu39b+T7WYxg4zGcTSHThfqr4uRjRxWQa4iN1438h3Z0S0NL2lR +p75mpoo6Kr3HGrHhFPC+Oh25z1uxav60sUYgovseO3Dvk5h9jHOW8sXvhXCtKSb8 +HgQ+HKDYD8tSg2J87otTlZCpV6LqYQXY+U3EJ/pure3511H3a6UCAwEAAaOCASQw +ggEgMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFNOUikxiEyoZLsyvcop9Ntea +HNxnMA4GA1UdDwEB/wQEAwIBBjCB3QYDVR0fBIHVMIHSMIGHoIGEoIGBhn9sZGFw +Oi8vZGlyZWN0b3J5LmQtdHJ1c3QubmV0L0NOPUQtVFJVU1QlMjBSb290JTIwQ2xh +c3MlMjAzJTIwQ0ElMjAyJTIwRVYlMjAyMDA5LE89RC1UcnVzdCUyMEdtYkgsQz1E +RT9jZXJ0aWZpY2F0ZXJldm9jYXRpb25saXN0MEagRKBChkBodHRwOi8vd3d3LmQt +dHJ1c3QubmV0L2NybC9kLXRydXN0X3Jvb3RfY2xhc3NfM19jYV8yX2V2XzIwMDku +Y3JsMA0GCSqGSIb3DQEBCwUAA4IBAQA07XtaPKSUiO8aEXUHL7P+PPoeUSbrh/Yp +3uDx1MYkCenBz1UbtDDZzhr+BlGmFaQt77JLvyAoJUnRpjZ3NOhk31KxEcdzes05 +nsKtjHEh8lprr988TlWvsoRlFIm5d8sqMb7Po23Pb0iUMkZv53GMoKaEGTcH8gNF +CSuGdXzfX2lXANtu2KZyIktQ1HWYVt+3GP9DQ1CuekR78HlR10M9p9OB0/DJT7na +xpeG0ILD5EJt/rDiZE4OJudANCa1CInXCGNjOCd1HjPqbqjdn5lPdE2BiYBL3ZqX +KVwvvoFBuYz/6n1gBp7N1z3TLqMVvKjmJuVvw9y4AyHqnxbxLFS1 +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIDtzCCAp+gAwIBAgIQDOfg5RfYRv6P5WD8G/AwOTANBgkqhkiG9w0BAQUFADBl +MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 +d3cuZGlnaWNlcnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJv +b3QgQ0EwHhcNMDYxMTEwMDAwMDAwWhcNMzExMTEwMDAwMDAwWjBlMQswCQYDVQQG +EwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNl +cnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3QgQ0EwggEi +MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCtDhXO5EOAXLGH87dg+XESpa7c +JpSIqvTO9SA5KFhgDPiA2qkVlTJhPLWxKISKityfCgyDF3qPkKyK53lTXDGEKvYP +mDI2dsze3Tyoou9q+yHyUmHfnyDXH+Kx2f4YZNISW1/5WBg1vEfNoTb5a3/UsDg+ +wRvDjDPZ2C8Y/igPs6eD1sNuRMBhNZYW/lmci3Zt1/GiSw0r/wty2p5g0I6QNcZ4 +VYcgoc/lbQrISXwxmDNsIumH0DJaoroTghHtORedmTpyoeb6pNnVFzF1roV9Iq4/ +AUaG9ih5yLHa5FcXxH4cDrC0kqZWs72yl+2qp/C3xag/lRbQ/6GW6whfGHdPAgMB +AAGjYzBhMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQW +BBRF66Kv9JLLgjEtUYunpyGd823IDzAfBgNVHSMEGDAWgBRF66Kv9JLLgjEtUYun +pyGd823IDzANBgkqhkiG9w0BAQUFAAOCAQEAog683+Lt8ONyc3pklL/3cmbYMuRC +dWKuh+vy1dneVrOfzM4UKLkNl2BcEkxY5NM9g0lFWJc1aRqoR+pWxnmrEthngYTf +fwk8lOa4JiwgvT2zKIn3X/8i4peEH+ll74fg38FnSbNd67IJKusm7Xi+fT8r87cm +NW1fiQG2SVufAQWbqz0lwcy2f8Lxb4bG+mRo64EtlOtCt/qMHt1i8b5QZ7dsvfPx +H2sMNgcWfzd8qVttevESRmCD1ycEvkvOl77DZypoEd+A5wwzZr8TDRRu838fYxAe ++o0bJW1sj6W3YQGx0qMmoRBxna3iw/nDmVG3KwcIzi7mULKn+gpFL6Lw8g== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIDljCCAn6gAwIBAgIQC5McOtY5Z+pnI7/Dr5r0SzANBgkqhkiG9w0BAQsFADBl +MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 +d3cuZGlnaWNlcnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJv +b3QgRzIwHhcNMTMwODAxMTIwMDAwWhcNMzgwMTE1MTIwMDAwWjBlMQswCQYDVQQG +EwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNl +cnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3QgRzIwggEi +MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDZ5ygvUj82ckmIkzTz+GoeMVSA +n61UQbVH35ao1K+ALbkKz3X9iaV9JPrjIgwrvJUXCzO/GU1BBpAAvQxNEP4Htecc +biJVMWWXvdMX0h5i89vqbFCMP4QMls+3ywPgym2hFEwbid3tALBSfK+RbLE4E9Hp +EgjAALAcKxHad3A2m67OeYfcgnDmCXRwVWmvo2ifv922ebPynXApVfSr/5Vh88lA +bx3RvpO704gqu52/clpWcTs/1PPRCv4o76Pu2ZmvA9OPYLfykqGxvYmJHzDNw6Yu +YjOuFgJ3RFrngQo8p0Quebg/BLxcoIfhG69Rjs3sLPr4/m3wOnyqi+RnlTGNAgMB +AAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGGMB0GA1UdDgQW +BBTOw0q5mVXyuNtgv6l+vVa1lzan1jANBgkqhkiG9w0BAQsFAAOCAQEAyqVVjOPI +QW5pJ6d1Ee88hjZv0p3GeDgdaZaikmkuOGybfQTUiaWxMTeKySHMq2zNixya1r9I +0jJmwYrA8y8678Dj1JGG0VDjA9tzd29KOVPt3ibHtX2vK0LRdWLjSisCx1BL4Gni +lmwORGYQRI+tBev4eaymG+g3NJ1TyWGqolKvSnAWhsI6yLETcDbYz+70CjTVW0z9 +B5yiutkBclzzTcHdDrEcDcRjvq30FPuJ7KJBDkzMyFdA0G4Dqs0MjomZmWzwPDCv +ON9vvKO+KSAnq3T/EyJ43pdSVR6DtVQgA+6uwE9W3jfMw3+qBCe703e4YtsXfJwo +IhNzbM8m9Yop5w== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICRjCCAc2gAwIBAgIQC6Fa+h3foLVJRK/NJKBs7DAKBggqhkjOPQQDAzBlMQsw +CQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cu +ZGlnaWNlcnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3Qg +RzMwHhcNMTMwODAxMTIwMDAwWhcNMzgwMTE1MTIwMDAwWjBlMQswCQYDVQQGEwJV +UzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQu +Y29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3QgRzMwdjAQBgcq +hkjOPQIBBgUrgQQAIgNiAAQZ57ysRGXtzbg/WPuNsVepRC0FFfLvC/8QdJ+1YlJf +Zn4f5dwbRXkLzMZTCp2NXQLZqVneAlr2lSoOjThKiknGvMYDOAdfVdp+CW7if17Q +RSAPWXYQ1qAk8C3eNvJsKTmjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/ +BAQDAgGGMB0GA1UdDgQWBBTL0L2p4ZgFUaFNN6KDec6NHSrkhDAKBggqhkjOPQQD +AwNnADBkAjAlpIFFAmsSS3V0T8gj43DydXLefInwz5FyYZ5eEJJZVrmDxxDnOOlY +JjZ91eQ0hjkCMHw2U/Aw5WJjOpnitqM7mzT6HtoQknFekROn3aRukswy1vUhZscv +6pZjamVFkpUBtA== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIDrzCCApegAwIBAgIQCDvgVpBCRrGhdWrJWZHHSjANBgkqhkiG9w0BAQUFADBh +MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 +d3cuZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBD +QTAeFw0wNjExMTAwMDAwMDBaFw0zMTExMTAwMDAwMDBaMGExCzAJBgNVBAYTAlVT +MRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5j +b20xIDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IENBMIIBIjANBgkqhkiG +9w0BAQEFAAOCAQ8AMIIBCgKCAQEA4jvhEXLeqKTTo1eqUKKPC3eQyaKl7hLOllsB +CSDMAZOnTjC3U/dDxGkAV53ijSLdhwZAAIEJzs4bg7/fzTtxRuLWZscFs3YnFo97 +nh6Vfe63SKMI2tavegw5BmV/Sl0fvBf4q77uKNd0f3p4mVmFaG5cIzJLv07A6Fpt +43C/dxC//AH2hdmoRBBYMql1GNXRor5H4idq9Joz+EkIYIvUX7Q6hL+hqkpMfT7P +T19sdl6gSzeRntwi5m3OFBqOasv+zbMUZBfHWymeMr/y7vrTC0LUq7dBMtoM1O/4 +gdW7jVg/tRvoSSiicNoxBN33shbyTApOB6jtSj1etX+jkMOvJwIDAQABo2MwYTAO +BgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUA95QNVbR +TLtm8KPiGxvDl7I90VUwHwYDVR0jBBgwFoAUA95QNVbRTLtm8KPiGxvDl7I90VUw +DQYJKoZIhvcNAQEFBQADggEBAMucN6pIExIK+t1EnE9SsPTfrgT1eXkIoyQY/Esr +hMAtudXH/vTBH1jLuG2cenTnmCmrEbXjcKChzUyImZOMkXDiqw8cvpOp/2PV5Adg +06O/nVsJ8dWO41P0jmP6P6fbtGbfYmbW0W5BjfIttep3Sp+dWOIrWcBAI+0tKIJF +PnlUkiaY4IBIqDfv8NZ5YBberOgOzW6sRBc4L0na4UU+Krk2U886UAb3LujEV0ls +YSEY1QSteDwsOoBrp+uvFRTp2InBuThs4pFsiv9kuXclVzDAGySj4dzp30d8tbQk +CAUw7C29C79Fv1C5qfPrmAESrciIxpg0X40KPMbp1ZWVbd4= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIDjjCCAnagAwIBAgIQAzrx5qcRqaC7KGSxHQn65TANBgkqhkiG9w0BAQsFADBh +MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 +d3cuZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBH +MjAeFw0xMzA4MDExMjAwMDBaFw0zODAxMTUxMjAwMDBaMGExCzAJBgNVBAYTAlVT +MRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5j +b20xIDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IEcyMIIBIjANBgkqhkiG +9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuzfNNNx7a8myaJCtSnX/RrohCgiN9RlUyfuI +2/Ou8jqJkTx65qsGGmvPrC3oXgkkRLpimn7Wo6h+4FR1IAWsULecYxpsMNzaHxmx +1x7e/dfgy5SDN67sH0NO3Xss0r0upS/kqbitOtSZpLYl6ZtrAGCSYP9PIUkY92eQ +q2EGnI/yuum06ZIya7XzV+hdG82MHauVBJVJ8zUtluNJbd134/tJS7SsVQepj5Wz +tCO7TG1F8PapspUwtP1MVYwnSlcUfIKdzXOS0xZKBgyMUNGPHgm+F6HmIcr9g+UQ +vIOlCsRnKPZzFBQ9RnbDhxSJITRNrw9FDKZJobq7nMWxM4MphQIDAQABo0IwQDAP +BgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBhjAdBgNVHQ4EFgQUTiJUIBiV +5uNu5g/6+rkS7QYXjzkwDQYJKoZIhvcNAQELBQADggEBAGBnKJRvDkhj6zHd6mcY +1Yl9PMWLSn/pvtsrF9+wX3N3KjITOYFnQoQj8kVnNeyIv/iPsGEMNKSuIEyExtv4 +NeF22d+mQrvHRAiGfzZ0JFrabA0UWTW98kndth/Jsw1HKj2ZL7tcu7XUIOGZX1NG +Fdtom/DzMNU+MeKNhJ7jitralj41E6Vf8PlwUHBHQRFXGU7Aj64GxJUTFy8bJZ91 +8rGOmaFvE7FBcf6IKshPECBV1/MUReXgRPTqh5Uykw7+U0b6LJ3/iyK5S9kJRaTe +pLiaWN0bfVKfjllDiIGknibVb63dDcY3fe0Dkhvld1927jyNxF1WW6LZZm6zNTfl +MrY= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICPzCCAcWgAwIBAgIQBVVWvPJepDU1w6QP1atFcjAKBggqhkjOPQQDAzBhMQsw +CQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cu +ZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBHMzAe +Fw0xMzA4MDExMjAwMDBaFw0zODAxMTUxMjAwMDBaMGExCzAJBgNVBAYTAlVTMRUw +EwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20x +IDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IEczMHYwEAYHKoZIzj0CAQYF +K4EEACIDYgAE3afZu4q4C/sLfyHS8L6+c/MzXRq8NOrexpu80JX28MzQC7phW1FG +fp4tn+6OYwwX7Adw9c+ELkCDnOg/QW07rdOkFFk2eJ0DQ+4QE2xy3q6Ip6FrtUPO +Z9wj/wMco+I+o0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBhjAd +BgNVHQ4EFgQUs9tIpPmhxdiuNkHMEWNpYim8S8YwCgYIKoZIzj0EAwMDaAAwZQIx +AK288mw/EkrRLTnDCgmXc/SINoyIJ7vmiI1Qhadj+Z4y3maTD/HMsQmP3Wyr+mt/ +oAIwOWZbwmSNuJ5Q3KjVSaLtx9zRSX8XAbjIho9OjIgrqJqpisXRAL34VOKa5Vt8 +sycX +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIDxTCCAq2gAwIBAgIQAqxcJmoLQJuPC3nyrkYldzANBgkqhkiG9w0BAQUFADBs +MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 +d3cuZGlnaWNlcnQuY29tMSswKQYDVQQDEyJEaWdpQ2VydCBIaWdoIEFzc3VyYW5j +ZSBFViBSb290IENBMB4XDTA2MTExMDAwMDAwMFoXDTMxMTExMDAwMDAwMFowbDEL +MAkGA1UEBhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UECxMQd3d3 +LmRpZ2ljZXJ0LmNvbTErMCkGA1UEAxMiRGlnaUNlcnQgSGlnaCBBc3N1cmFuY2Ug +RVYgUm9vdCBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMbM5XPm ++9S75S0tMqbf5YE/yc0lSbZxKsPVlDRnogocsF9ppkCxxLeyj9CYpKlBWTrT3JTW +PNt0OKRKzE0lgvdKpVMSOO7zSW1xkX5jtqumX8OkhPhPYlG++MXs2ziS4wblCJEM +xChBVfvLWokVfnHoNb9Ncgk9vjo4UFt3MRuNs8ckRZqnrG0AFFoEt7oT61EKmEFB +Ik5lYYeBQVCmeVyJ3hlKV9Uu5l0cUyx+mM0aBhakaHPQNAQTXKFx01p8VdteZOE3 +hzBWBOURtCmAEvF5OYiiAhF8J2a3iLd48soKqDirCmTCv2ZdlYTBoSUeh10aUAsg +EsxBu24LUTi4S8sCAwEAAaNjMGEwDgYDVR0PAQH/BAQDAgGGMA8GA1UdEwEB/wQF +MAMBAf8wHQYDVR0OBBYEFLE+w2kD+L9HAdSYJhoIAu9jZCvDMB8GA1UdIwQYMBaA +FLE+w2kD+L9HAdSYJhoIAu9jZCvDMA0GCSqGSIb3DQEBBQUAA4IBAQAcGgaX3Nec +nzyIZgYIVyHbIUf4KmeqvxgydkAQV8GK83rZEWWONfqe/EW1ntlMMUu4kehDLI6z +eM7b41N5cdblIZQB2lWHmiRk9opmzN6cN82oNLFpmyPInngiK3BD41VHMWEZ71jF +hS9OMPagMRYjyOfiZRYzy78aG6A9+MpeizGLYAiJLQwGXFK3xPkKmNEVX58Svnw2 +Yzi9RKR/5CYrCsSXaQ3pjOLAEFe4yHYSkVXySGnYvCoCWw9E1CAx2/S6cCZdkGCe +vEsXCS+0yx5DaMkHJ8HSXPfqIbloEpw8nL+e/IBcm2PN7EeqJSdnoDfzAIJ9VNep ++OkuE6N36B9K +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFkDCCA3igAwIBAgIQBZsbV56OITLiOQe9p3d1XDANBgkqhkiG9w0BAQwFADBi +MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 +d3cuZGlnaWNlcnQuY29tMSEwHwYDVQQDExhEaWdpQ2VydCBUcnVzdGVkIFJvb3Qg +RzQwHhcNMTMwODAxMTIwMDAwWhcNMzgwMTE1MTIwMDAwWjBiMQswCQYDVQQGEwJV +UzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQu +Y29tMSEwHwYDVQQDExhEaWdpQ2VydCBUcnVzdGVkIFJvb3QgRzQwggIiMA0GCSqG +SIb3DQEBAQUAA4ICDwAwggIKAoICAQC/5pBzaN675F1KPDAiMGkz7MKnJS7JIT3y +ithZwuEppz1Yq3aaza57G4QNxDAf8xukOBbrVsaXbR2rsnnyyhHS5F/WBTxSD1If +xp4VpX6+n6lXFllVcq9ok3DCsrp1mWpzMpTREEQQLt+C8weE5nQ7bXHiLQwb7iDV +ySAdYyktzuxeTsiT+CFhmzTrBcZe7FsavOvJz82sNEBfsXpm7nfISKhmV1efVFiO +DCu3T6cw2Vbuyntd463JT17lNecxy9qTXtyOj4DatpGYQJB5w3jHtrHEtWoYOAMQ +jdjUN6QuBX2I9YI+EJFwq1WCQTLX2wRzKm6RAXwhTNS8rhsDdV14Ztk6MUSaM0C/ +CNdaSaTC5qmgZ92kJ7yhTzm1EVgX9yRcRo9k98FpiHaYdj1ZXUJ2h4mXaXpI8OCi +EhtmmnTK3kse5w5jrubU75KSOp493ADkRSWJtppEGSt+wJS00mFt6zPZxd9LBADM +fRyVw4/3IbKyEbe7f/LVjHAsQWCqsWMYRJUadmJ+9oCw++hkpjPRiQfhvbfmQ6QY +uKZ3AeEPlAwhHbJUKSWJbOUOUlFHdL4mrLZBdd56rF+NP8m800ERElvlEFDrMcXK +chYiCd98THU/Y+whX8QgUWtvsauGi0/C1kVfnSD8oR7FwI+isX4KJpn15GkvmB0t +9dmpsh3lGwIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIB +hjAdBgNVHQ4EFgQU7NfjgtJxXWRM3y5nP+e6mK4cD08wDQYJKoZIhvcNAQEMBQAD +ggIBALth2X2pbL4XxJEbw6GiAI3jZGgPVs93rnD5/ZpKmbnJeFwMDF/k5hQpVgs2 +SV1EY+CtnJYYZhsjDT156W1r1lT40jzBQ0CuHVD1UvyQO7uYmWlrx8GnqGikJ9yd ++SeuMIW59mdNOj6PWTkiU0TryF0Dyu1Qen1iIQqAyHNm0aAFYF/opbSnr6j3bTWc +fFqK1qI4mfN4i/RN0iAL3gTujJtHgXINwBQy7zBZLq7gcfJW5GqXb5JQbZaNaHqa +sjYUegbyJLkJEVDXCLG4iXqEI2FCKeWjzaIgQdfRnGTZ6iahixTXTBmyUEFxPT9N +cCOGDErcgdLMMpSEDQgJlxxPwO5rIHQw0uA5NBCFIRUBCOhVMt5xSdkoF1BN5r5N +0XWs0Mr7QbhDparTwwVETyw2m+L64kW4I1NsBm9nVX9GtUw/bihaeSbSpKhil9Ie +4u1Ki7wb/UdKDd9nZn6yW0HQO+T0O/QEY+nvwlQAUaCKKsnOeMzV6ocEGLPOr0mI +r/OSmbaz5mEP0oUA51Aa5BuVnRmhuZyxm7EAHu/QD09CbMkKvO5D+jpxpchNJqU1 +/YldvIViHTLSoCtU7ZpXwdv6EM8Zt4tKG48BtieVU+i2iW1bvGjUI+iLUaJW+fCm +gKDWHrO8Dw9TdSmq6hN35N6MgSGtBxBHEa2HPQfRdbzP82Z+ +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIGSzCCBDOgAwIBAgIIamg+nFGby1MwDQYJKoZIhvcNAQELBQAwgbIxCzAJBgNV +BAYTAlRSMQ8wDQYDVQQHDAZBbmthcmExQDA+BgNVBAoMN0UtVHXEn3JhIEVCRyBC +aWxpxZ9pbSBUZWtub2xvamlsZXJpIHZlIEhpem1ldGxlcmkgQS7Fni4xJjAkBgNV +BAsMHUUtVHVncmEgU2VydGlmaWthc3lvbiBNZXJrZXppMSgwJgYDVQQDDB9FLVR1 +Z3JhIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MB4XDTEzMDMwNTEyMDk0OFoXDTIz +MDMwMzEyMDk0OFowgbIxCzAJBgNVBAYTAlRSMQ8wDQYDVQQHDAZBbmthcmExQDA+ +BgNVBAoMN0UtVHXEn3JhIEVCRyBCaWxpxZ9pbSBUZWtub2xvamlsZXJpIHZlIEhp +em1ldGxlcmkgQS7Fni4xJjAkBgNVBAsMHUUtVHVncmEgU2VydGlmaWthc3lvbiBN +ZXJrZXppMSgwJgYDVQQDDB9FLVR1Z3JhIENlcnRpZmljYXRpb24gQXV0aG9yaXR5 +MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA4vU/kwVRHoViVF56C/UY +B4Oufq9899SKa6VjQzm5S/fDxmSJPZQuVIBSOTkHS0vdhQd2h8y/L5VMzH2nPbxH +D5hw+IyFHnSOkm0bQNGZDbt1bsipa5rAhDGvykPL6ys06I+XawGb1Q5KCKpbknSF +Q9OArqGIW66z6l7LFpp3RMih9lRozt6Plyu6W0ACDGQXwLWTzeHxE2bODHnv0ZEo +q1+gElIwcxmOj+GMB6LDu0rw6h8VqO4lzKRG+Bsi77MOQ7osJLjFLFzUHPhdZL3D +k14opz8n8Y4e0ypQBaNV2cvnOVPAmJ6MVGKLJrD3fY185MaeZkJVgkfnsliNZvcH +fC425lAcP9tDJMW/hkd5s3kc91r0E+xs+D/iWR+V7kI+ua2oMoVJl0b+SzGPWsut +dEcf6ZG33ygEIqDUD13ieU/qbIWGvaimzuT6w+Gzrt48Ue7LE3wBf4QOXVGUnhMM +ti6lTPk5cDZvlsouDERVxcr6XQKj39ZkjFqzAQqptQpHF//vkUAqjqFGOjGY5RH8 +zLtJVor8udBhmm9lbObDyz51Sf6Pp+KJxWfXnUYTTjF2OySznhFlhqt/7x3U+Lzn +rFpct1pHXFXOVbQicVtbC/DP3KBhZOqp12gKY6fgDT+gr9Oq0n7vUaDmUStVkhUX +U8u3Zg5mTPj5dUyQ5xJwx0UCAwEAAaNjMGEwHQYDVR0OBBYEFC7j27JJ0JxUeVz6 +Jyr+zE7S6E5UMA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAULuPbsknQnFR5 +XPonKv7MTtLoTlQwDgYDVR0PAQH/BAQDAgEGMA0GCSqGSIb3DQEBCwUAA4ICAQAF +Nzr0TbdF4kV1JI+2d1LoHNgQk2Xz8lkGpD4eKexd0dCrfOAKkEh47U6YA5n+KGCR +HTAduGN8qOY1tfrTYXbm1gdLymmasoR6d5NFFxWfJNCYExL/u6Au/U5Mh/jOXKqY +GwXgAEZKgoClM4so3O0409/lPun++1ndYYRP0lSWE2ETPo+Aab6TR7U1Q9Jauz1c +77NCR807VRMGsAnb/WP2OogKmW9+4c4bU2pEZiNRCHu8W1Ki/QY3OEBhj0qWuJA3 ++GbHeJAAFS6LrVE1Uweoa2iu+U48BybNCAVwzDk/dr2l02cmAYamU9JgO3xDf1WK +vJUawSg5TB9D0pH0clmKuVb8P7Sd2nCcdlqMQ1DujjByTd//SffGqWfZbawCEeI6 +FiWnWAjLb1NBnEg4R2gz0dfHj9R0IdTDBZB6/86WiLEVKV0jq9BgoRJP3vQXzTLl +yb/IQ639Lo7xr+L0mPoSHyDYwKcMhcWQ9DstliaxLL5Mq+ux0orJ23gTDx4JnW2P +AJ8C2sH6H3p6CcRK5ogql5+Ji/03X186zjhZhkuvcQu02PJwT58yE+Owp1fl2tpD +y4Q08ijE6m30Ku/Ba3ba+367hTzSU8JNvnHhRdH9I2cNE3X7z2VnIp2usAnRCf8d +NL/+I5c30jn6PQ0GC7TbO6Orb1wdtn7os4I07QZcJA== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIEKjCCAxKgAwIBAgIEOGPe+DANBgkqhkiG9w0BAQUFADCBtDEUMBIGA1UEChML +RW50cnVzdC5uZXQxQDA+BgNVBAsUN3d3dy5lbnRydXN0Lm5ldC9DUFNfMjA0OCBp +bmNvcnAuIGJ5IHJlZi4gKGxpbWl0cyBsaWFiLikxJTAjBgNVBAsTHChjKSAxOTk5 +IEVudHJ1c3QubmV0IExpbWl0ZWQxMzAxBgNVBAMTKkVudHJ1c3QubmV0IENlcnRp +ZmljYXRpb24gQXV0aG9yaXR5ICgyMDQ4KTAeFw05OTEyMjQxNzUwNTFaFw0yOTA3 +MjQxNDE1MTJaMIG0MRQwEgYDVQQKEwtFbnRydXN0Lm5ldDFAMD4GA1UECxQ3d3d3 +LmVudHJ1c3QubmV0L0NQU18yMDQ4IGluY29ycC4gYnkgcmVmLiAobGltaXRzIGxp +YWIuKTElMCMGA1UECxMcKGMpIDE5OTkgRW50cnVzdC5uZXQgTGltaXRlZDEzMDEG +A1UEAxMqRW50cnVzdC5uZXQgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkgKDIwNDgp +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArU1LqRKGsuqjIAcVFmQq +K0vRvwtKTY7tgHalZ7d4QMBzQshowNtTK91euHaYNZOLGp18EzoOH1u3Hs/lJBQe +sYGpjX24zGtLA/ECDNyrpUAkAH90lKGdCCmziAv1h3edVc3kw37XamSrhRSGlVuX +MlBvPci6Zgzj/L24ScF2iUkZ/cCovYmjZy/Gn7xxGWC4LeksyZB2ZnuU4q941mVT +XTzWnLLPKQP5L6RQstRIzgUyVYr9smRMDuSYB3Xbf9+5CFVghTAp+XtIpGmG4zU/ +HoZdenoVve8AjhUiVBcAkCaTvA5JaJG/+EfTnZVCwQ5N328mz8MYIWJmQ3DW1cAH +4QIDAQABo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNV +HQ4EFgQUVeSB0RGAvtiJuQijMfmhJAkWuXAwDQYJKoZIhvcNAQEFBQADggEBADub +j1abMOdTmXx6eadNl9cZlZD7Bh/KM3xGY4+WZiT6QBshJ8rmcnPyT/4xmf3IDExo +U8aAghOY+rat2l098c5u9hURlIIM7j+VrxGrD9cv3h8Dj1csHsm7mhpElesYT6Yf +zX1XEC+bBAlahLVu2B064dae0Wx5XnkcFMXj0EyTO2U87d89vqbllRrDtRnDvV5b +u/8j72gZyxKTJ1wDLW8w0B62GqzeWvfRqqgnpv55gcR5mTNXuhKwqeBCbJPKVt7+ +bYQLCIt+jerXmCHG8+c8eS9enNFMFY3h7CI3zJpDC5fcgJCNs2ebb0gIFVbPv/Er +fF6adulZkMV8gzURZVE= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIEkTCCA3mgAwIBAgIERWtQVDANBgkqhkiG9w0BAQUFADCBsDELMAkGA1UEBhMC +VVMxFjAUBgNVBAoTDUVudHJ1c3QsIEluYy4xOTA3BgNVBAsTMHd3dy5lbnRydXN0 +Lm5ldC9DUFMgaXMgaW5jb3Jwb3JhdGVkIGJ5IHJlZmVyZW5jZTEfMB0GA1UECxMW +KGMpIDIwMDYgRW50cnVzdCwgSW5jLjEtMCsGA1UEAxMkRW50cnVzdCBSb290IENl +cnRpZmljYXRpb24gQXV0aG9yaXR5MB4XDTA2MTEyNzIwMjM0MloXDTI2MTEyNzIw +NTM0MlowgbAxCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1FbnRydXN0LCBJbmMuMTkw +NwYDVQQLEzB3d3cuZW50cnVzdC5uZXQvQ1BTIGlzIGluY29ycG9yYXRlZCBieSBy +ZWZlcmVuY2UxHzAdBgNVBAsTFihjKSAyMDA2IEVudHJ1c3QsIEluYy4xLTArBgNV +BAMTJEVudHJ1c3QgUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTCCASIwDQYJ +KoZIhvcNAQEBBQADggEPADCCAQoCggEBALaVtkNC+sZtKm9I35RMOVcF7sN5EUFo +Nu3s/poBj6E4KPz3EEZmLk0eGrEaTsbRwJWIsMn/MYszA9u3g3s+IIRe7bJWKKf4 +4LlAcTfFy0cOlypowCKVYhXbR9n10Cv/gkvJrT7eTNuQgFA/CYqEAOwwCj0Yzfv9 +KlmaI5UXLEWeH25DeW0MXJj+SKfFI0dcXv1u5x609mhF0YaDW6KKjbHjKYD+JXGI +rb68j6xSlkuqUY3kEzEZ6E5Nn9uss2rVvDlUccp6en+Q3X0dgNmBu1kmwhH+5pPi +94DkZfs0Nw4pgHBNrziGLp5/V6+eF67rHMsoIV+2HNjnogQi+dPa2MsCAwEAAaOB +sDCBrTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zArBgNVHRAEJDAi +gA8yMDA2MTEyNzIwMjM0MlqBDzIwMjYxMTI3MjA1MzQyWjAfBgNVHSMEGDAWgBRo +kORnpKZTgMeGZqTx90tD+4S9bTAdBgNVHQ4EFgQUaJDkZ6SmU4DHhmak8fdLQ/uE +vW0wHQYJKoZIhvZ9B0EABBAwDhsIVjcuMTo0LjADAgSQMA0GCSqGSIb3DQEBBQUA +A4IBAQCT1DCw1wMgKtD5Y+iRDAUgqV8ZyntyTtSx29CW+1RaGSwMCPeyvIWonX9t +O1KzKtvn1ISMY/YPyyYBkVBs9F8U4pN0wBOeMDpQ47RgxRzwIkSNcUesyBrJ6Zua +AGAT/3B+XxFNSRuzFVJ7yVTav52Vr2ua2J7p8eRDjeIRRDq/r72DQnNSi6q7pynP +9WQcCk3RvKqsnyrQ/39/2n3qse0wJcGE2jTSW3iDVuycNsMm4hH2Z0kdkquM++v/ +eu6FSqdQgPCnXEqULl8FmTxSQeDNtGPPAUO6nIPcj2A781q0tHuu2guQOHXvgR1m +0vdXcDazv/wor3ElhVsT/h5/WrQ8 +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIC+TCCAoCgAwIBAgINAKaLeSkAAAAAUNCR+TAKBggqhkjOPQQDAzCBvzELMAkG +A1UEBhMCVVMxFjAUBgNVBAoTDUVudHJ1c3QsIEluYy4xKDAmBgNVBAsTH1NlZSB3 +d3cuZW50cnVzdC5uZXQvbGVnYWwtdGVybXMxOTA3BgNVBAsTMChjKSAyMDEyIEVu +dHJ1c3QsIEluYy4gLSBmb3IgYXV0aG9yaXplZCB1c2Ugb25seTEzMDEGA1UEAxMq +RW50cnVzdCBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IC0gRUMxMB4XDTEy +MTIxODE1MjUzNloXDTM3MTIxODE1NTUzNlowgb8xCzAJBgNVBAYTAlVTMRYwFAYD +VQQKEw1FbnRydXN0LCBJbmMuMSgwJgYDVQQLEx9TZWUgd3d3LmVudHJ1c3QubmV0 +L2xlZ2FsLXRlcm1zMTkwNwYDVQQLEzAoYykgMjAxMiBFbnRydXN0LCBJbmMuIC0g +Zm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxMzAxBgNVBAMTKkVudHJ1c3QgUm9vdCBD +ZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSAtIEVDMTB2MBAGByqGSM49AgEGBSuBBAAi +A2IABIQTydC6bUF74mzQ61VfZgIaJPRbiWlH47jCffHyAsWfoPZb1YsGGYZPUxBt +ByQnoaD41UcZYUx9ypMn6nQM72+WCf5j7HBdNq1nd67JnXxVRDqiY1Ef9eNi1KlH +Bz7MIKNCMEAwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0O +BBYEFLdj5xrdjekIplWDpOBqUEFlEUJJMAoGCCqGSM49BAMDA2cAMGQCMGF52OVC +R98crlOZF7ZvHH3hvxGU0QOIdeSNiaSKd0bebWHvAvX7td/M/k7//qnmpwIwW5nX +hTcGtXsI/esni0qU+eH6p44mCOh8kmhtc9hvJqwhAriZtyZBWyVgrtBIGu4G +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIEPjCCAyagAwIBAgIESlOMKDANBgkqhkiG9w0BAQsFADCBvjELMAkGA1UEBhMC +VVMxFjAUBgNVBAoTDUVudHJ1c3QsIEluYy4xKDAmBgNVBAsTH1NlZSB3d3cuZW50 +cnVzdC5uZXQvbGVnYWwtdGVybXMxOTA3BgNVBAsTMChjKSAyMDA5IEVudHJ1c3Qs +IEluYy4gLSBmb3IgYXV0aG9yaXplZCB1c2Ugb25seTEyMDAGA1UEAxMpRW50cnVz +dCBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IC0gRzIwHhcNMDkwNzA3MTcy +NTU0WhcNMzAxMjA3MTc1NTU0WjCBvjELMAkGA1UEBhMCVVMxFjAUBgNVBAoTDUVu +dHJ1c3QsIEluYy4xKDAmBgNVBAsTH1NlZSB3d3cuZW50cnVzdC5uZXQvbGVnYWwt +dGVybXMxOTA3BgNVBAsTMChjKSAyMDA5IEVudHJ1c3QsIEluYy4gLSBmb3IgYXV0 +aG9yaXplZCB1c2Ugb25seTEyMDAGA1UEAxMpRW50cnVzdCBSb290IENlcnRpZmlj +YXRpb24gQXV0aG9yaXR5IC0gRzIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK +AoIBAQC6hLZy254Ma+KZ6TABp3bqMriVQRrJ2mFOWHLP/vaCeb9zYQYKpSfYs1/T +RU4cctZOMvJyig/3gxnQaoCAAEUesMfnmr8SVycco2gvCoe9amsOXmXzHHfV1IWN +cCG0szLni6LVhjkCsbjSR87kyUnEO6fe+1R9V77w6G7CebI6C1XiUJgWMhNcL3hW +wcKUs/Ja5CeanyTXxuzQmyWC48zCxEXFjJd6BmsqEZ+pCm5IO2/b1BEZQvePB7/1 +U1+cPvQXLOZprE4yTGJ36rfo5bs0vBmLrpxR57d+tVOxMyLlbc9wPBr64ptntoP0 +jaWvYkxN4FisZDQSA/i2jZRjJKRxAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAP +BgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBRqciZ60B7vfec7aVHUbI2fkBJmqzAN +BgkqhkiG9w0BAQsFAAOCAQEAeZ8dlsa2eT8ijYfThwMEYGprmi5ZiXMRrEPR9RP/ +jTkrwPK9T3CMqS/qF8QLVJ7UG5aYMzyorWKiAHarWWluBh1+xLlEjZivEtRh2woZ +Rkfz6/djwUAFQKXSt/S1mja/qYh2iARVBCuch38aNzx+LaUa2NSJXsq9rD1s2G2v +1fN2D807iDginWyTmsQ9v4IbZT+mD12q/OWyFcq1rca8PdCE6OoGcrBNOTJ4vz4R +nAuknZoh8/CbCzB428Hch0P+vGOaysXCHMnHjf87ElgI5rY97HosTvuDls4MPGmH +VHOkc8KT/1EQrBVUAdj8BbGJoX90g5pJ19xOe4pIb4tF9g== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFiDCCA3CgAwIBAgIIfQmX/vBH6nowDQYJKoZIhvcNAQELBQAwYjELMAkGA1UE +BhMCQ04xMjAwBgNVBAoMKUdVQU5HIERPTkcgQ0VSVElGSUNBVEUgQVVUSE9SSVRZ +IENPLixMVEQuMR8wHQYDVQQDDBZHRENBIFRydXN0QVVUSCBSNSBST09UMB4XDTE0 +MTEyNjA1MTMxNVoXDTQwMTIzMTE1NTk1OVowYjELMAkGA1UEBhMCQ04xMjAwBgNV +BAoMKUdVQU5HIERPTkcgQ0VSVElGSUNBVEUgQVVUSE9SSVRZIENPLixMVEQuMR8w +HQYDVQQDDBZHRENBIFRydXN0QVVUSCBSNSBST09UMIICIjANBgkqhkiG9w0BAQEF +AAOCAg8AMIICCgKCAgEA2aMW8Mh0dHeb7zMNOwZ+Vfy1YI92hhJCfVZmPoiC7XJj +Dp6L3TQsAlFRwxn9WVSEyfFrs0yw6ehGXTjGoqcuEVe6ghWinI9tsJlKCvLriXBj +TnnEt1u9ol2x8kECK62pOqPseQrsXzrj/e+APK00mxqriCZ7VqKChh/rNYmDf1+u +KU49tm7srsHwJ5uu4/Ts765/94Y9cnrrpftZTqfrlYwiOXnhLQiPzLyRuEH3FMEj +qcOtmkVEs7LXLM3GKeJQEK5cy4KOFxg2fZfmiJqwTTQJ9Cy5WmYqsBebnh52nUpm +MUHfP/vFBu8btn4aRjb3ZGM74zkYI+dndRTVdVeSN72+ahsmUPI2JgaQxXABZG12 +ZuGR224HwGGALrIuL4xwp9E7PLOR5G62xDtw8mySlwnNR30YwPO7ng/Wi64HtloP +zgsMR6flPri9fcebNaBhlzpBdRfMK5Z3KpIhHtmVdiBnaM8Nvd/WHwlqmuLMc3Gk +L30SgLdTMEZeS1SZD2fJpcjyIMGC7J0R38IC+xo70e0gmu9lZJIQDSri3nDxGGeC +jGHeuLzRL5z7D9Ar7Rt2ueQ5Vfj4oR24qoAATILnsn8JuLwwoC8N9VKejveSswoA +HQBUlwbgsQfZxw9cZX08bVlX5O2ljelAU58VS6Bx9hoh49pwBiFYFIeFd3mqgnkC +AwEAAaNCMEAwHQYDVR0OBBYEFOLJQJ9NzuiaoXzPDj9lxSmIahlRMA8GA1UdEwEB +/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGGMA0GCSqGSIb3DQEBCwUAA4ICAQDRSVfg +p8xoWLoBDysZzY2wYUWsEe1jUGn4H3++Fo/9nesLqjJHdtJnJO29fDMylyrHBYZm +DRd9FBUb1Ov9H5r2XpdptxolpAqzkT9fNqyL7FeoPueBihhXOYV0GkLH6VsTX4/5 +COmSdI31R9KrO9b7eGZONn356ZLpBN79SWP8bfsUcZNnL0dKt7n/HipzcEYwv1ry +L3ml4Y0M2fmyYzeMN2WFcGpcWwlyua1jPLHd+PwyvzeG5LuOmCd+uh8W4XAR8gPf +JWIyJyYYMoSf/wA6E7qaTfRPuBRwIrHKK5DOKcFw9C+df/KQHtZa37dG/OaG+svg +IHZ6uqbL9XzeYqWxi+7egmaKTjowHz+Ay60nugxe19CxVsp3cbK1daFQqUBDF8Io +2c9Si1vIY9RCPqAzekYu9wogRlR+ak8x8YF+QnQ4ZXMn7sZ8uI7XpTrXmKGcjBBV +09tL7ECQ8s1uV9JiDnxXk7Gnbc2dg7sq5+W2O3FYrf3RRbxake5TFW/TRQl1brqQ +XR4EzzffHqhmsYzmIGrv/EhOdJhCrylvLmrH+33RZjEizIYAfmaDDEL0vTSSwxrq +T8p+ck0LcIymSLumoRT2+1hEmRSuqguTaaApJUqlyyvdimYHFngVV3Eb7PVHhPOe +MTd61X8kreS8/f3MboPoDKi3QWwH3b08hpcv0g== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIB3DCCAYOgAwIBAgINAgPlfvU/k/2lCSGypjAKBggqhkjOPQQDAjBQMSQwIgYD +VQQLExtHbG9iYWxTaWduIEVDQyBSb290IENBIC0gUjQxEzARBgNVBAoTCkdsb2Jh +bFNpZ24xEzARBgNVBAMTCkdsb2JhbFNpZ24wHhcNMTIxMTEzMDAwMDAwWhcNMzgw +MTE5MDMxNDA3WjBQMSQwIgYDVQQLExtHbG9iYWxTaWduIEVDQyBSb290IENBIC0g +UjQxEzARBgNVBAoTCkdsb2JhbFNpZ24xEzARBgNVBAMTCkdsb2JhbFNpZ24wWTAT +BgcqhkjOPQIBBggqhkjOPQMBBwNCAAS4xnnTj2wlDp8uORkcA6SumuU5BwkWymOx +uYb4ilfBV85C+nOh92VC/x7BALJucw7/xyHlGKSq2XE/qNS5zowdo0IwQDAOBgNV +HQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUVLB7rUW44kB/ ++wpu+74zyTyjhNUwCgYIKoZIzj0EAwIDRwAwRAIgIk90crlgr/HmnKAWBVBfw147 +bmF0774BxL4YSFlhgjICICadVGNA3jdgUM/I2O2dgq43mLyjj0xMqTQrbO/7lZsm +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICHjCCAaSgAwIBAgIRYFlJ4CYuu1X5CneKcflK2GwwCgYIKoZIzj0EAwMwUDEk +MCIGA1UECxMbR2xvYmFsU2lnbiBFQ0MgUm9vdCBDQSAtIFI1MRMwEQYDVQQKEwpH +bG9iYWxTaWduMRMwEQYDVQQDEwpHbG9iYWxTaWduMB4XDTEyMTExMzAwMDAwMFoX +DTM4MDExOTAzMTQwN1owUDEkMCIGA1UECxMbR2xvYmFsU2lnbiBFQ0MgUm9vdCBD +QSAtIFI1MRMwEQYDVQQKEwpHbG9iYWxTaWduMRMwEQYDVQQDEwpHbG9iYWxTaWdu +MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAER0UOlvt9Xb/pOdEh+J8LttV7HpI6SFkc +8GIxLcB6KP4ap1yztsyX50XUWPrRd21DosCHZTQKH3rd6zwzocWdTaRvQZU4f8ke +hOvRnkmSh5SHDDqFSmafnVmTTZdhBoZKo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYD +VR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUPeYpSJvqB8ohREom3m7e0oPQn1kwCgYI +KoZIzj0EAwMDaAAwZQIxAOVpEslu28YxuglB4Zf4+/2a4n0Sye18ZNPLBSWLVtmg +515dTguDnFt2KaAJJiFqYgIwcdK1j1zqO+F4CYWodZI7yFz9SO8NdCKoCOJuxUnO +xwy8p2Fp8fc74SrL+SvzZpA3 +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIDdTCCAl2gAwIBAgILBAAAAAABFUtaw5QwDQYJKoZIhvcNAQEFBQAwVzELMAkG +A1UEBhMCQkUxGTAXBgNVBAoTEEdsb2JhbFNpZ24gbnYtc2ExEDAOBgNVBAsTB1Jv +b3QgQ0ExGzAZBgNVBAMTEkdsb2JhbFNpZ24gUm9vdCBDQTAeFw05ODA5MDExMjAw +MDBaFw0yODAxMjgxMjAwMDBaMFcxCzAJBgNVBAYTAkJFMRkwFwYDVQQKExBHbG9i +YWxTaWduIG52LXNhMRAwDgYDVQQLEwdSb290IENBMRswGQYDVQQDExJHbG9iYWxT +aWduIFJvb3QgQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDaDuaZ +jc6j40+Kfvvxi4Mla+pIH/EqsLmVEQS98GPR4mdmzxzdzxtIK+6NiY6arymAZavp +xy0Sy6scTHAHoT0KMM0VjU/43dSMUBUc71DuxC73/OlS8pF94G3VNTCOXkNz8kHp +1Wrjsok6Vjk4bwY8iGlbKk3Fp1S4bInMm/k8yuX9ifUSPJJ4ltbcdG6TRGHRjcdG +snUOhugZitVtbNV4FpWi6cgKOOvyJBNPc1STE4U6G7weNLWLBYy5d4ux2x8gkasJ +U26Qzns3dLlwR5EiUWMWea6xrkEmCMgZK9FGqkjWZCrXgzT/LCrBbBlDSgeF59N8 +9iFo7+ryUp9/k5DPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8E +BTADAQH/MB0GA1UdDgQWBBRge2YaRQ2XyolQL30EzTSo//z9SzANBgkqhkiG9w0B +AQUFAAOCAQEA1nPnfE920I2/7LqivjTFKDK1fPxsnCwrvQmeU79rXqoRSLblCKOz +yj1hTdNGCbM+w6DjY1Ub8rrvrTnhQ7k4o+YviiY776BQVvnGCv04zcQLcFGUl5gE +38NflNUVyRRBnMRddWQVDf9VMOyGj/8N7yy5Y0b2qvzfvGn9LhJIZJrglfCm7ymP +AbEVtQwdpf5pLGkkeB6zpxxxYu7KyJesF12KwvhHhm4qxFYxldBniYUr+WymXUad +DKqC5JlR3XC321Y9YeRq4VzW9v493kHMB65jUr9TU/Qr6cf9tveCX4XSQRjbgbME +HMUfpIBvFSDJ3gyICh3WZlXi/EjJKSZp4A== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIDXzCCAkegAwIBAgILBAAAAAABIVhTCKIwDQYJKoZIhvcNAQELBQAwTDEgMB4G +A1UECxMXR2xvYmFsU2lnbiBSb290IENBIC0gUjMxEzARBgNVBAoTCkdsb2JhbFNp +Z24xEzARBgNVBAMTCkdsb2JhbFNpZ24wHhcNMDkwMzE4MTAwMDAwWhcNMjkwMzE4 +MTAwMDAwWjBMMSAwHgYDVQQLExdHbG9iYWxTaWduIFJvb3QgQ0EgLSBSMzETMBEG +A1UEChMKR2xvYmFsU2lnbjETMBEGA1UEAxMKR2xvYmFsU2lnbjCCASIwDQYJKoZI +hvcNAQEBBQADggEPADCCAQoCggEBAMwldpB5BngiFvXAg7aEyiie/QV2EcWtiHL8 +RgJDx7KKnQRfJMsuS+FggkbhUqsMgUdwbN1k0ev1LKMPgj0MK66X17YUhhB5uzsT +gHeMCOFJ0mpiLx9e+pZo34knlTifBtc+ycsmWQ1z3rDI6SYOgxXG71uL0gRgykmm +KPZpO/bLyCiR5Z2KYVc3rHQU3HTgOu5yLy6c+9C7v/U9AOEGM+iCK65TpjoWc4zd +QQ4gOsC0p6Hpsk+QLjJg6VfLuQSSaGjlOCZgdbKfd/+RFO+uIEn8rUAVSNECMWEZ +XriX7613t2Saer9fwRPvm2L7DWzgVGkWqQPabumDk3F2xmmFghcCAwEAAaNCMEAw +DgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFI/wS3+o +LkUkrk1Q+mOai97i3Ru8MA0GCSqGSIb3DQEBCwUAA4IBAQBLQNvAUKr+yAzv95ZU +RUm7lgAJQayzE4aGKAczymvmdLm6AC2upArT9fHxD4q/c2dKg8dEe3jgr25sbwMp +jjM5RcOO5LlXbKr8EpbsU8Yt5CRsuZRj+9xTaGdWPoO4zzUhw8lo/s7awlOqzJCK +6fBdRoyV3XpYKBovHd7NADdBj+1EbddTKJd+82cEHhXXipa0095MJ6RMG3NzdvQX +mcIfeg7jLQitChws/zyrVQ4PkX4268NXSb7hLi18YIvDQVETI53O9zJrlAGomecs +Mx86OyXShkDOOyyGeMlhLxS67ttVb9+E7gUJTb0o2HLO02JQZR7rkpeDMdmztcpH +WD9f +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFgzCCA2ugAwIBAgIORea7A4Mzw4VlSOb/RVEwDQYJKoZIhvcNAQEMBQAwTDEg +MB4GA1UECxMXR2xvYmFsU2lnbiBSb290IENBIC0gUjYxEzARBgNVBAoTCkdsb2Jh +bFNpZ24xEzARBgNVBAMTCkdsb2JhbFNpZ24wHhcNMTQxMjEwMDAwMDAwWhcNMzQx +MjEwMDAwMDAwWjBMMSAwHgYDVQQLExdHbG9iYWxTaWduIFJvb3QgQ0EgLSBSNjET +MBEGA1UEChMKR2xvYmFsU2lnbjETMBEGA1UEAxMKR2xvYmFsU2lnbjCCAiIwDQYJ +KoZIhvcNAQEBBQADggIPADCCAgoCggIBAJUH6HPKZvnsFMp7PPcNCPG0RQssgrRI +xutbPK6DuEGSMxSkb3/pKszGsIhrxbaJ0cay/xTOURQh7ErdG1rG1ofuTToVBu1k +ZguSgMpE3nOUTvOniX9PeGMIyBJQbUJmL025eShNUhqKGoC3GYEOfsSKvGRMIRxD +aNc9PIrFsmbVkJq3MQbFvuJtMgamHvm566qjuL++gmNQ0PAYid/kD3n16qIfKtJw +LnvnvJO7bVPiSHyMEAc4/2ayd2F+4OqMPKq0pPbzlUoSB239jLKJz9CgYXfIWHSw +1CM69106yqLbnQneXUQtkPGBzVeS+n68UARjNN9rkxi+azayOeSsJDa38O+2HBNX +k7besvjihbdzorg1qkXy4J02oW9UivFyVm4uiMVRQkQVlO6jxTiWm05OWgtH8wY2 +SXcwvHE35absIQh1/OZhFj931dmRl4QKbNQCTXTAFO39OfuD8l4UoQSwC+n+7o/h +bguyCLNhZglqsQY6ZZZZwPA1/cnaKI0aEYdwgQqomnUdnjqGBQCe24DWJfncBZ4n +WUx2OVvq+aWh2IMP0f/fMBH5hc8zSPXKbWQULHpYT9NLCEnFlWQaYw55PfWzjMpY +rZxCRXluDocZXFSxZba/jJvcE+kNb7gu3GduyYsRtYQUigAZcIN5kZeR1Bonvzce +MgfYFGM8KEyvAgMBAAGjYzBhMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTAD +AQH/MB0GA1UdDgQWBBSubAWjkxPioufi1xzWx/B/yGdToDAfBgNVHSMEGDAWgBSu +bAWjkxPioufi1xzWx/B/yGdToDANBgkqhkiG9w0BAQwFAAOCAgEAgyXt6NH9lVLN +nsAEoJFp5lzQhN7craJP6Ed41mWYqVuoPId8AorRbrcWc+ZfwFSY1XS+wc3iEZGt +Ixg93eFyRJa0lV7Ae46ZeBZDE1ZXs6KzO7V33EByrKPrmzU+sQghoefEQzd5Mr61 +55wsTLxDKZmOMNOsIeDjHfrYBzN2VAAiKrlNIC5waNrlU/yDXNOd8v9EDERm8tLj +vUYAGm0CuiVdjaExUd1URhxN25mW7xocBFymFe944Hn+Xds+qkxV/ZoVqW/hpvvf +cDDpw+5CRu3CkwWJ+n1jez/QcYF8AOiYrg54NMMl+68KnyBr3TsTjxKM4kEaSHpz +oHdpx7Zcf4LIHv5YGygrqGytXm3ABdJ7t+uA/iU3/gKbaKxCXcPu9czc8FB10jZp +nOZ7BN9uBmm23goJSFmH63sUYHpkqmlD75HHTOwY3WzvUy2MmeFe8nI+z1TIvWfs +pA9MRf/TuTAjB0yPEL+GltmZWrSZVxykzLsViVO6LAUP5MSeGbEYNNVMnbrt9x+v +JJUEeKgDu+6B5dpffItKoZB0JaezPkvILFa9x8jvOOJckvB595yEunQtYQEgfn7R +8k8HWV+LLUNS60YMlOH1Zkd5d9VUWx+tJDfLRVpOoERIyNiwmcUVhAn21klJwGW4 +5hpxbqCo8YLoRT5s1gLXCmeDBVrJpBA= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIEADCCAuigAwIBAgIBADANBgkqhkiG9w0BAQUFADBjMQswCQYDVQQGEwJVUzEh +MB8GA1UEChMYVGhlIEdvIERhZGR5IEdyb3VwLCBJbmMuMTEwLwYDVQQLEyhHbyBE +YWRkeSBDbGFzcyAyIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MB4XDTA0MDYyOTE3 +MDYyMFoXDTM0MDYyOTE3MDYyMFowYzELMAkGA1UEBhMCVVMxITAfBgNVBAoTGFRo +ZSBHbyBEYWRkeSBHcm91cCwgSW5jLjExMC8GA1UECxMoR28gRGFkZHkgQ2xhc3Mg +MiBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTCCASAwDQYJKoZIhvcNAQEBBQADggEN +ADCCAQgCggEBAN6d1+pXGEmhW+vXX0iG6r7d/+TvZxz0ZWizV3GgXne77ZtJ6XCA +PVYYYwhv2vLM0D9/AlQiVBDYsoHUwHU9S3/Hd8M+eKsaA7Ugay9qK7HFiH7Eux6w +wdhFJ2+qN1j3hybX2C32qRe3H3I2TqYXP2WYktsqbl2i/ojgC95/5Y0V4evLOtXi +EqITLdiOr18SPaAIBQi2XKVlOARFmR6jYGB0xUGlcmIbYsUfb18aQr4CUWWoriMY +avx4A6lNf4DD+qta/KFApMoZFv6yyO9ecw3ud72a9nmYvLEHZ6IVDd2gWMZEewo+ +YihfukEHU1jPEX44dMX4/7VpkI+EdOqXG68CAQOjgcAwgb0wHQYDVR0OBBYEFNLE +sNKR1EwRcbNhyz2h/t2oatTjMIGNBgNVHSMEgYUwgYKAFNLEsNKR1EwRcbNhyz2h +/t2oatTjoWekZTBjMQswCQYDVQQGEwJVUzEhMB8GA1UEChMYVGhlIEdvIERhZGR5 +IEdyb3VwLCBJbmMuMTEwLwYDVQQLEyhHbyBEYWRkeSBDbGFzcyAyIENlcnRpZmlj +YXRpb24gQXV0aG9yaXR5ggEAMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQAD +ggEBADJL87LKPpH8EsahB4yOd6AzBhRckB4Y9wimPQoZ+YeAEW5p5JYXMP80kWNy +OO7MHAGjHZQopDH2esRU1/blMVgDoszOYtuURXO1v0XJJLXVggKtI3lpjbi2Tc7P +TMozI+gciKqdi0FuFskg5YmezTvacPd+mSYgFFQlq25zheabIZ0KbIIOqPjCDPoQ +HmyW74cNxA9hi63ugyuV+I6ShHI56yDqg+2DzZduCLzrTia2cyvk0/ZM/iZx4mER +dEr/VxqHD3VILs9RaRegAhJhldXRQLIQTO7ErBBDpqWeCtWVYpoNz4iCxTIM5Cuf +ReYNnyicsbkqWletNw+vHX/bvZ8= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIDxTCCAq2gAwIBAgIBADANBgkqhkiG9w0BAQsFADCBgzELMAkGA1UEBhMCVVMx +EDAOBgNVBAgTB0FyaXpvbmExEzARBgNVBAcTClNjb3R0c2RhbGUxGjAYBgNVBAoT +EUdvRGFkZHkuY29tLCBJbmMuMTEwLwYDVQQDEyhHbyBEYWRkeSBSb290IENlcnRp +ZmljYXRlIEF1dGhvcml0eSAtIEcyMB4XDTA5MDkwMTAwMDAwMFoXDTM3MTIzMTIz +NTk1OVowgYMxCzAJBgNVBAYTAlVTMRAwDgYDVQQIEwdBcml6b25hMRMwEQYDVQQH +EwpTY290dHNkYWxlMRowGAYDVQQKExFHb0RhZGR5LmNvbSwgSW5jLjExMC8GA1UE +AxMoR28gRGFkZHkgUm9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkgLSBHMjCCASIw +DQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAL9xYgjx+lk09xvJGKP3gElY6SKD +E6bFIEMBO4Tx5oVJnyfq9oQbTqC023CYxzIBsQU+B07u9PpPL1kwIuerGVZr4oAH +/PMWdYA5UXvl+TW2dE6pjYIT5LY/qQOD+qK+ihVqf94Lw7YZFAXK6sOoBJQ7Rnwy +DfMAZiLIjWltNowRGLfTshxgtDj6AozO091GB94KPutdfMh8+7ArU6SSYmlRJQVh +GkSBjCypQ5Yj36w6gZoOKcUcqeldHraenjAKOc7xiID7S13MMuyFYkMlNAJWJwGR +tDtwKj9useiciAF9n9T521NtYJ2/LOdYq7hfRvzOxBsDPAnrSTFcaUaz4EcCAwEA +AaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYE +FDqahQcQZyi27/a9BUFuIMGU2g/eMA0GCSqGSIb3DQEBCwUAA4IBAQCZ21151fmX +WWcDYfF+OwYxdS2hII5PZYe096acvNjpL9DbWu7PdIxztDhC2gV7+AJ1uP2lsdeu +9tfeE8tTEH6KRtGX+rcuKxGrkLAngPnon1rpN5+r5N9ss4UXnT3ZJE95kTXWXwTr +gIOrmgIttRD02JDHBHNA7XIloKmf7J6raBKZV8aPEjoJpL1E/QYVN8Gb5DKj7Tjo +2GTzLH4U/ALqn83/B2gX2yKQOC16jdFU8WnjXzPKej17CuPKf1855eJ1usV2GDPO +LPAvTK33sefOT6jEm0pUBsV/fdUID+Ic/n4XuKxe9tQWskMJDE32p2u0mYRlynqI +4uJEvlz36hz1 +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICwzCCAkqgAwIBAgIBADAKBggqhkjOPQQDAjCBqjELMAkGA1UEBhMCR1IxDzAN +BgNVBAcTBkF0aGVuczFEMEIGA1UEChM7SGVsbGVuaWMgQWNhZGVtaWMgYW5kIFJl +c2VhcmNoIEluc3RpdHV0aW9ucyBDZXJ0LiBBdXRob3JpdHkxRDBCBgNVBAMTO0hl +bGxlbmljIEFjYWRlbWljIGFuZCBSZXNlYXJjaCBJbnN0aXR1dGlvbnMgRUNDIFJv +b3RDQSAyMDE1MB4XDTE1MDcwNzEwMzcxMloXDTQwMDYzMDEwMzcxMlowgaoxCzAJ +BgNVBAYTAkdSMQ8wDQYDVQQHEwZBdGhlbnMxRDBCBgNVBAoTO0hlbGxlbmljIEFj +YWRlbWljIGFuZCBSZXNlYXJjaCBJbnN0aXR1dGlvbnMgQ2VydC4gQXV0aG9yaXR5 +MUQwQgYDVQQDEztIZWxsZW5pYyBBY2FkZW1pYyBhbmQgUmVzZWFyY2ggSW5zdGl0 +dXRpb25zIEVDQyBSb290Q0EgMjAxNTB2MBAGByqGSM49AgEGBSuBBAAiA2IABJKg +QehLgoRc4vgxEZmGZE4JJS+dQS8KrjVPdJWyUWRrjWvmP3CV8AVER6ZyOFB2lQJa +jq4onvktTpnvLEhvTCUp6NFxW98dwXU3tNf6e3pCnGoKVlp8aQuqgAkkbH7BRqNC +MEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFLQi +C4KZJAEOnLvkDv2/+5cgk5kqMAoGCCqGSM49BAMCA2cAMGQCMGfOFmI4oqxiRaep +lSTAGiecMjvAwNW6qef4BENThe5SId6d9SWDPp5YSy/XZxMOIQIwBeF1Ad5o7Sof +TUwJCA3sS61kFyjndc5FZXIhF8siQQ6ME5g4mlRtm8rifOoCWCKR +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIGCzCCA/OgAwIBAgIBADANBgkqhkiG9w0BAQsFADCBpjELMAkGA1UEBhMCR1Ix +DzANBgNVBAcTBkF0aGVuczFEMEIGA1UEChM7SGVsbGVuaWMgQWNhZGVtaWMgYW5k +IFJlc2VhcmNoIEluc3RpdHV0aW9ucyBDZXJ0LiBBdXRob3JpdHkxQDA+BgNVBAMT +N0hlbGxlbmljIEFjYWRlbWljIGFuZCBSZXNlYXJjaCBJbnN0aXR1dGlvbnMgUm9v +dENBIDIwMTUwHhcNMTUwNzA3MTAxMTIxWhcNNDAwNjMwMTAxMTIxWjCBpjELMAkG +A1UEBhMCR1IxDzANBgNVBAcTBkF0aGVuczFEMEIGA1UEChM7SGVsbGVuaWMgQWNh +ZGVtaWMgYW5kIFJlc2VhcmNoIEluc3RpdHV0aW9ucyBDZXJ0LiBBdXRob3JpdHkx +QDA+BgNVBAMTN0hlbGxlbmljIEFjYWRlbWljIGFuZCBSZXNlYXJjaCBJbnN0aXR1 +dGlvbnMgUm9vdENBIDIwMTUwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoIC +AQDC+Kk/G4n8PDwEXT2QNrCROnk8ZlrvbTkBSRq0t89/TSNTt5AA4xMqKKYx8ZEA +4yjsriFBzh/a/X0SWwGDD7mwX5nh8hKDgE0GPt+sr+ehiGsxr/CL0BgzuNtFajT0 +AoAkKAoCFZVedioNmToUW/bLy1O8E00BiDeUJRtCvCLYjqOWXjrZMts+6PAQZe10 +4S+nfK8nNLspfZu2zwnI5dMK/IhlZXQK3HMcXM1AsRzUtoSMTFDPaI6oWa7CJ06C +ojXdFPQf/7J31Ycvqm59JCfnxssm5uX+Zwdj2EUN3TpZZTlYepKZcj2chF6IIbjV +9Cz82XBST3i4vTwri5WY9bPRaM8gFH5MXF/ni+X1NYEZN9cRCLdmvtNKzoNXADrD +gfgXy5I2XdGj2HUb4Ysn6npIQf1FGQatJ5lOwXBH3bWfgVMS5bGMSF0xQxfjjMZ6 +Y5ZLKTBOhE5iGV48zpeQpX8B653g+IuJ3SWYPZK2fu/Z8VFRfS0myGlZYeCsargq +NhEEelC9MoS+L9xy1dcdFkfkR2YgP/SWxa+OAXqlD3pk9Q0Yh9muiNX6hME6wGko +LfINaFGq46V3xqSQDqE3izEjR8EJCOtu93ib14L8hCCZSRm2Ekax+0VVFqmjZayc +Bw/qa9wfLgZy7IaIEuQt218FL+TwA9MmM+eAws1CoRc0CwIDAQABo0IwQDAPBgNV +HRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUcRVnyMjJvXVd +ctA4GGqd83EkVAswDQYJKoZIhvcNAQELBQADggIBAHW7bVRLqhBYRjTyYtcWNl0I +XtVsyIe9tC5G8jH4fOpCtZMWVdyhDBKg2mF+D1hYc2Ryx+hFjtyp8iY/xnmMsVMI +M4GwVhO+5lFc2JsKT0ucVlMC6U/2DWDqTUJV6HwbISHTGzrMd/K4kPFox/la/vot +9L/J9UUbzjgQKjeKeaO04wlshYaT/4mWJ3iBj2fjRnRUjtkNaeJK9E10A/+yd+2V +Z5fkscWrv2oj6NSU4kQoYsRL4vDY4ilrGnB+JGGTe08DMiUNRSQrlrRGar9KC/ea +j8GsGsVn82800vpzY4zvFrCopEYq+OsS7HK07/grfoxSwIuEVPkvPuNVqNxmsdnh +X9izjFk0WaSrT2y7HxjbdavYy5LNlDhhDgcGH0tGEPEVvo2FXDtKK4F5D7Rpn0lQ +l033DlZdwJVqwjbDG2jJ9SrcR5q+ss7FJej6A7na+RZukYT1HCjI/CbM1xyQVqdf +bzoEvM14iQuODy+jqk+iGxI9FghAD/FGTNeqewjBCvVtJ94Cj8rDtSvK6evIIVM4 +pcw72Hc3MKJP2W/R8kCtQXoXxdZKNYm3QdV8hn9VTYNKpXMgwDqvkPGaJI7ZjnHK +e7iG2rKPmT4dEw0SEe7Uq/DpFXYC5ODfqiAeW2GFZECpkJcNrVPSWh2HagCXZWK0 +vm9qp/UsQu0yrbYhnr68 +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIDMDCCAhigAwIBAgICA+gwDQYJKoZIhvcNAQEFBQAwRzELMAkGA1UEBhMCSEsx +FjAUBgNVBAoTDUhvbmdrb25nIFBvc3QxIDAeBgNVBAMTF0hvbmdrb25nIFBvc3Qg +Um9vdCBDQSAxMB4XDTAzMDUxNTA1MTMxNFoXDTIzMDUxNTA0NTIyOVowRzELMAkG +A1UEBhMCSEsxFjAUBgNVBAoTDUhvbmdrb25nIFBvc3QxIDAeBgNVBAMTF0hvbmdr +b25nIFBvc3QgUm9vdCBDQSAxMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC +AQEArP84tulmAknjorThkPlAj3n54r15/gK97iSSHSL22oVyaf7XPwnU3ZG1ApzQ +jVrhVcNQhrkpJsLj2aDxaQMoIIBFIi1WpztUlVYiWR8o3x8gPW2iNr4joLFutbEn +PzlTCeqrauh0ssJlXI6/fMN4hM2eFvz1Lk8gKgifd/PFHsSaUmYeSF7jEAaPIpjh +ZY4bXSNmO7ilMlHIhqqhqZ5/dpTCpmy3QfDVyAY45tQM4vM7TG1QjMSDJ8EThFk9 +nnV0ttgCXjqQesBCNnLsak3c78QA3xMYV18meMjWCnl3v/evt3a5pQuEF10Q6m/h +q5URX208o1xNg1vysxmKgIsLhwIDAQABoyYwJDASBgNVHRMBAf8ECDAGAQH/AgED +MA4GA1UdDwEB/wQEAwIBxjANBgkqhkiG9w0BAQUFAAOCAQEADkbVPK7ih9legYsC +mEEIjEy82tvuJxuC52pF7BaLT4Wg87JwvVqWuspube5Gi27nKi6Wsxkz67SfqLI3 +7piol7Yutmcn1KZJ/RyTZXaeQi/cImyaT/JaFTmxcdcrUehtHJjA2Sr0oYJ71clB +oiMBdDhViw+5LmeiIAQ32pwL0xch4I+XeTRvhEgCIDMb5jREn5Fw9IBehEPCKdJs +EhTkYY2sEJCehFC78JZvRZ+K88psT/oROhUVRsPNH4NbLUES7VBnQRM9IauUiqpO +fMGx+6fWtScvl6tu4B3i0RwsH0Ti/L6RoZz71ilTc4afU9hDDl3WY4JxHYB0yvbi +AmvZWg== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFazCCA1OgAwIBAgIRAIIQz7DSQONZRGPgu2OCiwAwDQYJKoZIhvcNAQELBQAw +TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh +cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMTUwNjA0MTEwNDM4 +WhcNMzUwNjA0MTEwNDM4WjBPMQswCQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJu +ZXQgU2VjdXJpdHkgUmVzZWFyY2ggR3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBY +MTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK3oJHP0FDfzm54rVygc +h77ct984kIxuPOZXoHj3dcKi/vVqbvYATyjb3miGbESTtrFj/RQSa78f0uoxmyF+ +0TM8ukj13Xnfs7j/EvEhmkvBioZxaUpmZmyPfjxwv60pIgbz5MDmgK7iS4+3mX6U +A5/TR5d8mUgjU+g4rk8Kb4Mu0UlXjIB0ttov0DiNewNwIRt18jA8+o+u3dpjq+sW +T8KOEUt+zwvo/7V3LvSye0rgTBIlDHCNAymg4VMk7BPZ7hm/ELNKjD+Jo2FR3qyH +B5T0Y3HsLuJvW5iB4YlcNHlsdu87kGJ55tukmi8mxdAQ4Q7e2RCOFvu396j3x+UC +B5iPNgiV5+I3lg02dZ77DnKxHZu8A/lJBdiB3QW0KtZB6awBdpUKD9jf1b0SHzUv +KBds0pjBqAlkd25HN7rOrFleaJ1/ctaJxQZBKT5ZPt0m9STJEadao0xAH0ahmbWn +OlFuhjuefXKnEgV4We0+UXgVCwOPjdAvBbI+e0ocS3MFEvzG6uBQE3xDk3SzynTn +jh8BCNAw1FtxNrQHusEwMFxIt4I7mKZ9YIqioymCzLq9gwQbooMDQaHWBfEbwrbw +qHyGO0aoSCqI3Haadr8faqU9GY/rOPNk3sgrDQoo//fb4hVC1CLQJ13hef4Y53CI +rU7m2Ys6xt0nUW7/vGT1M0NPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNV +HRMBAf8EBTADAQH/MB0GA1UdDgQWBBR5tFnme7bl5AFzgAiIyBpY9umbbjANBgkq +hkiG9w0BAQsFAAOCAgEAVR9YqbyyqFDQDLHYGmkgJykIrGF1XIpu+ILlaS/V9lZL +ubhzEFnTIZd+50xx+7LSYK05qAvqFyFWhfFQDlnrzuBZ6brJFe+GnY+EgPbk6ZGQ +3BebYhtF8GaV0nxvwuo77x/Py9auJ/GpsMiu/X1+mvoiBOv/2X/qkSsisRcOj/KK +NFtY2PwByVS5uCbMiogziUwthDyC3+6WVwW6LLv3xLfHTjuCvjHIInNzktHCgKQ5 +ORAzI4JMPJ+GslWYHb4phowim57iaztXOoJwTdwJx4nLCgdNbOhdjsnvzqvHu7Ur +TkXWStAmzOVyyghqpZXjFaH3pO3JLF+l+/+sKAIuvtd7u+Nxe5AW0wdeRlN8NwdC +jNPElpzVmbUq4JUagEiuTDkHzsxHpFKVK7q4+63SM1N95R1NbdWhscdCb+ZAJzVc +oyi3B43njTOQ5yOf+1CceWxG1bQVs5ZufpsMljq4Ui0/1lvh+wjChP4kqKOJ2qxq +4RgqsahDYVvTH9w7jXbyLeiNdd8XM2w9U/t7y0Ff/9yi0GE44Za4rF2LN9d11TPA +mRGunUHBcnWEvgJBQl9nJEiU0Zsnvgc/ubhPgXRR4Xq37Z0j4r7g1SgEEzwxA57d +emyPxgcYxn/eR44/KJ4EBs+lVDR3veyJm+kXQ99b21/+jh5Xos1AnX5iItreGCc= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFYDCCA0igAwIBAgIQCgFCgAAAAUUjyES1AAAAAjANBgkqhkiG9w0BAQsFADBK +MQswCQYDVQQGEwJVUzESMBAGA1UEChMJSWRlblRydXN0MScwJQYDVQQDEx5JZGVu +VHJ1c3QgQ29tbWVyY2lhbCBSb290IENBIDEwHhcNMTQwMTE2MTgxMjIzWhcNMzQw +MTE2MTgxMjIzWjBKMQswCQYDVQQGEwJVUzESMBAGA1UEChMJSWRlblRydXN0MScw +JQYDVQQDEx5JZGVuVHJ1c3QgQ29tbWVyY2lhbCBSb290IENBIDEwggIiMA0GCSqG +SIb3DQEBAQUAA4ICDwAwggIKAoICAQCnUBneP5k91DNG8W9RYYKyqU+PZ4ldhNlT +3Qwo2dfw/66VQ3KZ+bVdfIrBQuExUHTRgQ18zZshq0PirK1ehm7zCYofWjK9ouuU ++ehcCuz/mNKvcbO0U59Oh++SvL3sTzIwiEsXXlfEU8L2ApeN2WIrvyQfYo3fw7gp +S0l4PJNgiCL8mdo2yMKi1CxUAGc1bnO/AljwpN3lsKImesrgNqUZFvX9t++uP0D1 +bVoE/c40yiTcdCMbXTMTEl3EASX2MN0CXZ/g1Ue9tOsbobtJSdifWwLziuQkkORi +T0/Br4sOdBeo0XKIanoBScy0RnnGF7HamB4HWfp1IYVl3ZBWzvurpWCdxJ35UrCL +vYf5jysjCiN2O/cz4ckA82n5S6LgTrx+kzmEB/dEcH7+B1rlsazRGMzyNeVJSQjK +Vsk9+w8YfYs7wRPCTY/JTw436R+hDmrfYi7LNQZReSzIJTj0+kuniVyc0uMNOYZK +dHzVWYfCP04MXFL0PfdSgvHqo6z9STQaKPNBiDoT7uje/5kdX7rL6B7yuVBgwDHT +c+XvvqDtMwt0viAgxGds8AgDelWAf0ZOlqf0Hj7h9tgJ4TNkK2PXMl6f+cB7D3hv +l7yTmvmcEpB4eoCHFddydJxVdHixuuFucAS6T6C6aMN7/zHwcz09lCqxC0EOoP5N +iGVreTO01wIDAQABo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB +/zAdBgNVHQ4EFgQU7UQZwNPwBovupHu+QucmVMiONnYwDQYJKoZIhvcNAQELBQAD +ggIBAA2ukDL2pkt8RHYZYR4nKM1eVO8lvOMIkPkp165oCOGUAFjvLi5+U1KMtlwH +6oi6mYtQlNeCgN9hCQCTrQ0U5s7B8jeUeLBfnLOic7iPBZM4zY0+sLj7wM+x8uwt +LRvM7Kqas6pgghstO8OEPVeKlh6cdbjTMM1gCIOQ045U8U1mwF10A0Cj7oV+wh93 +nAbowacYXVKV7cndJZ5t+qntozo00Fl72u1Q8zW/7esUTTHHYPTa8Yec4kjixsU3 ++wYQ+nVZZjFHKdp2mhzpgq7vmrlR94gjmmmVYjzlVYA211QC//G5Xc7UI2/YRYRK +W2XviQzdFKcgyxilJbQN+QHwotL0AMh0jqEqSI5l2xPE4iUXfeu+h1sXIFRRk0pT +AwvsXcoz7WL9RccvW9xYoIA55vrX/hMUpu09lEpCdNTDd1lzzY9GvlU47/rokTLq +l1gEIt44w8y8bckzOmoKaT+gyOpyj4xjhiO9bTyWnpXgSUyqorkqG5w2gXjtw+hG +4iZZRHUe2XWJUc0QhJ1hYMtd+ZciTY6Y5uN/9lu7rs3KSoFrXgvzUeF0K+l+J6fZ +mUlO+KWA2yUPHGNiiskzZ2s8EIPGrd6ozRaOjfAHN3Gf8qv8QfXBi+wAN10J5U6A +7/qxXDgGpRtK4dw4LTzcqx+QGtVKnO7RcGzM7vRX+Bi6hG6H +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFZjCCA06gAwIBAgIQCgFCgAAAAUUjz0Z8AAAAAjANBgkqhkiG9w0BAQsFADBN +MQswCQYDVQQGEwJVUzESMBAGA1UEChMJSWRlblRydXN0MSowKAYDVQQDEyFJZGVu +VHJ1c3QgUHVibGljIFNlY3RvciBSb290IENBIDEwHhcNMTQwMTE2MTc1MzMyWhcN +MzQwMTE2MTc1MzMyWjBNMQswCQYDVQQGEwJVUzESMBAGA1UEChMJSWRlblRydXN0 +MSowKAYDVQQDEyFJZGVuVHJ1c3QgUHVibGljIFNlY3RvciBSb290IENBIDEwggIi +MA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC2IpT8pEiv6EdrCvsnduTyP4o7 +ekosMSqMjbCpwzFrqHd2hCa2rIFCDQjrVVi7evi8ZX3yoG2LqEfpYnYeEe4IFNGy +RBb06tD6Hi9e28tzQa68ALBKK0CyrOE7S8ItneShm+waOh7wCLPQ5CQ1B5+ctMlS +bdsHyo+1W/CD80/HLaXIrcuVIKQxKFdYWuSNG5qrng0M8gozOSI5Cpcu81N3uURF +/YTLNiCBWS2ab21ISGHKTN9T0a9SvESfqy9rg3LvdYDaBjMbXcjaY8ZNzaxmMc3R +3j6HEDbhuaR672BQssvKplbgN6+rNBM5Jeg5ZuSYeqoSmJxZZoY+rfGwyj4GD3vw +EUs3oERte8uojHH01bWRNszwFcYr3lEXsZdMUD2xlVl8BX0tIdUAvwFnol57plzy +9yLxkA2T26pEUWbMfXYD62qoKjgZl3YNa4ph+bz27nb9cCvdKTz4Ch5bQhyLVi9V +GxyhLrXHFub4qjySjmm2AcG1hp2JDws4lFTo6tyePSW8Uybt1as5qsVATFSrsrTZ +2fjXctscvG29ZV/viDUqZi/u9rNl8DONfJhBaUYPQxxp+pu10GFqzcpL2UyQRqsV +WaFHVCkugyhfHMKiq3IXAAaOReyL4jM9f9oZRORicsPfIsbyVtTdX5Vy7W1f90gD +W/3FKqD2cyOEEBsB5wIDAQABo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/ +BAUwAwEB/zAdBgNVHQ4EFgQU43HgntinQtnbcZFrlJPrw6PRFKMwDQYJKoZIhvcN +AQELBQADggIBAEf63QqwEZE4rU1d9+UOl1QZgkiHVIyqZJnYWv6IAcVYpZmxI1Qj +t2odIFflAWJBF9MJ23XLblSQdf4an4EKwt3X9wnQW3IV5B4Jaj0z8yGa5hV+rVHV +DRDtfULAj+7AmgjVQdZcDiFpboBhDhXAuM/FSRJSzL46zNQuOAXeNf0fb7iAaJg9 +TaDKQGXSc3z1i9kKlT/YPyNtGtEqJBnZhbMX73huqVjRI9PHE+1yJX9dsXNw0H8G +lwmEKYBhHfpe/3OsoOOJuBxxFcbeMX8S3OFtm6/n6J91eEyrRjuazr8FGF1NFTwW +mhlQBJqymm9li1JfPFgEKCXAZmExfrngdbkaqIHWchezxQMxNRF4eKLg6TCMf4Df +WN88uieW4oA0beOY02QnrEh+KHdcxiVhJfiFDGX6xDIvpZgF5PgLZxYWxoK4Mhn5 ++bl53B/N66+rDt0b20XkeucC4pVd/GnwU2lhlXV5C15V5jgclKlZM57IcXR5f1GJ +tshquDDIajjDbp7hNxbqBWJMWxJH7ae0s1hWx0nzfxJoCTFx8G34Tkf71oXuxVhA +GaQdp/lLQzfcaFpPz+vCZHTetBXZ9FRUGi8c15dxVJCO2SCdUyt/q4/i6jC8UDfv +8Ue1fXwsBOxonbRJRBD0ckscZOf85muQ3Wl9af0AVqW3rLatt8o+Ae+c +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIF8TCCA9mgAwIBAgIQALC3WhZIX7/hy/WL1xnmfTANBgkqhkiG9w0BAQsFADA4 +MQswCQYDVQQGEwJFUzEUMBIGA1UECgwLSVpFTlBFIFMuQS4xEzARBgNVBAMMCkl6 +ZW5wZS5jb20wHhcNMDcxMjEzMTMwODI4WhcNMzcxMjEzMDgyNzI1WjA4MQswCQYD +VQQGEwJFUzEUMBIGA1UECgwLSVpFTlBFIFMuQS4xEzARBgNVBAMMCkl6ZW5wZS5j +b20wggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDJ03rKDx6sp4boFmVq +scIbRTJxldn+EFvMr+eleQGPicPK8lVx93e+d5TzcqQsRNiekpsUOqHnJJAKClaO +xdgmlOHZSOEtPtoKct2jmRXagaKH9HtuJneJWK3W6wyyQXpzbm3benhB6QiIEn6H +LmYRY2xU+zydcsC8Lv/Ct90NduM61/e0aL6i9eOBbsFGb12N4E3GVFWJGjMxCrFX +uaOKmMPsOzTFlUFpfnXCPCDFYbpRR6AgkJOhkEvzTnyFRVSa0QUmQbC1TR0zvsQD +yCV8wXDbO/QJLVQnSKwv4cSsPsjLkkxTOTcj7NMB+eAJRE1NZMDhDVqHIrytG6P+ +JrUV86f8hBnp7KGItERphIPzidF0BqnMC9bC3ieFUCbKF7jJeodWLBoBHmy+E60Q +rLUk9TiRodZL2vG70t5HtfG8gfZZa88ZU+mNFctKy6lvROUbQc/hhqfK0GqfvEyN +BjNaooXlkDWgYlwWTvDjovoDGrQscbNYLN57C9saD+veIR8GdwYDsMnvmfzAuU8L +hij+0rnq49qlw0dpEuDb8PYZi+17cNcC1u2HGCgsBCRMd+RIihrGO5rUD8r6ddIB +QFqNeb+Lz0vPqhbBleStTIo+F5HUsWLlguWABKQDfo2/2n+iD5dPDNMN+9fR5XJ+ +HMh3/1uaD7euBUbl8agW7EekFwIDAQABo4H2MIHzMIGwBgNVHREEgagwgaWBD2lu +Zm9AaXplbnBlLmNvbaSBkTCBjjFHMEUGA1UECgw+SVpFTlBFIFMuQS4gLSBDSUYg +QTAxMzM3MjYwLVJNZXJjLlZpdG9yaWEtR2FzdGVpeiBUMTA1NSBGNjIgUzgxQzBB +BgNVBAkMOkF2ZGEgZGVsIE1lZGl0ZXJyYW5lbyBFdG9yYmlkZWEgMTQgLSAwMTAx +MCBWaXRvcmlhLUdhc3RlaXowDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMC +AQYwHQYDVR0OBBYEFB0cZQ6o8iV7tJHP5LGx5r1VdGwFMA0GCSqGSIb3DQEBCwUA +A4ICAQB4pgwWSp9MiDrAyw6lFn2fuUhfGI8NYjb2zRlrrKvV9pF9rnHzP7MOeIWb +laQnIUdCSnxIOvVFfLMMjlF4rJUT3sb9fbgakEyrkgPH7UIBzg/YsfqikuFgba56 +awmqxinuaElnMIAkejEWOVt+8Rwu3WwJrfIxwYJOubv5vr8qhT/AQKM6WfxZSzwo +JNu0FXWuDYi6LnPAvViH5ULy617uHjAimcs30cQhbIHsvm0m5hzkQiCeR7Csg1lw +LDXWrzY0tM07+DKo7+N4ifuNRSzanLh+QBxh5z6ikixL8s36mLYp//Pye6kfLqCT +VyvehQP5aTfLnnhqBbTFMXiJ7HqnheG5ezzevh55hM6fcA5ZwjUukCox2eRFekGk +LhObNA5me0mrZJfQRsN5nXJQY6aYWwa9SG3YOYNw6DXwBdGqvOPbyALqfP2C2sJb +UjWumDqtujWTI6cfSN01RpiyEGjkpTHCClguGYEQyVB1/OpaFs4R1+7vUIgtYf8/ +QnMFlEPVjjxOAToZpR9GTnfQXeWBIiGH/pR9hNiTrdZoQ0iy2+tzJOeRf1SktoA+ +naM8THLCV8Sg1Mw4J87VBp6iSNnpn86CcDaTmjvfliHjWbcM2pE38P1ZWrOZyGls +QyYBNWNgVYkDOnXYukrZVP/u3oDYLdE41V4tC5h9Pmzb/CaIxw== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIECjCCAvKgAwIBAgIJAMJ+QwRORz8ZMA0GCSqGSIb3DQEBCwUAMIGCMQswCQYD +VQQGEwJIVTERMA8GA1UEBwwIQnVkYXBlc3QxFjAUBgNVBAoMDU1pY3Jvc2VjIEx0 +ZC4xJzAlBgNVBAMMHk1pY3Jvc2VjIGUtU3ppZ25vIFJvb3QgQ0EgMjAwOTEfMB0G +CSqGSIb3DQEJARYQaW5mb0BlLXN6aWduby5odTAeFw0wOTA2MTYxMTMwMThaFw0y +OTEyMzAxMTMwMThaMIGCMQswCQYDVQQGEwJIVTERMA8GA1UEBwwIQnVkYXBlc3Qx +FjAUBgNVBAoMDU1pY3Jvc2VjIEx0ZC4xJzAlBgNVBAMMHk1pY3Jvc2VjIGUtU3pp +Z25vIFJvb3QgQ0EgMjAwOTEfMB0GCSqGSIb3DQEJARYQaW5mb0BlLXN6aWduby5o +dTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAOn4j/NjrdqG2KfgQvvP +kd6mJviZpWNwrZuuyjNAfW2WbqEORO7hE52UQlKavXWFdCyoDh2Tthi3jCyoz/tc +cbna7P7ofo/kLx2yqHWH2Leh5TvPmUpG0IMZfcChEhyVbUr02MelTTMuhTlAdX4U +fIASmFDHQWe4oIBhVKZsTh/gnQ4H6cm6M+f+wFUoLAKApxn1ntxVUwOXewdI/5n7 +N4okxFnMUBBjjqqpGrCEGob5X7uxUG6k0QrM1XF+H6cbfPVTbiJfyyvm1HxdrtbC +xkzlBQHZ7Vf8wSN5/PrIJIOV87VqUQHQd9bpEqH5GoP7ghu5sJf0dgYzQ0mg/wu1 ++rUCAwEAAaOBgDB+MA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0G +A1UdDgQWBBTLD8bfQkPMPcu1SCOhGnqmKrs0aDAfBgNVHSMEGDAWgBTLD8bfQkPM +Pcu1SCOhGnqmKrs0aDAbBgNVHREEFDASgRBpbmZvQGUtc3ppZ25vLmh1MA0GCSqG +SIb3DQEBCwUAA4IBAQDJ0Q5eLtXMs3w+y/w9/w0olZMEyL/azXm4Q5DwpL7v8u8h +mLzU1F0G9u5C7DBsoKqpyvGvivo/C3NqPuouQH4frlRheesuCDfXI/OMn74dseGk +ddug4lQUsbocKaQY9hK6ohQU4zE1yED/t+AFdlfBHFny+L/k7SViXITwfn4fs775 +tyERzAMBVnCnEJIeGzSBHq2cGsMEPO0CYdYeBvNfOofyK/FFh+U9rNHHV4S9a67c +2Pm2G2JwCz02yULyMtd6YebS2z3PyKnJm9zbWETXbzivf3jTo60adbocwTZ8jx5t +HMN1Rq41Bab2XD0h7lbwyYIiLXpUq3DDfSJlgnCW +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIEFTCCAv2gAwIBAgIGSUEs5AAQMA0GCSqGSIb3DQEBCwUAMIGnMQswCQYDVQQG +EwJIVTERMA8GA1UEBwwIQnVkYXBlc3QxFTATBgNVBAoMDE5ldExvY2sgS2Z0LjE3 +MDUGA1UECwwuVGFuw7pzw610dsOhbnlraWFkw7NrIChDZXJ0aWZpY2F0aW9uIFNl +cnZpY2VzKTE1MDMGA1UEAwwsTmV0TG9jayBBcmFueSAoQ2xhc3MgR29sZCkgRsWR +dGFuw7pzw610dsOhbnkwHhcNMDgxMjExMTUwODIxWhcNMjgxMjA2MTUwODIxWjCB +pzELMAkGA1UEBhMCSFUxETAPBgNVBAcMCEJ1ZGFwZXN0MRUwEwYDVQQKDAxOZXRM +b2NrIEtmdC4xNzA1BgNVBAsMLlRhbsO6c8OtdHbDoW55a2lhZMOzayAoQ2VydGlm +aWNhdGlvbiBTZXJ2aWNlcykxNTAzBgNVBAMMLE5ldExvY2sgQXJhbnkgKENsYXNz +IEdvbGQpIEbFkXRhbsO6c8OtdHbDoW55MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A +MIIBCgKCAQEAxCRec75LbRTDofTjl5Bu0jBFHjzuZ9lk4BqKf8owyoPjIMHj9DrT +lF8afFttvzBPhCf2nx9JvMaZCpDyD/V/Q4Q3Y1GLeqVw/HpYzY6b7cNGbIRwXdrz +AZAj/E4wqX7hJ2Pn7WQ8oLjJM2P+FpD/sLj916jAwJRDC7bVWaaeVtAkH3B5r9s5 +VA1lddkVQZQBr17s9o3x/61k/iCa11zr/qYfCGSji3ZVrR47KGAuhyXoqq8fxmRG +ILdwfzzeSNuWU7c5d+Qa4scWhHaXWy+7GRWF+GmF9ZmnqfI0p6m2pgP8b4Y9VHx2 +BJtr+UBdADTHLpl1neWIA6pN+APSQnbAGwIDAKiLo0UwQzASBgNVHRMBAf8ECDAG +AQH/AgEEMA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUzPpnk/C2uNClwB7zU/2M +U9+D15YwDQYJKoZIhvcNAQELBQADggEBAKt/7hwWqZw8UQCgwBEIBaeZ5m8BiFRh +bvG5GK1Krf6BQCOUL/t1fC8oS2IkgYIL9WHxHG64YTjrgfpioTtaYtOUZcTh5m2C ++C8lcLIhJsFyUR+MLMOEkMNaj7rP9KdlpeuY0fsFskZ1FSNqb4VjMIDw1Z4fKRzC +bLBQWV2QWzuoDTDPv31/zvGdg73JRm4gpvlhUbohL3u+pRVjodSVh/GeufOJ8z2F +uLjbvrW5KfnaNwUASZQDhETnv0Mxz3WLJdH0pmT1kvarBes96aULNmLazAZfNou2 +XjG4Kvte9nHfRCaexOYNkbQudZWAUWpLMKawYqGT8ZvYzsRjdT9ZR7E= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIDtTCCAp2gAwIBAgIQdrEgUnTwhYdGs/gjGvbCwDANBgkqhkiG9w0BAQsFADBt +MQswCQYDVQQGEwJDSDEQMA4GA1UEChMHV0lTZUtleTEiMCAGA1UECxMZT0lTVEUg +Rm91bmRhdGlvbiBFbmRvcnNlZDEoMCYGA1UEAxMfT0lTVEUgV0lTZUtleSBHbG9i +YWwgUm9vdCBHQiBDQTAeFw0xNDEyMDExNTAwMzJaFw0zOTEyMDExNTEwMzFaMG0x +CzAJBgNVBAYTAkNIMRAwDgYDVQQKEwdXSVNlS2V5MSIwIAYDVQQLExlPSVNURSBG +b3VuZGF0aW9uIEVuZG9yc2VkMSgwJgYDVQQDEx9PSVNURSBXSVNlS2V5IEdsb2Jh +bCBSb290IEdCIENBMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA2Be3 +HEokKtaXscriHvt9OO+Y9bI5mE4nuBFde9IllIiCFSZqGzG7qFshISvYD06fWvGx +WuR51jIjK+FTzJlFXHtPrby/h0oLS5daqPZI7H17Dc0hBt+eFf1Biki3IPShehtX +1F1Q/7pn2COZH8g/497/b1t3sWtuuMlk9+HKQUYOKXHQuSP8yYFfTvdv37+ErXNk +u7dCjmn21HYdfp2nuFeKUWdy19SouJVUQHMD9ur06/4oQnc/nSMbsrY9gBQHTC5P +99UKFg29ZkM3fiNDecNAhvVMKdqOmq0NpQSHiB6F4+lT1ZvIiwNjeOvgGUpuuy9r +M2RYk61pv48b74JIxwIDAQABo1EwTzALBgNVHQ8EBAMCAYYwDwYDVR0TAQH/BAUw +AwEB/zAdBgNVHQ4EFgQUNQ/INmNe4qPs+TtmFc5RUuORmj0wEAYJKwYBBAGCNxUB +BAMCAQAwDQYJKoZIhvcNAQELBQADggEBAEBM+4eymYGQfp3FsLAmzYh7KzKNbrgh +cViXfa43FK8+5/ea4n32cZiZBKpDdHij40lhPnOMTZTg+XHEthYOU3gf1qKHLwI5 +gSk8rxWYITD+KJAAjNHhy/peyP34EEY7onhCkRd0VQreUGdNZtGn//3ZwLWoo4rO +ZvUPQ82nK1d7Y0Zqqi5S2PTt4W2tKZB4SLrhI6qjiey1q5bAtEuiHZeeevJuQHHf +aPFlTc58Bd9TZaml8LGXBHAVRgOY1NK/VLSgWH1Sb9pWJmLU2NuJMW8c8CLC02Ic +Nc1MaRVUGpCY3useX8p3x8uOPUNpnJpY0CQ73xtAln41rYHHTnG6iBM= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICaTCCAe+gAwIBAgIQISpWDK7aDKtARb8roi066jAKBggqhkjOPQQDAzBtMQsw +CQYDVQQGEwJDSDEQMA4GA1UEChMHV0lTZUtleTEiMCAGA1UECxMZT0lTVEUgRm91 +bmRhdGlvbiBFbmRvcnNlZDEoMCYGA1UEAxMfT0lTVEUgV0lTZUtleSBHbG9iYWwg +Um9vdCBHQyBDQTAeFw0xNzA1MDkwOTQ4MzRaFw00MjA1MDkwOTU4MzNaMG0xCzAJ +BgNVBAYTAkNIMRAwDgYDVQQKEwdXSVNlS2V5MSIwIAYDVQQLExlPSVNURSBGb3Vu +ZGF0aW9uIEVuZG9yc2VkMSgwJgYDVQQDEx9PSVNURSBXSVNlS2V5IEdsb2JhbCBS +b290IEdDIENBMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAETOlQwMYPchi82PG6s4ni +eUqjFqdrVCTbUf/q9Akkwwsin8tqJ4KBDdLArzHkdIJuyiXZjHWd8dvQmqJLIX4W +p2OQ0jnUsYd4XxiWD1AbNTcPasbc2RNNpI6QN+a9WzGRo1QwUjAOBgNVHQ8BAf8E +BAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUSIcUrOPDnpBgOtfKie7T +rYy0UGYwEAYJKwYBBAGCNxUBBAMCAQAwCgYIKoZIzj0EAwMDaAAwZQIwJsdpW9zV +57LnyAyMjMPdeYwbY9XJUpROTYJKcx6ygISpJcBMWm1JKWB4E+J+SOtkAjEA2zQg +Mgj/mkkCtojeFK9dbJlxjRo/i9fgojaGHAeCOnZT/cKi7e97sIBPWA9LUzm9 +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFYDCCA0igAwIBAgIUeFhfLq0sGUvjNwc1NBMotZbUZZMwDQYJKoZIhvcNAQEL +BQAwSDELMAkGA1UEBhMCQk0xGTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxHjAc +BgNVBAMTFVF1b1ZhZGlzIFJvb3QgQ0EgMSBHMzAeFw0xMjAxMTIxNzI3NDRaFw00 +MjAxMTIxNzI3NDRaMEgxCzAJBgNVBAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBM +aW1pdGVkMR4wHAYDVQQDExVRdW9WYWRpcyBSb290IENBIDEgRzMwggIiMA0GCSqG +SIb3DQEBAQUAA4ICDwAwggIKAoICAQCgvlAQjunybEC0BJyFuTHK3C3kEakEPBtV +wedYMB0ktMPvhd6MLOHBPd+C5k+tR4ds7FtJwUrVu4/sh6x/gpqG7D0DmVIB0jWe +rNrwU8lmPNSsAgHaJNM7qAJGr6Qc4/hzWHa39g6QDbXwz8z6+cZM5cOGMAqNF341 +68Xfuw6cwI2H44g4hWf6Pser4BOcBRiYz5P1sZK0/CPTz9XEJ0ngnjybCKOLXSoh +4Pw5qlPafX7PGglTvF0FBM+hSo+LdoINofjSxxR3W5A2B4GbPgb6Ul5jxaYA/qXp +UhtStZI5cgMJYr2wYBZupt0lwgNm3fME0UDiTouG9G/lg6AnhF4EwfWQvTA9xO+o +abw4m6SkltFi2mnAAZauy8RRNOoMqv8hjlmPSlzkYZqn0ukqeI1RPToV7qJZjqlc +3sX5kCLliEVx3ZGZbHqfPT2YfF72vhZooF6uCyP8Wg+qInYtyaEQHeTTRCOQiJ/G +KubX9ZqzWB4vMIkIG1SitZgj7Ah3HJVdYdHLiZxfokqRmu8hqkkWCKi9YSgxyXSt +hfbZxbGL0eUQMk1fiyA6PEkfM4VZDdvLCXVDaXP7a3F98N/ETH3Goy7IlXnLc6KO +Tk0k+17kBL5yG6YnLUlamXrXXAkgt3+UuU/xDRxeiEIbEbfnkduebPRq34wGmAOt +zCjvpUfzUwIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIB +BjAdBgNVHQ4EFgQUo5fW816iEOGrRZ88F2Q87gFwnMwwDQYJKoZIhvcNAQELBQAD +ggIBABj6W3X8PnrHX3fHyt/PX8MSxEBd1DKquGrX1RUVRpgjpeaQWxiZTOOtQqOC +MTaIzen7xASWSIsBx40Bz1szBpZGZnQdT+3Btrm0DWHMY37XLneMlhwqI2hrhVd2 +cDMT/uFPpiN3GPoajOi9ZcnPP/TJF9zrx7zABC4tRi9pZsMbj/7sPtPKlL92CiUN +qXsCHKnQO18LwIE6PWThv6ctTr1NxNgpxiIY0MWscgKCP6o6ojoilzHdCGPDdRS5 +YCgtW2jgFqlmgiNR9etT2DGbe+m3nUvriBbP+V04ikkwj+3x6xn0dxoxGE1nVGwv +b2X52z3sIexe9PSLymBlVNFxZPT5pqOBMzYzcfCkeF9OrYMh3jRJjehZrJ3ydlo2 +8hP0r+AJx2EqbPfgna67hkooby7utHnNkDPDs3b69fBsnQGQ+p6Q9pxyz0fawx/k +NSBT8lTR32GDpgLiJTjehTItXnOQUl1CxM49S+H5GYQd1aJQzEH7QRTDvdbJWqNj +ZgKAvQU6O0ec7AAmTPWIUb+oI38YB7AL7YsmoWTTYUrrXJ/es69nA7Mf3W1daWhp +q1467HxpvMc7hU6eFbm0FU/DlXpY18ls6Wy58yljXrQs8C097Vpl4KlbQMJImYFt +nh8GKjwStIsPm6Ik8KaN1nrgS7ZklmOVhMJKzRwuJIczYOXD +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFtzCCA5+gAwIBAgICBQkwDQYJKoZIhvcNAQEFBQAwRTELMAkGA1UEBhMCQk0x +GTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxGzAZBgNVBAMTElF1b1ZhZGlzIFJv +b3QgQ0EgMjAeFw0wNjExMjQxODI3MDBaFw0zMTExMjQxODIzMzNaMEUxCzAJBgNV +BAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBMaW1pdGVkMRswGQYDVQQDExJRdW9W +YWRpcyBSb290IENBIDIwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCa +GMpLlA0ALa8DKYrwD4HIrkwZhR0In6spRIXzL4GtMh6QRr+jhiYaHv5+HBg6XJxg +Fyo6dIMzMH1hVBHL7avg5tKifvVrbxi3Cgst/ek+7wrGsxDp3MJGF/hd/aTa/55J +WpzmM+Yklvc/ulsrHHo1wtZn/qtmUIttKGAr79dgw8eTvI02kfN/+NsRE8Scd3bB +rrcCaoF6qUWD4gXmuVbBlDePSHFjIuwXZQeVikvfj8ZaCuWw419eaxGrDPmF60Tp ++ARz8un+XJiM9XOva7R+zdRcAitMOeGylZUtQofX1bOQQ7dsE/He3fbE+Ik/0XX1 +ksOR1YqI0JDs3G3eicJlcZaLDQP9nL9bFqyS2+r+eXyt66/3FsvbzSUr5R/7mp/i +Ucw6UwxI5g69ybR2BlLmEROFcmMDBOAENisgGQLodKcftslWZvB1JdxnwQ5hYIiz +PtGo/KPaHbDRsSNU30R2be1B2MGyIrZTHN81Hdyhdyox5C315eXbyOD/5YDXC2Og +/zOhD7osFRXql7PSorW+8oyWHhqPHWykYTe5hnMz15eWniN9gqRMgeKh0bpnX5UH +oycR7hYQe7xFSkyyBNKr79X9DFHOUGoIMfmR2gyPZFwDwzqLID9ujWc9Otb+fVuI +yV77zGHcizN300QyNQliBJIWENieJ0f7OyHj+OsdWwIDAQABo4GwMIGtMA8GA1Ud +EwEB/wQFMAMBAf8wCwYDVR0PBAQDAgEGMB0GA1UdDgQWBBQahGK8SEwzJQTU7tD2 +A8QZRtGUazBuBgNVHSMEZzBlgBQahGK8SEwzJQTU7tD2A8QZRtGUa6FJpEcwRTEL +MAkGA1UEBhMCQk0xGTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxGzAZBgNVBAMT +ElF1b1ZhZGlzIFJvb3QgQ0EgMoICBQkwDQYJKoZIhvcNAQEFBQADggIBAD4KFk2f +BluornFdLwUvZ+YTRYPENvbzwCYMDbVHZF34tHLJRqUDGCdViXh9duqWNIAXINzn +g/iN/Ae42l9NLmeyhP3ZRPx3UIHmfLTJDQtyU/h2BwdBR5YM++CCJpNVjP4iH2Bl +fF/nJrP3MpCYUNQ3cVX2kiF495V5+vgtJodmVjB3pjd4M1IQWK4/YY7yarHvGH5K +WWPKjaJW1acvvFYfzznB4vsKqBUsfU16Y8Zsl0Q80m/DShcK+JDSV6IZUaUtl0Ha +B0+pUNqQjZRG4T7wlP0QADj1O+hA4bRuVhogzG9Yje0uRY/W6ZM/57Es3zrWIozc +hLsib9D45MY56QSIPMO661V6bYCZJPVsAfv4l7CUW+v90m/xd2gNNWQjrLhVoQPR +TUIZ3Ph1WVaj+ahJefivDrkRoHy3au000LYmYjgahwz46P0u05B/B5EqHdZ+XIWD +mbA4CD/pXvk1B+TJYm5Xf6dQlfe6yJvmjqIBxdZmv3lh8zwc4bmCXF2gw+nYSL0Z +ohEUGW6yhhtoPkg3Goi3XZZenMfvJ2II4pEZXNLxId26F0KCl3GBUzGpn/Z9Yr9y +4aOTHcyKJloJONDO1w2AFrR4pTqHTI2KpdVGl/IsELm8VCLAAVBpQ570su9t+Oza +8eOx79+Rj1QqCyXBJhnEUhAFZdWCEOrCMc0u +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFYDCCA0igAwIBAgIURFc0JFuBiZs18s64KztbpybwdSgwDQYJKoZIhvcNAQEL +BQAwSDELMAkGA1UEBhMCQk0xGTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxHjAc +BgNVBAMTFVF1b1ZhZGlzIFJvb3QgQ0EgMiBHMzAeFw0xMjAxMTIxODU5MzJaFw00 +MjAxMTIxODU5MzJaMEgxCzAJBgNVBAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBM +aW1pdGVkMR4wHAYDVQQDExVRdW9WYWRpcyBSb290IENBIDIgRzMwggIiMA0GCSqG +SIb3DQEBAQUAA4ICDwAwggIKAoICAQChriWyARjcV4g/Ruv5r+LrI3HimtFhZiFf +qq8nUeVuGxbULX1QsFN3vXg6YOJkApt8hpvWGo6t/x8Vf9WVHhLL5hSEBMHfNrMW +n4rjyduYNM7YMxcoRvynyfDStNVNCXJJ+fKH46nafaF9a7I6JaltUkSs+L5u+9ym +c5GQYaYDFCDy54ejiK2toIz/pgslUiXnFgHVy7g1gQyjO/Dh4fxaXc6AcW34Sas+ +O7q414AB+6XrW7PFXmAqMaCvN+ggOp+oMiwMzAkd056OXbxMmO7FGmh77FOm6RQ1 +o9/NgJ8MSPsc9PG/Srj61YxxSscfrf5BmrODXfKEVu+lV0POKa2Mq1W/xPtbAd0j +IaFYAI7D0GoT7RPjEiuA3GfmlbLNHiJuKvhB1PLKFAeNilUSxmn1uIZoL1NesNKq +IcGY5jDjZ1XHm26sGahVpkUG0CM62+tlXSoREfA7T8pt9DTEceT/AFr2XK4jYIVz +8eQQsSWu1ZK7E8EM4DnatDlXtas1qnIhO4M15zHfeiFuuDIIfR0ykRVKYnLP43eh +vNURG3YBZwjgQQvD6xVu+KQZ2aKrr+InUlYrAoosFCT5v0ICvybIxo/gbjh9Uy3l +7ZizlWNof/k19N+IxWA1ksB8aRxhlRbQ694Lrz4EEEVlWFA4r0jyWbYW8jwNkALG +cC4BrTwV1wIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIB +BjAdBgNVHQ4EFgQU7edvdlq/YOxJW8ald7tyFnGbxD0wDQYJKoZIhvcNAQELBQAD +ggIBAJHfgD9DCX5xwvfrs4iP4VGyvD11+ShdyLyZm3tdquXK4Qr36LLTn91nMX66 +AarHakE7kNQIXLJgapDwyM4DYvmL7ftuKtwGTTwpD4kWilhMSA/ohGHqPHKmd+RC +roijQ1h5fq7KpVMNqT1wvSAZYaRsOPxDMuHBR//47PERIjKWnML2W2mWeyAMQ0Ga +W/ZZGYjeVYg3UQt4XAoeo0L9x52ID8DyeAIkVJOviYeIyUqAHerQbj5hLja7NQ4n +lv1mNDthcnPxFlxHBlRJAHpYErAK74X9sbgzdWqTHBLmYF5vHX/JHyPLhGGfHoJE ++V+tYlUkmlKY7VHnoX6XOuYvHxHaU4AshZ6rNRDbIl9qxV6XU/IyAgkwo1jwDQHV +csaxfGl7w/U2Rcxhbl5MlMVerugOXou/983g7aEOGzPuVBj+D77vfoRrQ+NwmNtd +dbINWQeFFSM51vHfqSYP1kjHs6Yi9TM3WpVHn3u6GBVv/9YUZINJ0gpnIdsPNWNg +KCLjsZWDzYWm3S8P52dSbrsvhXz1SnPnxT7AvSESBT/8twNJAlvIJebiVDj1eYeM +HVOyToV7BjjHLPj4sHKNJeV3UvQDHEimUF+IIDBu8oJDqz2XhOdT+yHBTw8imoa4 +WSr2Rz0ZiC3oheGe7IUIarFsNMkd7EgrO3jtZsSOeWmD3n+M +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIGnTCCBIWgAwIBAgICBcYwDQYJKoZIhvcNAQEFBQAwRTELMAkGA1UEBhMCQk0x +GTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxGzAZBgNVBAMTElF1b1ZhZGlzIFJv +b3QgQ0EgMzAeFw0wNjExMjQxOTExMjNaFw0zMTExMjQxOTA2NDRaMEUxCzAJBgNV +BAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBMaW1pdGVkMRswGQYDVQQDExJRdW9W +YWRpcyBSb290IENBIDMwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDM +V0IWVJzmmNPTTe7+7cefQzlKZbPoFog02w1ZkXTPkrgEQK0CSzGrvI2RaNggDhoB +4hp7Thdd4oq3P5kazethq8Jlph+3t723j/z9cI8LoGe+AaJZz3HmDyl2/7FWeUUr +H556VOijKTVopAFPD6QuN+8bv+OPEKhyq1hX51SGyMnzW9os2l2ObjyjPtr7guXd +8lyyBTNvijbO0BNO/79KDDRMpsMhvVAEVeuxu537RR5kFd5VAYwCdrXLoT9Cabwv +vWhDFlaJKjdhkf2mrk7AyxRllDdLkgbvBNDInIjbC3uBr7E9KsRlOni27tyAsdLT +mZw67mtaa7ONt9XOnMK+pUsvFrGeaDsGb659n/je7Mwpp5ijJUMv7/FfJuGITfhe +btfZFG4ZM2mnO4SJk8RTVROhUXhA+LjJou57ulJCg54U7QVSWllWp5f8nT8KKdjc +T5EOE7zelaTfi5m+rJsziO+1ga8bxiJTyPbH7pcUsMV8eFLI8M5ud2CEpukqdiDt +WAEXMJPpGovgc2PZapKUSU60rUqFxKMiMPwJ7Wgic6aIDFUhWMXhOp8q3crhkODZ +c6tsgLjoC2SToJyMGf+z0gzskSaHirOi4XCPLArlzW1oUevaPwV/izLmE1xr/l9A +4iLItLRkT9a6fUg+qGkM17uGcclzuD87nSVL2v9A6wIDAQABo4IBlTCCAZEwDwYD +VR0TAQH/BAUwAwEB/zCB4QYDVR0gBIHZMIHWMIHTBgkrBgEEAb5YAAMwgcUwgZMG +CCsGAQUFBwICMIGGGoGDQW55IHVzZSBvZiB0aGlzIENlcnRpZmljYXRlIGNvbnN0 +aXR1dGVzIGFjY2VwdGFuY2Ugb2YgdGhlIFF1b1ZhZGlzIFJvb3QgQ0EgMyBDZXJ0 +aWZpY2F0ZSBQb2xpY3kgLyBDZXJ0aWZpY2F0aW9uIFByYWN0aWNlIFN0YXRlbWVu +dC4wLQYIKwYBBQUHAgEWIWh0dHA6Ly93d3cucXVvdmFkaXNnbG9iYWwuY29tL2Nw +czALBgNVHQ8EBAMCAQYwHQYDVR0OBBYEFPLAE+CCQz777i9nMpY1XNu4ywLQMG4G +A1UdIwRnMGWAFPLAE+CCQz777i9nMpY1XNu4ywLQoUmkRzBFMQswCQYDVQQGEwJC +TTEZMBcGA1UEChMQUXVvVmFkaXMgTGltaXRlZDEbMBkGA1UEAxMSUXVvVmFkaXMg +Um9vdCBDQSAzggIFxjANBgkqhkiG9w0BAQUFAAOCAgEAT62gLEz6wPJv92ZVqyM0 +7ucp2sNbtrCD2dDQ4iH782CnO11gUyeim/YIIirnv6By5ZwkajGxkHon24QRiSem +d1o417+shvzuXYO8BsbRd2sPbSQvS3pspweWyuOEn62Iix2rFo1bZhfZFvSLgNLd ++LJ2w/w4E6oM3kJpK27zPOuAJ9v1pkQNn1pVWQvVDVJIxa6f8i+AxeoyUDUSly7B +4f/xI4hROJ/yZlZ25w9Rl6VSDE1JUZU2Pb+iSwwQHYaZTKrzchGT5Or2m9qoXadN +t54CrnMAyNojA+j56hl0YgCUyyIgvpSnWbWCar6ZeXqp8kokUvd0/bpO5qgdAm6x +DYBEwa7TIzdfu4V8K5Iu6H6li92Z4b8nby1dqnuH/grdS/yO9SbkbnBCbjPsMZ57 +k8HkyWkaPcBrTiJt7qtYTcbQQcEr6k8Sh17rRdhs9ZgC06DYVYoGmRmioHfRMJ6s +zHXug/WwYjnPbFfiTNKRCw51KBuav/0aQ/HKd/s7j2G4aSgWQgRecCocIdiP4b0j +Wy10QJLZYxkNc91pvGJHvOB0K7Lrfb5BG7XARsWhIstfTsEokt4YutUqKLsRixeT +mJlglFwjz1onl14LBQaTNx47aTbrqZ5hHY8y2o4M1nQ+ewkk2gF3R8Q7zTSMmfXK +4SVhM7JZG+Ju1zdXtg2pEto= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFYDCCA0igAwIBAgIULvWbAiin23r/1aOp7r0DoM8Sah0wDQYJKoZIhvcNAQEL +BQAwSDELMAkGA1UEBhMCQk0xGTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxHjAc +BgNVBAMTFVF1b1ZhZGlzIFJvb3QgQ0EgMyBHMzAeFw0xMjAxMTIyMDI2MzJaFw00 +MjAxMTIyMDI2MzJaMEgxCzAJBgNVBAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBM +aW1pdGVkMR4wHAYDVQQDExVRdW9WYWRpcyBSb290IENBIDMgRzMwggIiMA0GCSqG +SIb3DQEBAQUAA4ICDwAwggIKAoICAQCzyw4QZ47qFJenMioKVjZ/aEzHs286IxSR +/xl/pcqs7rN2nXrpixurazHb+gtTTK/FpRp5PIpM/6zfJd5O2YIyC0TeytuMrKNu +FoM7pmRLMon7FhY4futD4tN0SsJiCnMK3UmzV9KwCoWdcTzeo8vAMvMBOSBDGzXR +U7Ox7sWTaYI+FrUoRqHe6okJ7UO4BUaKhvVZR74bbwEhELn9qdIoyhA5CcoTNs+c +ra1AdHkrAj80//ogaX3T7mH1urPnMNA3I4ZyYUUpSFlob3emLoG+B01vr87ERROR +FHAGjx+f+IdpsQ7vw4kZ6+ocYfx6bIrc1gMLnia6Et3UVDmrJqMz6nWB2i3ND0/k +A9HvFZcba5DFApCTZgIhsUfei5pKgLlVj7WiL8DWM2fafsSntARE60f75li59wzw +eyuxwHApw0BiLTtIadwjPEjrewl5qW3aqDCYz4ByA4imW0aucnl8CAMhZa634Ryl +sSqiMd5mBPfAdOhx3v89WcyWJhKLhZVXGqtrdQtEPREoPHtht+KPZ0/l7DxMYIBp +VzgeAVuNVejH38DMdyM0SXV89pgR6y3e7UEuFAUCf+D+IOs15xGsIs5XPd7JMG0Q +A4XN8f+MFrXBsj6IbGB/kE+V9/YtrQE5BwT6dYB9v0lQ7e/JxHwc64B+27bQ3RP+ +ydOc17KXqQIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIB +BjAdBgNVHQ4EFgQUxhfQvKjqAkPyGwaZXSuQILnXnOQwDQYJKoZIhvcNAQELBQAD +ggIBADRh2Va1EodVTd2jNTFGu6QHcrxfYWLopfsLN7E8trP6KZ1/AvWkyaiTt3px +KGmPc+FSkNrVvjrlt3ZqVoAh313m6Tqe5T72omnHKgqwGEfcIHB9UqM+WXzBusnI +FUBhynLWcKzSt/Ac5IYp8M7vaGPQtSCKFWGafoaYtMnCdvvMujAWzKNhxnQT5Wvv +oxXqA/4Ti2Tk08HS6IT7SdEQTXlm66r99I0xHnAUrdzeZxNMgRVhvLfZkXdxGYFg +u/BYpbWcC/ePIlUnwEsBbTuZDdQdm2NnL9DuDcpmvJRPpq3t/O5jrFc/ZSXPsoaP +0Aj/uHYUbt7lJ+yreLVTubY/6CD50qi+YUbKh4yE8/nxoGibIh6BJpsQBJFxwAYf +3KDTuVan45gtf4Od34wrnDKOMpTwATwiKp9Dwi7DmDkHOHv8XgBCH/MyJnmDhPbl +8MFREsALHgQjDFSlTC9JxUrRtm5gDWv8a4uFJGS3iQ6rJUdbPM9+Sb3H6QrG2vd+ +DhcI00iX0HGS8A85PjRqHH3Y8iKuu2n0M7SmSFXRDw4m6Oy2Cy2nhTXN/VnIn9HN +PlopNLk9hM6xZdRZkZFWdSHBd575euFgndOtBBj0fOtek49TSiIp+EgrPk2GrFt/ +ywaZWWDYWGWVjUTR939+J399roD1B0y2PpxxVJkES/1Y+Zj0 +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIClDCCAhqgAwIBAgIILCmcWxbtBZUwCgYIKoZIzj0EAwIwfzELMAkGA1UEBhMC +VVMxDjAMBgNVBAgMBVRleGFzMRAwDgYDVQQHDAdIb3VzdG9uMRgwFgYDVQQKDA9T +U0wgQ29ycG9yYXRpb24xNDAyBgNVBAMMK1NTTC5jb20gRVYgUm9vdCBDZXJ0aWZp +Y2F0aW9uIEF1dGhvcml0eSBFQ0MwHhcNMTYwMjEyMTgxNTIzWhcNNDEwMjEyMTgx +NTIzWjB/MQswCQYDVQQGEwJVUzEOMAwGA1UECAwFVGV4YXMxEDAOBgNVBAcMB0hv +dXN0b24xGDAWBgNVBAoMD1NTTCBDb3Jwb3JhdGlvbjE0MDIGA1UEAwwrU1NMLmNv +bSBFViBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IEVDQzB2MBAGByqGSM49 +AgEGBSuBBAAiA2IABKoSR5CYG/vvw0AHgyBO8TCCogbR8pKGYfL2IWjKAMTH6kMA +VIbc/R/fALhBYlzccBYy3h+Z1MzFB8gIH2EWB1E9fVwHU+M1OIzfzZ/ZLg1Kthku +WnBaBu2+8KGwytAJKaNjMGEwHQYDVR0OBBYEFFvKXuXe0oGqzagtZFG22XKbl+ZP +MA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAUW8pe5d7SgarNqC1kUbbZcpuX +5k8wDgYDVR0PAQH/BAQDAgGGMAoGCCqGSM49BAMCA2gAMGUCMQCK5kCJN+vp1RPZ +ytRrJPOwPYdGWBrssd9v+1a6cGvHOMzosYxPD/fxZ3YOg9AeUY8CMD32IygmTMZg +h5Mmm7I1HrrW9zzRHM76JTymGoEVW/MSD2zuZYrJh6j5B+BimoxcSg== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIF6zCCA9OgAwIBAgIIVrYpzTS8ePYwDQYJKoZIhvcNAQELBQAwgYIxCzAJBgNV +BAYTAlVTMQ4wDAYDVQQIDAVUZXhhczEQMA4GA1UEBwwHSG91c3RvbjEYMBYGA1UE +CgwPU1NMIENvcnBvcmF0aW9uMTcwNQYDVQQDDC5TU0wuY29tIEVWIFJvb3QgQ2Vy +dGlmaWNhdGlvbiBBdXRob3JpdHkgUlNBIFIyMB4XDTE3MDUzMTE4MTQzN1oXDTQy +MDUzMDE4MTQzN1owgYIxCzAJBgNVBAYTAlVTMQ4wDAYDVQQIDAVUZXhhczEQMA4G +A1UEBwwHSG91c3RvbjEYMBYGA1UECgwPU1NMIENvcnBvcmF0aW9uMTcwNQYDVQQD +DC5TU0wuY29tIEVWIFJvb3QgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkgUlNBIFIy +MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAjzZlQOHWTcDXtOlG2mvq +M0fNTPl9fb69LT3w23jhhqXZuglXaO1XPqDQCEGD5yhBJB/jchXQARr7XnAjssuf +OePPxU7Gkm0mxnu7s9onnQqG6YE3Bf7wcXHswxzpY6IXFJ3vG2fThVUCAtZJycxa +4bH3bzKfydQ7iEGonL3Lq9ttewkfokxykNorCPzPPFTOZw+oz12WGQvE43LrrdF9 +HSfvkusQv1vrO6/PgN3B0pYEW3p+pKk8OHakYo6gOV7qd89dAFmPZiw+B6KjBSYR +aZfqhbcPlgtLyEDhULouisv3D5oi53+aNxPN8k0TayHRwMwi8qFG9kRpnMphNQcA +b9ZhCBHqurj26bNg5U257J8UZslXWNvNh2n4ioYSA0e/ZhN2rHd9NCSFg83XqpyQ +Gp8hLH94t2S42Oim9HizVcuE0jLEeK6jj2HdzghTreyI/BXkmg3mnxp3zkyPuBQV +PWKchjgGAGYS5Fl2WlPAApiiECtoRHuOec4zSnaqW4EWG7WK2NAAe15itAnWhmMO +pgWVSbooi4iTsjQc2KRVbrcc0N6ZVTsj9CLg+SlmJuwgUHfbSguPvuUCYHBBXtSu +UDkiFCbLsjtzdFVHB3mBOagwE0TlBIqulhMlQg+5U8Sb/M3kHN48+qvWBkofZ6aY +MBzdLNvcGJVXZsb/XItW9XcCAwEAAaNjMGEwDwYDVR0TAQH/BAUwAwEB/zAfBgNV +HSMEGDAWgBT5YLvU49U09rj1BoAlp3PbRmmonjAdBgNVHQ4EFgQU+WC71OPVNPa4 +9QaAJadz20ZpqJ4wDgYDVR0PAQH/BAQDAgGGMA0GCSqGSIb3DQEBCwUAA4ICAQBW +s47LCp1Jjr+kxJG7ZhcFUZh1++VQLHqe8RT6q9OKPv+RKY9ji9i0qVQBDb6Thi/5 +Sm3HXvVX+cpVHBK+Rw82xd9qt9t1wkclf7nxY/hoLVUE0fKNsKTPvDxeH3jnpaAg +cLAExbf3cqfeIg29MyVGjGSSJuM+LmOW2puMPfgYCdcDzH2GguDKBAdRUNf/ktUM +79qGn5nX67evaOI5JpS6aLe/g9Pqemc9YmeuJeVy6OLk7K4S9ksrPJ/psEDzOFSz +/bdoyNrGj1E8svuR3Bznm53htw1yj+KkxKl4+esUrMZDBcJlOSgYAsOCsp0FvmXt +ll9ldDz7CTUue5wT/RsPXcdtgTpWD8w74a8CLyKsRspGPKAcTNZEtF4uXBVmCeEm +Kf7GUmG6sXP/wwyc5WxqlD8UykAWlYTzWamsX0xhk23RO8yilQwipmdnRC652dKK +QbNmC1r7fSOl8hqw/96bg5Qu0T/fkreRrwU7ZcegbLHNYhLDkBvjJc40vG93drEQ +w/cFGsDWr3RiSBd3kmmQYRzelYB0VI8YHMPzA9C/pEN1hlMYegouCRw2n5H9gooi +S9EOUCXdywMMF8mDAAhONU2Ki+3wApRmLER/y5UnlhetCTCstnEXbosX9hwJ1C07 +mKVx01QT2WDz9UtmT/rx7iASjbSsV7FFY6GsdqnC+w== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICjTCCAhSgAwIBAgIIdebfy8FoW6gwCgYIKoZIzj0EAwIwfDELMAkGA1UEBhMC +VVMxDjAMBgNVBAgMBVRleGFzMRAwDgYDVQQHDAdIb3VzdG9uMRgwFgYDVQQKDA9T +U0wgQ29ycG9yYXRpb24xMTAvBgNVBAMMKFNTTC5jb20gUm9vdCBDZXJ0aWZpY2F0 +aW9uIEF1dGhvcml0eSBFQ0MwHhcNMTYwMjEyMTgxNDAzWhcNNDEwMjEyMTgxNDAz +WjB8MQswCQYDVQQGEwJVUzEOMAwGA1UECAwFVGV4YXMxEDAOBgNVBAcMB0hvdXN0 +b24xGDAWBgNVBAoMD1NTTCBDb3Jwb3JhdGlvbjExMC8GA1UEAwwoU1NMLmNvbSBS +b290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IEVDQzB2MBAGByqGSM49AgEGBSuB +BAAiA2IABEVuqVDEpiM2nl8ojRfLliJkP9x6jh3MCLOicSS6jkm5BBtHllirLZXI +7Z4INcgn64mMU1jrYor+8FsPazFSY0E7ic3s7LaNGdM0B9y7xgZ/wkWV7Mt/qCPg +CemB+vNH06NjMGEwHQYDVR0OBBYEFILRhXMw5zUE044CkvvlpNHEIejNMA8GA1Ud +EwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAUgtGFczDnNQTTjgKS++Wk0cQh6M0wDgYD +VR0PAQH/BAQDAgGGMAoGCCqGSM49BAMCA2cAMGQCMG/n61kRpGDPYbCWe+0F+S8T +kdzt5fxQaxFGRrMcIQBiu77D5+jNB5n5DQtdcj7EqgIwH7y6C+IwJPt8bYBVCpk+ +gA0z5Wajs6O7pdWLjwkspl1+4vAHCGht0nxpbl/f5Wpl +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIF3TCCA8WgAwIBAgIIeyyb0xaAMpkwDQYJKoZIhvcNAQELBQAwfDELMAkGA1UE +BhMCVVMxDjAMBgNVBAgMBVRleGFzMRAwDgYDVQQHDAdIb3VzdG9uMRgwFgYDVQQK +DA9TU0wgQ29ycG9yYXRpb24xMTAvBgNVBAMMKFNTTC5jb20gUm9vdCBDZXJ0aWZp +Y2F0aW9uIEF1dGhvcml0eSBSU0EwHhcNMTYwMjEyMTczOTM5WhcNNDEwMjEyMTcz +OTM5WjB8MQswCQYDVQQGEwJVUzEOMAwGA1UECAwFVGV4YXMxEDAOBgNVBAcMB0hv +dXN0b24xGDAWBgNVBAoMD1NTTCBDb3Jwb3JhdGlvbjExMC8GA1UEAwwoU1NMLmNv +bSBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IFJTQTCCAiIwDQYJKoZIhvcN +AQEBBQADggIPADCCAgoCggIBAPkP3aMrfcvQKv7sZ4Wm5y4bunfh4/WvpOz6Sl2R +xFdHaxh3a3by/ZPkPQ/CFp4LZsNWlJ4Xg4XOVu/yFv0AYvUiCVToZRdOQbngT0aX +qhvIuG5iXmmxX9sqAn78bMrzQdjt0Oj8P2FI7bADFB0QDksZ4LtO7IZl/zbzXmcC +C52GVWH9ejjt/uIZALdvoVBidXQ8oPrIJZK0bnoix/geoeOy3ZExqysdBP+lSgQ3 +6YWkMyv94tZVNHwZpEpox7Ko07fKoZOI68GXvIz5HdkihCR0xwQ9aqkpk8zruFvh +/l8lqjRYyMEjVJ0bmBHDOJx+PYZspQ9AhnwC9FwCTyjLrnGfDzrIM/4RJTXq/LrF +YD3ZfBjVsqnTdXgDciLKOsMf7yzlLqn6niy2UUb9rwPW6mBo6oUWNmuF6R7As93E +JNyAKoFBbZQ+yODJgUEAnl6/f8UImKIYLEJAs/lvOCdLToD0PYFH4Ih86hzOtXVc +US4cK38acijnALXRdMbX5J+tB5O2UzU1/Dfkw/ZdFr4hc96SCvigY2q8lpJqPvi8 +ZVWb3vUNiSYE/CUapiVpy8JtynziWV+XrOvvLsi81xtZPCvM8hnIk2snYxnP/Okm ++Mpxm3+T/jRnhE6Z6/yzeAkzcLpmpnbtG3PrGqUNxCITIJRWCk4sbE6x/c+cCbqi +M+2HAgMBAAGjYzBhMB0GA1UdDgQWBBTdBAkHovV6fVJTEpKV7jiAJQ2mWTAPBgNV +HRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFN0ECQei9Xp9UlMSkpXuOIAlDaZZMA4G +A1UdDwEB/wQEAwIBhjANBgkqhkiG9w0BAQsFAAOCAgEAIBgRlCn7Jp0cHh5wYfGV +cpNxJK1ok1iOMq8bs3AD/CUrdIWQPXhq9LmLpZc7tRiRux6n+UBbkflVma8eEdBc +Hadm47GUBwwyOabqG7B52B2ccETjit3E+ZUfijhDPwGFpUenPUayvOUiaPd7nNgs +PgohyC0zrL/FgZkxdMF1ccW+sfAjRfSda/wZY52jvATGGAslu1OJD7OAUN5F7kR/ +q5R4ZJjT9ijdh9hwZXT7DrkT66cPYakylszeu+1jTBi7qUD3oFRuIIhxdRjqerQ0 +cuAjJ3dctpDqhiVAq+8zD8ufgr6iIPv2tS0a5sKFsXQP+8hlAqRSAUfdSSLBv9jr +a6x+3uxjMxW3IwiPxg+NQVrdjsW5j+VFP3jbutIbQLH+cU0/4IGiul607BXgk90I +H37hVZkLId6Tngr75qNJvTYw/ud3sqB1l7UtgYgXZSD32pAAn8lSzDLKNXz1PQ/Y +K9f1JmzJBjSWFupwWRoyeXkLtoh/D1JIPb9s2KJELtFOt3JY04kTlf5Eq/jXixtu +nLwsoFvVagCvXzfh1foQC5ichucmj87w7G6KVwuA406ywKBjYZC6VWg3dGq2ktuf +oYYitmUnDuy2n0Jg5GfCtdpBC8TTi2EbvPofkSvXRAdeuims2cXp71NIWuuA8ShY +Ic2wBlX7Jz9TkHCpBB5XJ7k= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIDcjCCAlqgAwIBAgIUPopdB+xV0jLVt+O2XwHrLdzk1uQwDQYJKoZIhvcNAQEL +BQAwUTELMAkGA1UEBhMCUEwxKDAmBgNVBAoMH0tyYWpvd2EgSXpiYSBSb3psaWN6 +ZW5pb3dhIFMuQS4xGDAWBgNVBAMMD1NaQUZJUiBST09UIENBMjAeFw0xNTEwMTkw +NzQzMzBaFw0zNTEwMTkwNzQzMzBaMFExCzAJBgNVBAYTAlBMMSgwJgYDVQQKDB9L +cmFqb3dhIEl6YmEgUm96bGljemVuaW93YSBTLkEuMRgwFgYDVQQDDA9TWkFGSVIg +Uk9PVCBDQTIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC3vD5QqEvN +QLXOYeeWyrSh2gwisPq1e3YAd4wLz32ohswmUeQgPYUM1ljj5/QqGJ3a0a4m7utT +3PSQ1hNKDJA8w/Ta0o4NkjrcsbH/ON7Dui1fgLkCvUqdGw+0w8LBZwPd3BucPbOw +3gAeqDRHu5rr/gsUvTaE2g0gv/pby6kWIK05YO4vdbbnl5z5Pv1+TW9NL++IDWr6 +3fE9biCloBK0TXC5ztdyO4mTp4CEHCdJckm1/zuVnsHMyAHs6A6KCpbns6aH5db5 +BSsNl0BwPLqsdVqc1U2dAgrSS5tmS0YHF2Wtn2yIANwiieDhZNRnvDF5YTy7ykHN +XGoAyDw4jlivAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQD +AgEGMB0GA1UdDgQWBBQuFqlKGLXLzPVvUPMjX/hd56zwyDANBgkqhkiG9w0BAQsF +AAOCAQEAtXP4A9xZWx126aMqe5Aosk3AM0+qmrHUuOQn/6mWmc5G4G18TKI4pAZw +8PRBEew/R40/cof5O/2kbytTAOD/OblqBw7rHRz2onKQy4I9EYKL0rufKq8h5mOG +nXkZ7/e7DDWQw4rtTw/1zBLZpD67oPwglV9PJi8RI4NOdQcPv5vRtB3pEAT+ymCP +oky4rc/hkA/NrgrHXXu3UNLUYfrVFdvXn4dRVOul4+vJhaAlIDf7js4MNIThPIGy +d05DpYhfhmehPea0XGG2Ptv+tyjFogeutcrKjSoS75ftwjCkySp6+/NNIxuZMzSg +LvWpCz/UXeHPhJ/iGcJfitYgHuNztw== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIDbTCCAlWgAwIBAgIBATANBgkqhkiG9w0BAQUFADBYMQswCQYDVQQGEwJKUDEr +MCkGA1UEChMiSmFwYW4gQ2VydGlmaWNhdGlvbiBTZXJ2aWNlcywgSW5jLjEcMBoG +A1UEAxMTU2VjdXJlU2lnbiBSb290Q0ExMTAeFw0wOTA0MDgwNDU2NDdaFw0yOTA0 +MDgwNDU2NDdaMFgxCzAJBgNVBAYTAkpQMSswKQYDVQQKEyJKYXBhbiBDZXJ0aWZp +Y2F0aW9uIFNlcnZpY2VzLCBJbmMuMRwwGgYDVQQDExNTZWN1cmVTaWduIFJvb3RD +QTExMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA/XeqpRyQBTvLTJsz +i1oURaTnkBbR31fSIRCkF/3frNYfp+TbfPfs37gD2pRY/V1yfIw/XwFndBWW4wI8 +h9uuywGOwvNmxoVF9ALGOrVisq/6nL+k5tSAMJjzDbaTj6nU2DbysPyKyiyhFTOV +MdrAG/LuYpmGYz+/3ZMqg6h2uRMft85OQoWPIucuGvKVCbIFtUROd6EgvanyTgp9 +UK31BQ1FT0Zx/Sg+U/sE2C3XZR1KG/rPO7AxmjVuyIsG0wCR8pQIZUyxNAYAeoni +8McDWc/V1uinMrPmmECGxc0nEovMe863ETxiYAcjPitAbpSACW22s293bzUIUPsC +h8U+iQIDAQABo0IwQDAdBgNVHQ4EFgQUW/hNT7KlhtQ60vFjmqC+CfZXt94wDgYD +VR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEB +AKChOBZmLqdWHyGcBvod7bkixTgm2E5P7KN/ed5GIaGHd48HCJqypMWvDzKYC3xm +KbabfSVSSUOrTC4rbnpwrxYO4wJs+0LmGJ1F2FXI6Dvd5+H0LgscNFxsWEr7jIhQ +X5Ucv+2rIrVls4W6ng+4reV6G4pQOh29Dbx7VFALuUKvVaAYga1lme++5Jy/xIWr +QbJUb9wlze144o4MjQlJ3WN7WmmWAiGovVJZ6X01y8hSyn+B/tlr0/cR7SXf+Of5 +pPpyl4RTDaXQMhhRdlkUbA/r7F+AjHVDg8OFmP9Mni0N5HeDk061lgeLKBObjBmN +QSdJQO7e5iNEOdyhIta6A/I= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIDuDCCAqCgAwIBAgIQDPCOXAgWpa1Cf/DrJxhZ0DANBgkqhkiG9w0BAQUFADBI +MQswCQYDVQQGEwJVUzEgMB4GA1UEChMXU2VjdXJlVHJ1c3QgQ29ycG9yYXRpb24x +FzAVBgNVBAMTDlNlY3VyZVRydXN0IENBMB4XDTA2MTEwNzE5MzExOFoXDTI5MTIz +MTE5NDA1NVowSDELMAkGA1UEBhMCVVMxIDAeBgNVBAoTF1NlY3VyZVRydXN0IENv +cnBvcmF0aW9uMRcwFQYDVQQDEw5TZWN1cmVUcnVzdCBDQTCCASIwDQYJKoZIhvcN +AQEBBQADggEPADCCAQoCggEBAKukgeWVzfX2FI7CT8rU4niVWJxB4Q2ZQCQXOZEz +Zum+4YOvYlyJ0fwkW2Gz4BERQRwdbvC4u/jep4G6pkjGnx29vo6pQT64lO0pGtSO +0gMdA+9tDWccV9cGrcrI9f4Or2YlSASWC12juhbDCE/RRvgUXPLIXgGZbf2IzIao +wW8xQmxSPmjL8xk037uHGFaAJsTQ3MBv396gwpEWoGQRS0S8Hvbn+mPeZqx2pHGj +7DaUaHp3pLHnDi+BeuK1cobvomuL8A/b01k/unK8RCSc43Oz969XL0Imnal0ugBS +8kvNU3xHCzaFDmapCJcWNFfBZveA4+1wVMeT4C4oFVmHursCAwEAAaOBnTCBmjAT +BgkrBgEEAYI3FAIEBh4EAEMAQTALBgNVHQ8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB +/zAdBgNVHQ4EFgQUQjK2FvoE/f5dS3rD/fdMQB1aQ68wNAYDVR0fBC0wKzApoCeg +JYYjaHR0cDovL2NybC5zZWN1cmV0cnVzdC5jb20vU1RDQS5jcmwwEAYJKwYBBAGC +NxUBBAMCAQAwDQYJKoZIhvcNAQEFBQADggEBADDtT0rhWDpSclu1pqNlGKa7UTt3 +6Z3q059c4EVlew3KW+JwULKUBRSuSceNQQcSc5R+DCMh/bwQf2AQWnL1mA6s7Ll/ +3XpvXdMc9P+IBWlCqQVxyLesJugutIxq/3HcuLHfmbx8IVQr5Fiiu1cprp6poxkm +D5kuCLDv/WnPmRoJjeOnnyvJNjR7JLN4TJUXpAYmHrZkUjZfYGfZnMUFdAvnZyPS +CPyI6a6Lf+Ew9Dd+/cYy2i2eRDAwbO4H3tI0/NL/QPZL9GZGBlSm8jIKYyYwa5vR +3ItHuuG51WLQoqD0ZwV4KWMabwTW+MZMo5qxN7SN5ShLHZ4swrhovO0C7jE= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIDvDCCAqSgAwIBAgIQB1YipOjUiolN9BPI8PjqpTANBgkqhkiG9w0BAQUFADBK +MQswCQYDVQQGEwJVUzEgMB4GA1UEChMXU2VjdXJlVHJ1c3QgQ29ycG9yYXRpb24x +GTAXBgNVBAMTEFNlY3VyZSBHbG9iYWwgQ0EwHhcNMDYxMTA3MTk0MjI4WhcNMjkx +MjMxMTk1MjA2WjBKMQswCQYDVQQGEwJVUzEgMB4GA1UEChMXU2VjdXJlVHJ1c3Qg +Q29ycG9yYXRpb24xGTAXBgNVBAMTEFNlY3VyZSBHbG9iYWwgQ0EwggEiMA0GCSqG +SIb3DQEBAQUAA4IBDwAwggEKAoIBAQCvNS7YrGxVaQZx5RNoJLNP2MwhR/jxYDiJ +iQPpvepeRlMJ3Fz1Wuj3RSoC6zFh1ykzTM7HfAo3fg+6MpjhHZevj8fcyTiW89sa +/FHtaMbQbqR8JNGuQsiWUGMu4P51/pinX0kuleM5M2SOHqRfkNJnPLLZ/kG5VacJ +jnIFHovdRIWCQtBJwB1g8NEXLJXr9qXBkqPFwqcIYA1gBBCWeZ4WNOaptvolRTnI +HmX5k/Wq8VLcmZg9pYYaDDUz+kulBAYVHDGA76oYa8J719rO+TMg1fW9ajMtgQT7 +sFzUnKPiXB3jqUJ1XnvUd+85VLrJChgbEplJL4hL/VBi0XPnj3pDAgMBAAGjgZ0w +gZowEwYJKwYBBAGCNxQCBAYeBABDAEEwCwYDVR0PBAQDAgGGMA8GA1UdEwEB/wQF +MAMBAf8wHQYDVR0OBBYEFK9EBMJBfkiD2045AuzshHrmzsmkMDQGA1UdHwQtMCsw +KaAnoCWGI2h0dHA6Ly9jcmwuc2VjdXJldHJ1c3QuY29tL1NHQ0EuY3JsMBAGCSsG +AQQBgjcVAQQDAgEAMA0GCSqGSIb3DQEBBQUAA4IBAQBjGghAfaReUw132HquHw0L +URYD7xh8yOOvaliTFGCRsoTciE6+OYo68+aCiV0BN7OrJKQVDpI1WkpEXk5X+nXO +H0jOZvQ8QCaSmGwb7iRGDBezUqXbpZGRzzfTb+cnCDpOGR86p1hcF895P4vkp9Mm +I50mD1hp/Ed+stCNi5O/KU9DaXR2Z0vPB4zmAve14bRDtUstFJ/53CYNv6ZHdAbY +iNE6KTCEztI5gGIbqMdXSbxqVVFnFUq+NQfk1XWYN3kwFNspnWzFacxHVaIw98xc +f8LDmBxrThaA63p4ZUWiABqvDA1VZDRIuJK58bRQKfJPIx/abKwfROHdI3hRW8cW +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIDdzCCAl+gAwIBAgIBADANBgkqhkiG9w0BAQsFADBdMQswCQYDVQQGEwJKUDEl +MCMGA1UEChMcU0VDT00gVHJ1c3QgU3lzdGVtcyBDTy4sTFRELjEnMCUGA1UECxMe +U2VjdXJpdHkgQ29tbXVuaWNhdGlvbiBSb290Q0EyMB4XDTA5MDUyOTA1MDAzOVoX +DTI5MDUyOTA1MDAzOVowXTELMAkGA1UEBhMCSlAxJTAjBgNVBAoTHFNFQ09NIFRy +dXN0IFN5c3RlbXMgQ08uLExURC4xJzAlBgNVBAsTHlNlY3VyaXR5IENvbW11bmlj +YXRpb24gUm9vdENBMjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANAV +OVKxUrO6xVmCxF1SrjpDZYBLx/KWvNs2l9amZIyoXvDjChz335c9S672XewhtUGr +zbl+dp+++T42NKA7wfYxEUV0kz1XgMX5iZnK5atq1LXaQZAQwdbWQonCv/Q4EpVM +VAX3NuRFg3sUZdbcDE3R3n4MqzvEFb46VqZab3ZpUql6ucjrappdUtAtCms1FgkQ +hNBqyjoGADdH5H5XTz+L62e4iKrFvlNVspHEfbmwhRkGeC7bYRr6hfVKkaHnFtWO +ojnflLhwHyg/i/xAXmODPIMqGplrz95Zajv8bxbXH/1KEOtOghY6rCcMU/Gt1SSw +awNQwS08Ft1ENCcadfsCAwEAAaNCMEAwHQYDVR0OBBYEFAqFqXdlBZh8QIH4D5cs +OPEK7DzPMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3 +DQEBCwUAA4IBAQBMOqNErLlFsceTfsgLCkLfZOoc7llsCLqJX2rKSpWeeo8HxdpF +coJxDjrSzG+ntKEju/Ykn8sX/oymzsLS28yN/HH8AynBbF0zX2S2ZTuJbxh2ePXc +okgfGT+Ok+vx+hfuzU7jBBJV1uXk3fs+BXziHV7Gp7yXT2g69ekuCkO2r1dcYmh8 +t/2jioSgrGK+KwmHNPBqAbubKVY8/gA3zyNs8U6qtnRGEmyR7jTV7JqR50S+kDFy +1UkC9gLl9B/rfNmWVan/7Ir5mUf/NVoCqgTLiluHcSmRvaS0eg29mvVXIwAHIRc/ +SjnRBUkLp7Y3gaVdjKozXoEofKd9J+sAro03 +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIDWjCCAkKgAwIBAgIBADANBgkqhkiG9w0BAQUFADBQMQswCQYDVQQGEwJKUDEY +MBYGA1UEChMPU0VDT00gVHJ1c3QubmV0MScwJQYDVQQLEx5TZWN1cml0eSBDb21t +dW5pY2F0aW9uIFJvb3RDQTEwHhcNMDMwOTMwMDQyMDQ5WhcNMjMwOTMwMDQyMDQ5 +WjBQMQswCQYDVQQGEwJKUDEYMBYGA1UEChMPU0VDT00gVHJ1c3QubmV0MScwJQYD +VQQLEx5TZWN1cml0eSBDb21tdW5pY2F0aW9uIFJvb3RDQTEwggEiMA0GCSqGSIb3 +DQEBAQUAA4IBDwAwggEKAoIBAQCzs/5/022x7xZ8V6UMbXaKL0u/ZPtM7orw8yl8 +9f/uKuDp6bpbZCKamm8sOiZpUQWZJtzVHGpxxpp9Hp3dfGzGjGdnSj74cbAZJ6kJ +DKaVv0uMDPpVmDvY6CKhS3E4eayXkmmziX7qIWgGmBSWh9JhNrxtJ1aeV+7AwFb9 +Ms+k2Y7CI9eNqPPYJayX5HA49LY6tJ07lyZDo6G8SVlyTCMwhwFY9k6+HGhWZq/N +QV3Is00qVUarH9oe4kA92819uZKAnDfdDJZkndwi92SL32HeFZRSFaB9UslLqCHJ +xrHty8OVYNEP8Ktw+N/LTX7s1vqr2b1/VPKl6Xn62dZ2JChzAgMBAAGjPzA9MB0G +A1UdDgQWBBSgc0mZaNyFW2XjmygvV5+9M7wHSDALBgNVHQ8EBAMCAQYwDwYDVR0T +AQH/BAUwAwEB/zANBgkqhkiG9w0BAQUFAAOCAQEAaECpqLvkT115swW1F7NgE+vG +kl3g0dNq/vu+m22/xwVtWSDEHPC32oRYAmP6SBbvT6UL90qY8j+eG61Ha2POCEfr +Uj94nK9NrvjVT8+amCoQQTlSxN3Zmw7vkwGusi7KaEIkQmywszo+zenaSMQVy+n5 +Bw+SUEmK3TGXX8npN6o7WWWXlDLJs58+OmJYxUmtYg5xpTKqL8aJdkNAExNnPaJU +JRDL8Try2frbSVa7pv6nQTXD4IhhyYjH3zYQIphZ6rBK+1YWc26sTfcioU+tHXot +RSflMMFe8toTyyVCUZVHA4xsIcx0Qu1T/zOLjw9XARYvz6buyXAiFL39vmwLAw== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIEDzCCAvegAwIBAgIBADANBgkqhkiG9w0BAQUFADBoMQswCQYDVQQGEwJVUzEl +MCMGA1UEChMcU3RhcmZpZWxkIFRlY2hub2xvZ2llcywgSW5jLjEyMDAGA1UECxMp +U3RhcmZpZWxkIENsYXNzIDIgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMDQw +NjI5MTczOTE2WhcNMzQwNjI5MTczOTE2WjBoMQswCQYDVQQGEwJVUzElMCMGA1UE +ChMcU3RhcmZpZWxkIFRlY2hub2xvZ2llcywgSW5jLjEyMDAGA1UECxMpU3RhcmZp +ZWxkIENsYXNzIDIgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwggEgMA0GCSqGSIb3 +DQEBAQUAA4IBDQAwggEIAoIBAQC3Msj+6XGmBIWtDBFk385N78gDGIc/oav7PKaf +8MOh2tTYbitTkPskpD6E8J7oX+zlJ0T1KKY/e97gKvDIr1MvnsoFAZMej2YcOadN ++lq2cwQlZut3f+dZxkqZJRRU6ybH838Z1TBwj6+wRir/resp7defqgSHo9T5iaU0 +X9tDkYI22WY8sbi5gv2cOj4QyDvvBmVmepsZGD3/cVE8MC5fvj13c7JdBmzDI1aa +K4UmkhynArPkPw2vCHmCuDY96pzTNbO8acr1zJ3o/WSNF4Azbl5KXZnJHoe0nRrA +1W4TNSNe35tfPe/W93bC6j67eA0cQmdrBNj41tpvi/JEoAGrAgEDo4HFMIHCMB0G +A1UdDgQWBBS/X7fRzt0fhvRbVazc1xDCDqmI5zCBkgYDVR0jBIGKMIGHgBS/X7fR +zt0fhvRbVazc1xDCDqmI56FspGowaDELMAkGA1UEBhMCVVMxJTAjBgNVBAoTHFN0 +YXJmaWVsZCBUZWNobm9sb2dpZXMsIEluYy4xMjAwBgNVBAsTKVN0YXJmaWVsZCBD +bGFzcyAyIENlcnRpZmljYXRpb24gQXV0aG9yaXR5ggEAMAwGA1UdEwQFMAMBAf8w +DQYJKoZIhvcNAQEFBQADggEBAAWdP4id0ckaVaGsafPzWdqbAYcaT1epoXkJKtv3 +L7IezMdeatiDh6GX70k1PncGQVhiv45YuApnP+yz3SFmH8lU+nLMPUxA2IGvd56D +eruix/U0F47ZEUD0/CwqTRV/p2JdLiXTAAsgGh1o+Re49L2L7ShZ3U0WixeDyLJl +xy16paq8U4Zt3VekyvggQQto8PT7dL5WXXp59fkdheMtlb71cZBDzI0fmgAKhynp +VSJYACPq4xJDKVtHCN2MQWplBqjlIapBtJUhlbl90TSrE9atvNziPTnNvT51cKEY +WQPJIrSPnNVeKtelttQKbfi3QBFGmh95DmK/D5fs4C8fF5Q= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIID3TCCAsWgAwIBAgIBADANBgkqhkiG9w0BAQsFADCBjzELMAkGA1UEBhMCVVMx +EDAOBgNVBAgTB0FyaXpvbmExEzARBgNVBAcTClNjb3R0c2RhbGUxJTAjBgNVBAoT +HFN0YXJmaWVsZCBUZWNobm9sb2dpZXMsIEluYy4xMjAwBgNVBAMTKVN0YXJmaWVs +ZCBSb290IENlcnRpZmljYXRlIEF1dGhvcml0eSAtIEcyMB4XDTA5MDkwMTAwMDAw +MFoXDTM3MTIzMTIzNTk1OVowgY8xCzAJBgNVBAYTAlVTMRAwDgYDVQQIEwdBcml6 +b25hMRMwEQYDVQQHEwpTY290dHNkYWxlMSUwIwYDVQQKExxTdGFyZmllbGQgVGVj +aG5vbG9naWVzLCBJbmMuMTIwMAYDVQQDEylTdGFyZmllbGQgUm9vdCBDZXJ0aWZp +Y2F0ZSBBdXRob3JpdHkgLSBHMjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC +ggEBAL3twQP89o/8ArFvW59I2Z154qK3A2FWGMNHttfKPTUuiUP3oWmb3ooa/RMg +nLRJdzIpVv257IzdIvpy3Cdhl+72WoTsbhm5iSzchFvVdPtrX8WJpRBSiUZV9Lh1 +HOZ/5FSuS/hVclcCGfgXcVnrHigHdMWdSL5stPSksPNkN3mSwOxGXn/hbVNMYq/N +Hwtjuzqd+/x5AJhhdM8mgkBj87JyahkNmcrUDnXMN/uLicFZ8WJ/X7NfZTD4p7dN +dloedl40wOiWVpmKs/B/pM293DIxfJHP4F8R+GuqSVzRmZTRouNjWwl2tVZi4Ut0 +HZbUJtQIBFnQmA4O5t78w+wfkPECAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAO +BgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFHwMMh+n2TB/xH1oo2Kooc6rB1snMA0G +CSqGSIb3DQEBCwUAA4IBAQARWfolTwNvlJk7mh+ChTnUdgWUXuEok21iXQnCoKjU +sHU48TRqneSfioYmUeYs0cYtbpUgSpIB7LiKZ3sx4mcujJUDJi5DnUox9g61DLu3 +4jd/IroAow57UvtruzvE03lRTs2Q9GcHGcg8RnoNAX3FWOdt5oUwF5okxBDgBPfg +8n/Uqgr/Qh037ZTlZFkSIHc40zI+OIF1lnP6aI+xy84fxez6nH7PfrHxBy22/L/K +pL/QlwVKvOoYKAKQvVR4CSFx09F9HdkWsKlhPdAKACL8x3vLCWRFCztAgfd9fDL1 +mMpYjn0q7pBZc2T5NnReJaH1ZgUufzkVqSr7UIuOhWn0 +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIID7zCCAtegAwIBAgIBADANBgkqhkiG9w0BAQsFADCBmDELMAkGA1UEBhMCVVMx +EDAOBgNVBAgTB0FyaXpvbmExEzARBgNVBAcTClNjb3R0c2RhbGUxJTAjBgNVBAoT +HFN0YXJmaWVsZCBUZWNobm9sb2dpZXMsIEluYy4xOzA5BgNVBAMTMlN0YXJmaWVs +ZCBTZXJ2aWNlcyBSb290IENlcnRpZmljYXRlIEF1dGhvcml0eSAtIEcyMB4XDTA5 +MDkwMTAwMDAwMFoXDTM3MTIzMTIzNTk1OVowgZgxCzAJBgNVBAYTAlVTMRAwDgYD +VQQIEwdBcml6b25hMRMwEQYDVQQHEwpTY290dHNkYWxlMSUwIwYDVQQKExxTdGFy +ZmllbGQgVGVjaG5vbG9naWVzLCBJbmMuMTswOQYDVQQDEzJTdGFyZmllbGQgU2Vy +dmljZXMgUm9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkgLSBHMjCCASIwDQYJKoZI +hvcNAQEBBQADggEPADCCAQoCggEBANUMOsQq+U7i9b4Zl1+OiFOxHz/Lz58gE20p +OsgPfTz3a3Y4Y9k2YKibXlwAgLIvWX/2h/klQ4bnaRtSmpDhcePYLQ1Ob/bISdm2 +8xpWriu2dBTrz/sm4xq6HZYuajtYlIlHVv8loJNwU4PahHQUw2eeBGg6345AWh1K +Ts9DkTvnVtYAcMtS7nt9rjrnvDH5RfbCYM8TWQIrgMw0R9+53pBlbQLPLJGmpufe +hRhJfGZOozptqbXuNC66DQO4M99H67FrjSXZm86B0UVGMpZwh94CDklDhbZsc7tk +6mFBrMnUVN+HL8cisibMn1lUaJ/8viovxFUcdUBgF4UCVTmLfwUCAwEAAaNCMEAw +DwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFJxfAN+q +AdcwKziIorhtSpzyEZGDMA0GCSqGSIb3DQEBCwUAA4IBAQBLNqaEd2ndOxmfZyMI +bw5hyf2E3F/YNoHN2BtBLZ9g3ccaaNnRbobhiCPPE95Dz+I0swSdHynVv/heyNXB +ve6SbzJ08pGCL72CQnqtKrcgfU28elUSwhXqvfdqlS5sdJ/PHLTyxQGjhdByPq1z +qwubdQxtRbeOlKyWN7Wg0I8VRw7j6IPdj/3vQQF3zCepYoUz8jcI73HPdwbeyBkd +iEDPfUYd/x7H4c7/I9vG+o1VTqkC50cRRj70/b17KSa7qWFiNyi2LSr2EIZkyXCn +0q23KXB56jzaYyWf/Wi3MOxw+3WKt21gZ7IeyLnp2KhvAotnDU0mV3HaIPzBSlCN +sSi6 +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFujCCA6KgAwIBAgIJALtAHEP1Xk+wMA0GCSqGSIb3DQEBBQUAMEUxCzAJBgNV +BAYTAkNIMRUwEwYDVQQKEwxTd2lzc1NpZ24gQUcxHzAdBgNVBAMTFlN3aXNzU2ln +biBHb2xkIENBIC0gRzIwHhcNMDYxMDI1MDgzMDM1WhcNMzYxMDI1MDgzMDM1WjBF +MQswCQYDVQQGEwJDSDEVMBMGA1UEChMMU3dpc3NTaWduIEFHMR8wHQYDVQQDExZT +d2lzc1NpZ24gR29sZCBDQSAtIEcyMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIIC +CgKCAgEAr+TufoskDhJuqVAtFkQ7kpJcyrhdhJJCEyq8ZVeCQD5XJM1QiyUqt2/8 +76LQwB8CJEoTlo8jE+YoWACjR8cGp4QjK7u9lit/VcyLwVcfDmJlD909Vopz2q5+ +bbqBHH5CjCA12UNNhPqE21Is8w4ndwtrvxEvcnifLtg+5hg3Wipy+dpikJKVyh+c +6bM8K8vzARO/Ws/BtQpgvd21mWRTuKCWs2/iJneRjOBiEAKfNA+k1ZIzUd6+jbqE +emA8atufK+ze3gE/bk3lUIbLtK/tREDFylqM2tIrfKjuvqblCqoOpd8FUrdVxyJd +MmqXl2MT28nbeTZ7hTpKxVKJ+STnnXepgv9VHKVxaSvRAiTysybUa9oEVeXBCsdt +MDeQKuSeFDNeFhdVxVu1yzSJkvGdJo+hB9TGsnhQ2wwMC3wLjEHXuendjIj3o02y +MszYF9rNt85mndT9Xv+9lz4pded+p2JYryU0pUHHPbwNUMoDAw8IWh+Vc3hiv69y +FGkOpeUDDniOJihC8AcLYiAQZzlG+qkDzAQ4embvIIO1jEpWjpEA/I5cgt6IoMPi +aG59je883WX0XaxR7ySArqpWl2/5rX3aYT+YdzylkbYcjCbaZaIJbcHiVOO5ykxM +gI93e2CaHt+28kgeDrpOVG2Y4OGiGqJ3UM/EY5LsRxmd6+ZrzsECAwEAAaOBrDCB +qTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUWyV7 +lqRlUX64OfPAeGZe6Drn8O4wHwYDVR0jBBgwFoAUWyV7lqRlUX64OfPAeGZe6Drn +8O4wRgYDVR0gBD8wPTA7BglghXQBWQECAQEwLjAsBggrBgEFBQcCARYgaHR0cDov +L3JlcG9zaXRvcnkuc3dpc3NzaWduLmNvbS8wDQYJKoZIhvcNAQEFBQADggIBACe6 +45R88a7A3hfm5djV9VSwg/S7zV4Fe0+fdWavPOhWfvxyeDgD2StiGwC5+OlgzczO +UYrHUDFu4Up+GC9pWbY9ZIEr44OE5iKHjn3g7gKZYbge9LgriBIWhMIxkziWMaa5 +O1M/wySTVltpkuzFwbs4AOPsF6m43Md8AYOfMke6UiI0HTJ6CVanfCU2qT1L2sCC +bwq7EsiHSycR+R4tx5M/nttfJmtS2S6K8RTGRI0Vqbe/vd6mGu6uLftIdxf+u+yv +GPUqUfA5hJeVbG4bwyvEdGB5JbAKJ9/fXtI5z0V9QkvfsywexcZdylU6oJxpmo/a +77KwPJ+HbBIrZXAVUjEaJM9vMSNQH4xPjyPDdEFjHFWoFN0+4FFQz/EbMFYOkrCC +hdiDyyJkvC24JdVUorgG6q2SpCSgwYa1ShNqR88uC1aVVMvOmttqtKay20EIhid3 +92qgQmwLOM7XdVAyksLfKzAiSNDVQTglXaTpXZ/GlHXQRf0wl0OPkKsKx4ZzYEpp +Ld6leNcG2mqeSz53OiATIgHQv2ieY2BrNU0LbbqhPcCT4H8js1WtciVORvnSFu+w +ZMEBnunKoGqYDs/YYPIvSbjkQuE4NRb0yG5P94FW6LqjviOvrv1vA+ACOzB2+htt +Qc8Bsem4yWb02ybzOqR08kkkW8mw0FfB+j564ZfJ +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFvTCCA6WgAwIBAgIITxvUL1S7L0swDQYJKoZIhvcNAQEFBQAwRzELMAkGA1UE +BhMCQ0gxFTATBgNVBAoTDFN3aXNzU2lnbiBBRzEhMB8GA1UEAxMYU3dpc3NTaWdu +IFNpbHZlciBDQSAtIEcyMB4XDTA2MTAyNTA4MzI0NloXDTM2MTAyNTA4MzI0Nlow +RzELMAkGA1UEBhMCQ0gxFTATBgNVBAoTDFN3aXNzU2lnbiBBRzEhMB8GA1UEAxMY +U3dpc3NTaWduIFNpbHZlciBDQSAtIEcyMIICIjANBgkqhkiG9w0BAQEFAAOCAg8A +MIICCgKCAgEAxPGHf9N4Mfc4yfjDmUO8x/e8N+dOcbpLj6VzHVxumK4DV644N0Mv +Fz0fyM5oEMF4rhkDKxD6LHmD9ui5aLlV8gREpzn5/ASLHvGiTSf5YXu6t+WiE7br +YT7QbNHm+/pe7R20nqA1W6GSy/BJkv6FCgU+5tkL4k+73JU3/JHpMjUi0R86TieF +nbAVlDLaYQ1HTWBCrpJH6INaUFjpiou5XaHc3ZlKHzZnu0jkg7Y360g6rw9njxcH +6ATK72oxh9TAtvmUcXtnZLi2kUpCe2UuMGoM9ZDulebyzYLs2aFK7PayS+VFheZt +eJMELpyCbTapxDFkH4aDCyr0NQp4yVXPQbBH6TCfmb5hqAaEuSh6XzjZG6k4sIN/ +c8HDO0gqgg8hm7jMqDXDhBuDsz6+pJVpATqJAHgE2cn0mRmrVn5bi4Y5FZGkECwJ +MoBgs5PAKrYYC51+jUnyEEp/+dVGLxmSo5mnJqy7jDzmDrxHB9xzUfFwZC8I+bRH +HTBsROopN4WSaGa8gzj+ezku01DwH/teYLappvonQfGbGHLy9YR0SslnxFSuSGTf +jNFusB3hB48IHpmccelM2KX3RxIfdNFRnobzwqIjQAtz20um53MGjMGg6cFZrEb6 +5i/4z3GcRm25xBWNOHkDRUjvxF3XCO6HOSKGsg0PWEP3calILv3q1h8CAwEAAaOB +rDCBqTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQU +F6DNweRBtjpbO8tFnb0cwpj6hlgwHwYDVR0jBBgwFoAUF6DNweRBtjpbO8tFnb0c +wpj6hlgwRgYDVR0gBD8wPTA7BglghXQBWQEDAQEwLjAsBggrBgEFBQcCARYgaHR0 +cDovL3JlcG9zaXRvcnkuc3dpc3NzaWduLmNvbS8wDQYJKoZIhvcNAQEFBQADggIB +AHPGgeAn0i0P4JUw4ppBf1AsX19iYamGamkYDHRJ1l2E6kFSGG9YrVBWIGrGvShp +WJHckRE1qTodvBqlYJ7YH39FkWnZfrt4csEGDyrOj4VwYaygzQu4OSlWhDJOhrs9 +xCrZ1x9y7v5RoSJBsXECYxqCsGKrXlcSH9/L3XWgwF15kIwb4FDm3jH+mHtwX6WQ +2K34ArZv02DdQEsixT2tOnqfGhpHkXkzuoLcMmkDlm4fS/Bx/uNncqCxv1yL5PqZ +IseEuRuNI5c/7SXgz2W79WEE790eslpBIlqhn10s6FvJbakMDHiqYMZWjwFaDGi8 +aRl5xB9+lwW/xekkUV7U1UtT7dkjWjYDZaPBA61BMPNGG4WQr2W11bHkFlt4dR2X +em1ZqSqPe97Dh4kQmUlzeMg9vVE1dCrV8X5pGyq7O70luJpaPXJhkGaH7gzWTdQR +dAtq/gsD/KNVV4n+SsuuWxcFyPKNIzFTONItaj+CuY0IavdeQXRuwxF+B6wpYJE/ +OMpXEA29MC/HpeZBoNquBYeaoKRlbEwJDIm6uNO5wJOKMPqN5ZprFQFOZ6raYlY+ +hAhm0sQ2fac+EPyI4NSA5QC9qvNOBqN6avlicuMJT+ubDgEj8Z+7fNzcbBGXJbLy +tGMU0gYqZ4yD9c7qB9iaah7s5Aq7KkzrCWA5zspi2C5u +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIDwzCCAqugAwIBAgIBATANBgkqhkiG9w0BAQsFADCBgjELMAkGA1UEBhMCREUx +KzApBgNVBAoMIlQtU3lzdGVtcyBFbnRlcnByaXNlIFNlcnZpY2VzIEdtYkgxHzAd +BgNVBAsMFlQtU3lzdGVtcyBUcnVzdCBDZW50ZXIxJTAjBgNVBAMMHFQtVGVsZVNl +YyBHbG9iYWxSb290IENsYXNzIDIwHhcNMDgxMDAxMTA0MDE0WhcNMzMxMDAxMjM1 +OTU5WjCBgjELMAkGA1UEBhMCREUxKzApBgNVBAoMIlQtU3lzdGVtcyBFbnRlcnBy +aXNlIFNlcnZpY2VzIEdtYkgxHzAdBgNVBAsMFlQtU3lzdGVtcyBUcnVzdCBDZW50 +ZXIxJTAjBgNVBAMMHFQtVGVsZVNlYyBHbG9iYWxSb290IENsYXNzIDIwggEiMA0G +CSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCqX9obX+hzkeXaXPSi5kfl82hVYAUd +AqSzm1nzHoqvNK38DcLZSBnuaY/JIPwhqgcZ7bBcrGXHX+0CfHt8LRvWurmAwhiC +FoT6ZrAIxlQjgeTNuUk/9k9uN0goOA/FvudocP05l03Sx5iRUKrERLMjfTlH6VJi +1hKTXrcxlkIF+3anHqP1wvzpesVsqXFP6st4vGCvx9702cu+fjOlbpSD8DT6Iavq +jnKgP6TeMFvvhk1qlVtDRKgQFRzlAVfFmPHmBiiRqiDFt1MmUUOyCxGVWOHAD3bZ +wI18gfNycJ5v/hqO2V81xrJvNHy+SE/iWjnX2J14np+GPgNeGYtEotXHAgMBAAGj +QjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBS/ +WSA2AHmgoCJrjNXyYdK4LMuCSjANBgkqhkiG9w0BAQsFAAOCAQEAMQOiYQsfdOhy +NsZt+U2e+iKo4YFWz827n+qrkRk4r6p8FU3ztqONpfSO9kSpp+ghla0+AGIWiPAC +uvxhI+YzmzB6azZie60EI4RYZeLbK4rnJVM3YlNfvNoBYimipidx5joifsFvHZVw +IEoHNN/q/xWA5brXethbdXwFeilHfkCoMRN3zUA7tFFHei4R40cR3p1m0IvVVGb6 +g1XqfMIpiRvpb7PO4gWEyS8+eIVibslfwXhjdFjASBgMmTnrpMwatXlajRWc2BQN +9noHV8cigwUtPJslJj0Ys6lDfMjIq2SPDqO/nBudMNva0Bkuqjzx+zOAduTNrRlP +BSeOE6Fuwg== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIDwzCCAqugAwIBAgIBATANBgkqhkiG9w0BAQsFADCBgjELMAkGA1UEBhMCREUx +KzApBgNVBAoMIlQtU3lzdGVtcyBFbnRlcnByaXNlIFNlcnZpY2VzIEdtYkgxHzAd +BgNVBAsMFlQtU3lzdGVtcyBUcnVzdCBDZW50ZXIxJTAjBgNVBAMMHFQtVGVsZVNl +YyBHbG9iYWxSb290IENsYXNzIDMwHhcNMDgxMDAxMTAyOTU2WhcNMzMxMDAxMjM1 +OTU5WjCBgjELMAkGA1UEBhMCREUxKzApBgNVBAoMIlQtU3lzdGVtcyBFbnRlcnBy +aXNlIFNlcnZpY2VzIEdtYkgxHzAdBgNVBAsMFlQtU3lzdGVtcyBUcnVzdCBDZW50 +ZXIxJTAjBgNVBAMMHFQtVGVsZVNlYyBHbG9iYWxSb290IENsYXNzIDMwggEiMA0G +CSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC9dZPwYiJvJK7genasfb3ZJNW4t/zN +8ELg63iIVl6bmlQdTQyK9tPPcPRStdiTBONGhnFBSivwKixVA9ZIw+A5OO3yXDw/ +RLyTPWGrTs0NvvAgJ1gORH8EGoel15YUNpDQSXuhdfsaa3Ox+M6pCSzyU9XDFES4 +hqX2iys52qMzVNn6chr3IhUciJFrf2blw2qAsCTz34ZFiP0Zf3WHHx+xGwpzJFu5 +ZeAsVMhg02YXP+HMVDNzkQI6pn97djmiH5a2OK61yJN0HZ65tOVgnS9W0eDrXltM +EnAMbEQgqxHY9Bn20pxSN+f6tsIxO0rUFJmtxxr1XV/6B7h8DR/Wgx6zAgMBAAGj +QjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBS1 +A/d2O2GCahKqGFPrAyGUv/7OyjANBgkqhkiG9w0BAQsFAAOCAQEAVj3vlNW92nOy +WL6ukK2YJ5f+AbGwUgC4TeQbIXQbfsDuXmkqJa9c1h3a0nnJ85cp4IaH3gRZD/FZ +1GSFS5mvJQQeyUapl96Cshtwn5z2r3Ex3XsFpSzTucpH9sry9uetuUg/vBa3wW30 +6gmv7PO15wWeph6KU1HWk4HMdJP2udqmJQV0eVp+QD6CSyYRMG7hP0HHRwA11fXT +91Q+gT3aSWqas+8QPebrb9HIIkfLzM8BMZLZGOMivgkeGj5asuRrDFR6fUNOuIml +e9eiPZaGzPImNC1qkp2aGtAw4l1OBLBfiyB+d8E9lYLRRpo7PHi4b6HQDWSieB4p +TpPDpFQUWw== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIEYzCCA0ugAwIBAgIBATANBgkqhkiG9w0BAQsFADCB0jELMAkGA1UEBhMCVFIx +GDAWBgNVBAcTD0dlYnplIC0gS29jYWVsaTFCMEAGA1UEChM5VHVya2l5ZSBCaWxp +bXNlbCB2ZSBUZWtub2xvamlrIEFyYXN0aXJtYSBLdXJ1bXUgLSBUVUJJVEFLMS0w +KwYDVQQLEyRLYW11IFNlcnRpZmlrYXN5b24gTWVya2V6aSAtIEthbXUgU00xNjA0 +BgNVBAMTLVRVQklUQUsgS2FtdSBTTSBTU0wgS29rIFNlcnRpZmlrYXNpIC0gU3Vy +dW0gMTAeFw0xMzExMjUwODI1NTVaFw00MzEwMjUwODI1NTVaMIHSMQswCQYDVQQG +EwJUUjEYMBYGA1UEBxMPR2ViemUgLSBLb2NhZWxpMUIwQAYDVQQKEzlUdXJraXll +IEJpbGltc2VsIHZlIFRla25vbG9qaWsgQXJhc3Rpcm1hIEt1cnVtdSAtIFRVQklU +QUsxLTArBgNVBAsTJEthbXUgU2VydGlmaWthc3lvbiBNZXJrZXppIC0gS2FtdSBT +TTE2MDQGA1UEAxMtVFVCSVRBSyBLYW11IFNNIFNTTCBLb2sgU2VydGlmaWthc2kg +LSBTdXJ1bSAxMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAr3UwM6q7 +a9OZLBI3hNmNe5eA027n/5tQlT6QlVZC1xl8JoSNkvoBHToP4mQ4t4y86Ij5iySr +LqP1N+RAjhgleYN1Hzv/bKjFxlb4tO2KRKOrbEz8HdDc72i9z+SqzvBV96I01INr +N3wcwv61A+xXzry0tcXtAA9TNypN9E8Mg/uGz8v+jE69h/mniyFXnHrfA2eJLJ2X +YacQuFWQfw4tJzh03+f92k4S400VIgLI4OD8D62K18lUUMw7D8oWgITQUVbDjlZ/ +iSIzL+aFCr2lqBs23tPcLG07xxO9WSMs5uWk99gL7eqQQESolbuT1dCANLZGeA4f +AJNG4e7p+exPFwIDAQABo0IwQDAdBgNVHQ4EFgQUZT/HiobGPN08VFw1+DrtUgxH +V8gwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEL +BQADggEBACo/4fEyjq7hmFxLXs9rHmoJ0iKpEsdeV31zVmSAhHqT5Am5EM2fKifh +AHe+SMg1qIGf5LgsyX8OsNJLN13qudULXjS99HMpw+0mFZx+CFOKWI3QSyjfwbPf +IPP54+M638yclNhOT8NrF7f3cuitZjO1JVOr4PhMqZ398g26rrnZqsZr+ZO7rqu4 +lzwDGrpDxpa5RXI4s6ehlj2Re37AIVNMh+3yC1SVUZPVIqUNivGTDj5UDrDYyU7c +8jEyVupk+eq1nRZmQnLzf9OxMUP8pI4X8W0jq5Rm+K37DwhuJi1/FwcJsoz7UMCf +lo3Ptv0AnVoUmr8CRPXBwp8iXqIPoeM= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFQTCCAymgAwIBAgICDL4wDQYJKoZIhvcNAQELBQAwUTELMAkGA1UEBhMCVFcx +EjAQBgNVBAoTCVRBSVdBTi1DQTEQMA4GA1UECxMHUm9vdCBDQTEcMBoGA1UEAxMT +VFdDQSBHbG9iYWwgUm9vdCBDQTAeFw0xMjA2MjcwNjI4MzNaFw0zMDEyMzExNTU5 +NTlaMFExCzAJBgNVBAYTAlRXMRIwEAYDVQQKEwlUQUlXQU4tQ0ExEDAOBgNVBAsT +B1Jvb3QgQ0ExHDAaBgNVBAMTE1RXQ0EgR2xvYmFsIFJvb3QgQ0EwggIiMA0GCSqG +SIb3DQEBAQUAA4ICDwAwggIKAoICAQCwBdvI64zEbooh745NnHEKH1Jw7W2CnJfF +10xORUnLQEK1EjRsGcJ0pDFfhQKX7EMzClPSnIyOt7h52yvVavKOZsTuKwEHktSz +0ALfUPZVr2YOy+BHYC8rMjk1Ujoog/h7FsYYuGLWRyWRzvAZEk2tY/XTP3VfKfCh +MBwqoJimFb3u/Rk28OKRQ4/6ytYQJ0lM793B8YVwm8rqqFpD/G2Gb3PpN0Wp8DbH +zIh1HrtsBv+baz4X7GGqcXzGHaL3SekVtTzWoWH1EfcFbx39Eb7QMAfCKbAJTibc +46KokWofwpFFiFzlmLhxpRUZyXx1EcxwdE8tmx2RRP1WKKD+u4ZqyPpcC1jcxkt2 +yKsi2XMPpfRaAok/T54igu6idFMqPVMnaR1sjjIsZAAmY2E2TqNGtz99sy2sbZCi +laLOz9qC5wc0GZbpuCGqKX6mOL6OKUohZnkfs8O1CWfe1tQHRvMq2uYiN2DLgbYP +oA/pyJV/v1WRBXrPPRXAb94JlAGD1zQbzECl8LibZ9WYkTunhHiVJqRaCPgrdLQA +BDzfuBSO6N+pjWxnkjMdwLfS7JLIvgm/LCkFbwJrnu+8vyq8W8BQj0FwcYeyTbcE +qYSjMq+u7msXi7Kx/mzhkIyIqJdIzshNy/MGz19qCkKxHh53L46g5pIOBvwFItIm +4TFRfTLcDwIDAQABoyMwITAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB +/zANBgkqhkiG9w0BAQsFAAOCAgEAXzSBdu+WHdXltdkCY4QWwa6gcFGn90xHNcgL +1yg9iXHZqjNB6hQbbCEAwGxCGX6faVsgQt+i0trEfJdLjbDorMjupWkEmQqSpqsn +LhpNgb+E1HAerUf+/UqdM+DyucRFCCEK2mlpc3INvjT+lIutwx4116KD7+U4x6WF +H6vPNOw/KP4M8VeGTslV9xzU2KV9Bnpv1d8Q34FOIWWxtuEXeZVFBs5fzNxGiWNo +RI2T9GRwoD2dKAXDOXC4Ynsg/eTb6QihuJ49CcdP+yz4k3ZB3lLg4VfSnQO8d57+ +nile98FRYB/e2guyLXW3Q0iT5/Z5xoRdgFlglPx4mI88k1HtQJAH32RjJMtOcQWh +15QaiDLxInQirqWm2BJpTGCjAu4r7NRjkgtevi92a6O2JryPA9gK8kxkRr05YuWW +6zRjESjMlfGt7+/cgFhI6Uu46mWs6fyAtbXIRfmswZ/ZuepiiI7E8UuDEq3mi4TW +nsLrgxifarsbJGAzcMzs9zLzXNl5fe+epP7JI8Mk7hWSsT2RTyaGvWZzJBPqpK5j +wa19hAM8EHiGG3njxPPyBJUgriOCxLM6AGK/5jYk4Ve6xx6QddVfP5VhK8E7zeWz +aGHQRiapIVJpLesux+t3zqY6tQMzT3bR51xUAV3LePTJDL/PEo4XLSNolOer/qmy +KwbQBM0= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIDezCCAmOgAwIBAgIBATANBgkqhkiG9w0BAQUFADBfMQswCQYDVQQGEwJUVzES +MBAGA1UECgwJVEFJV0FOLUNBMRAwDgYDVQQLDAdSb290IENBMSowKAYDVQQDDCFU +V0NBIFJvb3QgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMDgwODI4MDcyNDMz +WhcNMzAxMjMxMTU1OTU5WjBfMQswCQYDVQQGEwJUVzESMBAGA1UECgwJVEFJV0FO +LUNBMRAwDgYDVQQLDAdSb290IENBMSowKAYDVQQDDCFUV0NBIFJvb3QgQ2VydGlm +aWNhdGlvbiBBdXRob3JpdHkwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB +AQCwfnK4pAOU5qfeCTiRShFAh6d8WWQUe7UREN3+v9XAu1bihSX0NXIP+FPQQeFE +AcK0HMMxQhZHhTMidrIKbw/lJVBPhYa+v5guEGcevhEFhgWQxFnQfHgQsIBct+HH +K3XLfJ+utdGdIzdjp9xCoi2SBBtQwXu4PhvJVgSLL1KbralW6cH/ralYhzC2gfeX +RfwZVzsrb+RH9JlF/h3x+JejiB03HFyP4HYlmlD4oFT/RJB2I9IyxsOrBr/8+7/z +rX2SYgJbKdM1o5OaQ2RgXbL6Mv87BK9NQGr5x+PvI/1ry+UPizgN7gr8/g+YnzAx +3WxSZfmLgb4i4RxYA7qRG4kHAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNV +HRMBAf8EBTADAQH/MB0GA1UdDgQWBBRqOFsmjd6LWvJPelSDGRjjCDWmujANBgkq +hkiG9w0BAQUFAAOCAQEAPNV3PdrfibqHDAhUaiBQkr6wQT25JmSDCi/oQMCXKCeC +MErJk/9q56YAf4lCmtYR5VPOL8zy2gXE/uJQxDqGfczafhAJO5I1KlOy/usrBdls +XebQ79NqZp4VKIV66IIArB6nCWlWQtNoURi+VJq/REG6Sb4gumlc7rh3zc5sH62D +lhh9DrUUOYTxKOkto557HnpyWoOzeW/vtPzQCqVYT0bf+215WfKEIlKuD8z7fDvn +aspHYcN6+NOSBB+4IIThNlQWx0DeO4pz3N/GCUzf7Nr/1FNCocnyYh0igzyXxfkZ +YiesZSLX0zzG5Y6yU8xJzrww/nsOM5D77dIUkR8Hrw== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFODCCAyCgAwIBAgIRAJW+FqD3LkbxezmCcvqLzZYwDQYJKoZIhvcNAQEFBQAw +NzEUMBIGA1UECgwLVGVsaWFTb25lcmExHzAdBgNVBAMMFlRlbGlhU29uZXJhIFJv +b3QgQ0EgdjEwHhcNMDcxMDE4MTIwMDUwWhcNMzIxMDE4MTIwMDUwWjA3MRQwEgYD +VQQKDAtUZWxpYVNvbmVyYTEfMB0GA1UEAwwWVGVsaWFTb25lcmEgUm9vdCBDQSB2 +MTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAMK+6yfwIaPzaSZVfp3F +VRaRXP3vIb9TgHot0pGMYzHw7CTww6XScnwQbfQ3t+XmfHnqjLWCi65ItqwA3GV1 +7CpNX8GH9SBlK4GoRz6JI5UwFpB/6FcHSOcZrr9FZ7E3GwYq/t75rH2D+1665I+X +Z75Ljo1kB1c4VWk0Nj0TSO9P4tNmHqTPGrdeNjPUtAa9GAH9d4RQAEX1jF3oI7x+ +/jXh7VB7qTCNGdMJjmhnXb88lxhTuylixcpecsHHltTbLaC0H2kD7OriUPEMPPCs +81Mt8Bz17Ww5OXOAFshSsCPN4D7c3TxHoLs1iuKYaIu+5b9y7tL6pe0S7fyYGKkm +dtwoSxAgHNN/Fnct7W+A90m7UwW7XWjH1Mh1Fj+JWov3F0fUTPHSiXk+TT2YqGHe +Oh7S+F4D4MHJHIzTjU3TlTazN19jY5szFPAtJmtTfImMMsJu7D0hADnJoWjiUIMu +sDor8zagrC/kb2HCUQk5PotTubtn2txTuXZZNp1D5SDgPTJghSJRt8czu90VL6R4 +pgd7gUY2BIbdeTXHlSw7sKMXNeVzH7RcWe/a6hBle3rQf5+ztCo3O3CLm1u5K7fs +slESl1MpWtTwEhDcTwK7EpIvYtQ/aUN8Ddb8WHUBiJ1YFkveupD/RwGJBmr2X7KQ +arMCpgKIv7NHfirZ1fpoeDVNAgMBAAGjPzA9MA8GA1UdEwEB/wQFMAMBAf8wCwYD +VR0PBAQDAgEGMB0GA1UdDgQWBBTwj1k4ALP1j5qWDNXr+nuqF+gTEjANBgkqhkiG +9w0BAQUFAAOCAgEAvuRcYk4k9AwI//DTDGjkk0kiP0Qnb7tt3oNmzqjMDfz1mgbl +dxSR651Be5kqhOX//CHBXfDkH1e3damhXwIm/9fH907eT/j3HEbAek9ALCI18Bmx +0GtnLLCo4MBANzX2hFxc469CeP6nyQ1Q6g2EdvZR74NTxnr/DlZJLo961gzmJ1Tj +TQpgcmLNkQfWpb/ImWvtxBnmq0wROMVvMeJuScg/doAmAyYp4Db29iBT4xdwNBed +Y2gea+zDTYa4EzAvXUYNR0PVG6pZDrlcjQZIrXSHX8f8MVRBE+LHIQ6e4B4N4cB7 +Q4WQxYpYxmUKeFfyxiMPAdkgS94P+5KFdSpcc41teyWRyu5FrgZLAMzTsVlQ2jqI +OylDRl6XK1TOU2+NSueW+r9xDkKLfP0ooNBIytrEgUy7onOTJsjrDNYmiLbAJM+7 +vVvrdX3pCI6GMyx5dwlppYn8s3CQh3aP0yK7Qs69cwsgJirQmz1wHiRszYd2qReW +t88NkvuOGKmYSdGe/mBEciG5Ge3C9THxOUiIkCR1VBatzvT4aRRkOfujuLpwQMcn +HL/EVlP6Y2XQ8xwOFvVrhlhNGNTkDY6lnVuR3HYkUD/GKvvZt5y11ubQ2egZixVx +SK236thZiNSQvxaz2emsWWFUyBy6ysHK4bkgTI86k4mloMy/0/Z1pHWWbVY= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICjzCCAhWgAwIBAgIQXIuZxVqUxdJxVt7NiYDMJjAKBggqhkjOPQQDAzCBiDEL +MAkGA1UEBhMCVVMxEzARBgNVBAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0plcnNl +eSBDaXR5MR4wHAYDVQQKExVUaGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNVBAMT +JVVTRVJUcnVzdCBFQ0MgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMTAwMjAx +MDAwMDAwWhcNMzgwMTE4MjM1OTU5WjCBiDELMAkGA1UEBhMCVVMxEzARBgNVBAgT +Ck5ldyBKZXJzZXkxFDASBgNVBAcTC0plcnNleSBDaXR5MR4wHAYDVQQKExVUaGUg +VVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNVBAMTJVVTRVJUcnVzdCBFQ0MgQ2VydGlm +aWNhdGlvbiBBdXRob3JpdHkwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQarFRaqflo +I+d61SRvU8Za2EurxtW20eZzca7dnNYMYf3boIkDuAUU7FfO7l0/4iGzzvfUinng +o4N+LZfQYcTxmdwlkWOrfzCjtHDix6EznPO/LlxTsV+zfTJ/ijTjeXmjQjBAMB0G +A1UdDgQWBBQ64QmG1M8ZwpZ2dEl23OA1xmNjmjAOBgNVHQ8BAf8EBAMCAQYwDwYD +VR0TAQH/BAUwAwEB/zAKBggqhkjOPQQDAwNoADBlAjA2Z6EWCNzklwBBHU6+4WMB +zzuqQhFkoJ2UOQIReVx7Hfpkue4WQrO/isIJxOzksU0CMQDpKmFHjFJKS04YcPbW +RNZu9YO6bVi9JNlWSOrvxKJGgYhqOkbRqZtNyWHa0V1Xahg= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIF3jCCA8agAwIBAgIQAf1tMPyjylGoG7xkDjUDLTANBgkqhkiG9w0BAQwFADCB +iDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0pl +cnNleSBDaXR5MR4wHAYDVQQKExVUaGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNV +BAMTJVVTRVJUcnVzdCBSU0EgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMTAw +MjAxMDAwMDAwWhcNMzgwMTE4MjM1OTU5WjCBiDELMAkGA1UEBhMCVVMxEzARBgNV +BAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0plcnNleSBDaXR5MR4wHAYDVQQKExVU +aGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNVBAMTJVVTRVJUcnVzdCBSU0EgQ2Vy +dGlmaWNhdGlvbiBBdXRob3JpdHkwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIK +AoICAQCAEmUXNg7D2wiz0KxXDXbtzSfTTK1Qg2HiqiBNCS1kCdzOiZ/MPans9s/B +3PHTsdZ7NygRK0faOca8Ohm0X6a9fZ2jY0K2dvKpOyuR+OJv0OwWIJAJPuLodMkY +tJHUYmTbf6MG8YgYapAiPLz+E/CHFHv25B+O1ORRxhFnRghRy4YUVD+8M/5+bJz/ +Fp0YvVGONaanZshyZ9shZrHUm3gDwFA66Mzw3LyeTP6vBZY1H1dat//O+T23LLb2 +VN3I5xI6Ta5MirdcmrS3ID3KfyI0rn47aGYBROcBTkZTmzNg95S+UzeQc0PzMsNT +79uq/nROacdrjGCT3sTHDN/hMq7MkztReJVni+49Vv4M0GkPGw/zJSZrM233bkf6 +c0Plfg6lZrEpfDKEY1WJxA3Bk1QwGROs0303p+tdOmw1XNtB1xLaqUkL39iAigmT +Yo61Zs8liM2EuLE/pDkP2QKe6xJMlXzzawWpXhaDzLhn4ugTncxbgtNMs+1b/97l +c6wjOy0AvzVVdAlJ2ElYGn+SNuZRkg7zJn0cTRe8yexDJtC/QV9AqURE9JnnV4ee +UB9XVKg+/XRjL7FQZQnmWEIuQxpMtPAlR1n6BB6T1CZGSlCBst6+eLf8ZxXhyVeE +Hg9j1uliutZfVS7qXMYoCAQlObgOK6nyTJccBz8NUvXt7y+CDwIDAQABo0IwQDAd +BgNVHQ4EFgQUU3m/WqorSs9UgOHYm8Cd8rIDZsswDgYDVR0PAQH/BAQDAgEGMA8G +A1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEMBQADggIBAFzUfA3P9wF9QZllDHPF +Up/L+M+ZBn8b2kMVn54CVVeWFPFSPCeHlCjtHzoBN6J2/FNQwISbxmtOuowhT6KO +VWKR82kV2LyI48SqC/3vqOlLVSoGIG1VeCkZ7l8wXEskEVX/JJpuXior7gtNn3/3 +ATiUFJVDBwn7YKnuHKsSjKCaXqeYalltiz8I+8jRRa8YFWSQEg9zKC7F4iRO/Fjs +8PRF/iKz6y+O0tlFYQXBl2+odnKPi4w2r78NBc5xjeambx9spnFixdjQg3IM8WcR +iQycE0xyNN+81XHfqnHd4blsjDwSXWXavVcStkNr/+XeTWYRUc+ZruwXtuhxkYze +Sf7dNXGiFSeUHM9h4ya7b6NnJSFd5t0dCy5oGzuCr+yDZ4XUmFF0sbmZgIn/f3gZ +XHlKYC6SQK5MNyosycdiyA5d9zZbyuAlJQG03RoHnHcAP9Dc1ew91Pq7P8yF1m9/ +qS3fuQL39ZeatTXaw2ewh0qpKJ4jjv9cJ2vhsE/zB+4ALtRZh8tSQZXq9EfX7mRB +VXyNWQKV3WKdwrnuWih0hKWbt5DHDAff9Yk2dDLWKMGwsAvgnEzDHNb842m1R0aB +L6KCq9NjRHDEjf8tM7qtj3u1cIiuPhnPQCjY/MiQu12ZIvVS5ljFH4gxQ+6IHdfG +jjxDah2nGN59PRbxYvnKkKj9 +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIEMDCCAxigAwIBAgIQUJRs7Bjq1ZxN1ZfvdY+grTANBgkqhkiG9w0BAQUFADCB +gjELMAkGA1UEBhMCVVMxHjAcBgNVBAsTFXd3dy54cmFtcHNlY3VyaXR5LmNvbTEk +MCIGA1UEChMbWFJhbXAgU2VjdXJpdHkgU2VydmljZXMgSW5jMS0wKwYDVQQDEyRY +UmFtcCBHbG9iYWwgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMDQxMTAxMTcx +NDA0WhcNMzUwMTAxMDUzNzE5WjCBgjELMAkGA1UEBhMCVVMxHjAcBgNVBAsTFXd3 +dy54cmFtcHNlY3VyaXR5LmNvbTEkMCIGA1UEChMbWFJhbXAgU2VjdXJpdHkgU2Vy +dmljZXMgSW5jMS0wKwYDVQQDEyRYUmFtcCBHbG9iYWwgQ2VydGlmaWNhdGlvbiBB +dXRob3JpdHkwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCYJB69FbS6 +38eMpSe2OAtp87ZOqCwuIR1cRN8hXX4jdP5efrRKt6atH67gBhbim1vZZ3RrXYCP +KZ2GG9mcDZhtdhAoWORlsH9KmHmf4MMxfoArtYzAQDsRhtDLooY2YKTVMIJt2W7Q +DxIEM5dfT2Fa8OT5kavnHTu86M/0ay00fOJIYRyO82FEzG+gSqmUsE3a56k0enI4 +qEHMPJQRfevIpoy3hsvKMzvZPTeL+3o+hiznc9cKV6xkmxnr9A8ECIqsAxcZZPRa +JSKNNCyy9mgdEm3Tih4U2sSPpuIjhdV6Db1q4Ons7Be7QhtnqiXtRYMh/MHJfNVi +PvryxS3T/dRlAgMBAAGjgZ8wgZwwEwYJKwYBBAGCNxQCBAYeBABDAEEwCwYDVR0P +BAQDAgGGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFMZPoj0GY4QJnM5i5ASs +jVy16bYbMDYGA1UdHwQvMC0wK6ApoCeGJWh0dHA6Ly9jcmwueHJhbXBzZWN1cml0 +eS5jb20vWEdDQS5jcmwwEAYJKwYBBAGCNxUBBAMCAQEwDQYJKoZIhvcNAQEFBQAD +ggEBAJEVOQMBG2f7Shz5CmBbodpNl2L5JFMn14JkTpAuw0kbK5rc/Kh4ZzXxHfAR +vbdI4xD2Dd8/0sm2qlWkSLoC295ZLhVbO50WfUfXN+pfTXYSNrsf16GBBEYgoyxt +qZ4Bfj8pzgCT3/3JknOJiWSe5yvkHJEs0rnOfc5vMZnT5r7SHpDwCRR5XCOrTdLa +IR9NmXmd4c8nnxCbHIgNsIpkQTG4DmyQJKSbXHGPurt+HBvbaoAPIbzp26a3QPSy +i6mx5O+aGtA9aZnuqCij4Tyz8LIRnM98QObd50N9otg6tamN8jSZxNQQ4Qb9CYQQ +O+7ETPTsJ3xCwnR8gooJybQDJbw= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIDODCCAiCgAwIBAgIGIAYFFnACMA0GCSqGSIb3DQEBBQUAMDsxCzAJBgNVBAYT +AlJPMREwDwYDVQQKEwhjZXJ0U0lHTjEZMBcGA1UECxMQY2VydFNJR04gUk9PVCBD +QTAeFw0wNjA3MDQxNzIwMDRaFw0zMTA3MDQxNzIwMDRaMDsxCzAJBgNVBAYTAlJP +MREwDwYDVQQKEwhjZXJ0U0lHTjEZMBcGA1UECxMQY2VydFNJR04gUk9PVCBDQTCC +ASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALczuX7IJUqOtdu0KBuqV5Do +0SLTZLrTk+jUrIZhQGpgV2hUhE28alQCBf/fm5oqrl0Hj0rDKH/v+yv6efHHrfAQ +UySQi2bJqIirr1qjAOm+ukbuW3N7LBeCgV5iLKECZbO9xSsAfsT8AzNXDe3i+s5d +RdY4zTW2ssHQnIFKquSyAVwdj1+ZxLGt24gh65AIgoDzMKND5pCCrlUoSe1b16kQ +OA7+j0xbm0bqQfWwCHTD0IgztnzXdN/chNFDDnU5oSVAKOp4yw4sLjmdjItuFhwv +JoIQ4uNllAoEwF73XVv4EOLQunpL+943AAAaWyjj0pxzPjKHmKHJUS/X3qwzs08C +AwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAcYwHQYDVR0O +BBYEFOCMm9slSbPxfIbWskKHC9BroNnkMA0GCSqGSIb3DQEBBQUAA4IBAQA+0hyJ +LjX8+HXd5n9liPRyTMks1zJO890ZeUe9jjtbkw9QSSQTaxQGcu8J06Gh40CEyecY +MnQ8SG4Pn0vU9x7Tk4ZkVJdjclDVVc/6IJMCopvDI5NOFlV2oHB5bc0hH88vLbwZ +44gx+FkagQnIl6Z0x2DEW8xXjrJ1/RsCCdtZb3KTafcxQdaIOL+Hsr0Wefmq5L6I +Jd1hJyMctTEHBDa0GpC9oHRxUIltvBTjD4au8as+x6AJzKNI0eDbZOeStc+vckNw +i/nDhDwTqn6Sm1dTk/pwwpEOMfmbZ13pljheX7NzTogVZ96edhBiIL5VaZVDADlN +9u6wWk5JRFRYX0KD +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFsDCCA5igAwIBAgIQFci9ZUdcr7iXAF7kBtK8nTANBgkqhkiG9w0BAQUFADBe +MQswCQYDVQQGEwJUVzEjMCEGA1UECgwaQ2h1bmdod2EgVGVsZWNvbSBDby4sIEx0 +ZC4xKjAoBgNVBAsMIWVQS0kgUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAe +Fw0wNDEyMjAwMjMxMjdaFw0zNDEyMjAwMjMxMjdaMF4xCzAJBgNVBAYTAlRXMSMw +IQYDVQQKDBpDaHVuZ2h3YSBUZWxlY29tIENvLiwgTHRkLjEqMCgGA1UECwwhZVBL +SSBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MIICIjANBgkqhkiG9w0BAQEF +AAOCAg8AMIICCgKCAgEA4SUP7o3biDN1Z82tH306Tm2d0y8U82N0ywEhajfqhFAH +SyZbCUNsIZ5qyNUD9WBpj8zwIuQf5/dqIjG3LBXy4P4AakP/h2XGtRrBp0xtInAh +ijHyl3SJCRImHJ7K2RKilTza6We/CKBk49ZCt0Xvl/T29de1ShUCWH2YWEtgvM3X +DZoTM1PRYfl61dd4s5oz9wCGzh1NlDivqOx4UXCKXBCDUSH3ET00hl7lSM2XgYI1 +TBnsZfZrxQWh7kcT1rMhJ5QQCtkkO7q+RBNGMD+XPNjX12ruOzjjK9SXDrkb5wdJ +fzcq+Xd4z1TtW0ado4AOkUPB1ltfFLqfpo0kR0BZv3I4sjZsN/+Z0V0OWQqraffA +sgRFelQArr5T9rXn4fg8ozHSqf4hUmTFpmfwdQcGlBSBVcYn5AGPF8Fqcde+S/uU +WH1+ETOxQvdibBjWzwloPn9s9h6PYq2lY9sJpx8iQkEeb5mKPtf5P0B6ebClAZLS +nT0IFaUQAS2zMnaolQ2zepr7BxB4EW/hj8e6DyUadCrlHJhBmd8hh+iVBmoKs2pH +dmX2Os+PYhcZewoozRrSgx4hxyy/vv9haLdnG7t4TY3OZ+XkwY63I2binZB1NJip +NiuKmpS5nezMirH4JYlcWrYvjB9teSSnUmjDhDXiZo1jDiVN1Rmy5nk3pyKdVDEC +AwEAAaNqMGgwHQYDVR0OBBYEFB4M97Zn8uGSJglFwFU5Lnc/QkqiMAwGA1UdEwQF +MAMBAf8wOQYEZyoHAAQxMC8wLQIBADAJBgUrDgMCGgUAMAcGBWcqAwAABBRFsMLH +ClZ87lt4DJX5GFPBphzYEDANBgkqhkiG9w0BAQUFAAOCAgEACbODU1kBPpVJufGB +uvl2ICO1J2B01GqZNF5sAFPZn/KmsSQHRGoqxqWOeBLoR9lYGxMqXnmbnwoqZ6Yl +PwZpVnPDimZI+ymBV3QGypzqKOg4ZyYr8dW1P2WT+DZdjo2NQCCHGervJ8A9tDkP +JXtoUHRVnAxZfVo9QZQlUgjgRywVMRnVvwdVxrsStZf0X4OFunHB2WyBEXYKCrC/ +gpf36j36+uwtqSiUO1bd0lEursC9CBWMd1I0ltabrNMdjmEPNXubrjlpC2JgQCA2 +j6/7Nu4tCEoduL+bXPjqpRugc6bY+G7gMwRfaKonh+3ZwZCc7b3jajWvY9+rGNm6 +5ulK6lCKD2GTHuItGeIwlDWSXQ62B68ZgI9HkFFLLk3dheLSClIKF5r8GrBQAuUB +o2M3IUxExJtRmREOc5wGj1QupyheRDmHVi03vYVElOEMSyycw5KFNGHLD7ibSkNS +/jQ6fbjpKdx2qcgw+BRxgMYeNkh0IkFch4LoGHGLQYlE535YW6i4jRPpp2zDR+2z +Gp1iro2C6pSe3VkQw63d4k3jMdXH7OjysP6SHhYKGvzZ8/gntsm+HbRsZJB/9OTE +W9c3rkIO3aQab3yIVMUWbuF6aC74Or8NpDyJO3inTmODBCEIZ43ygknQW/2xzQ+D +hNQ+IIX3Sj0rnP0qCglN6oH4EZw= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIGWzCCBEOgAwIBAgIRAMrpG4nxVQMNo+ZBbcTjpuEwDQYJKoZIhvcNAQELBQAw +WjELMAkGA1UEBhMCRlIxEjAQBgNVBAoMCURoaW15b3RpczEcMBoGA1UECwwTMDAw +MiA0ODE0NjMwODEwMDAzNjEZMBcGA1UEAwwQQ2VydGlnbmEgUm9vdCBDQTAeFw0x +MzEwMDEwODMyMjdaFw0zMzEwMDEwODMyMjdaMFoxCzAJBgNVBAYTAkZSMRIwEAYD +VQQKDAlEaGlteW90aXMxHDAaBgNVBAsMEzAwMDIgNDgxNDYzMDgxMDAwMzYxGTAX +BgNVBAMMEENlcnRpZ25hIFJvb3QgQ0EwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAw +ggIKAoICAQDNGDllGlmx6mQWDoyUJJV8g9PFOSbcDO8WV43X2KyjQn+Cyu3NW9sO +ty3tRQgXstmzy9YXUnIo245Onoq2C/mehJpNdt4iKVzSs9IGPjA5qXSjklYcoW9M +CiBtnyN6tMbaLOQdLNyzKNAT8kxOAkmhVECe5uUFoC2EyP+YbNDrihqECB63aCPu +I9Vwzm1RaRDuoXrC0SIxwoKF0vJVdlB8JXrJhFwLrN1CTivngqIkicuQstDuI7pm +TLtipPlTWmR7fJj6o0ieD5Wupxj0auwuA0Wv8HT4Ks16XdG+RCYyKfHx9WzMfgIh +C59vpD++nVPiz32pLHxYGpfhPTc3GGYo0kDFUYqMwy3OU4gkWGQwFsWq4NYKpkDf +ePb1BHxpE4S80dGnBs8B92jAqFe7OmGtBIyT46388NtEbVncSVmurJqZNjBBe3Yz +IoejwpKGbvlw7q6Hh5UbxHq9MfPU0uWZ/75I7HX1eBYdpnDBfzwboZL7z8g81sWT +Co/1VTp2lc5ZmIoJlXcymoO6LAQ6l73UL77XbJuiyn1tJslV1c/DeVIICZkHJC1k +JWumIWmbat10TWuXekG9qxf5kBdIjzb5LdXF2+6qhUVB+s06RbFo5jZMm5BX7CO5 +hwjCxAnxl4YqKE3idMDaxIzb3+KhF1nOJFl0Mdp//TBt2dzhauH8XwIDAQABo4IB +GjCCARYwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYE +FBiHVuBud+4kNTxOc5of1uHieX4rMB8GA1UdIwQYMBaAFBiHVuBud+4kNTxOc5of +1uHieX4rMEQGA1UdIAQ9MDswOQYEVR0gADAxMC8GCCsGAQUFBwIBFiNodHRwczov +L3d3d3cuY2VydGlnbmEuZnIvYXV0b3JpdGVzLzBtBgNVHR8EZjBkMC+gLaArhilo +dHRwOi8vY3JsLmNlcnRpZ25hLmZyL2NlcnRpZ25hcm9vdGNhLmNybDAxoC+gLYYr +aHR0cDovL2NybC5kaGlteW90aXMuY29tL2NlcnRpZ25hcm9vdGNhLmNybDANBgkq +hkiG9w0BAQsFAAOCAgEAlLieT/DjlQgi581oQfccVdV8AOItOoldaDgvUSILSo3L +6btdPrtcPbEo/uRTVRPPoZAbAh1fZkYJMyjhDSSXcNMQH+pkV5a7XdrnxIxPTGRG +HVyH41neQtGbqH6mid2PHMkwgu07nM3A6RngatgCdTer9zQoKJHyBApPNeNgJgH6 +0BGM+RFq7q89w1DTj18zeTyGqHNFkIwgtnJzFyO+B2XleJINugHA64wcZr+shncB +lA2c5uk5jR+mUYyZDDl34bSb+hxnV29qao6pK0xXeXpXIs/NX2NGjVxZOob4Mkdi +o2cNGJHc+6Zr9UhhcyNZjgKnvETq9Emd8VRY+WCv2hikLyhF3HqgiIZd8zvn/yk1 +gPxkQ5Tm4xxvvq0OKmOZK8l+hfZx6AYDlf7ej0gcWtSS6Cvu5zHbugRqh5jnxV/v +faci9wHYTfmJ0A6aBVmknpjZbyvKcL5kwlWj9Omvw5Ip3IgWJJk8jSaYtlu3zM63 +Nwf9JtmYhST/WSMDmu2dnajkXjjO11INb9I/bbEFa0nOipFGc/T2L/Coc3cOZayh +jWZSaX5LaAzHHjcng6WMxwLkFM1JAbBzs/3GkDpv0mztO+7skb6iQ12LAEpmJURw +3kAP+HwV96LOPNdeE4yBFxgX0b3xdxA61GU5wSesVywlVP+i2k+KYTlerj1KjL0= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIGSzCCBDOgAwIBAgIRANm1Q3+vqTkPAAAAAFVlrVgwDQYJKoZIhvcNAQELBQAw +gb4xCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1FbnRydXN0LCBJbmMuMSgwJgYDVQQL +Ex9TZWUgd3d3LmVudHJ1c3QubmV0L2xlZ2FsLXRlcm1zMTkwNwYDVQQLEzAoYykg +MjAxNSBFbnRydXN0LCBJbmMuIC0gZm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxMjAw +BgNVBAMTKUVudHJ1c3QgUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSAtIEc0 +MB4XDTE1MDUyNzExMTExNloXDTM3MTIyNzExNDExNlowgb4xCzAJBgNVBAYTAlVT +MRYwFAYDVQQKEw1FbnRydXN0LCBJbmMuMSgwJgYDVQQLEx9TZWUgd3d3LmVudHJ1 +c3QubmV0L2xlZ2FsLXRlcm1zMTkwNwYDVQQLEzAoYykgMjAxNSBFbnRydXN0LCBJ +bmMuIC0gZm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxMjAwBgNVBAMTKUVudHJ1c3Qg +Um9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSAtIEc0MIICIjANBgkqhkiG9w0B +AQEFAAOCAg8AMIICCgKCAgEAsewsQu7i0TD/pZJH4i3DumSXbcr3DbVZwbPLqGgZ +2K+EbTBwXX7zLtJTmeH+H17ZSK9dE43b/2MzTdMAArzE+NEGCJR5WIoV3imz/f3E +T+iq4qA7ec2/a0My3dl0ELn39GjUu9CH1apLiipvKgS1sqbHoHrmSKvS0VnM1n4j +5pds8ELl3FFLFUHtSUrJ3hCX1nbB76W1NhSXNdh4IjVS70O92yfbYVaCNNzLiGAM +C1rlLAHGVK/XqsEQe9IFWrhAnoanw5CGAlZSCXqc0ieCU0plUmr1POeo8pyvi73T +DtTUXm6Hnmo9RR3RXRv06QqsYJn7ibT/mCzPfB3pAqoEmh643IhuJbNsZvc8kPNX +wbMv9W3y+8qh+CmdRouzavbmZwe+LGcKKh9asj5XxNMhIWNlUpEbsZmOeX7m640A +2Vqq6nPopIICR5b+W45UYaPrL0swsIsjdXJ8ITzI9vF01Bx7owVV7rtNOzK+mndm +nqxpkCIHH2E6lr7lmk/MBTwoWdPBDFSoWWG9yHJM6Nyfh3+9nEg2XpWjDrk4JFX8 +dWbrAuMINClKxuMrLzOg2qOGpRKX/YAr2hRC45K9PvJdXmd0LhyIRyk0X+IyqJwl +N4y6mACXi0mWHv0liqzc2thddG5msP9E36EYxr5ILzeUePiVSj9/E15dWf10hkNj +c0kCAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYD +VR0OBBYEFJ84xFYjwznooHFs6FRM5Og6sb9nMA0GCSqGSIb3DQEBCwUAA4ICAQAS +5UKme4sPDORGpbZgQIeMJX6tuGguW8ZAdjwD+MlZ9POrYs4QjbRaZIxowLByQzTS +Gwv2LFPSypBLhmb8qoMi9IsabyZIrHZ3CL/FmFz0Jomee8O5ZDIBf9PD3Vht7LGr +hFV0d4QEJ1JrhkzO3bll/9bGXp+aEJlLdWr+aumXIOTkdnrG0CSqkM0gkLpHZPt/ +B7NTeLUKYvJzQ85BK4FqLoUWlFPUa19yIqtRLULVAJyZv967lDtX/Zr1hstWO1uI +AeV8KEsD+UmDfLJ/fOPtjqF/YFOOVZ1QNBIPt5d7bIdKROf1beyAN/BYGW5KaHbw +H5Lk6rWS02FREAutp9lfx1/cH6NcjKF+m7ee01ZvZl4HliDtC3T7Zk6LERXpgUl+ +b7DUUH8i119lAg2m9IUe2K4GS0qn0jFmwvjO5QimpAKWRGhXxNUzzxkvFMSUHHuk +2fCfDrGA4tGeEWSpiBE6doLlYsKA2KSD7ZPvfC+QsDJMlhVoSFLUmQjAJOgc47Ol +IQ6SwJAfzyBfyjs4x7dtOvPmRLgOMWuIjnDrnBdSqEGULoe256YSxXXfW8AKbnuk +5F6G+TaU33fD6Q3AOfF5u0aOq0NZJ7cguyPpVkAh7DE9ZapD8j3fcEThuk0mEDuY +n/PIjhs4ViFqUZPTkcpG2om3PVODLAgfi49T3f+sHw== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFVzCCAz+gAwIBAgINAgPlk28xsBNJiGuiFzANBgkqhkiG9w0BAQwFADBHMQsw +CQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEU +MBIGA1UEAxMLR1RTIFJvb3QgUjEwHhcNMTYwNjIyMDAwMDAwWhcNMzYwNjIyMDAw +MDAwWjBHMQswCQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZp +Y2VzIExMQzEUMBIGA1UEAxMLR1RTIFJvb3QgUjEwggIiMA0GCSqGSIb3DQEBAQUA +A4ICDwAwggIKAoICAQC2EQKLHuOhd5s73L+UPreVp0A8of2C+X0yBoJx9vaMf/vo +27xqLpeXo4xL+Sv2sfnOhB2x+cWX3u+58qPpvBKJXqeqUqv4IyfLpLGcY9vXmX7w +Cl7raKb0xlpHDU0QM+NOsROjyBhsS+z8CZDfnWQpJSMHobTSPS5g4M/SCYe7zUjw +TcLCeoiKu7rPWRnWr4+wB7CeMfGCwcDfLqZtbBkOtdh+JhpFAz2weaSUKK0Pfybl +qAj+lug8aJRT7oM6iCsVlgmy4HqMLnXWnOunVmSPlk9orj2XwoSPwLxAwAtcvfaH +szVsrBhQf4TgTM2S0yDpM7xSma8ytSmzJSq0SPly4cpk9+aCEI3oncKKiPo4Zor8 +Y/kB+Xj9e1x3+naH+uzfsQ55lVe0vSbv1gHR6xYKu44LtcXFilWr06zqkUspzBmk +MiVOKvFlRNACzqrOSbTqn3yDsEB750Orp2yjj32JgfpMpf/VjsPOS+C12LOORc92 +wO1AK/1TD7Cn1TsNsYqiA94xrcx36m97PtbfkSIS5r762DL8EGMUUXLeXdYWk70p +aDPvOmbsB4om3xPXV2V4J95eSRQAogB/mqghtqmxlbCluQ0WEdrHbEg8QOB+DVrN +VjzRlwW5y0vtOUucxD/SVRNuJLDWcfr0wbrM7Rv1/oFB2ACYPTrIrnqYNxgFlQID +AQABo0IwQDAOBgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4E +FgQU5K8rJnEaK0gnhS9SZizv8IkTcT4wDQYJKoZIhvcNAQEMBQADggIBAJ+qQibb +C5u+/x6Wki4+omVKapi6Ist9wTrYggoGxval3sBOh2Z5ofmmWJyq+bXmYOfg6LEe +QkEzCzc9zolwFcq1JKjPa7XSQCGYzyI0zzvFIoTgxQ6KfF2I5DUkzps+GlQebtuy +h6f88/qBVRRiClmpIgUxPoLW7ttXNLwzldMXG+gnoot7TiYaelpkttGsN/H9oPM4 +7HLwEXWdyzRSjeZ2axfG34arJ45JK3VmgRAhpuo+9K4l/3wV3s6MJT/KYnAK9y8J +ZgfIPxz88NtFMN9iiMG1D53Dn0reWVlHxYciNuaCp+0KueIHoI17eko8cdLiA6Ef +MgfdG+RCzgwARWGAtQsgWSl4vflVy2PFPEz0tv/bal8xa5meLMFrUKTX5hgUvYU/ +Z6tGn6D/Qqc6f1zLXbBwHSs09dR2CQzreExZBfMzQsNhFRAbd03OIozUhfJFfbdT +6u9AWpQKXCBfTkBdYiJ23//OYb2MI3jSNwLgjt7RETeJ9r/tSQdirpLsQBqvFAnZ +0E6yove+7u7Y/9waLd64NnHi/Hm3lCXRSHNboTXns5lndcEZOitHTtNCjv0xyBZm +2tIMPNuzjsmhDYAPexZ3FL//2wmUspO8IFgV6dtxQ/PeEMMA3KgqlbbC1j+Qa3bb +bP6MvPJwNQzcmRk13NfIRmPVNnGuV/u3gm3c +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFVzCCAz+gAwIBAgINAgPlrsWNBCUaqxElqjANBgkqhkiG9w0BAQwFADBHMQsw +CQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEU +MBIGA1UEAxMLR1RTIFJvb3QgUjIwHhcNMTYwNjIyMDAwMDAwWhcNMzYwNjIyMDAw +MDAwWjBHMQswCQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZp +Y2VzIExMQzEUMBIGA1UEAxMLR1RTIFJvb3QgUjIwggIiMA0GCSqGSIb3DQEBAQUA +A4ICDwAwggIKAoICAQDO3v2m++zsFDQ8BwZabFn3GTXd98GdVarTzTukk3LvCvpt +nfbwhYBboUhSnznFt+4orO/LdmgUud+tAWyZH8QiHZ/+cnfgLFuv5AS/T3KgGjSY +6Dlo7JUle3ah5mm5hRm9iYz+re026nO8/4Piy33B0s5Ks40FnotJk9/BW9BuXvAu +MC6C/Pq8tBcKSOWIm8Wba96wyrQD8Nr0kLhlZPdcTK3ofmZemde4wj7I0BOdre7k +RXuJVfeKH2JShBKzwkCX44ofR5GmdFrS+LFjKBC4swm4VndAoiaYecb+3yXuPuWg +f9RhD1FLPD+M2uFwdNjCaKH5wQzpoeJ/u1U8dgbuak7MkogwTZq9TwtImoS1mKPV ++3PBV2HdKFZ1E66HjucMUQkQdYhMvI35ezzUIkgfKtzra7tEscszcTJGr61K8Yzo +dDqs5xoic4DSMPclQsciOzsSrZYuxsN2B6ogtzVJV+mSSeh2FnIxZyuWfoqjx5RW +Ir9qS34BIbIjMt/kmkRtWVtd9QCgHJvGeJeNkP+byKq0rxFROV7Z+2et1VsRnTKa +G73VululycslaVNVJ1zgyjbLiGH7HrfQy+4W+9OmTN6SpdTi3/UGVN4unUu0kzCq +gc7dGtxRcw1PcOnlthYhGXmy5okLdWTK1au8CcEYof/UVKGFPP0UJAOyh9OktwID +AQABo0IwQDAOBgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4E +FgQUu//KjiOfT5nK2+JopqUVJxce2Q4wDQYJKoZIhvcNAQEMBQADggIBAB/Kzt3H +vqGf2SdMC9wXmBFqiN495nFWcrKeGk6c1SuYJF2ba3uwM4IJvd8lRuqYnrYb/oM8 +0mJhwQTtzuDFycgTE1XnqGOtjHsB/ncw4c5omwX4Eu55MaBBRTUoCnGkJE+M3DyC +B19m3H0Q/gxhswWV7uGugQ+o+MePTagjAiZrHYNSVc61LwDKgEDg4XSsYPWHgJ2u +NmSRXbBoGOqKYcl3qJfEycel/FVL8/B/uWU9J2jQzGv6U53hkRrJXRqWbTKH7QMg +yALOWr7Z6v2yTcQvG99fevX4i8buMTolUVVnjWQye+mew4K6Ki3pHrTgSAai/Gev +HyICc/sgCq+dVEuhzf9gR7A/Xe8bVr2XIZYtCtFenTgCR2y59PYjJbigapordwj6 +xLEokCZYCDzifqrXPW+6MYgKBesntaFJ7qBFVHvmJ2WZICGoo7z7GJa7Um8M7YNR +TOlZ4iBgxcJlkoKM8xAfDoqXvneCbT+PHV28SSe9zE8P4c52hgQjxcCMElv924Sg +JPFI/2R80L5cFtHvma3AH/vLrrw4IgYmZNralw4/KBVEqE8AyvCazM90arQ+POuV +7LXTWtiBmelDGDfrs7vRWGJB82bSj6p4lVQgw1oudCvV0b4YacCs1aTPObpRhANl +6WLAYv7YTVWW4tAR+kg0Eeye7QUd5MjWHYbL +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICCTCCAY6gAwIBAgINAgPluILrIPglJ209ZjAKBggqhkjOPQQDAzBHMQswCQYD +VQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEUMBIG +A1UEAxMLR1RTIFJvb3QgUjMwHhcNMTYwNjIyMDAwMDAwWhcNMzYwNjIyMDAwMDAw +WjBHMQswCQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2Vz +IExMQzEUMBIGA1UEAxMLR1RTIFJvb3QgUjMwdjAQBgcqhkjOPQIBBgUrgQQAIgNi +AAQfTzOHMymKoYTey8chWEGJ6ladK0uFxh1MJ7x/JlFyb+Kf1qPKzEUURout736G +jOyxfi//qXGdGIRFBEFVbivqJn+7kAHjSxm65FSWRQmx1WyRRK2EE46ajA2ADDL2 +4CejQjBAMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQW +BBTB8Sa6oC2uhYHP0/EqEr24Cmf9vDAKBggqhkjOPQQDAwNpADBmAjEA9uEglRR7 +VKOQFhG/hMjqb2sXnh5GmCCbn9MN2azTL818+FsuVbu/3ZL3pAzcMeGiAjEA/Jdm +ZuVDFhOD3cffL74UOO0BzrEXGhF16b0DjyZ+hOXJYKaV11RZt+cRLInUue4X +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICCTCCAY6gAwIBAgINAgPlwGjvYxqccpBQUjAKBggqhkjOPQQDAzBHMQswCQYD +VQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEUMBIG +A1UEAxMLR1RTIFJvb3QgUjQwHhcNMTYwNjIyMDAwMDAwWhcNMzYwNjIyMDAwMDAw +WjBHMQswCQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2Vz +IExMQzEUMBIGA1UEAxMLR1RTIFJvb3QgUjQwdjAQBgcqhkjOPQIBBgUrgQQAIgNi +AATzdHOnaItgrkO4NcWBMHtLSZ37wWHO5t5GvWvVYRg1rkDdc/eJkTBa6zzuhXyi +QHY7qca4R9gq55KRanPpsXI5nymfopjTX15YhmUPoYRlBtHci8nHc8iMai/lxKvR +HYqjQjBAMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQW +BBSATNbrdP9JNqPV2Py1PsVq8JQdjDAKBggqhkjOPQQDAwNpADBmAjEA6ED/g94D +9J+uHXqnLrmvT/aDHQ4thQEd0dlq7A/Cr8deVl5c1RxYIigL9zC2L7F8AjEA8GE8 +p/SgguMh1YQdc4acLa/KNJvxn7kjNuK8YAOdgLOaVsjh4rsUecrNIdSUtUlD +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFzzCCA7egAwIBAgIUCBZfikyl7ADJk0DfxMauI7gcWqQwDQYJKoZIhvcNAQEL +BQAwbzELMAkGA1UEBhMCSEsxEjAQBgNVBAgTCUhvbmcgS29uZzESMBAGA1UEBxMJ +SG9uZyBLb25nMRYwFAYDVQQKEw1Ib25na29uZyBQb3N0MSAwHgYDVQQDExdIb25n +a29uZyBQb3N0IFJvb3QgQ0EgMzAeFw0xNzA2MDMwMjI5NDZaFw00MjA2MDMwMjI5 +NDZaMG8xCzAJBgNVBAYTAkhLMRIwEAYDVQQIEwlIb25nIEtvbmcxEjAQBgNVBAcT +CUhvbmcgS29uZzEWMBQGA1UEChMNSG9uZ2tvbmcgUG9zdDEgMB4GA1UEAxMXSG9u +Z2tvbmcgUG9zdCBSb290IENBIDMwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIK +AoICAQCziNfqzg8gTr7m1gNt7ln8wlffKWihgw4+aMdoWJwcYEuJQwy51BWy7sFO +dem1p+/l6TWZ5Mwc50tfjTMwIDNT2aa71T4Tjukfh0mtUC1Qyhi+AViiE3CWu4mI +VoBc+L0sPOFMV4i707mV78vH9toxdCim5lSJ9UExyuUmGs2C4HDaOym71QP1mbpV +9WTRYA6ziUm4ii8F0oRFKHyPaFASePwLtVPLwpgchKOesL4jpNrcyCse2m5FHomY +2vkALgbpDDtw1VAliJnLzXNg99X/NWfFobxeq81KuEXryGgeDQ0URhLj0mRiikKY +vLTGCAj4/ahMZJx2Ab0vqWwzD9g/KLg8aQFChn5pwckGyuV6RmXpwtZQQS4/t+Tt +bNe/JgERohYpSms0BpDsE9K2+2p20jzt8NYt3eEV7KObLyzJPivkaTv/ciWxNoZb +x39ri1UbSsUgYT2uy1DhCDq+sI9jQVMwCFk8mB13umOResoQUGC/8Ne8lYePl8X+ +l2oBlKN8W4UdKjk60FSh0Tlxnf0h+bV78OLgAo9uliQlLKAeLKjEiafv7ZkGL7YK +TE/bosw3Gq9HhS2KX8Q0NEwA/RiTZxPRN+ZItIsGxVd7GYYKecsAyVKvQv83j+Gj +Hno9UKtjBucVtT+2RTeUN7F+8kjDf8V1/peNRY8apxpyKBpADwIDAQABo2MwYTAP +BgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAfBgNVHSMEGDAWgBQXnc0e +i9Y5K3DTXNSguB+wAPzFYTAdBgNVHQ4EFgQUF53NHovWOStw01zUoLgfsAD8xWEw +DQYJKoZIhvcNAQELBQADggIBAFbVe27mIgHSQpsY1Q7XZiNc4/6gx5LS6ZStS6LG +7BJ8dNVI0lkUmcDrudHr9EgwW62nV3OZqdPlt9EuWSRY3GguLmLYauRwCy0gUCCk +MpXRAJi70/33MvJJrsZ64Ee+bs7Lo3I6LWldy8joRTnU+kLBEUx3XZL7av9YROXr +gZ6voJmtvqkBZss4HTzfQx/0TW60uhdG/H39h4F5ag0zD/ov+BS5gLNdTaqX4fnk +GMX41TiMJjz98iji7lpJiCzfeT2OnpA8vUFKOt1b9pq0zj8lMH8yfaIDlNDceqFS +3m6TjRgm/VWsvY+b0s+v54Ysyx8Jb6NvqYTUc79NoXQbTiNg8swOqn+knEwlqLJm +Ozj/2ZQw9nKEvmhVEA/GcywWaZMH/rFF7buiVWqw2rVKAiUnhde3t4ZEFolsgCs+ +l6mc1X5VTMbeRRAc6uk7nwNT7u56AQIWeNTowr5GdogTPyK7SBIdUgC0An4hGh6c +JfTzPV4e0hz5sy229zdcxsshTrD3mUcYhcErulWuBurQB7Lcq9CClnXO0lD+mefP +L5/ndtFhKvshuzHQqp9HpLIiyhY6UFfEW0NnxWViA0kB60PZ2Pierc+xYw5F9KBa +LJstxabArahH9CdMOA0uG0k7UvToiIMrVCjU8jVStDKDYmlkDJGcn5fqdBb9HxEG +mpv0 +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICWTCCAd+gAwIBAgIQZvI9r4fei7FK6gxXMQHC7DAKBggqhkjOPQQDAzBlMQsw +CQYDVQQGEwJVUzEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMTYwNAYD +VQQDEy1NaWNyb3NvZnQgRUNDIFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5IDIw +MTcwHhcNMTkxMjE4MjMwNjQ1WhcNNDIwNzE4MjMxNjA0WjBlMQswCQYDVQQGEwJV +UzEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMTYwNAYDVQQDEy1NaWNy +b3NvZnQgRUNDIFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5IDIwMTcwdjAQBgcq +hkjOPQIBBgUrgQQAIgNiAATUvD0CQnVBEyPNgASGAlEvaqiBYgtlzPbKnR5vSmZR +ogPZnZH6thaxjG7efM3beaYvzrvOcS/lpaso7GMEZpn4+vKTEAXhgShC48Zo9OYb +hGBKia/teQ87zvH2RPUBeMCjVDBSMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8E +BTADAQH/MB0GA1UdDgQWBBTIy5lycFIM+Oa+sgRXKSrPQhDtNTAQBgkrBgEEAYI3 +FQEEAwIBADAKBggqhkjOPQQDAwNoADBlAjBY8k3qDPlfXu5gKcs68tvWMoQZP3zV +L8KxzJOuULsJMsbG7X7JNpQS5GiFBqIb0C8CMQCZ6Ra0DvpWSNSkMBaReNtUjGUB +iudQZsIxtzm6uBoiB078a1QWIP8rtedMDE2mT3M= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFqDCCA5CgAwIBAgIQHtOXCV/YtLNHcB6qvn9FszANBgkqhkiG9w0BAQwFADBl +MQswCQYDVQQGEwJVUzEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMTYw +NAYDVQQDEy1NaWNyb3NvZnQgUlNBIFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5 +IDIwMTcwHhcNMTkxMjE4MjI1MTIyWhcNNDIwNzE4MjMwMDIzWjBlMQswCQYDVQQG +EwJVUzEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMTYwNAYDVQQDEy1N +aWNyb3NvZnQgUlNBIFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5IDIwMTcwggIi +MA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDKW76UM4wplZEWCpW9R2LBifOZ +Nt9GkMml7Xhqb0eRaPgnZ1AzHaGm++DlQ6OEAlcBXZxIQIJTELy/xztokLaCLeX0 +ZdDMbRnMlfl7rEqUrQ7eS0MdhweSE5CAg2Q1OQT85elss7YfUJQ4ZVBcF0a5toW1 +HLUX6NZFndiyJrDKxHBKrmCk3bPZ7Pw71VdyvD/IybLeS2v4I2wDwAW9lcfNcztm +gGTjGqwu+UcF8ga2m3P1eDNbx6H7JyqhtJqRjJHTOoI+dkC0zVJhUXAoP8XFWvLJ +jEm7FFtNyP9nTUwSlq31/niol4fX/V4ggNyhSyL71Imtus5Hl0dVe49FyGcohJUc +aDDv70ngNXtk55iwlNpNhTs+VcQor1fznhPbRiefHqJeRIOkpcrVE7NLP8TjwuaG +YaRSMLl6IE9vDzhTyzMMEyuP1pq9KsgtsRx9S1HKR9FIJ3Jdh+vVReZIZZ2vUpC6 +W6IYZVcSn2i51BVrlMRpIpj0M+Dt+VGOQVDJNE92kKz8OMHY4Xu54+OU4UZpyw4K +UGsTuqwPN1q3ErWQgR5WrlcihtnJ0tHXUeOrO8ZV/R4O03QK0dqq6mm4lyiPSMQH ++FJDOvTKVTUssKZqwJz58oHhEmrARdlns87/I6KJClTUFLkqqNfs+avNJVgyeY+Q +W5g5xAgGwax/Dj0ApQIDAQABo1QwUjAOBgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/ +BAUwAwEB/zAdBgNVHQ4EFgQUCctZf4aycI8awznjwNnpv7tNsiMwEAYJKwYBBAGC +NxUBBAMCAQAwDQYJKoZIhvcNAQEMBQADggIBAKyvPl3CEZaJjqPnktaXFbgToqZC +LgLNFgVZJ8og6Lq46BrsTaiXVq5lQ7GPAJtSzVXNUzltYkyLDVt8LkS/gxCP81OC +gMNPOsduET/m4xaRhPtthH80dK2Jp86519efhGSSvpWhrQlTM93uCupKUY5vVau6 +tZRGrox/2KJQJWVggEbbMwSubLWYdFQl3JPk+ONVFT24bcMKpBLBaYVu32TxU5nh +SnUgnZUP5NbcA/FZGOhHibJXWpS2qdgXKxdJ5XbLwVaZOjex/2kskZGT4d9Mozd2 +TaGf+G0eHdP67Pv0RR0Tbc/3WeUiJ3IrhvNXuzDtJE3cfVa7o7P4NHmJweDyAmH3 +pvwPuxwXC65B2Xy9J6P9LjrRk5Sxcx0ki69bIImtt2dmefU6xqaWM/5TkshGsRGR +xpl/j8nWZjEgQRCHLQzWwa80mMpkg/sTV9HB8Dx6jKXB/ZUhoHHBk2dxEuqPiApp +GWSZI1b7rCoucL5mxAyE7+WL85MB+GqQk2dLsmijtWKP6T+MejteD+eMuMZ87zf9 +dOLITzNy4ZQ5bb0Sr74MTnB8G2+NszKTc0QWbej09+CVgI+WXTik9KveCjCHk9hN +AHFiRSdLOkKEW39lt2c0Ui2cFmuqqNh7o0JMcccMyj6D5KbvtwEwXlGjefVwaaZB +RA+GsCyRxj3qrg+E +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFojCCA4qgAwIBAgIUAZQwHqIL3fXFMyqxQ0Rx+NZQTQ0wDQYJKoZIhvcNAQEM +BQAwaTELMAkGA1UEBhMCS1IxJjAkBgNVBAoMHU5BVkVSIEJVU0lORVNTIFBMQVRG +T1JNIENvcnAuMTIwMAYDVQQDDClOQVZFUiBHbG9iYWwgUm9vdCBDZXJ0aWZpY2F0 +aW9uIEF1dGhvcml0eTAeFw0xNzA4MTgwODU4NDJaFw0zNzA4MTgyMzU5NTlaMGkx +CzAJBgNVBAYTAktSMSYwJAYDVQQKDB1OQVZFUiBCVVNJTkVTUyBQTEFURk9STSBD +b3JwLjEyMDAGA1UEAwwpTkFWRVIgR2xvYmFsIFJvb3QgQ2VydGlmaWNhdGlvbiBB +dXRob3JpdHkwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC21PGTXLVA +iQqrDZBbUGOukJR0F0Vy1ntlWilLp1agS7gvQnXp2XskWjFlqxcX0TM62RHcQDaH +38dq6SZeWYp34+hInDEW+j6RscrJo+KfziFTowI2MMtSAuXaMl3Dxeb57hHHi8lE +HoSTGEq0n+USZGnQJoViAbbJAh2+g1G7XNr4rRVqmfeSVPc0W+m/6imBEtRTkZaz +kVrd/pBzKPswRrXKCAfHcXLJZtM0l/aM9BhK4dA9WkW2aacp+yPOiNgSnABIqKYP +szuSjXEOdMWLyEz59JuOuDxp7W87UC9Y7cSw0BwbagzivESq2M0UXZR4Yb8Obtoq +vC8MC3GmsxY/nOb5zJ9TNeIDoKAYv7vxvvTWjIcNQvcGufFt7QSUqP620wbGQGHf +nZ3zVHbOUzoBppJB7ASjjw2i1QnK1sua8e9DXcCrpUHPXFNwcMmIpi3Ua2FzUCaG +YQ5fG8Ir4ozVu53BA0K6lNpfqbDKzE0K70dpAy8i+/Eozr9dUGWokG2zdLAIx6yo +0es+nPxdGoMuK8u180SdOqcXYZaicdNwlhVNt0xz7hlcxVs+Qf6sdWA7G2POAN3a +CJBitOUt7kinaxeZVL6HSuOpXgRM6xBtVNbv8ejyYhbLgGvtPe31HzClrkvJE+2K +AQHJuFFYwGY6sWZLxNUxAmLpdIQM201GLQIDAQABo0IwQDAdBgNVHQ4EFgQU0p+I +36HNLL3s9TsBAZMzJ7LrYEswDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMB +Af8wDQYJKoZIhvcNAQEMBQADggIBADLKgLOdPVQG3dLSLvCkASELZ0jKbY7gyKoN +qo0hV4/GPnrK21HUUrPUloSlWGB/5QuOH/XcChWB5Tu2tyIvCZwTFrFsDDUIbatj +cu3cvuzHV+YwIHHW1xDBE1UBjCpD5EHxzzp6U5LOogMFDTjfArsQLtk70pt6wKGm ++LUx5vR1yblTmXVHIloUFcd4G7ad6Qz4G3bxhYTeodoS76TiEJd6eN4MUZeoIUCL +hr0N8F5OSza7OyAfikJW4Qsav3vQIkMsRIz75Sq0bBwcupTgE34h5prCy8VCZLQe +lHsIJchxzIdFV4XTnyliIoNRlwAYl3dqmJLJfGBs32x9SuRwTMKeuB330DTHD8z7 +p/8Dvq1wkNoL3chtl1+afwkyQf3NosxabUzyqkn+Zvjp2DXrDige7kgvOtB5CTh8 +piKCk5XQA76+AqAF3SAi428diDRgxuYKuQl1C/AH6GmWNcf7I4GOODm4RStDeKLR +LBT/DShycpWbXgnbiUSYqqFJu3FS8r/2/yehNq+4tneI3TqkbZs0kNwUXTC/t+sX +5Ie3cdCh13cV1ELX8vMxmV2b3RZtP+oGI/hGoiLtk/bdmuYqh7GYVPEi92tF4+KO +dh2ajcQGjTa3FPOdVGm3jjzVpG2Tgbet9r1ke8LJaDmgkpzNNIaRkPpkUZ3+/uul +9XXeifdy +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIF2jCCA8KgAwIBAgIMBfcOhtpJ80Y1LrqyMA0GCSqGSIb3DQEBCwUAMIGIMQsw +CQYDVQQGEwJVUzERMA8GA1UECAwISWxsaW5vaXMxEDAOBgNVBAcMB0NoaWNhZ28x +ITAfBgNVBAoMGFRydXN0d2F2ZSBIb2xkaW5ncywgSW5jLjExMC8GA1UEAwwoVHJ1 +c3R3YXZlIEdsb2JhbCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAeFw0xNzA4MjMx +OTM0MTJaFw00MjA4MjMxOTM0MTJaMIGIMQswCQYDVQQGEwJVUzERMA8GA1UECAwI +SWxsaW5vaXMxEDAOBgNVBAcMB0NoaWNhZ28xITAfBgNVBAoMGFRydXN0d2F2ZSBI +b2xkaW5ncywgSW5jLjExMC8GA1UEAwwoVHJ1c3R3YXZlIEdsb2JhbCBDZXJ0aWZp +Y2F0aW9uIEF1dGhvcml0eTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIB +ALldUShLPDeS0YLOvR29zd24q88KPuFd5dyqCblXAj7mY2Hf8g+CY66j96xz0Xzn +swuvCAAJWX/NKSqIk4cXGIDtiLK0thAfLdZfVaITXdHG6wZWiYj+rDKd/VzDBcdu +7oaJuogDnXIhhpCujwOl3J+IKMujkkkP7NAP4m1ET4BqstTnoApTAbqOl5F2brz8 +1Ws25kCI1nsvXwXoLG0R8+eyvpJETNKXpP7ScoFDB5zpET71ixpZfR9oWN0EACyW +80OzfpgZdNmcc9kYvkHHNHnZ9GLCQ7mzJ7Aiy/k9UscwR7PJPrhq4ufogXBeQotP +JqX+OsIgbrv4Fo7NDKm0G2x2EOFYeUY+VM6AqFcJNykbmROPDMjWLBz7BegIlT1l +RtzuzWniTY+HKE40Cz7PFNm73bZQmq131BnW2hqIyE4bJ3XYsgjxroMwuREOzYfw +hI0Vcnyh78zyiGG69Gm7DIwLdVcEuE4qFC49DxweMqZiNu5m4iK4BUBjECLzMx10 +coos9TkpoNPnG4CELcU9402x/RpvumUHO1jsQkUm+9jaJXLE9gCxInm943xZYkqc +BW89zubWR2OZxiRvchLIrH+QtAuRcOi35hYQcRfO3gZPSEF9NUqjifLJS3tBEW1n +twiYTOURGa5CgNz7kAXU+FDKvuStx8KU1xad5hePrzb7AgMBAAGjQjBAMA8GA1Ud +EwEB/wQFMAMBAf8wHQYDVR0OBBYEFJngGWcNYtt2s9o9uFvo/ULSMQ6HMA4GA1Ud +DwEB/wQEAwIBBjANBgkqhkiG9w0BAQsFAAOCAgEAmHNw4rDT7TnsTGDZqRKGFx6W +0OhUKDtkLSGm+J1WE2pIPU/HPinbbViDVD2HfSMF1OQc3Og4ZYbFdada2zUFvXfe +uyk3QAUHw5RSn8pk3fEbK9xGChACMf1KaA0HZJDmHvUqoai7PF35owgLEQzxPy0Q +lG/+4jSHg9bP5Rs1bdID4bANqKCqRieCNqcVtgimQlRXtpla4gt5kNdXElE1GYhB +aCXUNxeEFfsBctyV3lImIJgm4nb1J2/6ADtKYdkNy1GTKv0WBpanI5ojSP5RvbbE +sLFUzt5sQa0WZ37b/TjNuThOssFgy50X31ieemKyJo90lZvkWx3SD92YHJtZuSPT +MaCm/zjdzyBP6VhWOmfD0faZmZ26NraAL4hHT4a/RDqA5Dccprrql5gR0IRiR2Qe +qu5AvzSxnI9O4fKSTx+O856X3vOmeWqJcU9LJxdI/uz0UA9PSX3MReO9ekDFQdxh +VicGaeVyQYHTtgGJoC86cnn+OjC/QezHYj6RS8fZMXZC+fc8Y+wmjHMMfRod6qh8 +h6jCJ3zhM0EPz8/8AKAigJ5Kp28AsEFFtyLKaEjFQqKu3R3y4G5OBVixwJAWKqQ9 +EEC+j2Jjg6mcgn0tAumDMHzLJ8n9HmYAsC7TIS+OMxZsmO0QqAfWzJPP29FpHOTK +yeC2nOnOcXHebD8WpHk= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICYDCCAgegAwIBAgIMDWpfCD8oXD5Rld9dMAoGCCqGSM49BAMCMIGRMQswCQYD +VQQGEwJVUzERMA8GA1UECBMISWxsaW5vaXMxEDAOBgNVBAcTB0NoaWNhZ28xITAf +BgNVBAoTGFRydXN0d2F2ZSBIb2xkaW5ncywgSW5jLjE6MDgGA1UEAxMxVHJ1c3R3 +YXZlIEdsb2JhbCBFQ0MgUDI1NiBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAeFw0x +NzA4MjMxOTM1MTBaFw00MjA4MjMxOTM1MTBaMIGRMQswCQYDVQQGEwJVUzERMA8G +A1UECBMISWxsaW5vaXMxEDAOBgNVBAcTB0NoaWNhZ28xITAfBgNVBAoTGFRydXN0 +d2F2ZSBIb2xkaW5ncywgSW5jLjE6MDgGA1UEAxMxVHJ1c3R3YXZlIEdsb2JhbCBF +Q0MgUDI1NiBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTBZMBMGByqGSM49AgEGCCqG +SM49AwEHA0IABH77bOYj43MyCMpg5lOcunSNGLB4kFKA3TjASh3RqMyTpJcGOMoN +FWLGjgEqZZ2q3zSRLoHB5DOSMcT9CTqmP62jQzBBMA8GA1UdEwEB/wQFMAMBAf8w +DwYDVR0PAQH/BAUDAwcGADAdBgNVHQ4EFgQUo0EGrJBt0UrrdaVKEJmzsaGLSvcw +CgYIKoZIzj0EAwIDRwAwRAIgB+ZU2g6gWrKuEZ+Hxbb/ad4lvvigtwjzRM4q3wgh +DDcCIC0mA6AFvWvR9lz4ZcyGbbOcNEhjhAnFjXca4syc4XR7 +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICnTCCAiSgAwIBAgIMCL2Fl2yZJ6SAaEc7MAoGCCqGSM49BAMDMIGRMQswCQYD +VQQGEwJVUzERMA8GA1UECBMISWxsaW5vaXMxEDAOBgNVBAcTB0NoaWNhZ28xITAf +BgNVBAoTGFRydXN0d2F2ZSBIb2xkaW5ncywgSW5jLjE6MDgGA1UEAxMxVHJ1c3R3 +YXZlIEdsb2JhbCBFQ0MgUDM4NCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAeFw0x +NzA4MjMxOTM2NDNaFw00MjA4MjMxOTM2NDNaMIGRMQswCQYDVQQGEwJVUzERMA8G +A1UECBMISWxsaW5vaXMxEDAOBgNVBAcTB0NoaWNhZ28xITAfBgNVBAoTGFRydXN0 +d2F2ZSBIb2xkaW5ncywgSW5jLjE6MDgGA1UEAxMxVHJ1c3R3YXZlIEdsb2JhbCBF +Q0MgUDM4NCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTB2MBAGByqGSM49AgEGBSuB +BAAiA2IABGvaDXU1CDFHBa5FmVXxERMuSvgQMSOjfoPTfygIOiYaOs+Xgh+AtycJ +j9GOMMQKmw6sWASr9zZ9lCOkmwqKi6vr/TklZvFe/oyujUF5nQlgziip04pt89ZF +1PKYhDhloKNDMEEwDwYDVR0TAQH/BAUwAwEB/zAPBgNVHQ8BAf8EBQMDBwYAMB0G +A1UdDgQWBBRVqYSJ0sEyvRjLbKYHTsjnnb6CkDAKBggqhkjOPQQDAwNnADBkAjA3 +AZKXRRJ+oPM+rRk6ct30UJMDEr5E0k9BpIycnR+j9sKS50gU/k6bpZFXrsY3crsC +MGclCrEMXu6pY5Jv5ZAL/mYiykf9ijH3g/56vxC+GCsej/YpHpRZ744hN8tRmKVu +Sw== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFWjCCA0KgAwIBAgIQT9Irj/VkyDOeTzRYZiNwYDANBgkqhkiG9w0BAQsFADBH +MQswCQYDVQQGEwJDTjERMA8GA1UECgwIVW5pVHJ1c3QxJTAjBgNVBAMMHFVDQSBF +eHRlbmRlZCBWYWxpZGF0aW9uIFJvb3QwHhcNMTUwMzEzMDAwMDAwWhcNMzgxMjMx +MDAwMDAwWjBHMQswCQYDVQQGEwJDTjERMA8GA1UECgwIVW5pVHJ1c3QxJTAjBgNV +BAMMHFVDQSBFeHRlbmRlZCBWYWxpZGF0aW9uIFJvb3QwggIiMA0GCSqGSIb3DQEB +AQUAA4ICDwAwggIKAoICAQCpCQcoEwKwmeBkqh5DFnpzsZGgdT6o+uM4AHrsiWog +D4vFsJszA1qGxliG1cGFu0/GnEBNyr7uaZa4rYEwmnySBesFK5pI0Lh2PpbIILvS +sPGP2KxFRv+qZ2C0d35qHzwaUnoEPQc8hQ2E0B92CvdqFN9y4zR8V05WAT558aop +O2z6+I9tTcg1367r3CTueUWnhbYFiN6IXSV8l2RnCdm/WhUFhvMJHuxYMjMR83dk +sHYf5BA1FxvyDrFspCqjc/wJHx4yGVMR59mzLC52LqGj3n5qiAno8geK+LLNEOfi +c0CTuwjRP+H8C5SzJe98ptfRr5//lpr1kXuYC3fUfugH0mK1lTnj8/FtDw5lhIpj +VMWAtuCeS31HJqcBCF3RiJ7XwzJE+oJKCmhUfzhTA8ykADNkUVkLo4KRel7sFsLz +KuZi2irbWWIQJUoqgQtHB0MGcIfS+pMRKXpITeuUx3BNr2fVUbGAIAEBtHoIppB/ +TuDvB0GHr2qlXov7z1CymlSvw4m6WC31MJixNnI5fkkE/SmnTHnkBVfblLkWU41G +sx2VYVdWf6/wFlthWG82UBEL2KwrlRYaDh8IzTY0ZRBiZtWAXxQgXy0MoHgKaNYs +1+lvK9JKBZP8nm9rZ/+I8U6laUpSNwXqxhaN0sSZ0YIrO7o1dfdRUVjzyAfd5LQD +fwIDAQABo0IwQDAdBgNVHQ4EFgQU2XQ65DA9DfcS3H5aBZ8eNJr34RQwDwYDVR0T +AQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAYYwDQYJKoZIhvcNAQELBQADggIBADaN +l8xCFWQpN5smLNb7rhVpLGsaGvdftvkHTFnq88nIua7Mui563MD1sC3AO6+fcAUR +ap8lTwEpcOPlDOHqWnzcSbvBHiqB9RZLcpHIojG5qtr8nR/zXUACE/xOHAbKsxSQ +VBcZEhrxH9cMaVr2cXj0lH2RC47skFSOvG+hTKv8dGT9cZr4QQehzZHkPJrgmzI5 +c6sq1WnIeJEmMX3ixzDx/BR4dxIOE/TdFpS/S2d7cFOFyrC78zhNLJA5wA3CXWvp +4uXViI3WLL+rG761KIcSF3Ru/H38j9CHJrAb+7lsq+KePRXBOy5nAliRn+/4Qh8s +t2j1da3Ptfb/EX3C8CSlrdP6oDyp+l3cpaDvRKS+1ujl5BOWF3sGPjLtx7dCvHaj +2GU4Kzg1USEODm8uNBNA4StnDG1KQTAYI1oyVZnJF+A83vbsea0rWBmirSwiGpWO +vpaQXUJXxPkUAzUrHC1RVwinOt4/5Mi0A3PCwSaAuwtCH60NryZy2sy+s6ODWA2C +xR9GUeOcGMyNm43sSet1UNWMKFnKdDTajAshqx7qG+XH/RU+wBeq+yNuJkbL+vmx +cmtpzyKEC2IPrNkZAJSidjzULZrtBJ4tBmIQN1IchXIbJ+XMxjHsN+xjWZsLHXbM +fjKaiJUINlK73nZfdklJrX+9ZSCyycErdhh2n1ax +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFRjCCAy6gAwIBAgIQXd+x2lqj7V2+WmUgZQOQ7zANBgkqhkiG9w0BAQsFADA9 +MQswCQYDVQQGEwJDTjERMA8GA1UECgwIVW5pVHJ1c3QxGzAZBgNVBAMMElVDQSBH +bG9iYWwgRzIgUm9vdDAeFw0xNjAzMTEwMDAwMDBaFw00MDEyMzEwMDAwMDBaMD0x +CzAJBgNVBAYTAkNOMREwDwYDVQQKDAhVbmlUcnVzdDEbMBkGA1UEAwwSVUNBIEds +b2JhbCBHMiBSb290MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAxeYr +b3zvJgUno4Ek2m/LAfmZmqkywiKHYUGRO8vDaBsGxUypK8FnFyIdK+35KYmToni9 +kmugow2ifsqTs6bRjDXVdfkX9s9FxeV67HeToI8jrg4aA3++1NDtLnurRiNb/yzm +VHqUwCoV8MmNsHo7JOHXaOIxPAYzRrZUEaalLyJUKlgNAQLx+hVRZ2zA+te2G3/R +VogvGjqNO7uCEeBHANBSh6v7hn4PJGtAnTRnvI3HLYZveT6OqTwXS3+wmeOwcWDc +C/Vkw85DvG1xudLeJ1uK6NjGruFZfc8oLTW4lVYa8bJYS7cSN8h8s+1LgOGN+jIj +tm+3SJUIsUROhYw6AlQgL9+/V087OpAh18EmNVQg7Mc/R+zvWr9LesGtOxdQXGLY +D0tK3Cv6brxzks3sx1DoQZbXqX5t2Okdj4q1uViSukqSKwxW/YDrCPBeKW4bHAyv +j5OJrdu9o54hyokZ7N+1wxrrFv54NkzWbtA+FxyQF2smuvt6L78RHBgOLXMDj6Dl +NaBa4kx1HXHhOThTeEDMg5PXCp6dW4+K5OXgSORIskfNTip1KnvyIvbJvgmRlld6 +iIis7nCs+dwp4wwcOxJORNanTrAmyPPZGpeRaOrvjUYG0lZFWJo8DA+DuAUlwznP +O6Q0ibd5Ei9Hxeepl2n8pndntd978XplFeRhVmUCAwEAAaNCMEAwDgYDVR0PAQH/ +BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFIHEjMz15DD/pQwIX4wV +ZyF0Ad/fMA0GCSqGSIb3DQEBCwUAA4ICAQATZSL1jiutROTL/7lo5sOASD0Ee/oj +L3rtNtqyzm325p7lX1iPyzcyochltq44PTUbPrw7tgTQvPlJ9Zv3hcU2tsu8+Mg5 +1eRfB70VVJd0ysrtT7q6ZHafgbiERUlMjW+i67HM0cOU2kTC5uLqGOiiHycFutfl +1qnN3e92mI0ADs0b+gO3joBYDic/UvuUospeZcnWhNq5NXHzJsBPd+aBJ9J3O5oU +b3n09tDh05S60FdRvScFDcH9yBIw7m+NESsIndTUv4BFFJqIRNow6rSn4+7vW4LV +PtateJLbXDzz2K36uGt/xDYotgIVilQsnLAXc47QN6MUPJiVAAwpBVueSUmxX8fj +y88nZY41F7dXyDDZQVu5FLbowg+UMaeUmMxq67XhJ/UQqAHojhJi6IjMtX9Gl8Cb +EGY4GjZGXyJoPd/JxhMnq1MGrKI8hgZlb7F+sSlEmqO6SWkoaY/X5V+tBIZkbxqg +DMUIYs6Ao9Dz7GjevjPHF1t/gMRMTLGmhIrDO7gJzRSBuhjjVFc2/tsvfEehOjPI ++Vg7RE+xygKJBJYoaMVLuCaJu9YzL1DV/pqJuhgyklTGW+Cd+V7lDSKb9triyCGy +YiGqhkCyLmTTX8jjfhFnRR8F/uOi77Oos/N9j/gMHyIfLXC0uAE0djAA5SN4p1bX +UB+K+wb1whnw0A== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFRzCCAy+gAwIBAgIJEQA0tk7GNi02MA0GCSqGSIb3DQEBCwUAMEExCzAJBgNV +BAYTAlJPMRQwEgYDVQQKEwtDRVJUU0lHTiBTQTEcMBoGA1UECxMTY2VydFNJR04g +Uk9PVCBDQSBHMjAeFw0xNzAyMDYwOTI3MzVaFw00MjAyMDYwOTI3MzVaMEExCzAJ +BgNVBAYTAlJPMRQwEgYDVQQKEwtDRVJUU0lHTiBTQTEcMBoGA1UECxMTY2VydFNJ +R04gUk9PVCBDQSBHMjCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAMDF +dRmRfUR0dIf+DjuW3NgBFszuY5HnC2/OOwppGnzC46+CjobXXo9X69MhWf05N0Iw +vlDqtg+piNguLWkh59E3GE59kdUWX2tbAMI5Qw02hVK5U2UPHULlj88F0+7cDBrZ +uIt4ImfkabBoxTzkbFpG583H+u/E7Eu9aqSs/cwoUe+StCmrqzWaTOTECMYmzPhp +n+Sc8CnTXPnGFiWeI8MgwT0PPzhAsP6CRDiqWhqKa2NYOLQV07YRaXseVO6MGiKs +cpc/I1mbySKEwQdPzH/iV8oScLumZfNpdWO9lfsbl83kqK/20U6o2YpxJM02PbyW +xPFsqa7lzw1uKA2wDrXKUXt4FMMgL3/7FFXhEZn91QqhngLjYl/rNUssuHLoPj1P +rCy7Lobio3aP5ZMqz6WryFyNSwb/EkaseMsUBzXgqd+L6a8VTxaJW732jcZZroiF +DsGJ6x9nxUWO/203Nit4ZoORUSs9/1F3dmKh7Gc+PoGD4FapUB8fepmrY7+EF3fx +DTvf95xhszWYijqy7DwaNz9+j5LP2RIUZNoQAhVB/0/E6xyjyfqZ90bp4RjZsbgy +LcsUDFDYg2WD7rlcz8sFWkz6GZdr1l0T08JcVLwyc6B49fFtHsufpaafItzRUZ6C +eWRgKRM+o/1Pcmqr4tTluCRVLERLiohEnMqE0yo7AgMBAAGjQjBAMA8GA1UdEwEB +/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBSCIS1mxteg4BXrzkwJ +d8RgnlRuAzANBgkqhkiG9w0BAQsFAAOCAgEAYN4auOfyYILVAzOBywaK8SJJ6ejq +kX/GM15oGQOGO0MBzwdw5AgeZYWR5hEit/UCI46uuR59H35s5r0l1ZUa8gWmr4UC +b6741jH/JclKyMeKqdmfS0mbEVeZkkMR3rYzpMzXjWR91M08KCy0mpbqTfXERMQl +qiCA2ClV9+BB/AYm/7k29UMUA2Z44RGx2iBfRgB4ACGlHgAoYXhvqAEBj500mv/0 +OJD7uNGzcgbJceaBxXntC6Z58hMLnPddDnskk7RI24Zf3lCGeOdA5jGokHZwYa+c +NywRtYK3qq4kNFtyDGkNzVmf9nGvnAvRCjj5BiKDUyUM/FHE5r7iOZULJK2v0ZXk +ltd0ZGtxTgI8qoXzIKNDOXZbbFD+mpwUHmUUihW9o4JFWklWatKcsWMy5WHgUyIO +pwpJ6st+H6jiYoD2EEVSmAYY3qXNL3+q1Ok+CHLsIwMCPKaq2LxndD0UF/tUSxfj +03k9bWtJySgOLnRQvwzZRjoQhsmnP+mg7H/rpXdYaXHmgwo38oZJar55CJD2AhZk +PuXaTH4MNMn5X7azKFGnpyuqSfqNZSlO42sTp5SjLVFteAxEy9/eCG/Oo2Sr05WE +1LlSVHJ7liXMvGnjSG4N0MedJ5qq+BOS3R7fY581qRY27Iy4g/Q9iY/NtBde17MX +QRBdJ3NghVdJIgc= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICQDCCAeWgAwIBAgIMAVRI7yH9l1kN9QQKMAoGCCqGSM49BAMCMHExCzAJBgNV +BAYTAkhVMREwDwYDVQQHDAhCdWRhcGVzdDEWMBQGA1UECgwNTWljcm9zZWMgTHRk +LjEXMBUGA1UEYQwOVkFUSFUtMjM1ODQ0OTcxHjAcBgNVBAMMFWUtU3ppZ25vIFJv +b3QgQ0EgMjAxNzAeFw0xNzA4MjIxMjA3MDZaFw00MjA4MjIxMjA3MDZaMHExCzAJ +BgNVBAYTAkhVMREwDwYDVQQHDAhCdWRhcGVzdDEWMBQGA1UECgwNTWljcm9zZWMg +THRkLjEXMBUGA1UEYQwOVkFUSFUtMjM1ODQ0OTcxHjAcBgNVBAMMFWUtU3ppZ25v +IFJvb3QgQ0EgMjAxNzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABJbcPYrYsHtv +xie+RJCxs1YVe45DJH0ahFnuY2iyxl6H0BVIHqiQrb1TotreOpCmYF9oMrWGQd+H +Wyx7xf58etqjYzBhMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0G +A1UdDgQWBBSHERUI0arBeAyxr87GyZDvvzAEwDAfBgNVHSMEGDAWgBSHERUI0arB +eAyxr87GyZDvvzAEwDAKBggqhkjOPQQDAgNJADBGAiEAtVfd14pVCzbhhkT61Nlo +jbjcI4qKDdQvfepz7L9NbKgCIQDLpbQS+ue16M9+k/zzNY9vTlp8tLxOsvxyqltZ ++efcMQ== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICKzCCAbGgAwIBAgIKe3G2gla4EnycqDAKBggqhkjOPQQDAzBaMQswCQYDVQQG +EwJVUzETMBEGA1UECxMKZW1TaWduIFBLSTEUMBIGA1UEChMLZU11ZGhyYSBJbmMx +IDAeBgNVBAMTF2VtU2lnbiBFQ0MgUm9vdCBDQSAtIEMzMB4XDTE4MDIxODE4MzAw +MFoXDTQzMDIxODE4MzAwMFowWjELMAkGA1UEBhMCVVMxEzARBgNVBAsTCmVtU2ln +biBQS0kxFDASBgNVBAoTC2VNdWRocmEgSW5jMSAwHgYDVQQDExdlbVNpZ24gRUND +IFJvb3QgQ0EgLSBDMzB2MBAGByqGSM49AgEGBSuBBAAiA2IABP2lYa57JhAd6bci +MK4G9IGzsUJxlTm801Ljr6/58pc1kjZGDoeVjbk5Wum739D+yAdBPLtVb4Ojavti +sIGJAnB9SMVK4+kiVCJNk7tCDK93nCOmfddhEc5lx/h//vXyqaNCMEAwHQYDVR0O +BBYEFPtaSNCAIEDyqOkAB2kZd6fmw/TPMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMB +Af8EBTADAQH/MAoGCCqGSM49BAMDA2gAMGUCMQC02C8Cif22TGK6Q04ThHK1rt0c +3ta13FaPWEBaLd4gTCKDypOofu4SQMfWh0/434UCMBwUZOR8loMRnLDRWmFLpg9J +0wD8ofzkpf9/rdcw0Md3f76BB1UwUCAU9Vc4CqgxUQ== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICTjCCAdOgAwIBAgIKPPYHqWhwDtqLhDAKBggqhkjOPQQDAzBrMQswCQYDVQQG +EwJJTjETMBEGA1UECxMKZW1TaWduIFBLSTElMCMGA1UEChMcZU11ZGhyYSBUZWNo +bm9sb2dpZXMgTGltaXRlZDEgMB4GA1UEAxMXZW1TaWduIEVDQyBSb290IENBIC0g +RzMwHhcNMTgwMjE4MTgzMDAwWhcNNDMwMjE4MTgzMDAwWjBrMQswCQYDVQQGEwJJ +TjETMBEGA1UECxMKZW1TaWduIFBLSTElMCMGA1UEChMcZU11ZGhyYSBUZWNobm9s +b2dpZXMgTGltaXRlZDEgMB4GA1UEAxMXZW1TaWduIEVDQyBSb290IENBIC0gRzMw +djAQBgcqhkjOPQIBBgUrgQQAIgNiAAQjpQy4LRL1KPOxst3iAhKAnjlfSU2fySU0 +WXTsuwYc58Byr+iuL+FBVIcUqEqy6HyC5ltqtdyzdc6LBtCGI79G1Y4PPwT01xyS +fvalY8L1X44uT6EYGQIrMgqCZH0Wk9GjQjBAMB0GA1UdDgQWBBR8XQKEE9TMipuB +zhccLikenEhjQjAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAKBggq +hkjOPQQDAwNpADBmAjEAvvNhzwIQHWSVB7gYboiFBS+DCBeQyh+KTOgNG3qxrdWB +CUfvO6wIBHxcmbHtRwfSAjEAnbpV/KlK6O3t5nYBQnvI+GDZjVGLVTv7jHvrZQnD ++JbNR6iC8hZVdyR+EhCVBCyj +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIDczCCAlugAwIBAgILAK7PALrEzzL4Q7IwDQYJKoZIhvcNAQELBQAwVjELMAkG +A1UEBhMCVVMxEzARBgNVBAsTCmVtU2lnbiBQS0kxFDASBgNVBAoTC2VNdWRocmEg +SW5jMRwwGgYDVQQDExNlbVNpZ24gUm9vdCBDQSAtIEMxMB4XDTE4MDIxODE4MzAw +MFoXDTQzMDIxODE4MzAwMFowVjELMAkGA1UEBhMCVVMxEzARBgNVBAsTCmVtU2ln +biBQS0kxFDASBgNVBAoTC2VNdWRocmEgSW5jMRwwGgYDVQQDExNlbVNpZ24gUm9v +dCBDQSAtIEMxMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAz+upufGZ +BczYKCFK83M0UYRWEPWgTywS4/oTmifQz/l5GnRfHXk5/Fv4cI7gklL35CX5VIPZ +HdPIWoU/Xse2B+4+wM6ar6xWQio5JXDWv7V7Nq2s9nPczdcdioOl+yuQFTdrHCZH +3DspVpNqs8FqOp099cGXOFgFixwR4+S0uF2FHYP+eF8LRWgYSKVGczQ7/g/IdrvH +GPMF0Ybzhe3nudkyrVWIzqa2kbBPrH4VI5b2P/AgNBbeCsbEBEV5f6f9vtKppa+c +xSMq9zwhbL2vj07FOrLzNBL834AaSaTUqZX3noleoomslMuoaJuvimUnzYnu3Yy1 +aylwQ6BpC+S5DwIDAQABo0IwQDAdBgNVHQ4EFgQU/qHgcB4qAzlSWkK+XJGFehiq +TbUwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEL +BQADggEBAMJKVvoVIXsoounlHfv4LcQ5lkFMOycsxGwYFYDGrK9HWS8mC+M2sO87 +/kOXSTKZEhVb3xEp/6tT+LvBeA+snFOvV71ojD1pM/CjoCNjO2RnIkSt1XHLVip4 +kqNPEjE2NuLe/gDEo2APJ62gsIq1NnpSob0n9CAnYuhNlCQT5AoE6TyrLshDCUrG +YQTlSTR+08TI9Q/Aqum6VF7zYytPT1DU/rl7mYw9wC68AivTxEDkigcxHpvOJpkT ++xHqmiIMERnHXhuBUDDIlhJu58tBf5E7oke3VIAb3ADMmpDqw8NQBmIMMMAVSKeo +WXzhriKi4gp6D/piq1JM4fHfyr6DDUI= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIDlDCCAnygAwIBAgIKMfXkYgxsWO3W2DANBgkqhkiG9w0BAQsFADBnMQswCQYD +VQQGEwJJTjETMBEGA1UECxMKZW1TaWduIFBLSTElMCMGA1UEChMcZU11ZGhyYSBU +ZWNobm9sb2dpZXMgTGltaXRlZDEcMBoGA1UEAxMTZW1TaWduIFJvb3QgQ0EgLSBH +MTAeFw0xODAyMTgxODMwMDBaFw00MzAyMTgxODMwMDBaMGcxCzAJBgNVBAYTAklO +MRMwEQYDVQQLEwplbVNpZ24gUEtJMSUwIwYDVQQKExxlTXVkaHJhIFRlY2hub2xv +Z2llcyBMaW1pdGVkMRwwGgYDVQQDExNlbVNpZ24gUm9vdCBDQSAtIEcxMIIBIjAN +BgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAk0u76WaK7p1b1TST0Bsew+eeuGQz +f2N4aLTNLnF115sgxk0pvLZoYIr3IZpWNVrzdr3YzZr/k1ZLpVkGoZM0Kd0WNHVO +8oG0x5ZOrRkVUkr+PHB1cM2vK6sVmjM8qrOLqs1D/fXqcP/tzxE7lM5OMhbTI0Aq +d7OvPAEsbO2ZLIvZTmmYsvePQbAyeGHWDV/D+qJAkh1cF+ZwPjXnorfCYuKrpDhM +tTk1b+oDafo6VGiFbdbyL0NVHpENDtjVaqSW0RM8LHhQ6DqS0hdW5TUaQBw+jSzt +Od9C4INBdN+jzcKGYEho42kLVACL5HZpIQ15TjQIXhTCzLG3rdd8cIrHhQIDAQAB +o0IwQDAdBgNVHQ4EFgQU++8Nhp6w492pufEhF38+/PB3KxowDgYDVR0PAQH/BAQD +AgEGMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAFn/8oz1h31x +PaOfG1vR2vjTnGs2vZupYeveFix0PZ7mddrXuqe8QhfnPZHr5X3dPpzxz5KsbEjM +wiI/aTvFthUvozXGaCocV685743QNcMYDHsAVhzNixl03r4PEuDQqqE/AjSxcM6d +GNYIAwlG7mDgfrbESQRRfXBgvKqy/3lyeqYdPV8q+Mri/Tm3R7nrft8EI6/6nAYH +6ftjk4BAtcZsCjEozgyfz7MjNYBBjWzEN3uBL4ChQEKF6dk4jeihU80Bv2noWgby +RQuQ+q7hv53yrlc8pa6yVvSLZUDp/TGBLPQ5Cdjua6e0ph0VpZj3AYHYhX3zUVxx +iN66zB+Afko= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICbjCCAfOgAwIBAgIQYvYybOXE42hcG2LdnC6dlTAKBggqhkjOPQQDAzB4MQsw +CQYDVQQGEwJFUzERMA8GA1UECgwIRk5NVC1SQ00xDjAMBgNVBAsMBUNlcmVzMRgw +FgYDVQRhDA9WQVRFUy1RMjgyNjAwNEoxLDAqBgNVBAMMI0FDIFJBSVogRk5NVC1S +Q00gU0VSVklET1JFUyBTRUdVUk9TMB4XDTE4MTIyMDA5MzczM1oXDTQzMTIyMDA5 +MzczM1oweDELMAkGA1UEBhMCRVMxETAPBgNVBAoMCEZOTVQtUkNNMQ4wDAYDVQQL +DAVDZXJlczEYMBYGA1UEYQwPVkFURVMtUTI4MjYwMDRKMSwwKgYDVQQDDCNBQyBS +QUlaIEZOTVQtUkNNIFNFUlZJRE9SRVMgU0VHVVJPUzB2MBAGByqGSM49AgEGBSuB +BAAiA2IABPa6V1PIyqvfNkpSIeSX0oNnnvBlUdBeh8dHsVnyV0ebAAKTRBdp20LH +sbI6GA60XYyzZl2hNPk2LEnb80b8s0RpRBNm/dfF/a82Tc4DTQdxz69qBdKiQ1oK +Um8BA06Oi6NCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYD +VR0OBBYEFAG5L++/EYZg8k/QQW6rcx/n0m5JMAoGCCqGSM49BAMDA2kAMGYCMQCu +SuMrQMN0EfKVrRYj3k4MGuZdpSRea0R7/DjiT8ucRRcRTBQnJlU5dUoDzBOQn5IC +MQD6SmxgiHPz7riYYqnOK8LZiqZwMR2vsJRM60/G49HzYqc8/5MuB1xJAWdpEgJy +v+c= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIF7zCCA9egAwIBAgIIDdPjvGz5a7EwDQYJKoZIhvcNAQELBQAwgYQxEjAQBgNV +BAUTCUc2MzI4NzUxMDELMAkGA1UEBhMCRVMxJzAlBgNVBAoTHkFORiBBdXRvcmlk +YWQgZGUgQ2VydGlmaWNhY2lvbjEUMBIGA1UECxMLQU5GIENBIFJhaXoxIjAgBgNV +BAMTGUFORiBTZWN1cmUgU2VydmVyIFJvb3QgQ0EwHhcNMTkwOTA0MTAwMDM4WhcN +MzkwODMwMTAwMDM4WjCBhDESMBAGA1UEBRMJRzYzMjg3NTEwMQswCQYDVQQGEwJF +UzEnMCUGA1UEChMeQU5GIEF1dG9yaWRhZCBkZSBDZXJ0aWZpY2FjaW9uMRQwEgYD +VQQLEwtBTkYgQ0EgUmFpejEiMCAGA1UEAxMZQU5GIFNlY3VyZSBTZXJ2ZXIgUm9v +dCBDQTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBANvrayvmZFSVgpCj +cqQZAZ2cC4Ffc0m6p6zzBE57lgvsEeBbphzOG9INgxwruJ4dfkUyYA8H6XdYfp9q +yGFOtibBTI3/TO80sh9l2Ll49a2pcbnvT1gdpd50IJeh7WhM3pIXS7yr/2WanvtH +2Vdy8wmhrnZEE26cLUQ5vPnHO6RYPUG9tMJJo8gN0pcvB2VSAKduyK9o7PQUlrZX +H1bDOZ8rbeTzPvY1ZNoMHKGESy9LS+IsJJ1tk0DrtSOOMspvRdOoiXsezx76W0OL +zc2oD2rKDF65nkeP8Nm2CgtYZRczuSPkdxl9y0oukntPLxB3sY0vaJxizOBQ+OyR +p1RMVwnVdmPF6GUe7m1qzwmd+nxPrWAI/VaZDxUse6mAq4xhj0oHdkLePfTdsiQz +W7i1o0TJrH93PB0j7IKppuLIBkwC/qxcmZkLLxCKpvR/1Yd0DVlJRfbwcVw5Kda/ +SiOL9V8BY9KHcyi1Swr1+KuCLH5zJTIdC2MKF4EA/7Z2Xue0sUDKIbvVgFHlSFJn +LNJhiQcND85Cd8BEc5xEUKDbEAotlRyBr+Qc5RQe8TZBAQIvfXOn3kLMTOmJDVb3 +n5HUA8ZsyY/b2BzgQJhdZpmYgG4t/wHFzstGH6wCxkPmrqKEPMVOHj1tyRRM4y5B +u8o5vzY8KhmqQYdOpc5LMnndkEl/AgMBAAGjYzBhMB8GA1UdIwQYMBaAFJxf0Gxj +o1+TypOYCK2Mh6UsXME3MB0GA1UdDgQWBBScX9BsY6Nfk8qTmAitjIelLFzBNzAO +BgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOC +AgEATh65isagmD9uw2nAalxJUqzLK114OMHVVISfk/CHGT0sZonrDUL8zPB1hT+L +9IBdeeUXZ701guLyPI59WzbLWoAAKfLOKyzxj6ptBZNscsdW699QIyjlRRA96Gej +rw5VD5AJYu9LWaL2U/HANeQvwSS9eS9OICI7/RogsKQOLHDtdD+4E5UGUcjohybK +pFtqFiGS3XNgnhAY3jyB6ugYw3yJ8otQPr0R4hUDqDZ9MwFsSBXXiJCZBMXM5gf0 +vPSQ7RPi6ovDj6MzD8EpTBNO2hVWcXNyglD2mjN8orGoGjR0ZVzO0eurU+AagNjq +OknkJjCb5RyKqKkVMoaZkgoQI1YS4PbOTOK7vtuNknMBZi9iPrJyJ0U27U1W45eZ +/zo1PqVUSlJZS2Db7v54EX9K3BR5YLZrZAPbFYPhor72I5dQ8AkzNqdxliXzuUJ9 +2zg/LFis6ELhDtjTO0wugumDLmsx2d1Hhk9tl5EuT+IocTUW0fJz/iUrB0ckYyfI ++PbZa/wSMVYIwFNCr5zQM378BvAxRAMU8Vjq8moNqRGyg77FGr8H6lnco4g175x2 +MjxNBiLOFeXdntiP2t7SxDnlF4HPOEfrf4htWRvfn0IUrn7PqLBmZdo3r5+qPeoo +tt7VMVgWglvquxl1AnMaykgaIZOQCo6ThKd9OyMYkomgjaw= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICZTCCAeugAwIBAgIQeI8nXIESUiClBNAt3bpz9DAKBggqhkjOPQQDAzB0MQsw +CQYDVQQGEwJQTDEhMB8GA1UEChMYQXNzZWNvIERhdGEgU3lzdGVtcyBTLkEuMScw +JQYDVQQLEx5DZXJ0dW0gQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkxGTAXBgNVBAMT +EENlcnR1bSBFQy0zODQgQ0EwHhcNMTgwMzI2MDcyNDU0WhcNNDMwMzI2MDcyNDU0 +WjB0MQswCQYDVQQGEwJQTDEhMB8GA1UEChMYQXNzZWNvIERhdGEgU3lzdGVtcyBT +LkEuMScwJQYDVQQLEx5DZXJ0dW0gQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkxGTAX +BgNVBAMTEENlcnR1bSBFQy0zODQgQ0EwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAATE +KI6rGFtqvm5kN2PkzeyrOvfMobgOgknXhimfoZTy42B4mIF4Bk3y7JoOV2CDn7Tm +Fy8as10CW4kjPMIRBSqniBMY81CE1700LCeJVf/OTOffph8oxPBUw7l8t1Ot68Kj +QjBAMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFI0GZnQkdjrzife81r1HfS+8 +EF9LMA4GA1UdDwEB/wQEAwIBBjAKBggqhkjOPQQDAwNoADBlAjADVS2m5hjEfO/J +UG7BJw+ch69u1RsIGL2SKcHvlJF40jocVYli5RsJHrpka/F2tNQCMQC0QoSZ/6vn +nvuRlydd3LBbMHHOXjgaatkl5+r3YZJW+OraNsKHZZYuciUvf9/DE8k= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFwDCCA6igAwIBAgIQHr9ZULjJgDdMBvfrVU+17TANBgkqhkiG9w0BAQ0FADB6 +MQswCQYDVQQGEwJQTDEhMB8GA1UEChMYQXNzZWNvIERhdGEgU3lzdGVtcyBTLkEu +MScwJQYDVQQLEx5DZXJ0dW0gQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkxHzAdBgNV +BAMTFkNlcnR1bSBUcnVzdGVkIFJvb3QgQ0EwHhcNMTgwMzE2MTIxMDEzWhcNNDMw +MzE2MTIxMDEzWjB6MQswCQYDVQQGEwJQTDEhMB8GA1UEChMYQXNzZWNvIERhdGEg +U3lzdGVtcyBTLkEuMScwJQYDVQQLEx5DZXJ0dW0gQ2VydGlmaWNhdGlvbiBBdXRo +b3JpdHkxHzAdBgNVBAMTFkNlcnR1bSBUcnVzdGVkIFJvb3QgQ0EwggIiMA0GCSqG +SIb3DQEBAQUAA4ICDwAwggIKAoICAQDRLY67tzbqbTeRn06TpwXkKQMlzhyC93yZ +n0EGze2jusDbCSzBfN8pfktlL5On1AFrAygYo9idBcEq2EXxkd7fO9CAAozPOA/q +p1x4EaTByIVcJdPTsuclzxFUl6s1wB52HO8AU5853BSlLCIls3Jy/I2z5T4IHhQq +NwuIPMqw9MjCoa68wb4pZ1Xi/K1ZXP69VyywkI3C7Te2fJmItdUDmj0VDT06qKhF +8JVOJVkdzZhpu9PMMsmN74H+rX2Ju7pgE8pllWeg8xn2A1bUatMn4qGtg/BKEiJ3 +HAVz4hlxQsDsdUaakFjgao4rpUYwBI4Zshfjvqm6f1bxJAPXsiEodg42MEx51UGa +mqi4NboMOvJEGyCI98Ul1z3G4z5D3Yf+xOr1Uz5MZf87Sst4WmsXXw3Hw09Omiqi +7VdNIuJGmj8PkTQkfVXjjJU30xrwCSss0smNtA0Aq2cpKNgB9RkEth2+dv5yXMSF +ytKAQd8FqKPVhJBPC/PgP5sZ0jeJP/J7UhyM9uH3PAeXjA6iWYEMspA90+NZRu0P +qafegGtaqge2Gcu8V/OXIXoMsSt0Puvap2ctTMSYnjYJdmZm/Bo/6khUHL4wvYBQ +v3y1zgD2DGHZ5yQD4OMBgQ692IU0iL2yNqh7XAjlRICMb/gv1SHKHRzQ+8S1h9E6 +Tsd2tTVItQIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBSM+xx1 +vALTn04uSNn5YFSqxLNP+jAOBgNVHQ8BAf8EBAMCAQYwDQYJKoZIhvcNAQENBQAD +ggIBAEii1QALLtA/vBzVtVRJHlpr9OTy4EA34MwUe7nJ+jW1dReTagVphZzNTxl4 +WxmB82M+w85bj/UvXgF2Ez8sALnNllI5SW0ETsXpD4YN4fqzX4IS8TrOZgYkNCvo +zMrnadyHncI013nR03e4qllY/p0m+jiGPp2Kh2RX5Rc64vmNueMzeMGQ2Ljdt4NR +5MTMI9UGfOZR0800McD2RrsLrfw9EAUqO0qRJe6M1ISHgCq8CYyqOhNf6DR5UMEQ +GfnTKB7U0VEwKbOukGfWHwpjscWpxkIxYxeU72nLL/qMFH3EQxiJ2fAyQOaA4kZf +5ePBAFmo+eggvIksDkc0C+pXwlM2/KfUrzHN/gLldfq5Jwn58/U7yn2fqSLLiMmq +0Uc9NneoWWRrJ8/vJ8HjJLWG965+Mk2weWjROeiQWMODvA8s1pfrzgzhIMfatz7D +P78v3DSk+yshzWePS/Tj6tQ/50+6uaWTRRxmHyH6ZF5v4HaUMst19W7l9o/HuKTM +qJZ9ZPskWkoDbGs4xugDQ5r3V7mzKWmTOPQD8rv7gmsHINFSH5pkAnuYZttcTVoP +0ISVoDwUQwbKytu4QTbaakRnh6+v40URFWkIsr4WOZckbxJF0WddCajJFdr60qZf +E2Efv4WstK2tBZQIgx51F9NxO5NQI1mg7TyRVJ12AMXDuDjb +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICCzCCAZGgAwIBAgISEdK7ujNu1LzmJGjFDYQdmOhDMAoGCCqGSM49BAMDMEYx +CzAJBgNVBAYTAkJFMRkwFwYDVQQKExBHbG9iYWxTaWduIG52LXNhMRwwGgYDVQQD +ExNHbG9iYWxTaWduIFJvb3QgRTQ2MB4XDTE5MDMyMDAwMDAwMFoXDTQ2MDMyMDAw +MDAwMFowRjELMAkGA1UEBhMCQkUxGTAXBgNVBAoTEEdsb2JhbFNpZ24gbnYtc2Ex +HDAaBgNVBAMTE0dsb2JhbFNpZ24gUm9vdCBFNDYwdjAQBgcqhkjOPQIBBgUrgQQA +IgNiAAScDrHPt+ieUnd1NPqlRqetMhkytAepJ8qUuwzSChDH2omwlwxwEwkBjtjq +R+q+soArzfwoDdusvKSGN+1wCAB16pMLey5SnCNoIwZD7JIvU4Tb+0cUB+hflGdd +yXqBPCCjQjBAMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB0GA1Ud +DgQWBBQxCpCPtsad0kRLgLWi5h+xEk8blTAKBggqhkjOPQQDAwNoADBlAjEA31SQ +7Zvvi5QCkxeCmb6zniz2C5GMn0oUsfZkvLtoURMMA/cVi4RguYv/Uo7njLwcAjA8 ++RHUjE7AwWHCFUyqqx0LMV87HOIAl0Qx5v5zli/altP+CAezNIm8BZ/3Hobui3A= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFWjCCA0KgAwIBAgISEdK7udcjGJ5AXwqdLdDfJWfRMA0GCSqGSIb3DQEBDAUA +MEYxCzAJBgNVBAYTAkJFMRkwFwYDVQQKExBHbG9iYWxTaWduIG52LXNhMRwwGgYD +VQQDExNHbG9iYWxTaWduIFJvb3QgUjQ2MB4XDTE5MDMyMDAwMDAwMFoXDTQ2MDMy +MDAwMDAwMFowRjELMAkGA1UEBhMCQkUxGTAXBgNVBAoTEEdsb2JhbFNpZ24gbnYt +c2ExHDAaBgNVBAMTE0dsb2JhbFNpZ24gUm9vdCBSNDYwggIiMA0GCSqGSIb3DQEB +AQUAA4ICDwAwggIKAoICAQCsrHQy6LNl5brtQyYdpokNRbopiLKkHWPd08EsCVeJ +OaFV6Wc0dwxu5FUdUiXSE2te4R2pt32JMl8Nnp8semNgQB+msLZ4j5lUlghYruQG +vGIFAha/r6gjA7aUD7xubMLL1aa7DOn2wQL7Id5m3RerdELv8HQvJfTqa1VbkNud +316HCkD7rRlr+/fKYIje2sGP1q7Vf9Q8g+7XFkyDRTNrJ9CG0Bwta/OrffGFqfUo +0q3v84RLHIf8E6M6cqJaESvWJ3En7YEtbWaBkoe0G1h6zD8K+kZPTXhc+CtI4wSE +y132tGqzZfxCnlEmIyDLPRT5ge1lFgBPGmSXZgjPjHvjK8Cd+RTyG/FWaha/LIWF +zXg4mutCagI0GIMXTpRW+LaCtfOW3T3zvn8gdz57GSNrLNRyc0NXfeD412lPFzYE ++cCQYDdF3uYM2HSNrpyibXRdQr4G9dlkbgIQrImwTDsHTUB+JMWKmIJ5jqSngiCN +I/onccnfxkF0oE32kRbcRoxfKWMxWXEM2G/CtjJ9++ZdU6Z+Ffy7dXxd7Pj2Fxzs +x2sZy/N78CsHpdlseVR2bJ0cpm4O6XkMqCNqo98bMDGfsVR7/mrLZqrcZdCinkqa +ByFrgY/bxFn63iLABJzjqls2k+g9vXqhnQt2sQvHnf3PmKgGwvgqo6GDoLclcqUC +4wIDAQABo0IwQDAOBgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNV +HQ4EFgQUA1yrc4GHqMywptWU4jaWSf8FmSwwDQYJKoZIhvcNAQEMBQADggIBAHx4 +7PYCLLtbfpIrXTncvtgdokIzTfnvpCo7RGkerNlFo048p9gkUbJUHJNOxO97k4Vg +JuoJSOD1u8fpaNK7ajFxzHmuEajwmf3lH7wvqMxX63bEIaZHU1VNaL8FpO7XJqti +2kM3S+LGteWygxk6x9PbTZ4IevPuzz5i+6zoYMzRx6Fcg0XERczzF2sUyQQCPtIk +pnnpHs6i58FZFZ8d4kuaPp92CC1r2LpXFNqD6v6MVenQTqnMdzGxRBF6XLE+0xRF +FRhiJBPSy03OXIPBNvIQtQ6IbbjhVp+J3pZmOUdkLG5NrmJ7v2B0GbhWrJKsFjLt +rWhV/pi60zTe9Mlhww6G9kuEYO4Ne7UyWHmRVSyBQ7N0H3qqJZ4d16GLuc1CLgSk +ZoNNiTW2bKg2SnkheCLQQrzRQDGQob4Ez8pn7fXwgNNgyYMqIgXQBztSvwyeqiv5 +u+YfjyW6hY0XHgL+XVAEV8/+LbzvXMAaq7afJMbfc2hIkCwU9D9SGuTSyxTDYWnP +4vkYxboznxSjBF25cfe1lNj2M8FawTSLfJvdkzrnE6JwYZ+vj+vYxXX4M2bUdGc6 +N3ec592kD3ZDZopD8p/7DEJ4Y9HiD2971KE9dJeFt0g5QdYg/NA6s/rob8SKunE3 +vouXsXgxT7PntgMTzlSdriVZzH81Xwj3QEUxeCp6 +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFgjCCA2qgAwIBAgILWku9WvtPilv6ZeUwDQYJKoZIhvcNAQELBQAwTTELMAkG +A1UEBhMCQVQxIzAhBgNVBAoTGmUtY29tbWVyY2UgbW9uaXRvcmluZyBHbWJIMRkw +FwYDVQQDExBHTE9CQUxUUlVTVCAyMDIwMB4XDTIwMDIxMDAwMDAwMFoXDTQwMDYx +MDAwMDAwMFowTTELMAkGA1UEBhMCQVQxIzAhBgNVBAoTGmUtY29tbWVyY2UgbW9u +aXRvcmluZyBHbWJIMRkwFwYDVQQDExBHTE9CQUxUUlVTVCAyMDIwMIICIjANBgkq +hkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAri5WrRsc7/aVj6B3GyvTY4+ETUWiD59b +RatZe1E0+eyLinjF3WuvvcTfk0Uev5E4C64OFudBc/jbu9G4UeDLgztzOG53ig9Z +YybNpyrOVPu44sB8R85gfD+yc/LAGbaKkoc1DZAoouQVBGM+uq/ufF7MpotQsjj3 +QWPKzv9pj2gOlTblzLmMCcpL3TGQlsjMH/1WljTbjhzqLL6FLmPdqqmV0/0plRPw +yJiT2S0WR5ARg6I6IqIoV6Lr/sCMKKCmfecqQjuCgGOlYx8ZzHyyZqjC0203b+J+ +BlHZRYQfEs4kUmSFC0iAToexIiIwquuuvuAC4EDosEKAA1GqtH6qRNdDYfOiaxaJ +SaSjpCuKAsR49GiKweR6NrFvG5Ybd0mN1MkGco/PU+PcF4UgStyYJ9ORJitHHmkH +r96i5OTUawuzXnzUJIBHKWk7buis/UDr2O1xcSvy6Fgd60GXIsUf1DnQJ4+H4xj0 +4KlGDfV0OoIu0G4skaMxXDtG6nsEEFZegB31pWXogvziB4xiRfUg3kZwhqG8k9Me +dKZssCz3AwyIDMvUclOGvGBG85hqwvG/Q/lwIHfKN0F5VVJjjVsSn8VoxIidrPIw +q7ejMZdnrY8XD2zHc+0klGvIg5rQmjdJBKuxFshsSUktq6HQjJLyQUp5ISXbY9e2 +nKd+Qmn7OmMCAwEAAaNjMGEwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMC +AQYwHQYDVR0OBBYEFNwuH9FhN3nkq9XVsxJxaD1qaJwiMB8GA1UdIwQYMBaAFNwu +H9FhN3nkq9XVsxJxaD1qaJwiMA0GCSqGSIb3DQEBCwUAA4ICAQCR8EICaEDuw2jA +VC/f7GLDw56KoDEoqoOOpFaWEhCGVrqXctJUMHytGdUdaG/7FELYjQ7ztdGl4wJC +XtzoRlgHNQIw4Lx0SsFDKv/bGtCwr2zD/cuz9X9tAy5ZVp0tLTWMstZDFyySCstd +6IwPS3BD0IL/qMy/pJTAvoe9iuOTe8aPmxadJ2W8esVCgmxcB9CpwYhgROmYhRZf ++I/KARDOJcP5YBugxZfD0yyIMaK9MOzQ0MAS8cE54+X1+NZK3TTN+2/BT+MAi1bi +kvcoskJ3ciNnxz8RFbLEAwW+uxF7Cr+obuf/WEPPm2eggAe2HcqtbepBEX4tdJP7 +wry+UUTF72glJ4DjyKDUEuzZpTcdN3y0kcra1LGWge9oXHYQSa9+pTeAsRxSvTOB +TI/53WXZFM2KJVj04sWDpQmQ1GwUY7VA3+vA/MRYfg0UFodUJ25W5HCEuGwyEn6C +MUO+1918oa2u1qsgEu8KwxCMSZY13At1XrFP1U80DhEgB3VDRemjEdqso5nCtnkn +4rnvyOL2NSl6dPrFf4IFYqYK6miyeUcGbvJXqBUzxvd4Sj1Ce2t+/vdG6tHrju+I +aFvowdlxfv1k7/9nR4hYJS8+hge9+6jlgqispdNpQ80xiEmEU5LAsTkbOYMBMMTy +qfrQA71yN2BWHzZ8vTmR9W0Nv3vXkg== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIGFDCCA/ygAwIBAgIIG3Dp0v+ubHEwDQYJKoZIhvcNAQELBQAwUTELMAkGA1UE +BhMCRVMxQjBABgNVBAMMOUF1dG9yaWRhZCBkZSBDZXJ0aWZpY2FjaW9uIEZpcm1h +cHJvZmVzaW9uYWwgQ0lGIEE2MjYzNDA2ODAeFw0xNDA5MjMxNTIyMDdaFw0zNjA1 +MDUxNTIyMDdaMFExCzAJBgNVBAYTAkVTMUIwQAYDVQQDDDlBdXRvcmlkYWQgZGUg +Q2VydGlmaWNhY2lvbiBGaXJtYXByb2Zlc2lvbmFsIENJRiBBNjI2MzQwNjgwggIi +MA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDKlmuO6vj78aI14H9M2uDDUtd9 +thDIAl6zQyrET2qyyhxdKJp4ERppWVevtSBC5IsP5t9bpgOSL/UR5GLXMnE42QQM +cas9UX4PB99jBVzpv5RvwSmCwLTaUbDBPLutN0pcyvFLNg4kq7/DhHf9qFD0sefG +L9ItWY16Ck6WaVICqjaY7Pz6FIMMNx/Jkjd/14Et5cS54D40/mf0PmbR0/RAz15i +NA9wBj4gGFrO93IbJWyTdBSTo3OxDqqHECNZXyAFGUftaI6SEspd/NYrspI8IM/h +X68gvqB2f3bl7BqGYTM+53u0P6APjqK5am+5hyZvQWyIplD9amML9ZMWGxmPsu2b +m8mQ9QEM3xk9Dz44I8kvjwzRAv4bVdZO0I08r0+k8/6vKtMFnXkIoctXMbScyJCy +Z/QYFpM6/EfY0XiWMR+6KwxfXZmtY4laJCB22N/9q06mIqqdXuYnin1oKaPnirja +EbsXLZmdEyRG98Xi2J+Of8ePdG1asuhy9azuJBCtLxTa/y2aRnFHvkLfuwHb9H/T +KI8xWVvTyQKmtFLKbpf7Q8UIJm+K9Lv9nyiqDdVF8xM6HdjAeI9BZzwelGSuewvF +6NkBiDkal4ZkQdU7hwxu+g/GvUgUvzlN1J5Bto+WHWOWk9mVBngxaJ43BjuAiUVh +OSPHG0SjFeUc+JIwuwIDAQABo4HvMIHsMB0GA1UdDgQWBBRlzeurNR4APn7VdMAc +tHNHDhpkLzASBgNVHRMBAf8ECDAGAQH/AgEBMIGmBgNVHSAEgZ4wgZswgZgGBFUd +IAAwgY8wLwYIKwYBBQUHAgEWI2h0dHA6Ly93d3cuZmlybWFwcm9mZXNpb25hbC5j +b20vY3BzMFwGCCsGAQUFBwICMFAeTgBQAGEAcwBlAG8AIABkAGUAIABsAGEAIABC +AG8AbgBhAG4AbwB2AGEAIAA0ADcAIABCAGEAcgBjAGUAbABvAG4AYQAgADAAOAAw +ADEANzAOBgNVHQ8BAf8EBAMCAQYwDQYJKoZIhvcNAQELBQADggIBAHSHKAIrdx9m +iWTtj3QuRhy7qPj4Cx2Dtjqn6EWKB7fgPiDL4QjbEwj4KKE1soCzC1HA01aajTNF +Sa9J8OA9B3pFE1r/yJfY0xgsfZb43aJlQ3CTkBW6kN/oGbDbLIpgD7dvlAceHabJ +hfa9NPhAeGIQcDq+fUs5gakQ1JZBu/hfHAsdCPKxsIl68veg4MSPi3i1O1ilI45P +Vf42O+AMt8oqMEEgtIDNrvx2ZnOorm7hfNoD6JQg5iKj0B+QXSBTFCZX2lSX3xZE +EAEeiGaPcjiT3SC3NL7X8e5jjkd5KAb881lFJWAiMxujX6i6KtoaPc1A6ozuBRWV +1aUsIC+nmCjuRfzxuIgALI9C2lHVnOUTaHFFQ4ueCyE8S1wF3BqfmI7avSKecs2t +CsvMo2ebKHTEm9caPARYpoKdrcd7b/+Alun4jWq9GJAd/0kakFI3ky88Al2CdgtR +5xbHV/g4+afNmyJU72OwFW1TZQNKXkqgsqeOSQBZONXH9IBk9W6VULgRfhVwOEqw +f9DEMnDAGf/JOC0ULGb0QkTmVXYbgBVX/8Cnp6o5qtjTcNAuuuuUavpfNIbnYrX9 +ivAwhZTJryQCL2/W3Wf+47BVTwSYT6RBVuKT0Gro1vP7ZeDOdcQxWQzugsgMYDNK +GbqEZycPvEJdvSRUDewdcAZfpLz6IHxV +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIB9zCCAX2gAwIBAgIQBiUzsUcDMydc+Y2aub/M+DAKBggqhkjOPQQDAzA9MQsw +CQYDVQQGEwJVUzESMBAGA1UEChMJQ2VydGFpbmx5MRowGAYDVQQDExFDZXJ0YWlu +bHkgUm9vdCBFMTAeFw0yMTA0MDEwMDAwMDBaFw00NjA0MDEwMDAwMDBaMD0xCzAJ +BgNVBAYTAlVTMRIwEAYDVQQKEwlDZXJ0YWlubHkxGjAYBgNVBAMTEUNlcnRhaW5s +eSBSb290IEUxMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAE3m/4fxzf7flHh4axpMCK ++IKXgOqPyEpeKn2IaKcBYhSRJHpcnqMXfYqGITQYUBsQ3tA3SybHGWCA6TS9YBk2 +QNYphwk8kXr2vBMj3VlOBF7PyAIcGFPBMdjaIOlEjeR2o0IwQDAOBgNVHQ8BAf8E +BAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQU8ygYy2R17ikq6+2uI1g4 +hevIIgcwCgYIKoZIzj0EAwMDaAAwZQIxALGOWiDDshliTd6wT99u0nCK8Z9+aozm +ut6Dacpps6kFtZaSF4fC0urQe87YQVt8rgIwRt7qy12a7DLCZRawTDBcMPPaTnOG +BtjOiQRINzf43TNRnXCve1XYAS59BWQOhriR +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFRzCCAy+gAwIBAgIRAI4P+UuQcWhlM1T01EQ5t+AwDQYJKoZIhvcNAQELBQAw +PTELMAkGA1UEBhMCVVMxEjAQBgNVBAoTCUNlcnRhaW5seTEaMBgGA1UEAxMRQ2Vy +dGFpbmx5IFJvb3QgUjEwHhcNMjEwNDAxMDAwMDAwWhcNNDYwNDAxMDAwMDAwWjA9 +MQswCQYDVQQGEwJVUzESMBAGA1UEChMJQ2VydGFpbmx5MRowGAYDVQQDExFDZXJ0 +YWlubHkgUm9vdCBSMTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBANA2 +1B/q3avk0bbm+yLA3RMNansiExyXPGhjZjKcA7WNpIGD2ngwEc/csiu+kr+O5MQT +vqRoTNoCaBZ0vrLdBORrKt03H2As2/X3oXyVtwxwhi7xOu9S98zTm/mLvg7fMbed +aFySpvXl8wo0tf97ouSHocavFwDvA5HtqRxOcT3Si2yJ9HiG5mpJoM610rCrm/b0 +1C7jcvk2xusVtyWMOvwlDbMicyF0yEqWYZL1LwsYpfSt4u5BvQF5+paMjRcCMLT5 +r3gajLQ2EBAHBXDQ9DGQilHFhiZ5shGIXsXwClTNSaa/ApzSRKft43jvRl5tcdF5 +cBxGX1HpyTfcX35pe0HfNEXgO4T0oYoKNp43zGJS4YkNKPl6I7ENPT2a/Z2B7yyQ +wHtETrtJ4A5KVpK8y7XdeReJkd5hiXSSqOMyhb5OhaRLWcsrxXiOcVTQAjeZjOVJ +6uBUcqQRBi8LjMFbvrWhsFNunLhgkR9Za/kt9JQKl7XsxXYDVBtlUrpMklZRNaBA +2CnbrlJ2Oy0wQJuK0EJWtLeIAaSHO1OWzaMWj/Nmqhexx2DgwUMFDO6bW2BvBlyH +Wyf5QBGenDPBt+U1VwV/J84XIIwc/PH72jEpSe31C4SnT8H2TsIonPru4K8H+zMR +eiFPCyEQtkA6qyI6BJyLm4SGcprSp6XEtHWRqSsjAgMBAAGjQjBAMA4GA1UdDwEB +/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBTgqj8ljZ9EXME66C6u +d0yEPmcM9DANBgkqhkiG9w0BAQsFAAOCAgEAuVevuBLaV4OPaAszHQNTVfSVcOQr +PbA56/qJYv331hgELyE03fFo8NWWWt7CgKPBjcZq91l3rhVkz1t5BXdm6ozTaw3d +8VkswTOlMIAVRQdFGjEitpIAq5lNOo93r6kiyi9jyhXWx8bwPWz8HA2YEGGeEaIi +1wrykXprOQ4vMMM2SZ/g6Q8CRFA3lFV96p/2O7qUpUzpvD5RtOjKkjZUbVwlKNrd +rRT90+7iIgXr0PK3aBLXWopBGsaSpVo7Y0VPv+E6dyIvXL9G+VoDhRNCX8reU9di +taY1BMJH/5n9hN9czulegChB8n3nHpDYT3Y+gjwN/KUD+nsa2UUeYNrEjvn8K8l7 +lcUq/6qJ34IxD3L/DCfXCh5WAFAeDJDBlrXYFIW7pw0WwfgHJBu6haEaBQmAupVj +yTrsJZ9/nbqkRxWbRHDxakvWOF5D8xh+UG7pWijmZeZ3Gzr9Hb4DJqPb1OG7fpYn +Kx3upPvaJVQTA945xsMfTZDsjxtK0hzthZU4UHlG1sGQUDGpXJpuHfUzVounmdLy +yCwzk5Iwx06MZTMQZBf9JBeW0Y3COmor6xOLRPIh80oat3df1+2IpHLlOR+Vnb5n +wXARPbv0+Em34yaXOp/SX3z7wJl8OSngex2/DaeP0ik0biQVy96QXr8axGbqwua6 +OV+KmalBWQewLK8= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICGTCCAZ+gAwIBAgIQCeCTZaz32ci5PhwLBCou8zAKBggqhkjOPQQDAzBOMQsw +CQYDVQQGEwJVUzEXMBUGA1UEChMORGlnaUNlcnQsIEluYy4xJjAkBgNVBAMTHURp +Z2lDZXJ0IFRMUyBFQ0MgUDM4NCBSb290IEc1MB4XDTIxMDExNTAwMDAwMFoXDTQ2 +MDExNDIzNTk1OVowTjELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDkRpZ2lDZXJ0LCBJ +bmMuMSYwJAYDVQQDEx1EaWdpQ2VydCBUTFMgRUNDIFAzODQgUm9vdCBHNTB2MBAG +ByqGSM49AgEGBSuBBAAiA2IABMFEoc8Rl1Ca3iOCNQfN0MsYndLxf3c1TzvdlHJS +7cI7+Oz6e2tYIOyZrsn8aLN1udsJ7MgT9U7GCh1mMEy7H0cKPGEQQil8pQgO4CLp +0zVozptjn4S1mU1YoI71VOeVyaNCMEAwHQYDVR0OBBYEFMFRRVBZqz7nLFr6ICIS +B4CIfBFqMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MAoGCCqGSM49 +BAMDA2gAMGUCMQCJao1H5+z8blUD2WdsJk6Dxv3J+ysTvLd6jLRl0mlpYxNjOyZQ +LgGheQaRnUi/wr4CMEfDFXuxoJGZSZOoPHzoRgaLLPIxAJSdYsiJvRmEFOml+wG4 +DXZDjC5Ty3zfDBeWUA== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFZjCCA06gAwIBAgIQCPm0eKj6ftpqMzeJ3nzPijANBgkqhkiG9w0BAQwFADBN +MQswCQYDVQQGEwJVUzEXMBUGA1UEChMORGlnaUNlcnQsIEluYy4xJTAjBgNVBAMT +HERpZ2lDZXJ0IFRMUyBSU0E0MDk2IFJvb3QgRzUwHhcNMjEwMTE1MDAwMDAwWhcN +NDYwMTE0MjM1OTU5WjBNMQswCQYDVQQGEwJVUzEXMBUGA1UEChMORGlnaUNlcnQs +IEluYy4xJTAjBgNVBAMTHERpZ2lDZXJ0IFRMUyBSU0E0MDk2IFJvb3QgRzUwggIi +MA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCz0PTJeRGd/fxmgefM1eS87IE+ +ajWOLrfn3q/5B03PMJ3qCQuZvWxX2hhKuHisOjmopkisLnLlvevxGs3npAOpPxG0 +2C+JFvuUAT27L/gTBaF4HI4o4EXgg/RZG5Wzrn4DReW+wkL+7vI8toUTmDKdFqgp +wgscONyfMXdcvyej/Cestyu9dJsXLfKB2l2w4SMXPohKEiPQ6s+d3gMXsUJKoBZM +pG2T6T867jp8nVid9E6P/DsjyG244gXazOvswzH016cpVIDPRFtMbzCe88zdH5RD +nU1/cHAN1DrRN/BsnZvAFJNY781BOHW8EwOVfH/jXOnVDdXifBBiqmvwPXbzP6Po +sMH976pXTayGpxi0KcEsDr9kvimM2AItzVwv8n/vFfQMFawKsPHTDU9qTXeXAaDx +Zre3zu/O7Oyldcqs4+Fj97ihBMi8ez9dLRYiVu1ISf6nL3kwJZu6ay0/nTvEF+cd +Lvvyz6b84xQslpghjLSR6Rlgg/IwKwZzUNWYOwbpx4oMYIwo+FKbbuH2TbsGJJvX +KyY//SovcfXWJL5/MZ4PbeiPT02jP/816t9JXkGPhvnxd3lLG7SjXi/7RgLQZhNe +XoVPzthwiHvOAbWWl9fNff2C+MIkwcoBOU+NosEUQB+cZtUMCUbW8tDRSHZWOkPL +tgoRObqME2wGtZ7P6wIDAQABo0IwQDAdBgNVHQ4EFgQUUTMc7TZArxfTJc1paPKv +TiM+s0EwDgYDVR0PAQH/BAQDAgGGMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcN +AQEMBQADggIBAGCmr1tfV9qJ20tQqcQjNSH/0GEwhJG3PxDPJY7Jv0Y02cEhJhxw +GXIeo8mH/qlDZJY6yFMECrZBu8RHANmfGBg7sg7zNOok992vIGCukihfNudd5N7H +PNtQOa27PShNlnx2xlv0wdsUpasZYgcYQF+Xkdycx6u1UQ3maVNVzDl92sURVXLF +O4uJ+DQtpBflF+aZfTCIITfNMBc9uPK8qHWgQ9w+iUuQrm0D4ByjoJYJu32jtyoQ +REtGBzRj7TG5BO6jm5qu5jF49OokYTurWGT/u4cnYiWB39yhL/btp/96j1EuMPik +AdKFOV8BmZZvWltwGUb+hmA+rYAQCd05JS9Yf7vSdPD3Rh9GOUrYU9DzLjtxpdRv +/PNn5AeP3SYZ4Y1b+qOTEZvpyDrDVWiakuFSdjjo4bq9+0/V77PnSIMx8IIh47a+ +p6tv75/fTM8BuGJqIz3nCU2AG3swpMPdB380vqQmsvZB6Akd4yCYqjdP//fx4ilw +MUc/dNAUFvohigLVigmUdy7yWSiLfFCSCmZ4OIN1xLVaqBHG5cGdZlXPU8Sv13WF +qUITVuwhd4GTWgzqltlJyqEI8pc7bZsEGCREjnwB8twl2F6GmrE52/WRMmrRpnCK +ovfepEWFJqgejF0pW8hL2JpqA15w8oVPbEtoL8pU9ozaMv7Da4M/OMZ+ +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIC2zCCAmCgAwIBAgIQfMmPK4TX3+oPyWWa00tNljAKBggqhkjOPQQDAzBIMQsw +CQYDVQQGEwJERTEVMBMGA1UEChMMRC1UcnVzdCBHbWJIMSIwIAYDVQQDExlELVRS +VVNUIEJSIFJvb3QgQ0EgMSAyMDIwMB4XDTIwMDIxMTA5NDUwMFoXDTM1MDIxMTA5 +NDQ1OVowSDELMAkGA1UEBhMCREUxFTATBgNVBAoTDEQtVHJ1c3QgR21iSDEiMCAG +A1UEAxMZRC1UUlVTVCBCUiBSb290IENBIDEgMjAyMDB2MBAGByqGSM49AgEGBSuB +BAAiA2IABMbLxyjR+4T1mu9CFCDhQ2tuda38KwOE1HaTJddZO0Flax7mNCq7dPYS +zuht56vkPE4/RAiLzRZxy7+SmfSk1zxQVFKQhYN4lGdnoxwJGT11NIXe7WB9xwy0 +QVK5buXuQqOCAQ0wggEJMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFHOREKv/ +VbNafAkl1bK6CKBrqx9tMA4GA1UdDwEB/wQEAwIBBjCBxgYDVR0fBIG+MIG7MD6g +PKA6hjhodHRwOi8vY3JsLmQtdHJ1c3QubmV0L2NybC9kLXRydXN0X2JyX3Jvb3Rf +Y2FfMV8yMDIwLmNybDB5oHegdYZzbGRhcDovL2RpcmVjdG9yeS5kLXRydXN0Lm5l +dC9DTj1ELVRSVVNUJTIwQlIlMjBSb290JTIwQ0ElMjAxJTIwMjAyMCxPPUQtVHJ1 +c3QlMjBHbWJILEM9REU/Y2VydGlmaWNhdGVyZXZvY2F0aW9ubGlzdDAKBggqhkjO +PQQDAwNpADBmAjEAlJAtE/rhY/hhY+ithXhUkZy4kzg+GkHaQBZTQgjKL47xPoFW +wKrY7RjEsK70PvomAjEA8yjixtsrmfu3Ubgko6SUeho/5jbiA1czijDLgsfWFBHV +dWNbFJWcHwHP2NVypw87 +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIC2zCCAmCgAwIBAgIQXwJB13qHfEwDo6yWjfv/0DAKBggqhkjOPQQDAzBIMQsw +CQYDVQQGEwJERTEVMBMGA1UEChMMRC1UcnVzdCBHbWJIMSIwIAYDVQQDExlELVRS +VVNUIEVWIFJvb3QgQ0EgMSAyMDIwMB4XDTIwMDIxMTEwMDAwMFoXDTM1MDIxMTA5 +NTk1OVowSDELMAkGA1UEBhMCREUxFTATBgNVBAoTDEQtVHJ1c3QgR21iSDEiMCAG +A1UEAxMZRC1UUlVTVCBFViBSb290IENBIDEgMjAyMDB2MBAGByqGSM49AgEGBSuB +BAAiA2IABPEL3YZDIBnfl4XoIkqbz52Yv7QFJsnL46bSj8WeeHsxiamJrSc8ZRCC +/N/DnU7wMyPE0jL1HLDfMxddxfCxivnvubcUyilKwg+pf3VlSSowZ/Rk99Yad9rD +wpdhQntJraOCAQ0wggEJMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFH8QARY3 +OqQo5FD4pPfsazK2/umLMA4GA1UdDwEB/wQEAwIBBjCBxgYDVR0fBIG+MIG7MD6g +PKA6hjhodHRwOi8vY3JsLmQtdHJ1c3QubmV0L2NybC9kLXRydXN0X2V2X3Jvb3Rf +Y2FfMV8yMDIwLmNybDB5oHegdYZzbGRhcDovL2RpcmVjdG9yeS5kLXRydXN0Lm5l +dC9DTj1ELVRSVVNUJTIwRVYlMjBSb290JTIwQ0ElMjAxJTIwMjAyMCxPPUQtVHJ1 +c3QlMjBHbWJILEM9REU/Y2VydGlmaWNhdGVyZXZvY2F0aW9ubGlzdDAKBggqhkjO +PQQDAwNpADBmAjEAyjzGKnXCXnViOTYAYFqLwZOZzNnbQTs7h5kXO9XMT8oi96CA +y/m0sRtW9XLS/BnRAjEAkfcwkz8QRitxpNA7RJvAKQIFskF3UfN5Wp6OFKBOQtJb +gfM0agPnIjhQW+0ZT0MW +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICpTCCAiqgAwIBAgIUJkYZdzHhT28oNt45UYbm1JeIIsEwCgYIKoZIzj0EAwMw +gYAxCzAJBgNVBAYTAlRSMQ8wDQYDVQQHEwZBbmthcmExGTAXBgNVBAoTEEUtVHVn +cmEgRUJHIEEuUy4xHTAbBgNVBAsTFEUtVHVncmEgVHJ1c3QgQ2VudGVyMSYwJAYD +VQQDEx1FLVR1Z3JhIEdsb2JhbCBSb290IENBIEVDQyB2MzAeFw0yMDAzMTgwOTQ2 +NThaFw00NTAzMTIwOTQ2NThaMIGAMQswCQYDVQQGEwJUUjEPMA0GA1UEBxMGQW5r +YXJhMRkwFwYDVQQKExBFLVR1Z3JhIEVCRyBBLlMuMR0wGwYDVQQLExRFLVR1Z3Jh +IFRydXN0IENlbnRlcjEmMCQGA1UEAxMdRS1UdWdyYSBHbG9iYWwgUm9vdCBDQSBF +Q0MgdjMwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAASOmCm/xxAeJ9urA8woLNheSBkQ +KczLWYHMjLiSF4mDKpL2w6QdTGLVn9agRtwcvHbB40fQWxPa56WzZkjnIZpKT4YK +fWzqTTKACrJ6CZtpS5iB4i7sAnCWH/31Rs7K3IKjYzBhMA8GA1UdEwEB/wQFMAMB +Af8wHwYDVR0jBBgwFoAU/4Ixcj75xGZsrTie0bBRiKWQzPUwHQYDVR0OBBYEFP+C +MXI++cRmbK04ntGwUYilkMz1MA4GA1UdDwEB/wQEAwIBBjAKBggqhkjOPQQDAwNp +ADBmAjEA5gVYaWHlLcoNy/EZCL3W/VGSGn5jVASQkZo1kTmZ+gepZpO6yGjUij/6 +7W4WAie3AjEA3VoXK3YdZUKWpqxdinlW2Iob35reX8dQj7FbcQwm32pAAOwzkSFx +vmjkI6TZraE3 +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIF8zCCA9ugAwIBAgIUDU3FzRYilZYIfrgLfxUGNPt5EDQwDQYJKoZIhvcNAQEL +BQAwgYAxCzAJBgNVBAYTAlRSMQ8wDQYDVQQHEwZBbmthcmExGTAXBgNVBAoTEEUt +VHVncmEgRUJHIEEuUy4xHTAbBgNVBAsTFEUtVHVncmEgVHJ1c3QgQ2VudGVyMSYw +JAYDVQQDEx1FLVR1Z3JhIEdsb2JhbCBSb290IENBIFJTQSB2MzAeFw0yMDAzMTgw +OTA3MTdaFw00NTAzMTIwOTA3MTdaMIGAMQswCQYDVQQGEwJUUjEPMA0GA1UEBxMG +QW5rYXJhMRkwFwYDVQQKExBFLVR1Z3JhIEVCRyBBLlMuMR0wGwYDVQQLExRFLVR1 +Z3JhIFRydXN0IENlbnRlcjEmMCQGA1UEAxMdRS1UdWdyYSBHbG9iYWwgUm9vdCBD +QSBSU0EgdjMwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCiZvCJt3J7 +7gnJY9LTQ91ew6aEOErxjYG7FL1H6EAX8z3DeEVypi6Q3po61CBxyryfHUuXCscx +uj7X/iWpKo429NEvx7epXTPcMHD4QGxLsqYxYdE0PD0xesevxKenhOGXpOhL9hd8 +7jwH7eKKV9y2+/hDJVDqJ4GohryPUkqWOmAalrv9c/SF/YP9f4RtNGx/ardLAQO/ +rWm31zLZ9Vdq6YaCPqVmMbMWPcLzJmAy01IesGykNz709a/r4d+ABs8qQedmCeFL +l+d3vSFtKbZnwy1+7dZ5ZdHPOrbRsV5WYVB6Ws5OUDGAA5hH5+QYfERaxqSzO8bG +wzrwbMOLyKSRBfP12baqBqG3q+Sx6iEUXIOk/P+2UNOMEiaZdnDpwA+mdPy70Bt4 +znKS4iicvObpCdg604nmvi533wEKb5b25Y08TVJ2Glbhc34XrD2tbKNSEhhw5oBO +M/J+JjKsBY04pOZ2PJ8QaQ5tndLBeSBrW88zjdGUdjXnXVXHt6woq0bM5zshtQoK +5EpZ3IE1S0SVEgpnpaH/WwAH0sDM+T/8nzPyAPiMbIedBi3x7+PmBvrFZhNb/FAH +nnGGstpvdDDPk1Po3CLW3iAfYY2jLqN4MpBs3KwytQXk9TwzDdbgh3cXTJ2w2Amo +DVf3RIXwyAS+XF1a4xeOVGNpf0l0ZAWMowIDAQABo2MwYTAPBgNVHRMBAf8EBTAD +AQH/MB8GA1UdIwQYMBaAFLK0ruYt9ybVqnUtdkvAG1Mh0EjvMB0GA1UdDgQWBBSy +tK7mLfcm1ap1LXZLwBtTIdBI7zAOBgNVHQ8BAf8EBAMCAQYwDQYJKoZIhvcNAQEL +BQADggIBAImocn+M684uGMQQgC0QDP/7FM0E4BQ8Tpr7nym/Ip5XuYJzEmMmtcyQ +6dIqKe6cLcwsmb5FJ+Sxce3kOJUxQfJ9emN438o2Fi+CiJ+8EUdPdk3ILY7r3y18 +Tjvarvbj2l0Upq7ohUSdBm6O++96SmotKygY/r+QLHUWnw/qln0F7psTpURs+APQ +3SPh/QMSEgj0GDSz4DcLdxEBSL9htLX4GdnLTeqjjO/98Aa1bZL0SmFQhO3sSdPk +vmjmLuMxC1QLGpLWgti2omU8ZgT5Vdps+9u1FGZNlIM7zR6mK7L+d0CGq+ffCsn9 +9t2HVhjYsCxVYJb6CH5SkPVLpi6HfMsg2wY+oF0Dd32iPBMbKaITVaA9FCKvb7jQ +mhty3QUBjYZgv6Rn7rWlDdF/5horYmbDB7rnoEgcOMPpRfunf/ztAmgayncSd6YA +VSgU7NbHEqIbZULpkejLPoeJVF3Zr52XnGnnCv8PWniLYypMfUeUP95L6VPQMPHF +9p5J3zugkaOj/s1YzOrfr28oO6Bpm4/srK4rVJ2bBLFHIK+WEj5jlB0E5y67hscM +moi/dkfv97ALl2bSRM9gUgfh1SxKOidhd8rXj+eHDjD/DLsE4mHDosiXYY60MGo8 +bcIHX0pzLz/5FooBZu+6kcpSV3uu1OYP3Qt6f4ueJiDPO++BcYNZ +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICVDCCAdugAwIBAgIQZ3SdjXfYO2rbIvT/WeK/zjAKBggqhkjOPQQDAzBsMQsw +CQYDVQQGEwJHUjE3MDUGA1UECgwuSGVsbGVuaWMgQWNhZGVtaWMgYW5kIFJlc2Vh +cmNoIEluc3RpdHV0aW9ucyBDQTEkMCIGA1UEAwwbSEFSSUNBIFRMUyBFQ0MgUm9v +dCBDQSAyMDIxMB4XDTIxMDIxOTExMDExMFoXDTQ1MDIxMzExMDEwOVowbDELMAkG +A1UEBhMCR1IxNzA1BgNVBAoMLkhlbGxlbmljIEFjYWRlbWljIGFuZCBSZXNlYXJj +aCBJbnN0aXR1dGlvbnMgQ0ExJDAiBgNVBAMMG0hBUklDQSBUTFMgRUNDIFJvb3Qg +Q0EgMjAyMTB2MBAGByqGSM49AgEGBSuBBAAiA2IABDgI/rGgltJ6rK9JOtDA4MM7 +KKrxcm1lAEeIhPyaJmuqS7psBAqIXhfyVYf8MLA04jRYVxqEU+kw2anylnTDUR9Y +STHMmE5gEYd103KUkE+bECUqqHgtvpBBWJAVcqeht6NCMEAwDwYDVR0TAQH/BAUw +AwEB/zAdBgNVHQ4EFgQUyRtTgRL+BNUW0aq8mm+3oJUZbsowDgYDVR0PAQH/BAQD +AgGGMAoGCCqGSM49BAMDA2cAMGQCMBHervjcToiwqfAircJRQO9gcS3ujwLEXQNw +SaSS6sUUiHCm0w2wqsosQJz76YJumgIwK0eaB8bRwoF8yguWGEEbo/QwCZ61IygN +nxS2PFOiTAZpffpskcYqSUXm7LcT4Tps +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFpDCCA4ygAwIBAgIQOcqTHO9D88aOk8f0ZIk4fjANBgkqhkiG9w0BAQsFADBs +MQswCQYDVQQGEwJHUjE3MDUGA1UECgwuSGVsbGVuaWMgQWNhZGVtaWMgYW5kIFJl +c2VhcmNoIEluc3RpdHV0aW9ucyBDQTEkMCIGA1UEAwwbSEFSSUNBIFRMUyBSU0Eg +Um9vdCBDQSAyMDIxMB4XDTIxMDIxOTEwNTUzOFoXDTQ1MDIxMzEwNTUzN1owbDEL +MAkGA1UEBhMCR1IxNzA1BgNVBAoMLkhlbGxlbmljIEFjYWRlbWljIGFuZCBSZXNl +YXJjaCBJbnN0aXR1dGlvbnMgQ0ExJDAiBgNVBAMMG0hBUklDQSBUTFMgUlNBIFJv +b3QgQ0EgMjAyMTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAIvC569l +mwVnlskNJLnQDmT8zuIkGCyEf3dRywQRNrhe7Wlxp57kJQmXZ8FHws+RFjZiPTgE +4VGC/6zStGndLuwRo0Xua2s7TL+MjaQenRG56Tj5eg4MmOIjHdFOY9TnuEFE+2uv +a9of08WRiFukiZLRgeaMOVig1mlDqa2YUlhu2wr7a89o+uOkXjpFc5gH6l8Cct4M +pbOfrqkdtx2z/IpZ525yZa31MJQjB/OCFks1mJxTuy/K5FrZx40d/JiZ+yykgmvw +Kh+OC19xXFyuQnspiYHLA6OZyoieC0AJQTPb5lh6/a6ZcMBaD9YThnEvdmn8kN3b +LW7R8pv1GmuebxWMevBLKKAiOIAkbDakO/IwkfN4E8/BPzWr8R0RI7VDIp4BkrcY +AuUR0YLbFQDMYTfBKnya4dC6s1BG7oKsnTH4+yPiAwBIcKMJJnkVU2DzOFytOOqB +AGMUuTNe3QvboEUHGjMJ+E20pwKmafTCWQWIZYVWrkvL4N48fS0ayOn7H6NhStYq +E613TBoYm5EPWNgGVMWX+Ko/IIqmhaZ39qb8HOLubpQzKoNQhArlT4b4UEV4AIHr +W2jjJo3Me1xR9BQsQL4aYB16cmEdH2MtiKrOokWQCPxrvrNQKlr9qEgYRtaQQJKQ +CoReaDH46+0N0x3GfZkYVVYnZS6NRcUk7M7jAgMBAAGjQjBAMA8GA1UdEwEB/wQF +MAMBAf8wHQYDVR0OBBYEFApII6ZgpJIKM+qTW8VX6iVNvRLuMA4GA1UdDwEB/wQE +AwIBhjANBgkqhkiG9w0BAQsFAAOCAgEAPpBIqm5iFSVmewzVjIuJndftTgfvnNAU +X15QvWiWkKQUEapobQk1OUAJ2vQJLDSle1mESSmXdMgHHkdt8s4cUCbjnj1AUz/3 +f5Z2EMVGpdAgS1D0NTsY9FVqQRtHBmg8uwkIYtlfVUKqrFOFrJVWNlar5AWMxaja +H6NpvVMPxP/cyuN+8kyIhkdGGvMA9YCRotxDQpSbIPDRzbLrLFPCU3hKTwSUQZqP +JzLB5UkZv/HywouoCjkxKLR9YjYsTewfM7Z+d21+UPCfDtcRj88YxeMn/ibvBZ3P +zzfF0HvaO7AWhAw6k9a+F9sPPg4ZeAnHqQJyIkv3N3a6dcSFA1pj1bF1BcK5vZSt +jBWZp5N99sXzqnTPBIWUmAD04vnKJGW/4GKvyMX6ssmeVkjaef2WdhW+o45WxLM0 +/L5H9MG0qPzVMIho7suuyWPEdr6sOBjhXlzPrjoiUevRi7PzKzMHVIf6tLITe7pT +BGIBnfHAT+7hOtSLIBD6Alfm78ELt5BGnBkpjNxvoEppaZS3JGWg/6w/zgH7IS79 +aPib8qXPMThcFarmlwDB31qlpzmq6YR/PFGoOtmUW4y/Twhx5duoXNTSpv4Ao8YW +xw/ogM4cKGR0GQjTQuPOAF1/sdwTsOEFy9EgqoZ0njnnkf3/W9b3raYvAwtt41dU +63ZTGI0RmLo= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFajCCA1KgAwIBAgIQLd2szmKXlKFD6LDNdmpeYDANBgkqhkiG9w0BAQsFADBP +MQswCQYDVQQGEwJUVzEjMCEGA1UECgwaQ2h1bmdod2EgVGVsZWNvbSBDby4sIEx0 +ZC4xGzAZBgNVBAMMEkhpUEtJIFJvb3QgQ0EgLSBHMTAeFw0xOTAyMjIwOTQ2MDRa +Fw0zNzEyMzExNTU5NTlaME8xCzAJBgNVBAYTAlRXMSMwIQYDVQQKDBpDaHVuZ2h3 +YSBUZWxlY29tIENvLiwgTHRkLjEbMBkGA1UEAwwSSGlQS0kgUm9vdCBDQSAtIEcx +MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA9B5/UnMyDHPkvRN0o9Qw +qNCuS9i233VHZvR85zkEHmpwINJaR3JnVfSl6J3VHiGh8Ge6zCFovkRTv4354twv +Vcg3Px+kwJyz5HdcoEb+d/oaoDjq7Zpy3iu9lFc6uux55199QmQ5eiY29yTw1S+6 +lZgRZq2XNdZ1AYDgr/SEYYwNHl98h5ZeQa/rh+r4XfEuiAU+TCK72h8q3VJGZDnz +Qs7ZngyzsHeXZJzA9KMuH5UHsBffMNsAGJZMoYFL3QRtU6M9/Aes1MU3guvklQgZ +KILSQjqj2FPseYlgSGDIcpJQ3AOPgz+yQlda22rpEZfdhSi8MEyr48KxRURHH+CK +FgeW0iEPU8DtqX7UTuybCeyvQqww1r/REEXgphaypcXTT3OUM3ECoWqj1jOXTyFj +HluP2cFeRXF3D4FdXyGarYPM+l7WjSNfGz1BryB1ZlpK9p/7qxj3ccC2HTHsOyDr +y+K49a6SsvfhhEvyovKTmiKe0xRvNlS9H15ZFblzqMF8b3ti6RZsR1pl8w4Rm0bZ +/W3c1pzAtH2lsN0/Vm+h+fbkEkj9Bn8SV7apI09bA8PgcSojt/ewsTu8mL3WmKgM +a/aOEmem8rJY5AIJEzypuxC00jBF8ez3ABHfZfjcK0NVvxaXxA/VLGGEqnKG/uY6 +fsI/fe78LxQ+5oXdUG+3Se0CAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAdBgNV +HQ4EFgQU8ncX+l6o/vY9cdVouslGDDjYr7AwDgYDVR0PAQH/BAQDAgGGMA0GCSqG +SIb3DQEBCwUAA4ICAQBQUfB13HAE4/+qddRxosuej6ip0691x1TPOhwEmSKsxBHi +7zNKpiMdDg1H2DfHb680f0+BazVP6XKlMeJ45/dOlBhbQH3PayFUhuaVevvGyuqc +SE5XCV0vrPSltJczWNWseanMX/mF+lLFjfiRFOs6DRfQUsJ748JzjkZ4Bjgs6Fza +ZsT0pPBWGTMpWmWSBUdGSquEwx4noR8RkpkndZMPvDY7l1ePJlsMu5wP1G4wB9Tc +XzZoZjmDlicmisjEOf6aIW/Vcobpf2Lll07QJNBAsNB1CI69aO4I1258EHBGG3zg +iLKecoaZAeO/n0kZtCW+VmWuF2PlHt/o/0elv+EmBYTksMCv5wiZqAxeJoBF1Pho +L5aPruJKHJwWDBNvOIf2u8g0X5IDUXlwpt/L9ZlNec1OvFefQ05rLisY+GpzjLrF +Ne85akEez3GoorKGB1s6yeHvP2UEgEcyRHCVTjFnanRbEEV16rCf0OY1/k6fi8wr +kkVbbiVghUbN0aqwdmaTd5a+g744tiROJgvM7XpWGuDpWsZkrUx6AEhEL7lAuxM+ +vhV4nYWBSipX3tUZQ9rbyltHhoMLP7YNdnhzeSJesYAfz77RP1YQmCuVh6EfnWQU +YDksswBVLuT1sw5XxJFBAJw/6KXf6vb/yPCtbVKoF6ubYfwSUTXkJf2vqmqGOQ== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICGzCCAaGgAwIBAgIQQdKd0XLq7qeAwSxs6S+HUjAKBggqhkjOPQQDAzBPMQsw +CQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJuZXQgU2VjdXJpdHkgUmVzZWFyY2gg +R3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBYMjAeFw0yMDA5MDQwMDAwMDBaFw00 +MDA5MTcxNjAwMDBaME8xCzAJBgNVBAYTAlVTMSkwJwYDVQQKEyBJbnRlcm5ldCBT +ZWN1cml0eSBSZXNlYXJjaCBHcm91cDEVMBMGA1UEAxMMSVNSRyBSb290IFgyMHYw +EAYHKoZIzj0CAQYFK4EEACIDYgAEzZvVn4CDCuwJSvMWSj5cz3es3mcFDR0HttwW ++1qLFNvicWDEukWVEYmO6gbf9yoWHKS5xcUy4APgHoIYOIvXRdgKam7mAHf7AlF9 +ItgKbppbd9/w+kHsOdx1ymgHDB/qo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0T +AQH/BAUwAwEB/zAdBgNVHQ4EFgQUfEKWrt5LSDv6kviejM9ti6lyN5UwCgYIKoZI +zj0EAwMDaAAwZQIwe3lORlCEwkSHRhtFcP9Ymd70/aTSVaYgLXTWNLxBo1BfASdW +tL4ndQavEi51mI38AjEAi/V3bNTIZargCyzuFJ0nN6T5U6VR5CmD1/iQMVtCnwr1 +/q4AaOeMSQ+2b1tbFfLn +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICODCCAb6gAwIBAgIJANZdm7N4gS7rMAoGCCqGSM49BAMDMGExCzAJBgNVBAYT +AkpQMSUwIwYDVQQKExxTRUNPTSBUcnVzdCBTeXN0ZW1zIENPLixMVEQuMSswKQYD +VQQDEyJTZWN1cml0eSBDb21tdW5pY2F0aW9uIEVDQyBSb290Q0ExMB4XDTE2MDYx +NjA1MTUyOFoXDTM4MDExODA1MTUyOFowYTELMAkGA1UEBhMCSlAxJTAjBgNVBAoT +HFNFQ09NIFRydXN0IFN5c3RlbXMgQ08uLExURC4xKzApBgNVBAMTIlNlY3VyaXR5 +IENvbW11bmljYXRpb24gRUNDIFJvb3RDQTEwdjAQBgcqhkjOPQIBBgUrgQQAIgNi +AASkpW9gAwPDvTH00xecK4R1rOX9PVdu12O/5gSJko6BnOPpR27KkBLIE+Cnnfdl +dB9sELLo5OnvbYUymUSxXv3MdhDYW72ixvnWQuRXdtyQwjWpS4g8EkdtXP9JTxpK +ULGjQjBAMB0GA1UdDgQWBBSGHOf+LaVKiwj+KBH6vqNm+GBZLzAOBgNVHQ8BAf8E +BAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAKBggqhkjOPQQDAwNoADBlAjAVXUI9/Lbu +9zuxNuie9sRGKEkz0FhDKmMpzE2xtHqiuQ04pV1IKv3LsnNdo4gIxwwCMQDAqy0O +be0YottT6SXbVQjgUMzfRGEWgqtJsLKB7HOHeLRMsmIbEvoWTSVLY70eN9k= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFfzCCA2egAwIBAgIJAOF8N0D9G/5nMA0GCSqGSIb3DQEBDAUAMF0xCzAJBgNV +BAYTAkpQMSUwIwYDVQQKExxTRUNPTSBUcnVzdCBTeXN0ZW1zIENPLixMVEQuMScw +JQYDVQQDEx5TZWN1cml0eSBDb21tdW5pY2F0aW9uIFJvb3RDQTMwHhcNMTYwNjE2 +MDYxNzE2WhcNMzgwMTE4MDYxNzE2WjBdMQswCQYDVQQGEwJKUDElMCMGA1UEChMc +U0VDT00gVHJ1c3QgU3lzdGVtcyBDTy4sTFRELjEnMCUGA1UEAxMeU2VjdXJpdHkg +Q29tbXVuaWNhdGlvbiBSb290Q0EzMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIIC +CgKCAgEA48lySfcw3gl8qUCBWNO0Ot26YQ+TUG5pPDXC7ltzkBtnTCHsXzW7OT4r +CmDvu20rhvtxosis5FaU+cmvsXLUIKx00rgVrVH+hXShuRD+BYD5UpOzQD11EKzA +lrenfna84xtSGc4RHwsENPXY9Wk8d/Nk9A2qhd7gCVAEF5aEt8iKvE1y/By7z/MG +TfmfZPd+pmaGNXHIEYBMwXFAWB6+oHP2/D5Q4eAvJj1+XCO1eXDe+uDRpdYMQXF7 +9+qMHIjH7Iv10S9VlkZ8WjtYO/u62C21Jdp6Ts9EriGmnpjKIG58u4iFW/vAEGK7 +8vknR+/RiTlDxN/e4UG/VHMgly1s2vPUB6PmudhvrvyMGS7TZ2crldtYXLVqAvO4 +g160a75BflcJdURQVc1aEWEhCmHCqYj9E7wtiS/NYeCVvsq1e+F7NGcLH7YMx3we +GVPKp7FKFSBWFHA9K4IsD50VHUeAR/94mQ4xr28+j+2GaR57GIgUssL8gjMunEst ++3A7caoreyYn8xrC3PsXuKHqy6C0rtOUfnrQq8PsOC0RLoi/1D+tEjtCrI8Cbn3M +0V9hvqG8OmpI6iZVIhZdXw3/JzOfGAN0iltSIEdrRU0id4xVJ/CvHozJgyJUt5rQ +T9nO/NkuHJYosQLTA70lUhw0Zk8jq/R3gpYd0VcwCBEF/VfR2ccCAwEAAaNCMEAw +HQYDVR0OBBYEFGQUfPxYchamCik0FW8qy7z8r6irMA4GA1UdDwEB/wQEAwIBBjAP +BgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBDAUAA4ICAQDcAiMI4u8hOscNtybS +YpOnpSNyByCCYN8Y11StaSWSntkUz5m5UoHPrmyKO1o5yGwBQ8IibQLwYs1OY0PA +FNr0Y/Dq9HHuTofjcan0yVflLl8cebsjqodEV+m9NU1Bu0soo5iyG9kLFwfl9+qd +9XbXv8S2gVj/yP9kaWJ5rW4OH3/uHWnlt3Jxs/6lATWUVCvAUm2PVcTJ0rjLyjQI +UYWg9by0F1jqClx6vWPGOi//lkkZhOpn2ASxYfQAW0q3nHE3GYV5v4GwxxMOdnE+ +OoAGrgYWp421wsTL/0ClXI2lyTrtcoHKXJg80jQDdwj98ClZXSEIx2C/pHF7uNke +gr4Jr2VvKKu/S7XuPghHJ6APbw+LP6yVGPO5DtxnVW5inkYO0QR4ynKudtml+LLf +iAlhi+8kTtFZP1rUPcmTPCtk9YENFpb3ksP+MW/oKjJ0DvRMmEoYDjBU1cXrvMUV +nuiZIesnKwkK2/HmcBhWuwzkvvnoEKQTkrgc4NtnHVMDpCKn3F2SEDzq//wbEBrD +2NCcnWXL0CsnMQMeNuE9dnUM/0Umud1RvCPHX9jYhxBAEg09ODfnRDwYwFMJZI// +1ZqmfHAuc1Uh6N//g7kdPjIe1qZ9LPFm6Vwdp6POXiUyK+OVrCoHzrQoeIY8Laad +TdJ0MN1kURXbg4NR16/9M51NZg== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFdDCCA1ygAwIBAgIPAWdfJ9b+euPkrL4JWwWeMA0GCSqGSIb3DQEBCwUAMEQx +CzAJBgNVBAYTAkZJMRowGAYDVQQKDBFUZWxpYSBGaW5sYW5kIE95ajEZMBcGA1UE +AwwQVGVsaWEgUm9vdCBDQSB2MjAeFw0xODExMjkxMTU1NTRaFw00MzExMjkxMTU1 +NTRaMEQxCzAJBgNVBAYTAkZJMRowGAYDVQQKDBFUZWxpYSBGaW5sYW5kIE95ajEZ +MBcGA1UEAwwQVGVsaWEgUm9vdCBDQSB2MjCCAiIwDQYJKoZIhvcNAQEBBQADggIP +ADCCAgoCggIBALLQPwe84nvQa5n44ndp586dpAO8gm2h/oFlH0wnrI4AuhZ76zBq +AMCzdGh+sq/H1WKzej9Qyow2RCRj0jbpDIX2Q3bVTKFgcmfiKDOlyzG4OiIjNLh9 +vVYiQJ3q9HsDrWj8soFPmNB06o3lfc1jw6P23pLCWBnglrvFxKk9pXSW/q/5iaq9 +lRdU2HhE8Qx3FZLgmEKnpNaqIJLNwaCzlrI6hEKNfdWV5Nbb6WLEWLN5xYzTNTOD +n3WhUidhOPFZPY5Q4L15POdslv5e2QJltI5c0BE0312/UqeBAMN/mUWZFdUXyApT +7GPzmX3MaRKGwhfwAZ6/hLzRUssbkmbOpFPlob/E2wnW5olWK8jjfN7j/4nlNW4o +6GwLI1GpJQXrSPjdscr6bAhR77cYbETKJuFzxokGgeWKrLDiKca5JLNrRBH0pUPC +TEPlcDaMtjNXepUugqD0XBCzYYP2AgWGLnwtbNwDRm41k9V6lS/eINhbfpSQBGq6 +WT0EBXWdN6IOLj3rwaRSg/7Qa9RmjtzG6RJOHSpXqhC8fF6CfaamyfItufUXJ63R +DolUK5X6wK0dmBR4M0KGCqlztft0DbcbMBnEWg4cJ7faGND/isgFuvGqHKI3t+ZI +pEYslOqodmJHixBTB0hXbOKSTbauBcvcwUpej6w9GU7C7WB1K9vBykLVAgMBAAGj +YzBhMB8GA1UdIwQYMBaAFHKs5DN5qkWH9v2sHZ7Wxy+G2CQ5MB0GA1UdDgQWBBRy +rOQzeapFh/b9rB2e1scvhtgkOTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUw +AwEB/zANBgkqhkiG9w0BAQsFAAOCAgEAoDtZpwmUPjaE0n4vOaWWl/oRrfxn83EJ +8rKJhGdEr7nv7ZbsnGTbMjBvZ5qsfl+yqwE2foH65IRe0qw24GtixX1LDoJt0nZi +0f6X+J8wfBj5tFJ3gh1229MdqfDBmgC9bXXYfef6xzijnHDoRnkDry5023X4blMM +A8iZGok1GTzTyVR8qPAs5m4HeW9q4ebqkYJpCh3DflminmtGFZhb069GHWLIzoBS +SRE/yQQSwxN8PzuKlts8oB4KtItUsiRnDe+Cy748fdHif64W1lZYudogsYMVoe+K +TTJvQS8TUoKU1xrBeKJR3Stwbbca+few4GeXVtt8YVMJAygCQMez2P2ccGrGKMOF +6eLtGpOg3kuYooQ+BXcBlj37tCAPnHICehIv1aO6UXivKitEZU61/Qrowc15h2Er +3oBXRb9n8ZuRXqWk7FlIEA04x7D6w0RtBPV4UBySllva9bguulvP5fBqnUsvWHMt +Ty3EHD70sz+rFQ47GUGKpMFXEmZxTPpT41frYpUJnlTd0cI8Vzy9OK2YZLe4A5pT +VmBds9hCG1xLEooc6+t9xnppxyd/pPiL8uSUZodL6ZQHCRJ5irLrdATczvREWeAW +ysUsWNc8e89ihmpQfTU2Zqf7N+cox9jQraVplI/owd8k+BsHMYeB2F326CjYSlKA +rBPuUBQemMc= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFszCCA5ugAwIBAgIUEwLV4kBMkkaGFmddtLu7sms+/BMwDQYJKoZIhvcNAQEL +BQAwYTELMAkGA1UEBhMCVE4xNzA1BgNVBAoMLkFnZW5jZSBOYXRpb25hbGUgZGUg +Q2VydGlmaWNhdGlvbiBFbGVjdHJvbmlxdWUxGTAXBgNVBAMMEFR1blRydXN0IFJv +b3QgQ0EwHhcNMTkwNDI2MDg1NzU2WhcNNDQwNDI2MDg1NzU2WjBhMQswCQYDVQQG +EwJUTjE3MDUGA1UECgwuQWdlbmNlIE5hdGlvbmFsZSBkZSBDZXJ0aWZpY2F0aW9u +IEVsZWN0cm9uaXF1ZTEZMBcGA1UEAwwQVHVuVHJ1c3QgUm9vdCBDQTCCAiIwDQYJ +KoZIhvcNAQEBBQADggIPADCCAgoCggIBAMPN0/y9BFPdDCA61YguBUtB9YOCfvdZ +n56eY+hz2vYGqU8ftPkLHzmMmiDQfgbU7DTZhrx1W4eI8NLZ1KMKsmwb60ksPqxd +2JQDoOw05TDENX37Jk0bbjBU2PWARZw5rZzJJQRNmpA+TkBuimvNKWfGzC3gdOgF +VwpIUPp6Q9p+7FuaDmJ2/uqdHYVy7BG7NegfJ7/Boce7SBbdVtfMTqDhuazb1YMZ +GoXRlJfXyqNlC/M4+QKu3fZnz8k/9YosRxqZbwUN/dAdgjH8KcwAWJeRTIAAHDOF +li/LQcKLEITDCSSJH7UP2dl3RxiSlGBcx5kDPP73lad9UKGAwqmDrViWVSHbhlnU +r8a83YFuB9tgYv7sEG7aaAH0gxupPqJbI9dkxt/con3YS7qC0lH4Zr8GRuR5KiY2 +eY8fTpkdso8MDhz/yV3A/ZAQprE38806JG60hZC/gLkMjNWb1sjxVj8agIl6qeIb +MlEsPvLfe/ZdeikZjuXIvTZxi11Mwh0/rViizz1wTaZQmCXcI/m4WEEIcb9PuISg +jwBUFfyRbVinljvrS5YnzWuioYasDXxU5mZMZl+QviGaAkYt5IPCgLnPSz7ofzwB +7I9ezX/SKEIBlYrilz0QIX32nRzFNKHsLA4KUiwSVXAkPcvCFDVDXSdOvsC9qnyW +5/yeYa1E0wCXAgMBAAGjYzBhMB0GA1UdDgQWBBQGmpsfU33x9aTI04Y+oXNZtPdE +ITAPBgNVHRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFAaamx9TffH1pMjThj6hc1m0 +90QhMA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQsFAAOCAgEAqgVutt0Vyb+z +xiD2BkewhpMl0425yAA/l/VSJ4hxyXT968pk21vvHl26v9Hr7lxpuhbI87mP0zYu +QEkHDVneixCwSQXi/5E/S7fdAo74gShczNxtr18UnH1YeA32gAm56Q6XKRm4t+v4 +FstVEuTGfbvE7Pi1HE4+Z7/FXxttbUcoqgRYYdZ2vyJ/0Adqp2RT8JeNnYA/u8EH +22Wv5psymsNUk8QcCMNE+3tjEUPRahphanltkE8pjkcFwRJpadbGNjHh/PqAulxP +xOu3Mqz4dWEX1xAZufHSCe96Qp1bWgvUxpVOKs7/B9dPfhgGiPEZtdmYu65xxBzn +dFlY7wyJz4sfdZMaBBSSSFCp61cpABbjNhzI+L/wM9VBD8TMPN3pM0MBkRArHtG5 +Xc0yGYuPjCB31yLEQtyEFpslbei0VXF/sHyz03FJuc9SpAQ/3D2gu68zngowYI7b +nV2UqL1g52KAdoGDDIzMMEZJ4gzSqK/rYXHv5yJiqfdcZGyfFoxnNidF9Ql7v/YQ +CvGwjVRDjAS6oz/v4jXH+XTgbzRB0L9zZVcg+ZtnemZoJE6AZb0QmQZZ8mWvuMZH +u/2QeItBcy6vVR/cO5JyboTT0GFMDcx2V+IthSIVNg3rAZ3r2OvEhJn7wAzMMujj +d9qDRIueVSjAi1jTkD5OGwDxFa2DK5o= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICDzCCAZWgAwIBAgIUbmq8WapTvpg5Z6LSa6Q75m0c1towCgYIKoZIzj0EAwMw +RzELMAkGA1UEBhMCQ04xHDAaBgNVBAoTE2lUcnVzQ2hpbmEgQ28uLEx0ZC4xGjAY +BgNVBAMTEXZUcnVzIEVDQyBSb290IENBMB4XDTE4MDczMTA3MjY0NFoXDTQzMDcz +MTA3MjY0NFowRzELMAkGA1UEBhMCQ04xHDAaBgNVBAoTE2lUcnVzQ2hpbmEgQ28u +LEx0ZC4xGjAYBgNVBAMTEXZUcnVzIEVDQyBSb290IENBMHYwEAYHKoZIzj0CAQYF +K4EEACIDYgAEZVBKrox5lkqqHAjDo6LN/llWQXf9JpRCux3NCNtzslt188+cToL0 +v/hhJoVs1oVbcnDS/dtitN9Ti72xRFhiQgnH+n9bEOf+QP3A2MMrMudwpremIFUd +e4BdS49nTPEQo0IwQDAdBgNVHQ4EFgQUmDnNvtiyjPeyq+GtJK97fKHbH88wDwYD +VR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwCgYIKoZIzj0EAwMDaAAwZQIw +V53dVvHH4+m4SVBrm2nDb+zDfSXkV5UTQJtS0zvzQBm8JsctBp61ezaf9SXUY2sA +AjEA6dPGnlaaKsyh2j/IZivTWJwghfqrkYpwcBE4YGQLYgmRWAD5Tfs0aNoJrSEG +GJTO +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFVjCCAz6gAwIBAgIUQ+NxE9izWRRdt86M/TX9b7wFjUUwDQYJKoZIhvcNAQEL +BQAwQzELMAkGA1UEBhMCQ04xHDAaBgNVBAoTE2lUcnVzQ2hpbmEgQ28uLEx0ZC4x +FjAUBgNVBAMTDXZUcnVzIFJvb3QgQ0EwHhcNMTgwNzMxMDcyNDA1WhcNNDMwNzMx +MDcyNDA1WjBDMQswCQYDVQQGEwJDTjEcMBoGA1UEChMTaVRydXNDaGluYSBDby4s +THRkLjEWMBQGA1UEAxMNdlRydXMgUm9vdCBDQTCCAiIwDQYJKoZIhvcNAQEBBQAD +ggIPADCCAgoCggIBAL1VfGHTuB0EYgWgrmy3cLRB6ksDXhA/kFocizuwZotsSKYc +IrrVQJLuM7IjWcmOvFjai57QGfIvWcaMY1q6n6MLsLOaXLoRuBLpDLvPbmyAhykU +AyyNJJrIZIO1aqwTLDPxn9wsYTwaP3BVm60AUn/PBLn+NvqcwBauYv6WTEN+VRS+ +GrPSbcKvdmaVayqwlHeFXgQPYh1jdfdr58tbmnDsPmcF8P4HCIDPKNsFxhQnL4Z9 +8Cfe/+Z+M0jnCx5Y0ScrUw5XSmXX+6KAYPxMvDVTAWqXcoKv8R1w6Jz1717CbMdH +flqUhSZNO7rrTOiwCcJlwp2dCZtOtZcFrPUGoPc2BX70kLJrxLT5ZOrpGgrIDajt +J8nU57O5q4IikCc9Kuh8kO+8T/3iCiSn3mUkpF3qwHYw03dQ+A0Em5Q2AXPKBlim +0zvc+gRGE1WKyURHuFE5Gi7oNOJ5y1lKCn+8pu8fA2dqWSslYpPZUxlmPCdiKYZN +pGvu/9ROutW04o5IWgAZCfEF2c6Rsffr6TlP9m8EQ5pV9T4FFL2/s1m02I4zhKOQ +UqqzApVg+QxMaPnu1RcN+HFXtSXkKe5lXa/R7jwXC1pDxaWG6iSe4gUH3DRCEpHW +OXSuTEGC2/KmSNGzm/MzqvOmwMVO9fSddmPmAsYiS8GVP1BkLFTltvA8Kc9XAgMB +AAGjQjBAMB0GA1UdDgQWBBRUYnBj8XWEQ1iO0RYgscasGrz2iTAPBgNVHRMBAf8E +BTADAQH/MA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQsFAAOCAgEAKbqSSaet +8PFww+SX8J+pJdVrnjT+5hpk9jprUrIQeBqfTNqK2uwcN1LgQkv7bHbKJAs5EhWd +nxEt/Hlk3ODg9d3gV8mlsnZwUKT+twpw1aA08XXXTUm6EdGz2OyC/+sOxL9kLX1j +bhd47F18iMjrjld22VkE+rxSH0Ws8HqA7Oxvdq6R2xCOBNyS36D25q5J08FsEhvM +Kar5CKXiNxTKsbhm7xqC5PD48acWabfbqWE8n/Uxy+QARsIvdLGx14HuqCaVvIiv +TDUHKgLKeBRtRytAVunLKmChZwOgzoy8sHJnxDHO2zTlJQNgJXtxmOTAGytfdELS +S8VZCAeHvsXDf+eW2eHcKJfWjwXj9ZtOyh1QRwVTsMo554WgicEFOwE30z9J4nfr +I8iIZjs9OXYhRvHsXyO466JmdXTBQPfYaJqT4i2pLr0cox7IdMakLXogqzu4sEb9 +b91fUlV1YvCXoHzXOP0l382gmxDPi7g4Xl7FtKYCNqEeXxzP4padKar9mK5S4fNB +UvupLnKWnyfjqnN9+BojZns7q2WwMgFLFT49ok8MKzWixtlnEjUwzXYuFrOZnk1P +Ti07NEPhmg4NpGaXutIcSkwsKouLgU9xGqndXHt7CMUADTdA43x7VF8vhV929ven +sBxXVsFy6K2ir40zSbofitzmdHxghm+Hl3s= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIE3TCCAsWgAwIBAgIKPxb5sAAAAAAAFzANBgkqhkiG9w0BAQ0FADAfMR0wGwYD +VQQDExRZYW5kZXhJbnRlcm5hbFJvb3RDQTAeFw0xNzA2MjAxNjQ0MzdaFw0yNzA2 +MjAxNjU0MzdaMFUxEjAQBgoJkiaJk/IsZAEZFgJydTEWMBQGCgmSJomT8ixkARkW +BnlhbmRleDESMBAGCgmSJomT8ixkARkWAmxkMRMwEQYDVQQDEwpZYW5kZXhDTENB +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqgNnjk0JKPcbsk1+KG2t +eM1AfMnEe5RkAJuBBuwVV49snhcvO1jhKBx/pCnjr6biICc1/oAFDVgU8yVYYPwp +WZ2vH3ZtscjJ/RAT/NS9OKKG7kKknhFhVYxua5xhoIQmm6usBNYYiTcWoFm1eHC8 +I9oddOLSscZYbh3unVRvt+3V+drVmUx9oSUKpqMgfysiv1MN6zB3vq9TFkbhz53E +k0tEcV+W2NnDaeFhLKy284FDKLvOdTDj1EDsSAihxl7sNEKpupNuhgyy2siOqUb+ +d5mO/CRfaAKGg3E6hDM3pEi48E506dJdjPXWfHKSvuguMLRlb2RWdVocRZuyWxOh +0QIDAQABo4HkMIHhMBAGCSsGAQQBgjcVAQQDAgEAMB0GA1UdDgQWBBRMU5uItjx+ +TOicX1+ovC1Xq2PSnzAZBgkrBgEEAYI3FAIEDB4KAFMAdQBiAEMAQTALBgNVHQ8E +BAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAfBgNVHSMEGDAWgBSrucX/oe/mUx0zOSKE +0XbUN04tajBUBgNVHR8ETTBLMEmgR6BFhkNodHRwOi8vY3Jscy55YW5kZXgucnUv +WWFuZGV4SW50ZXJuYWxSb290Q0EvWWFuZGV4SW50ZXJuYWxSb290Q0EuY3JsMA0G +CSqGSIb3DQEBDQUAA4ICAQAsR5Lb4Pv2FD0Kk+4oc1GEOnehxKLsQtdV81nrU+IV +l9pr2oNMdi8lwIolvHZRllLM4Ba5AcRH6YJ5fe7AjKm+5EdSkhqVWo2UOllRCbtS +wmL50+erOAkxstSlRkO6b8x1L0MOBKv54E5YcQ/Wwt27ldSb6RkEmJBGvmxObAaf +5zc51pqSqao9tnldYaCblEQ/Zmy43FliIpa2eUJoh8DqK8bVo2gcI3wbQ32tWs9u +wvKk8fo4lAdhCwhv+QHuqau1VAY9hPU106bsFIDUmijTMxjAobKBi6CkIX6EbNHU +Jv4DzYVLlDd2y0CADdn2F6I70xpCBn5cquSGuvFbqZjQDmIHwb7WQSxadkiGRWfc +zVTnmiHjJONJJIpE2t+FOV3hc+8o98OzOtNaH2QQ9j6dnKvtIGKGFeNSDp0vXPOi +QhHiIyuB7eWx+g2whktQ74UCpGDSXYnEW3s8w5wezVWIEmouq7q4rCEkTNvJ7Ico +43AgUdPzAFS2zYktw1C+cbUALM8smvXbXrXOBzMmscjIhtXvLMrpPeh23VfdJfQB +0rN2BmRCLUE8JOV+o0k98XMm83oN+lGkL1l+hyoj3ok1uI3JrsWOcDyjOds3ptcN +KimJLm27ndjcxDNo/iA6gefMJuCxFRaqI+eF4P0jSkMgnnQqZkvLGFuHCw8eRDhm +bw== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFGTCCAwGgAwIBAgIQJMM7ZIy2SYxCBgK7WcFwnjANBgkqhkiG9w0BAQ0FADAf +MR0wGwYDVQQDExRZYW5kZXhJbnRlcm5hbFJvb3RDQTAeFw0xMzAyMTExMzQxNDNa +Fw0zMzAyMTExMzUxNDJaMB8xHTAbBgNVBAMTFFlhbmRleEludGVybmFsUm9vdENB +MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAgb4xoQjBQ7oEFk8EHVGy +1pDEmPWw0Wgw5nX9RM7LL2xQWyUuEq+Lf9Dgh+O725aZ9+SO2oEs47DHHt81/fne +5N6xOftRrCpy8hGtUR/A3bvjnQgjs+zdXvcO9cTuuzzPTFSts/iZATZsAruiepMx +SGj9S1fGwvYws/yiXWNoNBz4Tu1Tlp0g+5fp/ADjnxc6DqNk6w01mJRDbx+6rlBO +aIH2tQmJXDVoFdrhmBK9qOfjxWlIYGy83TnrvdXwi5mKTMtpEREMgyNLX75UjpvO +NkZgBvEXPQq+g91wBGsWIE2sYlguXiBniQgAJOyRuSdTxcJoG8tZkLDPRi5RouWY +gxXr13edn1TRDGco2hkdtSUBlajBMSvAq+H0hkslzWD/R+BXkn9dh0/DFnxVt4XU +5JbFyd/sKV/rF4Vygfw9ssh1ZIWdqkfZ2QXOZ2gH4AEeoN/9vEfUPwqPVzL0XEZK +r4s2WjU9mE5tHrVsQOZ80wnvYHYi2JHbl0hr5ghs4RIyJwx6LEEnj2tzMFec4f7o +dQeSsZpgRJmpvpAfRTxhIRjZBrKxnMytedAkUPguBQwjVCn7+EaKiJfpu42JG8Mm ++/dHi+Q9Tc+0tX5pKOIpQMlMxMHw8MfPmUjC3AAd9lsmCtuybYoeN2IRdbzzchJ8 +l1ZuoI3gH7pcIeElfVSqSBkCAwEAAaNRME8wCwYDVR0PBAQDAgGGMA8GA1UdEwEB +/wQFMAMBAf8wHQYDVR0OBBYEFKu5xf+h7+ZTHTM5IoTRdtQ3Ti1qMBAGCSsGAQQB +gjcVAQQDAgEAMA0GCSqGSIb3DQEBDQUAA4ICAQAVpyJ1qLjqRLC34F1UXkC3vxpO +nV6WgzpzA+DUNog4Y6RhTnh0Bsir+I+FTl0zFCm7JpT/3NP9VjfEitMkHehmHhQK +c7cIBZSF62K477OTvLz+9ku2O/bGTtYv9fAvR4BmzFfyPDoAKOjJSghD1p/7El+1 +eSjvcUBzLnBUtxO/iYXRNo7B3+1qo4F5Hz7rPRLI0UWW/0UAfVCO2fFtyF6C1iEY +/q0Ldbf3YIaMkf2WgGhnX9yH/8OiIij2r0LVNHS811apyycjep8y/NkG4q1Z9jEi +VEX3P6NEL8dWtXQlvlNGMcfDT3lmB+tS32CPEUwce/Ble646rukbERRwFfxXojpf +C6ium+LtJc7qnK6ygnYF4D6mz4H+3WaxJd1S1hGQxOb/3WVw63tZFnN62F6/nc5g +6T44Yb7ND6y3nVcygLpbQsws6HsjX65CoSjrrPn0YhKxNBscF7M7tLTW/5LK9uhk +yjRCkJ0YagpeLxfV1l1ZJZaTPZvY9+ylHnWHhzlq0FzcrooSSsp4i44DB2K7O2ID +87leymZkKUY6PMDa4GkDJx0dG4UXDhRETMf+NkYgtLJ+UIzMNskwVDcxO4kVL+Hi +Pj78bnC5yCw8P5YylR45LdxLzLO68unoXOyFz1etGXzszw8lJI9LNubYxk77mK8H +LpuQKbSbIERsmR+QqQ== +-----END CERTIFICATE----- diff --git a/sink-connector-lightweight/docker/clickhouse-sink-connector-lt-service.yml b/sink-connector-lightweight/docker/clickhouse-sink-connector-lt-service.yml index c14b6f29c..68c56bd22 100644 --- a/sink-connector-lightweight/docker/clickhouse-sink-connector-lt-service.yml +++ b/sink-connector-lightweight/docker/clickhouse-sink-connector-lt-service.yml @@ -3,14 +3,19 @@ version: "3.4" services: clickhouse-sink-connector-lt: image: ${CLICKHOUSE_SINK_CONNECTOR_LT_IMAGE} - entrypoint: ["sh", "-c", "java -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005 -Xms4g -Xmx4g -Dlog4j2.configurationFile=log4j2.xml -jar /app.jar /config.yml com.altinity.clickhouse.debezium.embedded.ClickHouseDebeziumEmbeddedApplication"] + entrypoint: ["sh", "-c", "java -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005 -Xms4g -Xmx4g -Dlog4j2.configurationFile=log4j2.xml -Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false -Dcom.sun.management.jmxremote.port=39999 -jar /app.jar /config.yml com.altinity.clickhouse.debezium.embedded.ClickHouseDebeziumEmbeddedApplication"] restart: "no" ports: - "8083:8083" - "5005:5005" - "7000:7000" + - "39999:39999" extra_hosts: - "host.docker.internal:host-gateway" volumes: - ./log4j2.xml:/log4j2.xml - ./config.yml:/config.yml + logging: + options: + max-size: "100m" + max-file: "5" diff --git a/sink-connector-lightweight/docker/config.yml b/sink-connector-lightweight/docker/config.yml index 527912994..5386af27e 100644 --- a/sink-connector-lightweight/docker/config.yml +++ b/sink-connector-lightweight/docker/config.yml @@ -4,6 +4,11 @@ # Unique name for the connector. Attempting to register again with the same name will fail. name: "company-1" +# Primary key used for state storage. Refer to State Storage documentation. +# If multiple connectors are writing to the same ClickHouse instance, this +# value needs to be unique for a connector. +topic.prefix: "sink-connector-1" + # IP address or hostname of the MySQL database server. database.hostname: "mysql-master" @@ -145,11 +150,13 @@ database.connectionTimeZone: "UTC" #disable.drop.truncate: If set to true, the connector will ignore drop and truncate events. The default is false. #disable.drop.truncate: "false" -#restart.event.loop: This will restart the CDC event loop if there are no messages received after timeout specified in restart.event.loop.timeout.period.secs -restart.event.loop: "true" +#restart.event.loop: This will restart the CDC event loop if there are no messages received after +#timeout specified in restart.event.loop.timeout.period.secs. +#Workaround to restart debezium loop(in case of freeze) +#restart.event.loop: "true" #restart.event.loop.timeout.period.secs: Defines the restart timeout period. -restart.event.loop.timeout.period.secs: "3000" +#restart.event.loop.timeout.period.secs: "3000" # Flush time of the buffer in milliseconds. The buffer that is stored in memory before being flushed to ClickHouse. #buffer.flush.time.ms: "1000" @@ -166,6 +173,9 @@ restart.event.loop.timeout.period.secs: "3000" # Sink Connector maximum queue size #sink.connector.max.queue.size: "100000" +#Metrics (Prometheus target), required for Grafana Dashboard +metrics.enable: "true" + # Skip schema history capturing, use the following configuration # to reduce slow startup when replicating dbs with large number of tables #schema.history.internal.store.only.captured.tables.ddl: "true" diff --git a/sink-connector-lightweight/docker/config/jmx-config.yml b/sink-connector-lightweight/docker/config/jmx-config.yml new file mode 100644 index 000000000..d6b5717d4 --- /dev/null +++ b/sink-connector-lightweight/docker/config/jmx-config.yml @@ -0,0 +1,7 @@ +startDelaySeconds: 0 +lowercaseOutputName: false +lowercaseOutputLabelNames: false +ssl: false +hostPort: clickhouse-sink-connector-lt:39999 +rules: + - pattern: ".*" diff --git a/sink-connector-lightweight/docker/config/prometheus.yml b/sink-connector-lightweight/docker/config/prometheus.yml index 5f8113f16..b6db47ebd 100644 --- a/sink-connector-lightweight/docker/config/prometheus.yml +++ b/sink-connector-lightweight/docker/config/prometheus.yml @@ -6,3 +6,7 @@ scrape_configs: static_configs: - targets: - clickhouse-sink-connector-lt:8083 + - job_name: 'jmx' + static_configs: + - targets: + - jmx_exporter:9072 \ No newline at end of file diff --git a/sink-connector-lightweight/docker/config_postgres.yml b/sink-connector-lightweight/docker/config_postgres.yml index 981763787..50aee659a 100644 --- a/sink-connector-lightweight/docker/config_postgres.yml +++ b/sink-connector-lightweight/docker/config_postgres.yml @@ -84,16 +84,6 @@ ENGINE = ReplacingMergeTree(_version) ORDER BY id SETTINGS index_granularity = 8192" offset.storage.jdbc.offset.table.delete: "delete from %s where 1=1" -schema.history.internal: "io.debezium.storage.jdbc.history.JdbcSchemaHistory" -schema.history.internal.jdbc.url: "jdbc:clickhouse://clickhouse:8123/altinity_sink_connector" -schema.history.internal.jdbc.user: "root" -schema.history.internal.jdbc.password: "root" -schema.history.internal.jdbc.schema.history.table.ddl: "CREATE TABLE if not exists %s -(`id` VARCHAR(36) NOT NULL, `history_data` VARCHAR(65000), `history_data_seq` INTEGER, `record_insert_ts` TIMESTAMP NOT NULL, `record_insert_seq` INTEGER NOT NULL) ENGINE=ReplacingMergeTree(record_insert_seq) order by id" - -# schema.history.internal.schema.history.table.name: The name of the database table where connector schema history is to be stored. -schema.history.internal.jdbc.schema.history.table.name: "altinity_sink_connector.replicate_schema_history" - # enable.snapshot.ddl: If set to true, the connector wil parse the DDL statements as part of initial load. enable.snapshot.ddl: "true" @@ -115,6 +105,9 @@ database.dbname: "public" # ignore_delete: If set to true, the connector will ignore delete events. The default is false. #ignore_delete: "true" +#Metrics (Prometheus target), required for Grafana Dashboard +metrics.enable: "true" + #disable.ddl: If set to true, the connector will ignore DDL events. The default is false. #disable.ddl: "false" diff --git a/sink-connector-lightweight/docker/config_postgres_local.yml b/sink-connector-lightweight/docker/config_postgres_local.yml new file mode 100644 index 000000000..167b363cf --- /dev/null +++ b/sink-connector-lightweight/docker/config_postgres_local.yml @@ -0,0 +1,113 @@ +#### Some of the properties are part of Debezium PostgreSQL Connector +#### https://debezium.io/documentation/reference/stable/connectors/postgresql.html +# name: Unique name for the connector. Attempting to register again with the same name will fail. +name: "debezium-embedded-postgres" + +auto.create.tables.replicated: "true" + +# database.hostname: IP address or hostname of the PostgreSQL database server. +database.hostname: "localhost" + +# database.port: Integer port number of the PostgreSQL database server listening for client connections. +database.port: "5432" + +# database.user: Name of the PostgreSQL database user to be used when connecting to the database. +database.user: "root" + +# database.password: Password of the PostgreSQL database user to be used when connecting to the database. +database.password: "root" + +# database.server.name: The name of the PostgreSQL database from which events are to be captured when not using snapshot mode. +database.server.name: "ER54" + +# schema.include.list: An optional list of regular expressions that match schema names to be monitored; +schema.include.list: public,public2 + +slot.name: connector2 + +# plugin.name: The name of the PostgreSQL logical decoding plug-in installed on the PostgreSQL server. Supported values are decoderbufs, and pgoutput. +plugin.name: "pgoutput" + +# table.include.list: An optional list of regular expressions that match fully-qualified table identifiers for tables to be monitored; +#table.include.list: "public.tm,public.tm2" + +# clickhouse.server.url: Specify only the hostname of the Clickhouse Server. +clickhouse.server.url: "localhost" + +# clickhouse.server.user: Clickhouse Server User +clickhouse.server.user: "root" + +# clickhouse.server.password: Clickhouse Server Password +clickhouse.server.password: "root" + +# clickhouse.server.port: Clickhouse Server Port +clickhouse.server.port: "8123" + +# database.allowPublicKeyRetrieval: "true" https://rmoff.net/2019/10/23/debezium-mysql-v8-public-key-retrieval-is-not-allowed/ +database.allowPublicKeyRetrieval: "true" + +# snapshot.mode: Debezium can use different modes when it runs a snapshot. The snapshot mode is determined by the snapshot.mode configuration property. +snapshot.mode: "initial" + +# offset.flush.interval.ms: The number of milliseconds to wait before flushing recent offsets to Kafka. This ensures that offsets are committed within the specified time interval. +offset.flush.interval.ms: 5000 + +# connector.class: The Java class for the connector. This must be set to io.debezium.connector.postgresql.PostgresConnector. +connector.class: "io.debezium.connector.postgresql.PostgresConnector" + +# offset.storage: The Java class that implements the offset storage strategy. This must be set to io.debezium.storage.jdbc.offset.JdbcOffsetBackingStore. +offset.storage: "io.debezium.storage.jdbc.offset.JdbcOffsetBackingStore" + +# offset.storage.jdbc.offset.table.name: The name of the database table where connector offsets are to be stored. +offset.storage.jdbc.offset.table.name: "altinity_sink_connector.replica_source_info" + +# offset.storage.jdbc.url: The JDBC URL for the database where connector offsets are to be stored. +offset.storage.jdbc.url: "jdbc:clickhouse://localhost:8123/altinity_sink_connector" + +# offset.storage.jdbc.user: The name of the database user to be used when connecting to the database where connector offsets are to be stored. +offset.storage.jdbc.user: "root" + +# offset.storage.jdbc.password: The password of the database user to be used when connecting to the database where connector offsets are to be stored. +offset.storage.jdbc.password: "root" + +# offset.storage.jdbc.offset.table.ddl: The DDL statement used to create the database table where connector offsets are to be stored. +offset.storage.jdbc.offset.table.ddl: "CREATE TABLE if not exists %s +( + `id` String, + `offset_key` String, + `offset_val` String, + `record_insert_ts` DateTime, + `record_insert_seq` UInt64, + `_version` UInt64 MATERIALIZED toUnixTimestamp64Nano(now64(9)) +) +ENGINE = ReplacingMergeTree(_version) +ORDER BY id +SETTINGS index_granularity = 8192" +offset.storage.jdbc.offset.table.delete: "delete from %s where 1=1" + +# enable.snapshot.ddl: If set to true, the connector wil parse the DDL statements as part of initial load. +enable.snapshot.ddl: "true" + +# auto.create.tables: If set to true, the connector will create the database tables for the destination tables if they do not already exist. +auto.create.tables: "true" + +# database.dbname: The name of the PostgreSQL database from which events are to be captured when not using snapshot mode. +database.dbname: "public" + +# clickhouse.datetime.timezone: This timezone will override the default timezone of ClickHouse server. Timezone columns will be set to this timezone. +#clickhouse.datetime.timezone: "UTC" + +# skip_replica_start: If set to true, the connector will skip replication on startup. sink-connector-client start_replica will start replication. +#skip_replica_start: "false" + +# binary.handling.mode: The mode for handling binary values. Possible values are bytes, base64, and decode. The default is bytes. +#binary.handling.mode: "base64" + +# ignore_delete: If set to true, the connector will ignore delete events. The default is false. +#ignore_delete: "true" + +#disable.ddl: If set to true, the connector will ignore DDL events. The default is false. +#disable.ddl: "false" + +#disable.drop.truncate: If set to true, the connector will ignore drop and truncate events. The default is false. +#disable.drop.truncate: "false" diff --git a/sink-connector-lightweight/docker/docker-compose-postgres-standalone.yml b/sink-connector-lightweight/docker/docker-compose-postgres-standalone.yml index 0e6882e46..61e93daff 100644 --- a/sink-connector-lightweight/docker/docker-compose-postgres-standalone.yml +++ b/sink-connector-lightweight/docker/docker-compose-postgres-standalone.yml @@ -1,8 +1,22 @@ services: - clickhouse-sink-connector-lt: + postgres: extends: - file: clickhouse-sink-connector-lt-service.yml - service: clickhouse-sink-connector-lt + file: postgres-service.yml + service: postgres + + + clickhouse: + extends: + file: clickhouse-service.yml + service: clickhouse + depends_on: + zookeeper: + condition: service_healthy + + zookeeper: + extends: + file: zookeeper-service.yml + service: zookeeper ### MONITORING #### prometheus: @@ -20,4 +34,4 @@ services: - ./config/grafana/config/altinity_sink_connector.json:/var/lib/grafana/dashboards/altinity_sink_connector.json depends_on: - prometheus - ## END OF MONITORING ### \ No newline at end of file + ## END OF MONITORING ### diff --git a/sink-connector-lightweight/docker/docker-compose.yml b/sink-connector-lightweight/docker/docker-compose.yml index 23556cc59..fa01a7fc3 100644 --- a/sink-connector-lightweight/docker/docker-compose.yml +++ b/sink-connector-lightweight/docker/docker-compose.yml @@ -12,12 +12,12 @@ services: file: mysql-master-service.yml service: mysql-master - mysql-slave: - extends: - file: mysql-slave-service.yml - service: mysql-slave - depends_on: - - mysql-master +# mysql-slave: +# extends: +# file: mysql-slave-service.yml +# service: mysql-slave +# depends_on: +# - mysql-master clickhouse: extends: @@ -53,4 +53,15 @@ services: - ./config/grafana/config/altinity_sink_connector.json:/var/lib/grafana/dashboards/altinity_sink_connector.json depends_on: - prometheus - ## END OF MONITORING ### \ No newline at end of file + + jmx_exporter: + container_name: jmx_exporter + image: sscaling/jmx-prometheus-exporter + restart: "no" + ports: + - "9072:9072" + environment: + SERVICE_PORT: 9072 + volumes: + - ./config/jmx-config.yml:/opt/jmx_exporter/config.yml + ## END OF MONITORING ### diff --git a/sink-connector-lightweight/docker/getLatestRelease.sh b/sink-connector-lightweight/docker/getLatestRelease.sh new file mode 100755 index 000000000..6a32b9642 --- /dev/null +++ b/sink-connector-lightweight/docker/getLatestRelease.sh @@ -0,0 +1,41 @@ +#!/bin/bash + +# Replace with your GitHub repository (e.g., user/repo) +REPO="altinity/clickhouse-sink-connector" + +# Fetch the latest release from GitHub API +latest_release=$(curl -s https://api.github.com/repos/$REPO/releases/latest) + +# Also get the date of the latest release +latest_release_date=$(echo $latest_release | jq -r '.published_at') + +# Extract the tag name (version) from the JSON response +latest_version=$(echo $latest_release | jq -r '.tag_name') + + +#echo "****************************************************************************************************" +# Display a message to the usage of the latest_version in color green +echo -e "\e[32mThe latest release is: $latest_version published on: $latest_release_date\e[0m" +# Add new lines. +#echo -e "\n\n" + +# echo that the enviroment variable CLICKHOUSE_SINK_CONNECTOR_LT_IMAGE will be +# set to the latest version +#echo "Setting CLICKHOUSE_SINK_CONNECTOR_LT_IMAGE to the latest version" + +echo -e "****** Run the following command to set the environment variable pointing to the latest version ******" +echo -e "\n" +echo "****************************************************************************************************" + +# Display a message to the usage of the latest_version in color green +echo -e "\e[32m export CLICKHOUSE_SINK_CONNECTOR_LT_IMAGE=altinity/clickhouse-sink-connector:$latest_version-lt'\e[0m" +echo "****************************************************************************************************" +echo -e "\n" + +#export CLICKHOUSE_SINK_CONNECTOR_LT_IMAGE='altinity/clickhouse-sink-connector:'$latest_version-lt +export CLICKHOUSE_SINK_CONNECTOR_LT_IMAGE='altinity/clickhouse-sink-connector:'$latest_version-lt + +#echo run docker-compose up to start sink connector in green +#echo -e "**** Run docker-compose up to start sink connector ***" +#Add stars +#echo "****************************************************************************************************" diff --git a/sink-connector-lightweight/docker/grafana-service.yml b/sink-connector-lightweight/docker/grafana-service.yml index fee429890..7a5c428e7 100644 --- a/sink-connector-lightweight/docker/grafana-service.yml +++ b/sink-connector-lightweight/docker/grafana-service.yml @@ -2,7 +2,10 @@ version: "3.4" services: grafana: - image: grafana/grafana:latest + build: + context: . + dockerfile: Dockerfile_grafana + #imagyour-ca-certe: grafana/grafana:latest restart: "no" ports: - "3000:3000" @@ -11,4 +14,6 @@ services: - GF_USERS_DEFAULT_THEME=light - GF_PLUGINS_ALLOW_LOADING_UNSIGNED_PLUGINS=vertamedia-clickhouse-datasource,grafana-clickhouse-datasource - GF_INSTALL_PLUGINS=vertamedia-clickhouse-datasource - - GF_LOG_LEVEL=debug \ No newline at end of file + - GF_LOG_LEVEL=debug + volumes: + - ./ca-certificates.crt:/usr/local/share/ca-certificates/ca-certificates.crt diff --git a/sink-connector-lightweight/docker/log4j2.xml b/sink-connector-lightweight/docker/log4j2.xml index 4bbda25d2..ce546897c 100644 --- a/sink-connector-lightweight/docker/log4j2.xml +++ b/sink-connector-lightweight/docker/log4j2.xml @@ -11,12 +11,19 @@ additivity="false"> --> + + + - - + + + + diff --git a/sink-connector-lightweight/docker/start-docker-compose.sh b/sink-connector-lightweight/docker/start-docker-compose.sh index dc416456a..6ef878a9a 100755 --- a/sink-connector-lightweight/docker/start-docker-compose.sh +++ b/sink-connector-lightweight/docker/start-docker-compose.sh @@ -1,15 +1,40 @@ #!/bin/bash +# Replace with your GitHub repository (e.g., user/repo) +REPO="altinity/clickhouse-sink-connector" + +# Fetch the latest release from GitHub API +latest_release=$(curl -s https://api.github.com/repos/$REPO/releases/latest) + +# Also get the date of the latest release +latest_release_date=$(echo $latest_release | jq -r '.published_at') + +# Extract the tag name (version) from the JSON response +latest_version=$(echo $latest_release | jq -r '.tag_name') + + +echo "****************************************************************************************************" +# Display a message to the usage of the latest_version in color green +echo -e "\e[32mThe latest release is: $latest_version published on: $latest_release_date\e[0m" +# echo that the enviroment variable CLICKHOUSE_SINK_CONNECTOR_LT_IMAGE will be +# set to the latest version +echo "Setting CLICKHOUSE_SINK_CONNECTOR_LT_IMAGE to the latest version" +# Display a message to the usage of the latest_version in color green +echo -e "\e[32mCLICKHOUSE_SINK_CONNECTOR_LT_IMAGE=altinity/clickhouse-sink-connector:$latest_version-lt'\e[0m" + +#Add stars +echo "****************************************************************************************************" +sleep 5 if [ -z $1 ] then - echo 'Using the latest tag for Sink connector' - export CLICKHOUSE_SINK_CONNECTOR_LT_IMAGE='altinityinfra/clickhouse-sink-connector:408-97b1d3d83ef93c1b76a2b1c4d9c544dc67fbbec3' + #echo 'Using the latest tag for Sink connector' + export CLICKHOUSE_SINK_CONNECTOR_LT_IMAGE='altinity/clickhouse-sink-connector:'$latest_version-lt else export CLICKHOUSE_SINK_CONNECTOR_LT_IMAGE=$1 fi -./stop-docker-compose.sh +#./stop-docker-compose.sh # Altinity sink images are tagged daily with this tag yyyy-mm-dd(2022-07-19) -docker-compose -f docker-compose-mysql.yml up --remove-orphans --force-recreate --renew-anon-volumes +docker-compose up --renew-anon-volumes \ No newline at end of file diff --git a/sink-connector-lightweight/pom.xml b/sink-connector-lightweight/pom.xml index c6bacc575..9df17122e 100644 --- a/sink-connector-lightweight/pom.xml +++ b/sink-connector-lightweight/pom.xml @@ -13,7 +13,7 @@ 17 UTF-8 UTF-8 - 2.7.0.Beta2 + 2.7.2.Final 5.9.1 1.19.1 3.1.1 @@ -24,6 +24,7 @@ 2.14.0.Final 3.0.0-M7 0.0.9 + 5.5.0 @@ -58,7 +59,7 @@ io.javalin javalin - 5.5.0 + ${javalin-version} org.slf4j @@ -66,6 +67,23 @@ + + + org.jetbrains.kotlin + kotlin-stdlib + 1.9.0 + + + org.jetbrains.kotlin + kotlin-stdlib-common + 1.9.0 + + + io.javalin + javalin-testtools + ${javalin-version} + test + com.googlecode.json-simple json-simple @@ -173,7 +191,11 @@ - + + org.locationtech.jts + jts-core + 1.18.2 + @@ -262,6 +284,12 @@ ${version.testcontainers} test + + org.testcontainers + mariadb + ${version.testcontainers} + test + org.testcontainers mongodb diff --git a/sink-connector-lightweight/src/main/java/com/altinity/clickhouse/debezium/embedded/ClickHouseDebeziumEmbeddedApplication.java b/sink-connector-lightweight/src/main/java/com/altinity/clickhouse/debezium/embedded/ClickHouseDebeziumEmbeddedApplication.java index 6842b8e43..90dc38a1f 100644 --- a/sink-connector-lightweight/src/main/java/com/altinity/clickhouse/debezium/embedded/ClickHouseDebeziumEmbeddedApplication.java +++ b/sink-connector-lightweight/src/main/java/com/altinity/clickhouse/debezium/embedded/ClickHouseDebeziumEmbeddedApplication.java @@ -52,7 +52,6 @@ public class ClickHouseDebeziumEmbeddedApplication { public static void main(String[] args) throws Exception { - //BasicConfigurator.configure(); Log4jBridgeHandler.install(false, "", true); System.setProperty("java.util.logging.manager", "org.apache.logging.log4j.jul.LogManager"); @@ -69,6 +68,7 @@ public static void main(String[] args) throws Exception { } injector = Guice.createInjector(new AppInjector()); + printDockerInfo(); props = new Properties(); if(args.length > 0) { log.info(String.format("****** CONFIGURATION FILE: %s ********", args[0])); @@ -86,8 +86,7 @@ public static void main(String[] args) throws Exception { setupMonitoringThread(new ClickHouseSinkConnectorConfig(PropertiesHelper.toMap(props)), props); - embeddedApplication.start(injector.getInstance(DebeziumRecordParserService.class), - injector.getInstance(DDLParserService.class), props, false); + embeddedApplication.start(injector.getInstance(DebeziumRecordParserService.class), props, false); try { DebeziumEmbeddedRestApi.startRestApi(props, injector, debeziumChangeEventCapture, userProperties); @@ -96,6 +95,22 @@ public static void main(String[] args) throws Exception { } } + private static void printDockerInfo() { + try { + + String dockerTag = System.getenv("DOCKER_TAG"); + // log the docker tag if it is set + if(dockerTag != null) { + //Extract the string after : + // altinityinfra/clickhouse-sink-connector:${{ env.IMAGE_TAG }}-lt + //String version = dockerTag.substring(dockerTag.indexOf(":") + 1); + log.info("***** Sink Connector Release version: *********** " + dockerTag); + } + } catch(Exception e) { + log.error("Error printing docker info", e); + } + } + /** * Function to load properties from user provided file path * @param filePath user provided file path @@ -125,8 +140,7 @@ public static CompletableFuture startDebeziumEventLoop(Injector injector Thread.sleep(500); // embeddedApplication = new ClickHouseDebeziumEmbeddedApplication(); - embeddedApplication.start(injector.getInstance(DebeziumRecordParserService.class), - injector.getInstance(DDLParserService.class), props, true); + embeddedApplication.start(injector.getInstance(DebeziumRecordParserService.class), props, true); return null; }); @@ -135,7 +149,7 @@ public static CompletableFuture startDebeziumEventLoop(Injector injector public static void start(DebeziumRecordParserService recordParserService, - DDLParserService ddlParserService, Properties props, boolean forceStart) throws Exception { + Properties props, boolean forceStart) throws Exception { if(forceStart == true) { // Reload the configuration file. @@ -143,7 +157,7 @@ public static void start(DebeziumRecordParserService recordParserService, loadPropertiesFile(configurationFile); } debeziumChangeEventCapture = new DebeziumChangeEventCapture(); - debeziumChangeEventCapture.setup(props, recordParserService, ddlParserService, forceStart); + debeziumChangeEventCapture.setup(props, recordParserService, forceStart); } public static void stop() throws IOException { @@ -194,8 +208,7 @@ public void run() { log.info("******* Restarting Event Loop ********"); debeziumChangeEventCapture.stop(); Thread.sleep(3000); - start(injector.getInstance(DebeziumRecordParserService.class), - injector.getInstance(DDLParserService.class), props, true); + start(injector.getInstance(DebeziumRecordParserService.class), props, true); } catch (IOException e) { log.error("**** ERROR: Restarting Event Loop ****", e); throw new RuntimeException(e); diff --git a/sink-connector-lightweight/src/main/java/com/altinity/clickhouse/debezium/embedded/api/DebeziumEmbeddedRestApi.java b/sink-connector-lightweight/src/main/java/com/altinity/clickhouse/debezium/embedded/api/DebeziumEmbeddedRestApi.java index b2e3fcdb6..b2772f63b 100644 --- a/sink-connector-lightweight/src/main/java/com/altinity/clickhouse/debezium/embedded/api/DebeziumEmbeddedRestApi.java +++ b/sink-connector-lightweight/src/main/java/com/altinity/clickhouse/debezium/embedded/api/DebeziumEmbeddedRestApi.java @@ -60,6 +60,23 @@ public static void startRestApi(Properties props, Injector injector, }); + //Delete offsets + app.delete("/offsets", ctx -> { + ClickHouseSinkConnectorConfig config = new ClickHouseSinkConnectorConfig(PropertiesHelper.toMap(finalProps1)); + String response = ""; + + try { + debeziumChangeEventCapture.deleteOffsets(finalProps1); + } catch (Exception e) { + log.error("Client - Error deleting offsets", e); + ctx.result(e.toString()); + ctx.status(HttpStatus.INTERNAL_SERVER_ERROR); + return; + } + ctx.result(response); + + }); + app.post("/binlog", ctx -> { if(debeziumChangeEventCapture.isReplicationRunning()) { ctx.status(HttpStatus.BAD_REQUEST); @@ -102,6 +119,22 @@ public static void startRestApi(Properties props, Injector injector, gtid); log.info("Received update-binlog request: " + body); }); + //Delete offsets + app.delete("/schema-history", ctx -> { + ClickHouseSinkConnectorConfig config = new ClickHouseSinkConnectorConfig(PropertiesHelper.toMap(finalProps1)); + String response = ""; + + try { + debeziumChangeEventCapture.deleteSchemaHistory(config, finalProps1); + } catch (Exception e) { + log.error("Client - Error deleting schema history", e); + ctx.result(e.toString()); + ctx.status(HttpStatus.INTERNAL_SERVER_ERROR); + return; + } + ctx.result(response); + + }); app.post("/lsn", ctx -> { String body = ctx.body(); @@ -136,4 +169,9 @@ public static void stop() { if(app != null) app.stop(); } + + // Return the app instance. + public static Javalin app() { + return app; + } } diff --git a/sink-connector-lightweight/src/main/java/com/altinity/clickhouse/debezium/embedded/cdc/DebeziumChangeEventCapture.java b/sink-connector-lightweight/src/main/java/com/altinity/clickhouse/debezium/embedded/cdc/DebeziumChangeEventCapture.java index 23e966bd2..eeab41d69 100644 --- a/sink-connector-lightweight/src/main/java/com/altinity/clickhouse/debezium/embedded/cdc/DebeziumChangeEventCapture.java +++ b/sink-connector-lightweight/src/main/java/com/altinity/clickhouse/debezium/embedded/cdc/DebeziumChangeEventCapture.java @@ -13,15 +13,20 @@ import com.altinity.clickhouse.sink.connector.db.operations.ClickHouseAlterTable; import com.altinity.clickhouse.sink.connector.executor.ClickHouseBatchExecutor; import com.altinity.clickhouse.sink.connector.executor.ClickHouseBatchRunnable; +import com.altinity.clickhouse.sink.connector.executor.ClickHouseBatchWriter; +import com.altinity.clickhouse.sink.connector.executor.DebeziumOffsetManagement; import com.altinity.clickhouse.sink.connector.model.ClickHouseStruct; import com.altinity.clickhouse.sink.connector.model.DBCredentials; import com.clickhouse.jdbc.ClickHouseConnection; import com.google.common.annotations.VisibleForTesting; import com.google.common.util.concurrent.ThreadFactoryBuilder; +import io.debezium.config.CommonConnectorConfig; import io.debezium.embedded.Connect; import io.debezium.engine.ChangeEvent; import io.debezium.engine.DebeziumEngine; import io.debezium.engine.spi.OffsetCommitPolicy; +import io.debezium.relational.history.SchemaHistory; +import io.debezium.storage.jdbc.history.JdbcSchemaHistoryConfig; import io.debezium.storage.jdbc.offset.JdbcOffsetBackingStoreConfig; import org.apache.commons.lang3.tuple.Pair; import org.apache.kafka.connect.data.Field; @@ -37,6 +42,7 @@ import java.sql.ResultSet; import java.sql.ResultSetMetaData; import java.sql.SQLException; +import java.time.LocalDateTime; import java.util.*; import java.util.concurrent.*; import java.util.concurrent.atomic.AtomicBoolean; @@ -81,11 +87,18 @@ public class DebeziumChangeEventCapture { // Keep one clickhouse connection. private ClickHouseConnection conn; + ClickHouseBatchWriter singleThreadedWriter; + + + DebeziumOffsetStorage debeziumOffsetStorage; + public DebeziumChangeEventCapture() { singleThreadDebeziumEventExecutor = Executors.newFixedThreadPool(1); + this.debeziumOffsetStorage = new DebeziumOffsetStorage(); } + /** * Function to perform DDL operation on the main thread. * @param DDL DDL to be executed. @@ -93,76 +106,114 @@ public DebeziumChangeEventCapture() { * @param sr * @param config */ - private void performDDLOperation(String DDL, Properties props, SourceRecord sr, ClickHouseSinkConnectorConfig config) { - - String databaseName = "system"; - if(sr != null && sr.key() != null) { - if(sr.key() instanceof Struct) { - Struct keyStruct = (Struct) sr.key(); - String recordDbName = (String) keyStruct.get("databaseName"); - if(recordDbName != null && recordDbName.isEmpty() == false) { - databaseName = recordDbName; - } - } - } - - if(writer == null) { - - DBCredentials dbCredentials = parseDBConfiguration(config); - String jdbcUrl = BaseDbWriter.getConnectionString(dbCredentials.getHostName(), dbCredentials.getPort(), - databaseName); - ClickHouseConnection conn = BaseDbWriter.createConnection(jdbcUrl, "Client_1", - dbCredentials.getUserName(), dbCredentials.getPassword(), config); - writer = new BaseDbWriter(dbCredentials.getHostName(), dbCredentials.getPort(), - databaseName, dbCredentials.getUserName(), - dbCredentials.getPassword(), config, conn); + private void performDDLOperation(String DDL, Properties props, SourceRecord sr, + ClickHouseSinkConnectorConfig config, + DebeziumEngine.RecordCommitter> + recordCommitter, ChangeEvent cdcRecord, + boolean lastRecordInBatch) { + String databaseName = getDatabaseName(sr); + + if (writer == null) { + writer = createWriter(config, databaseName); } StringBuffer clickHouseQuery = new StringBuffer(); AtomicBoolean isDropOrTruncate = new AtomicBoolean(false); - MySQLDDLParserService mySQLDDLParserService = new MySQLDDLParserService(config, databaseName); + MySQLDDLParserService mySQLDDLParserService = new MySQLDDLParserService(writer, config, databaseName); mySQLDDLParserService.parseSql(DDL, "", clickHouseQuery, isDropOrTruncate); - ClickHouseAlterTable cat = new ClickHouseAlterTable(); if (checkIfDDLNeedsToBeIgnored(props, sr, isDropOrTruncate)) { log.info("Ignored Source DB DDL: " + DDL + " Snapshot:" + isSnapshotDDL(sr)); return; - } else { - log.info("Executed Source DB DDL: " + DDL + " Snapshot:" + isSnapshotDDL(sr)); } - long currentTime = System.currentTimeMillis(); - boolean ddlProcessingResult = true; - Metrics.updateDdlMetrics(DDL, currentTime, 0, ddlProcessingResult); - try { - String formattedQuery = clickHouseQuery.toString().replaceAll(",$", ""); - if (formattedQuery != null && formattedQuery.isEmpty() == false) { - if (formattedQuery.contains("\n")) { - String[] queries = formattedQuery.split("\n"); - for (String query : queries) { - if (query != null && query.isEmpty() == false) { - log.info("ClickHouse DDL: " + query); - cat.runQuery(query, writer.getConnection()); - } - } - } else { - log.info("ClickHouse DDL: " + formattedQuery); - cat.runQuery(formattedQuery, writer.getConnection()); + log.info("Executed Source DB DDL: " + DDL + " Snapshot:" + isSnapshotDDL(sr)); + // Add max retries of 10 + // Add sleep time of 10 seconds + int MAX_DDL_RETRIES = 10; + int SLEEP_TIME = 10000; + int numRetries = 0; + + // Check if configuration is set to retry DDL + String retryDDL = props.getProperty(SinkConnectorLightWeightConfig.DDL_RETRY.toString()); + boolean retryDDLProperty = false; + if(retryDDL != null && retryDDL.equalsIgnoreCase("true" )) { + retryDDLProperty = true; + } + + while(numRetries < MAX_DDL_RETRIES) { + try { + executeDDL(clickHouseQuery.toString(), writer); + DebeziumOffsetManagement.acknowledgeRecords(recordCommitter, + cdcRecord, lastRecordInBatch); + break; + } catch (Exception e) { + log.error("Error executing DDL", e); + if(retryDDLProperty == false) { + break; } - } else { - log.error("DDL translation failed: " + DDL); + try { + Thread.sleep(SLEEP_TIME); + } catch (InterruptedException ex) { + log.error("Error sleeping", ex); + } + numRetries++; + } + if(numRetries >= MAX_DDL_RETRIES) { + throw new RuntimeException("Max retries exceeded for DDL"); + } + } + updateMetrics(DDL, writer); + } + + /** + * Function to get the database name from the SourceRecord. + * If the database name is not present in the SourceRecord, then + * the database name is set to "system". + * Also if a database is overridden in the configuration, then + * the database name is set to the overridden database name. + * @param sr + * @return + */ + private String getDatabaseName(SourceRecord sr) { + if (sr != null && sr.key() instanceof Struct) { + String recordDbName = (String) ((Struct) sr.key()).get("databaseName"); + if (recordDbName != null && !recordDbName.isEmpty()) { + + return recordDbName; } - } catch (Exception e) { - log.error("Error running DDL Query: " + e); - ddlProcessingResult = false; } + return "system"; + } + + private BaseDbWriter createWriter(ClickHouseSinkConnectorConfig config, String databaseName) { + DBCredentials dbCredentials = parseDBConfiguration(config); + String jdbcUrl = BaseDbWriter.getConnectionString(dbCredentials.getHostName(), dbCredentials.getPort(), databaseName); + ClickHouseConnection conn = BaseDbWriter.createConnection(jdbcUrl, "Client_1", dbCredentials.getUserName(), dbCredentials.getPassword(), config); + return new BaseDbWriter(dbCredentials.getHostName(), dbCredentials.getPort(), databaseName, dbCredentials.getUserName(), dbCredentials.getPassword(), config, conn); + } + + private void executeDDL(String clickHouseQuery, BaseDbWriter writer) throws SQLException { + ClickHouseAlterTable cat = new ClickHouseAlterTable(); + String[] queries = clickHouseQuery.replaceAll(",$", "").split("\n"); + for (String query : queries) { + if (!query.isEmpty()) { + log.info("ClickHouse DDL: " + query); + cat.runQuery(query, writer.getConnection()); + } + } + } + + private void updateMetrics(String DDL, BaseDbWriter writer) { + long currentTime = System.currentTimeMillis(); + boolean ddlProcessingResult = true; + Metrics.updateDdlMetrics(DDL, currentTime, 0, ddlProcessingResult); try { String clickHouseVersion = writer.getClickHouseVersion(); - isNewReplacingMergeTreeEngine = new DBMetadata() - .checkIfNewReplacingMergeTree(clickHouseVersion); + isNewReplacingMergeTreeEngine = new DBMetadata().checkIfNewReplacingMergeTree(clickHouseVersion); } catch (Exception e) { - log.error("Error retrieving version"); + log.error("Error retrieving version", e); } long elapsedTime = System.currentTimeMillis() - currentTime; @@ -188,7 +239,7 @@ private ClickHouseStruct processEveryChangeRecord(Properties props, ChangeEvent< Struct struct = (Struct) sr.value(); if (struct == null) { - log.warn(String.format("STRUCT EMPTY - not a valid CDC record + Record(%s)", record.toString())); + log.debug(String.format("STRUCT EMPTY - not a valid CDC record + Record(%s)", record.toString())); return null; } if (struct.schema() == null) { @@ -214,7 +265,7 @@ private ClickHouseStruct processEveryChangeRecord(Properties props, ChangeEvent< this.executor.shutdown(); this.executor.awaitTermination(60, TimeUnit.SECONDS); - performDDLOperation(DDL, props, sr, config); + performDDLOperation(DDL, props, sr, config, recordCommitter, record, lastRecordInBatch); setupProcessingThread(config); } @@ -307,7 +358,7 @@ private void createDatabaseForDebeziumStorage(ClickHouseSinkConnectorConfig conf "system", dbCredentials.getUserName(), dbCredentials.getPassword(), config, conn); - Pair tableNameDatabaseName = getDebeziumStorageDatabaseName(props); + Pair tableNameDatabaseName = getDebeziumOffsetStorageDatabaseName(props); String databaseName = tableNameDatabaseName.getRight(); String createDbQuery = String.format("create database if not exists %s", databaseName); @@ -338,7 +389,7 @@ private void createViewForShowReplicaStatus(ClickHouseSinkConnectorConfig config BaseDbWriter writer = new BaseDbWriter(dbCredentials.getHostName(), dbCredentials.getPort(), "system", dbCredentials.getUserName(), dbCredentials.getPassword(), config, conn); - Pair tableNameDatabaseName = getDebeziumStorageDatabaseName(props); + Pair tableNameDatabaseName = getDebeziumOffsetStorageDatabaseName(props); String tableName = tableNameDatabaseName.getLeft(); String dbName = tableNameDatabaseName.getRight(); @@ -354,15 +405,30 @@ private void createViewForShowReplicaStatus(ClickHouseSinkConnectorConfig config } /** - * + * Function to get the database name and table name for the offset storage table. * @param props * @return */ - private Pair getDebeziumStorageDatabaseName(Properties props) { + private Pair getDebeziumOffsetStorageDatabaseName(Properties props) { String tableName = props.getProperty(JdbcOffsetBackingStoreConfig.OFFSET_STORAGE_PREFIX + JdbcOffsetBackingStoreConfig.PROP_TABLE_NAME.name()); + return splitTableName(tableName); + } + + /** + * + * @param props + * @return + */ + private Pair getDebeziumSchemaHistoryDatabaseName(Properties props) { + String tableName = props.getProperty(SchemaHistory.CONFIGURATION_FIELD_PREFIX_STRING + + JdbcSchemaHistoryConfig.PROP_TABLE_NAME.name()); + return splitTableName(tableName); + } + + private Pair splitTableName(String tableName) { // if tablename is dbname.tablename and contains a dot. String databaseName = "system"; // split tablename with dot. @@ -376,6 +442,19 @@ private Pair getDebeziumStorageDatabaseName(Properties props) { return Pair.of(tableName, databaseName); } + /** + * Function to delete offsets from Debezium storage. + * @param props + */ + public void deleteOffsets(Properties props) throws SQLException { + DBCredentials dbCredentials = parseDBConfiguration(new ClickHouseSinkConnectorConfig(PropertiesHelper.toMap(props))); + BaseDbWriter writer = new BaseDbWriter(dbCredentials.getHostName(), dbCredentials.getPort(), + dbCredentials.getDatabase(), dbCredentials.getUserName(), + dbCredentials.getPassword(), new ClickHouseSinkConnectorConfig(PropertiesHelper.toMap(props)), this.conn); + String offsetKey = this.debeziumOffsetStorage.getOffsetKey(props); + + this.debeziumOffsetStorage.deleteOffsetStorageRow(offsetKey, props, writer); + } /** * Function to get the status of Debezium storage. @@ -385,7 +464,7 @@ private Pair getDebeziumStorageDatabaseName(Properties props) { public String getDebeziumStorageStatus(ClickHouseSinkConnectorConfig config, Properties props) throws Exception { String response = ""; - Pair tableNameDatabaseName = getDebeziumStorageDatabaseName(props); + Pair tableNameDatabaseName = getDebeziumOffsetStorageDatabaseName(props); String tableName = tableNameDatabaseName.getLeft(); String databaseName = tableNameDatabaseName.getRight(); @@ -438,7 +517,11 @@ public String getDebeziumStorageStatus(ClickHouseSinkConnectorConfig config, Pro JSONObject row = new JSONObject(); colNames.forEach(cn -> { try { - row.put(cn, resultSet.getObject(cn)); + Object v = resultSet.getObject(cn); + if (v != null && v instanceof LocalDateTime) { + v = ((LocalDateTime) v).toString(); + } + row.put(cn, v); } catch (SQLException e) { e.printStackTrace(); } @@ -459,7 +542,7 @@ public long getLatestRecordTimestamp(ClickHouseSinkConnectorConfig config, Prope long result = -1; DBCredentials dbCredentials = parseDBConfiguration(config); - Pair tableNameDatabaseName = getDebeziumStorageDatabaseName(props); + Pair tableNameDatabaseName = getDebeziumOffsetStorageDatabaseName(props); String tableName = tableNameDatabaseName.getLeft(); String databaseName = tableNameDatabaseName.getRight(); @@ -467,7 +550,7 @@ public long getLatestRecordTimestamp(ClickHouseSinkConnectorConfig config, Prope databaseName, dbCredentials.getUserName(), dbCredentials.getPassword(), config, this.conn); - String latestRecordTs = new DebeziumOffsetStorage().getDebeziumLatestRecordTimestamp(props, writer); + String latestRecordTs = this.debeziumOffsetStorage.getDebeziumLatestRecordTimestamp(props, writer); // Convert date string from 2024-01-26 21:57:47 format to milliseconds. SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); @@ -497,7 +580,7 @@ public void updateDebeziumStorageStatus(ClickHouseSinkConnectorConfig config, Pr String binlogFile, String binLogPosition, String gtid) throws SQLException, ParseException { - Pair tableNameDatabaseName = getDebeziumStorageDatabaseName(props); + Pair tableNameDatabaseName = getDebeziumOffsetStorageDatabaseName(props); String tableName = tableNameDatabaseName.getLeft(); String databaseName = tableNameDatabaseName.getRight(); @@ -506,18 +589,41 @@ public void updateDebeziumStorageStatus(ClickHouseSinkConnectorConfig config, Pr BaseDbWriter writer = new BaseDbWriter(dbCredentials.getHostName(), dbCredentials.getPort(), databaseName, dbCredentials.getUserName(), dbCredentials.getPassword(), config, this.conn); - String offsetValue = new DebeziumOffsetStorage().getDebeziumStorageStatusQuery(props, writer); + String offsetValue = this.debeziumOffsetStorage.getDebeziumStorageStatusQuery(props, writer); - String offsetKey = new DebeziumOffsetStorage().getOffsetKey(props); - String updateOffsetValue = new DebeziumOffsetStorage().updateBinLogInformation(offsetValue, + String offsetKey = this.debeziumOffsetStorage.getOffsetKey(props); + String updateOffsetValue = this.debeziumOffsetStorage.updateBinLogInformation(offsetValue, binlogFile, binLogPosition, gtid); - new DebeziumOffsetStorage().deleteOffsetStorageRow(offsetKey, props, writer); - new DebeziumOffsetStorage().updateDebeziumStorageRow(writer, tableName, offsetKey, updateOffsetValue, + this.debeziumOffsetStorage.deleteOffsetStorageRow(offsetKey, props, writer); + this.debeziumOffsetStorage.updateDebeziumStorageRow(writer, tableName, offsetKey, updateOffsetValue, System.currentTimeMillis()); } + /** + * + * @param config + * @param props + * @throws SQLException + */ + public void deleteSchemaHistory(ClickHouseSinkConnectorConfig config, Properties props) throws SQLException { + DBCredentials dbCredentials = parseDBConfiguration(config); + Pair tableNameDatabaseName = getDebeziumSchemaHistoryDatabaseName(props); + String tableName = tableNameDatabaseName.getLeft(); + String databaseName = tableNameDatabaseName.getRight(); + + BaseDbWriter writer = new BaseDbWriter(dbCredentials.getHostName(), dbCredentials.getPort(), + databaseName, dbCredentials.getUserName(), + dbCredentials.getPassword(), config, this.conn); + + // Get topic.prefix from properies + String topicPrefix = props.getProperty(CommonConnectorConfig.TOPIC_PREFIX.name()); + // String topicPrefix = config.getString(CommonConnectorConfig.TOPIC_PREFIX.name()); + // Jdbc adds the database name to the table name, so we need to remove it + new DebeziumOffsetStorage().deleteSchemaHistoryTable(topicPrefix, tableName, writer); + + } /** * Function to update the status of Debezium storage (LSN). * @param config @@ -530,7 +636,7 @@ public void updateDebeziumStorageStatus(ClickHouseSinkConnectorConfig config, Pr String lsn) throws SQLException, ParseException { - Pair tableNameDatabaseName = getDebeziumStorageDatabaseName(props); + Pair tableNameDatabaseName = getDebeziumOffsetStorageDatabaseName(props); String tableName = tableNameDatabaseName.getLeft(); String databaseName = tableNameDatabaseName.getRight(); DBCredentials dbCredentials = parseDBConfiguration(config); @@ -538,14 +644,14 @@ public void updateDebeziumStorageStatus(ClickHouseSinkConnectorConfig config, Pr BaseDbWriter writer = new BaseDbWriter(dbCredentials.getHostName(), dbCredentials.getPort(), databaseName, dbCredentials.getUserName(), dbCredentials.getPassword(), config, this.conn); - String offsetValue = new DebeziumOffsetStorage().getDebeziumStorageStatusQuery(props, writer); + String offsetValue = this.debeziumOffsetStorage.getDebeziumStorageStatusQuery(props, writer); - String offsetKey = new DebeziumOffsetStorage().getOffsetKey(props); - String updateOffsetValue = new DebeziumOffsetStorage().updateLsnInformation(offsetValue, - Long.parseLong(lsn)); + String offsetKey = this.debeziumOffsetStorage.getOffsetKey(props); + String updateOffsetValue = this.debeziumOffsetStorage.updateLsnInformation(offsetValue, + lsn); - new DebeziumOffsetStorage().deleteOffsetStorageRow(offsetKey, props, writer); - new DebeziumOffsetStorage().updateDebeziumStorageRow(writer, tableName, offsetKey, updateOffsetValue, + this.debeziumOffsetStorage.deleteOffsetStorageRow(offsetKey, props, writer); + this.debeziumOffsetStorage.updateDebeziumStorageRow(writer, tableName, offsetKey, updateOffsetValue, System.currentTimeMillis()); } @@ -573,6 +679,7 @@ public void setupDebeziumEventCapture(Properties props, DebeziumRecordParserServ // Create the engine with this configuration ... try { DebeziumEngine.Builder> changeEventBuilder = DebeziumEngine.create(Connect.class); + changeEventBuilder.using(props); changeEventBuilder.notifying(new DebeziumEngine.ChangeConsumer>() { @Override @@ -596,7 +703,7 @@ public void handleBatch(List> list, if(batch.size() > 0) { - appendToRecords(batch); + appendToRecords(batch, config); } } }); @@ -676,7 +783,7 @@ public void connectorStopped() { * @param debeziumRecordParserService */ public void setup(Properties props, DebeziumRecordParserService debeziumRecordParserService, - DDLParserService ddlParserService, boolean forceStart) throws IOException, ClassNotFoundException { + boolean forceStart) throws IOException, ClassNotFoundException { // Check if max queue size was defined by the user. if(props.getProperty(ClickHouseSinkConnectorConfigVariables.MAX_QUEUE_SIZE.toString()) != null) { @@ -775,23 +882,33 @@ DBCredentials parseDBConfiguration(ClickHouseSinkConnectorConfig config) { */ private void setupProcessingThread(ClickHouseSinkConnectorConfig config) { - // Setup separate thread to read messages from shared buffer. - // this.records = new ConcurrentLinkedQueue<>(); - //this.runnable = new ClickHouseBatchRunnable(this.records, config, new HashMap()); - ThreadFactory namedThreadFactory = - new ThreadFactoryBuilder().setNameFormat("Sink Connector thread-pool-%d").build(); - this.executor = new ClickHouseBatchExecutor(config.getInt(ClickHouseSinkConnectorConfigVariables.THREAD_POOL_SIZE.toString()), namedThreadFactory); - for(int i = 0; i < config.getInt(ClickHouseSinkConnectorConfigVariables.THREAD_POOL_SIZE.toString()); i++) { - this.executor.scheduleAtFixedRate(new ClickHouseBatchRunnable(this.records, config, new HashMap()), 0, - config.getLong(ClickHouseSinkConnectorConfigVariables.BUFFER_FLUSH_TIME.toString()), TimeUnit.MILLISECONDS); + if(config.getBoolean(ClickHouseSinkConnectorConfigVariables.SINGLE_THREADED.toString())) { + log.info("********* Running in Single Threaded mode *********"); + singleThreadedWriter = new ClickHouseBatchWriter(config, new HashMap()); } + + ThreadFactory namedThreadFactory = + new ThreadFactoryBuilder().setNameFormat("Sink Connector thread-pool-%d").build(); + this.executor = new ClickHouseBatchExecutor(config.getInt(ClickHouseSinkConnectorConfigVariables.THREAD_POOL_SIZE.toString()), namedThreadFactory); + for (int i = 0; i < config.getInt(ClickHouseSinkConnectorConfigVariables.THREAD_POOL_SIZE.toString()); i++) { + this.executor.scheduleAtFixedRate(new ClickHouseBatchRunnable(this.records, config, new HashMap()), 0, + config.getLong(ClickHouseSinkConnectorConfigVariables.BUFFER_FLUSH_TIME.toString()), TimeUnit.MILLISECONDS); + } + //this.executor.scheduleAtFixedRate(this.runnable, 0, config.getLong(ClickHouseSinkConnectorConfigVariables.BUFFER_FLUSH_TIME.toString()), TimeUnit.MILLISECONDS); } - private void appendToRecords(List convertedRecords) { + private void appendToRecords(List convertedRecords, ClickHouseSinkConnectorConfig config) { + + // If config is set to single threaded. + if(config.getBoolean(ClickHouseSinkConnectorConfigVariables.SINGLE_THREADED.toString())) { + singleThreadedWriter.persistRecords(convertedRecords); - synchronized (this.records) { - this.records.add(convertedRecords); + } else { + + synchronized (this.records) { + this.records.add(convertedRecords); + } } diff --git a/sink-connector-lightweight/src/main/java/com/altinity/clickhouse/debezium/embedded/cdc/DebeziumOffsetStorage.java b/sink-connector-lightweight/src/main/java/com/altinity/clickhouse/debezium/embedded/cdc/DebeziumOffsetStorage.java index 0fb0f61c4..903f92878 100644 --- a/sink-connector-lightweight/src/main/java/com/altinity/clickhouse/debezium/embedded/cdc/DebeziumOffsetStorage.java +++ b/sink-connector-lightweight/src/main/java/com/altinity/clickhouse/debezium/embedded/cdc/DebeziumOffsetStorage.java @@ -2,6 +2,8 @@ import com.altinity.clickhouse.sink.connector.db.BaseDbWriter; +import com.clickhouse.logging.Logger; +import com.clickhouse.logging.LoggerFactory; import io.debezium.storage.jdbc.offset.JdbcOffsetBackingStoreConfig; import org.json.simple.JSONObject; import org.json.simple.parser.JSONParser; @@ -32,6 +34,7 @@ public class DebeziumOffsetStorage { public static final String SOURCE_PASSWORD = "source_password"; + private static final Logger log = LoggerFactory.getLogger(DebeziumOffsetStorage.class); public String getOffsetKey(Properties props) { String connectorName = props.getProperty("name"); @@ -51,6 +54,21 @@ public void deleteOffsetStorageRow(String offsetKey, writer.executeQuery(debeziumStorageStatusQuery); } + /** + * Function to truncate the schema history table + * @param offsetKey + * @param writer + * @throws SQLException + */ + public void deleteSchemaHistoryTable(String offsetKey, + String tableName, + BaseDbWriter writer) throws SQLException { + + + String debeziumStorageStatusQuery = String.format("delete from `%s` where JSONExtractRaw(JSONExtractRaw(history_data,'source'), 'server')='%s'" , tableName, offsetKey); + log.info("Deleting schema history table query: " + debeziumStorageStatusQuery); + writer.executeQuery(debeziumStorageStatusQuery); + } /** * Function to get the latest timestamp of the record in the table * @param props @@ -117,14 +135,26 @@ public String updateBinLogInformation(String record, String binLogFile, String b * @return * @throws ParseException */ - public String updateLsnInformation(String record, long lsn) throws ParseException { + public String updateLsnInformation(String record, String lsn) throws ParseException { + + Long lsnLong; + // If lsn is a string 1/AF00, extract the hex number after slash. + if(lsn.contains("/")) { + lsn = lsn.split("/")[1]; + // convert lsn from hex to long. + lsnLong = Long.parseLong(lsn, 16); + } else { + // convert to long. + lsnLong = Long.parseLong(lsn); + } JSONObject jsonObject = new JSONObject(); if(record != null || !record.isEmpty()) { jsonObject = (JSONObject) new JSONParser().parse(record); } - jsonObject.put(LSN_PROCESSED, lsn); - jsonObject.put(LSN, lsn); + + jsonObject.put(LSN_PROCESSED, lsnLong); + jsonObject.put(LSN, lsnLong); return jsonObject.toJSONString(); } diff --git a/sink-connector-lightweight/src/main/java/com/altinity/clickhouse/debezium/embedded/config/SinkConnectorLightWeightConfig.java b/sink-connector-lightweight/src/main/java/com/altinity/clickhouse/debezium/embedded/config/SinkConnectorLightWeightConfig.java index 91cc59ed5..004699abf 100644 --- a/sink-connector-lightweight/src/main/java/com/altinity/clickhouse/debezium/embedded/config/SinkConnectorLightWeightConfig.java +++ b/sink-connector-lightweight/src/main/java/com/altinity/clickhouse/debezium/embedded/config/SinkConnectorLightWeightConfig.java @@ -1,5 +1,8 @@ package com.altinity.clickhouse.debezium.embedded.config; +import java.util.HashMap; +import java.util.Map; + /** * This class is used to store all the configuration variables * specific to the Sink Connector lightweight version @@ -17,4 +20,22 @@ public class SinkConnectorLightWeightConfig { public static final String CLI_PORT = "cli.port"; + + public static final String DDL_RETRY = "ddl.retry"; + + + // Create a Map of all the configuration variables + // with the value as a description of the variable + // Createa a Map of all the configuration variables + // with the value as a description of the variable + private static final Map configVariables = new HashMap<>(); + static { + configVariables.put(DISABLE_DDL, "This configuration will disable execution of DDL that are read from binlog(Applies only to MySQL)."); + configVariables.put(DISABLE_DROP_TRUNCATE, "Disable drop truncate DDL Statements(Only applies to MySQL) "); + configVariables.put(ENABLE_SNAPSHOT_DDL, "Enable snapshot DDL (Enables execution of DDL that are received during snapshot) (Applies only to MySQL)"); + configVariables.put(CLI_PORT, "Sink connector Client CLI port"); + configVariables.put(DDL_RETRY, "If this configuration is set to true, the sink connector will retry executing DDL after a failure"); + } + + } diff --git a/sink-connector-lightweight/src/main/java/com/altinity/clickhouse/debezium/embedded/ddl/parser/Constants.java b/sink-connector-lightweight/src/main/java/com/altinity/clickhouse/debezium/embedded/ddl/parser/Constants.java index a76f04cb4..dd20a54c6 100644 --- a/sink-connector-lightweight/src/main/java/com/altinity/clickhouse/debezium/embedded/ddl/parser/Constants.java +++ b/sink-connector-lightweight/src/main/java/com/altinity/clickhouse/debezium/embedded/ddl/parser/Constants.java @@ -1,5 +1,9 @@ package com.altinity.clickhouse.debezium.embedded.ddl.parser; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + public class Constants { public static final String ALIAS = "MATERIALIZED"; @@ -49,9 +53,14 @@ public class Constants { public static final String DROP_COLUMN = "DROP COLUMN %s"; - + public static final String DROP_CONSTRAINT = "DROP CONSTRAINT %s"; public static final String NEW_REPLACING_MERGE_TREE_VERSION = "23.2"; + // There are certain Data types where Nullable is not supported. + // For example, Point, Geometry, Enum, Array, Map, Decimal, UUID, DateTime64, Date, Time, DateTime, Nullable(DateTime), Nullable(Date), Nullable(Time), Nullable(DateTime64), Nullable(UUID) + // Create a set of these data types. + public static final Set NULLABLE_NOT_SUPPORTED_DATA_TYPES = new HashSet<>(Arrays.asList("point", "polygon")); + } diff --git a/sink-connector-lightweight/src/main/java/com/altinity/clickhouse/debezium/embedded/ddl/parser/MySQLDDLParserService.java b/sink-connector-lightweight/src/main/java/com/altinity/clickhouse/debezium/embedded/ddl/parser/MySQLDDLParserService.java index a1f611a39..6831afa2e 100644 --- a/sink-connector-lightweight/src/main/java/com/altinity/clickhouse/debezium/embedded/ddl/parser/MySQLDDLParserService.java +++ b/sink-connector-lightweight/src/main/java/com/altinity/clickhouse/debezium/embedded/ddl/parser/MySQLDDLParserService.java @@ -2,6 +2,8 @@ import com.altinity.clickhouse.sink.connector.ClickHouseSinkConnectorConfig; +import com.altinity.clickhouse.sink.connector.db.BaseDbWriter; +import com.altinity.clickhouse.sink.connector.db.DbWriter; import com.google.inject.Inject; import com.google.inject.Singleton; import io.debezium.antlr.CaseChangingCharStream; @@ -28,6 +30,7 @@ public class MySQLDDLParserService implements DDLParserService { private ClickHouseSinkConnectorConfig config; + private BaseDbWriter writer; @Inject public MySQLDDLParserService() { @@ -37,6 +40,12 @@ public MySQLDDLParserService(ClickHouseSinkConnectorConfig config, String databa this.databaseName = databaseName; } + public MySQLDDLParserService(BaseDbWriter writer, ClickHouseSinkConnectorConfig config, String databaseName) { + this.writer = writer; + this.config = config; + this.databaseName = databaseName; + } + @Override public String parseSql(String sql, String tableName, StringBuffer parsedQuery) { @@ -51,7 +60,7 @@ public String parseSql(String sql, String tableName, StringBuffer parsedQuery) { parser.addErrorListener(errorListener); lexer.addErrorListener(errorListener); - MySqlDDLParserListenerImpl listener = new MySqlDDLParserListenerImpl(parsedQuery, tableName, databaseName, config); + MySqlDDLParserListenerImpl listener = new MySqlDDLParserListenerImpl(writer, parsedQuery, tableName, databaseName, config); ParseTreeWalker walker = new ParseTreeWalker(); walker.walk(listener, parser.root()); @@ -71,7 +80,7 @@ public String parseSql(String sql, String tableName, StringBuffer parsedQuery, parser.addErrorListener(errorListener); lexer.addErrorListener(errorListener); - MySqlDDLParserListenerImpl listener = new MySqlDDLParserListenerImpl(parsedQuery, tableName, databaseName, this.config); + MySqlDDLParserListenerImpl listener = new MySqlDDLParserListenerImpl(writer, parsedQuery, tableName, databaseName, this.config); ParseTreeWalker walker = new ParseTreeWalker(); walker.walk(listener, parser.root()); diff --git a/sink-connector-lightweight/src/main/java/com/altinity/clickhouse/debezium/embedded/ddl/parser/MySqlDDLParserListenerImpl.java b/sink-connector-lightweight/src/main/java/com/altinity/clickhouse/debezium/embedded/ddl/parser/MySqlDDLParserListenerImpl.java index ebdc90f63..d068b8b6b 100644 --- a/sink-connector-lightweight/src/main/java/com/altinity/clickhouse/debezium/embedded/ddl/parser/MySqlDDLParserListenerImpl.java +++ b/sink-connector-lightweight/src/main/java/com/altinity/clickhouse/debezium/embedded/ddl/parser/MySqlDDLParserListenerImpl.java @@ -8,6 +8,8 @@ import com.altinity.clickhouse.sink.connector.ClickHouseSinkConnectorConfig; import com.altinity.clickhouse.sink.connector.ClickHouseSinkConnectorConfigVariables; import com.altinity.clickhouse.sink.connector.common.Utils; +import com.altinity.clickhouse.sink.connector.db.BaseDbWriter; +import com.altinity.clickhouse.sink.connector.db.DBMetadata; import io.debezium.ddl.parser.mysql.generated.MySqlParser; import io.debezium.ddl.parser.mysql.generated.MySqlParser.AlterByAddColumnContext; import io.debezium.ddl.parser.mysql.generated.MySqlParser.TableNameContext; @@ -18,6 +20,7 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import java.sql.SQLException; import java.time.ZoneId; import java.util.*; @@ -31,20 +34,55 @@ public class MySqlDDLParserListenerImpl extends MySQLDDLParserBaseListener { String tableName; ClickHouseSinkConnectorConfig config; ZoneId userProvidedTimeZone; + Map sourceToDestinationMap = new HashMap<>(); String databaseName; - public MySqlDDLParserListenerImpl(StringBuffer transformedQuery, String tableName, + BaseDbWriter writer; + + DBMetadata dbMetadata; + + public MySqlDDLParserListenerImpl(BaseDbWriter writer, StringBuffer transformedQuery, String tableName, String databaseName, ClickHouseSinkConnectorConfig config) { - this.databaseName = databaseName; + this.config = config; + try { + if (this.config.getString(ClickHouseSinkConnectorConfigVariables.CLICKHOUSE_DATABASE_OVERRIDE_MAP.toString()) != null) + sourceToDestinationMap = Utils.parseSourceToDestinationDatabaseMap(this.config. + getString(ClickHouseSinkConnectorConfigVariables.CLICKHOUSE_DATABASE_OVERRIDE_MAP.toString())); + + } catch(Exception e) { + log.error("enterCreateDatabase: Error parsing source to destination database map:" + e.toString()); + } + + this.databaseName = overrideDatabaseName(databaseName); + this.query = transformedQuery; this.tableName = tableName; - this.config = config; + this.config = config; + this.dbMetadata = new DBMetadata(); + this.writer = writer; this.userProvidedTimeZone = parseTimeZone(); } + /** + * Function to override the database name. + * @param databaseName + * @return + */ + private String overrideDatabaseName(String databaseName) { + + // databaseName might contain backticks. Remove them. + if(databaseName.contains("`")) { + databaseName = databaseName.replace("`", ""); + } + + if(sourceToDestinationMap.containsKey(databaseName)) { + return sourceToDestinationMap.get(databaseName); + } + return databaseName; + } public ZoneId parseTimeZone() { String userProvidedTimeZone = config.getString(ClickHouseSinkConnectorConfigVariables @@ -73,22 +111,9 @@ public void enterCreateDatabase(MySqlParser.CreateDatabaseContext createDatabase String databaseName = tree.getText(); if(!databaseName.isEmpty()) { - // Check if the database is overridden - Map sourceToDestinationMap = new HashMap<>(); - - try { - if (this.config.getString(ClickHouseSinkConnectorConfigVariables.CLICKHOUSE_DATABASE_OVERRIDE_MAP.toString()) != null) - sourceToDestinationMap = Utils.parseSourceToDestinationDatabaseMap(this.config. - getString(ClickHouseSinkConnectorConfigVariables.CLICKHOUSE_DATABASE_OVERRIDE_MAP.toString())); - } catch(Exception e) { - log.error("enterCreateDatabase: Error parsing source to destination database map:" + e.toString()); - } - if(sourceToDestinationMap.containsKey(databaseName)) { - this.query.append(String.format(Constants.CREATE_DATABASE, sourceToDestinationMap.get(databaseName))); - } else { - this.query.append(String.format(Constants.CREATE_DATABASE, databaseName)); - } + String overrideDatabaseName = overrideDatabaseName(tree.getText()); + this.query.append(String.format(Constants.CREATE_DATABASE, overrideDatabaseName)); } } } @@ -187,7 +212,10 @@ private Set parseCreateTable(MySqlParser.CreateTableContext ctx, StringB this.tableName = tree.getText(); // If tableName already includes the database name don't include database name in this.query if(tableName.contains(".")) { - this.query.append(tableName); + // split tableName into databaseName and tableName + String[] tableNameSplit = tableName.split("\\."); + this.query.append(this.databaseName).append(".").append(tableNameSplit[1]); + //this.query.append(tableName); } else this.query.append(databaseName).append(".").append(tree.getText()); @@ -307,7 +335,9 @@ private void parseColumnDefinitions(ParseTree subtree, StringBuilder orderByColu continue; } - if(isNullColumn) { + // Nullable should not be added for POINT data type + String lowerCaseDataType = colDataType.toLowerCase(); + if(!Constants.NULLABLE_NOT_SUPPORTED_DATA_TYPES.contains(lowerCaseDataType) && isNullColumn) { this.query.append(Constants.NULLABLE).append("(").append(colDataType) .append(")").append(","); } @@ -353,7 +383,7 @@ else if(parsedDataType.contains("(") && parsedDataType.contains(")") && } } - chDataType = DataTypeConverter.convertToString(columnName, + chDataType = DataTypeConverter.convertToString(this.config, columnName, scale, precision, dtc, this.userProvidedTimeZone); return chDataType; @@ -415,6 +445,8 @@ private void parseAlterTable(ParseTree tree) { boolean isNullColumn = false; boolean isAlterChangeColumn = false; + boolean nullExplicitlySet = false; + if (tree instanceof AlterByAddColumnContext) { modifier = Constants.ADD_COLUMN; modifierWithNull = Constants.ADD_COLUMN_NULLABLE; @@ -453,6 +485,7 @@ private void parseAlterTable(ParseTree tree) { for (ParseTree columnDefChild : ((MySqlParser.ColumnDefinitionContext) columnChild).children) { if (columnDefChild instanceof MySqlParser.NullColumnConstraintContext) { + nullExplicitlySet = true; if (columnDefChild.getText().equalsIgnoreCase(Constants.NULL)) isNullColumn = true; else if(columnDefChild.getText().equalsIgnoreCase(Constants.NOT_NULL)) { @@ -484,7 +517,27 @@ else if(columnDefChild.getText().equalsIgnoreCase(Constants.NOT_NULL)) { } } - + // if null is not explicitly set. + if (!nullExplicitlySet) { + try { + if(writer == null) { + log.error("Error with DB connection"); + throw new SQLException("Error with DB connection"); + } + else { + Map isNullableList = dbMetadata.getColumnsIsNullableForTable(tableName, writer.getConnection(), databaseName); + if (isNullableList.get(columnName) != null && isNullableList.get(columnName)) { + isNullColumn = true; + } else { + isNullColumn = false; + } + } + } catch (Exception e) { + log.error("Error retrieving NULL column schema from ClickHouse", e); + } + // Check if the column scehma is nullable from ClickHouse. + // Map isNullableList = dbMetadata.getColumnsIsNullableForTable(tableName, writer.getConnection(), databaseName); + } if (columnName != null && columnType != null) if (isNullColumn) { this.query.append(" ").append(String.format(modifierWithNull, columnName, columnType)).append(" "); @@ -542,7 +595,18 @@ public void enterAlterTable(MySqlParser.AlterTableContext alterTableContext) { if (tree instanceof AlterByAddColumnContext) { parseAlterTable(tree); - } else if (tree instanceof MySqlParser.AlterByModifyColumnContext) { + } + else if (tree instanceof MySqlParser.AlterByDropConstraintCheckContext) { + // Drop Constraint. + this.query.append(" "); + for (ParseTree dropConstraintTree : ((MySqlParser.AlterByDropConstraintCheckContext) (tree)).children) { + if (dropConstraintTree instanceof MySqlParser.UidContext) { + System.out.println("Drop Constraint"); + this.query.append(String.format(Constants.DROP_CONSTRAINT, dropConstraintTree.getText())); + } + } + } + else if (tree instanceof MySqlParser.AlterByModifyColumnContext) { parseAlterTable(tree); } else if (tree instanceof MySqlParser.AlterByDropColumnContext) { // Drop Column. @@ -668,8 +732,12 @@ public void enterRenameTable(MySqlParser.RenameTableContext renameTableContext) originalTableName = renameTableContextChildren.get(0).getText(); newTableName = renameTableContextChildren.get(2).getText(); // If the table name already includes the database name dont include it in the query. - if(originalTableName.contains(".")) { - this.query.append(originalTableName).append(" to ").append(newTableName); + if(originalTableName.contains(".") && newTableName.contains(".")) { + // Split database and table name. + String[] databaseAndTableNameArray = originalTableName.split("\\."); + String[] newDatabaseAndTableNameArray = newTableName.split("\\."); + this.query.append(this.databaseName).append(".").append(databaseAndTableNameArray[1]).append(" to "). + append(this.databaseName).append(".").append(newDatabaseAndTableNameArray[1]); } else this.query.append(databaseName).append(".").append(originalTableName).append(" to "). append(databaseName).append(".").append(newTableName); diff --git a/sink-connector-lightweight/src/main/java/com/altinity/clickhouse/debezium/embedded/parser/DataTypeConverter.java b/sink-connector-lightweight/src/main/java/com/altinity/clickhouse/debezium/embedded/parser/DataTypeConverter.java index 00a6d92eb..d07008ba3 100644 --- a/sink-connector-lightweight/src/main/java/com/altinity/clickhouse/debezium/embedded/parser/DataTypeConverter.java +++ b/sink-connector-lightweight/src/main/java/com/altinity/clickhouse/debezium/embedded/parser/DataTypeConverter.java @@ -1,63 +1,71 @@ package com.altinity.clickhouse.debezium.embedded.parser; +import com.altinity.clickhouse.sink.connector.ClickHouseSinkConnectorConfig; import com.altinity.clickhouse.sink.connector.converters.ClickHouseDataTypeMapper; import com.clickhouse.data.ClickHouseDataType; import io.debezium.antlr.DataTypeResolver; +import io.debezium.bean.DefaultBeanRegistry; import io.debezium.config.CommonConnectorConfig; +import io.debezium.config.Configuration; +import io.debezium.connector.binlog.BinlogConnectorConfig; +import io.debezium.connector.binlog.charset.BinlogCharsetRegistry; +import io.debezium.connector.mysql.MySqlConnectorConfig; +import io.debezium.connector.mysql.charset.MySqlCharsetRegistryServiceProvider; import io.debezium.connector.mysql.jdbc.MySqlValueConverters; import io.debezium.ddl.parser.mysql.generated.MySqlParser; import io.debezium.jdbc.JdbcValueConverters; import io.debezium.jdbc.TemporalPrecisionMode; import io.debezium.relational.Column; +import io.debezium.relational.RelationalDatabaseConnectorConfig; import io.debezium.relational.ddl.DataType; +import io.debezium.service.DefaultServiceRegistry; +import io.debezium.service.spi.ServiceRegistry; import org.apache.kafka.connect.data.SchemaBuilder; import java.sql.Types; import java.time.ZoneId; import java.util.Arrays; +import java.util.Map; /** * */ public class DataTypeConverter { - /** - * Function that takes the Antlr parsed column type - * and converts it into ClickHouse type. - * - * @param columnName - * @param columnDefChild - * @return - */ - public static ClickHouseDataType convert(String columnName, MySqlParser.DataTypeContext columnDefChild) { - MySqlValueConverters mysqlConverter = new MySqlValueConverters( - JdbcValueConverters.DecimalMode.PRECISE, - TemporalPrecisionMode.ADAPTIVE, - JdbcValueConverters.BigIntUnsignedMode.LONG, - CommonConnectorConfig.BinaryHandlingMode.BYTES, - x ->x, CommonConnectorConfig.EventConvertingFailureHandlingMode.WARN); - - - DataType dataType = initializeDataTypeResolver().resolveDataType(columnDefChild); - //DataType dataType = MySqlParser.dataTypeResolver.resolveDataType(dataTypeContext); - Column column = Column.editor().name(columnName).type(dataType.name()).jdbcType(dataType.jdbcType()).length((int) dataType.length()).scale(dataType.scale()).create(); - SchemaBuilder schemaBuilder = mysqlConverter.schemaBuilder(column); - - return ClickHouseDataTypeMapper.getClickHouseDataType(schemaBuilder.schema().type(), schemaBuilder.schema().name()); - } public static DataType getDataType(MySqlParser.DataTypeContext columnDefChild) { String convertedDataType = null; return initializeDataTypeResolver().resolveDataType(columnDefChild); } - public static String convertToString(String columnName, int scale, int precision, MySqlParser.DataTypeContext columnDefChild, ZoneId userProvidedTimeZone) { + public static String convertToString(ClickHouseSinkConnectorConfig config, String columnName, int scale, int precision, MySqlParser.DataTypeContext columnDefChild, ZoneId userProvidedTimeZone) { + new DefaultBeanRegistry(); + + // Convert ClickHouseConnectorConfig to configuration. + Configuration configuration = Configuration.create() + .with(BinlogConnectorConfig.DECIMAL_HANDLING_MODE, "decimalHandlingMode") + .with(BinlogConnectorConfig.TIME_PRECISION_MODE, "temporalPrecisionMode") + .with(BinlogConnectorConfig.BIGINT_UNSIGNED_HANDLING_MODE, "bigIntUnsignedHandlingMode") + .with(BinlogConnectorConfig.BINARY_HANDLING_MODE, "binaryHandlingMode") + .with(BinlogConnectorConfig.EVENT_CONVERTING_FAILURE_HANDLING_MODE, "eventConvertingFailureHandlingMode") + .build(); + + final MySqlConnectorConfig connectorConfig = new MySqlConnectorConfig(configuration); + + // Convert Properties to Configuration. +// Configuration configuration = Configuration.create().build(); +// // Iterate through properties and fill configuration. +// for (Map.Entry entry : properties.entrySet()) { +// configuration = configuration.edit().with(entry.getKey().toString(), entry.getValue().toString()).build(); +// } + ServiceRegistry serviceRegistry = new DefaultServiceRegistry( Configuration.create().build(), new DefaultBeanRegistry()); + BinlogCharsetRegistry charsetRegistry = new MySqlCharsetRegistryServiceProvider().createService(Configuration.create().build(), serviceRegistry); MySqlValueConverters mysqlConverter = new MySqlValueConverters( JdbcValueConverters.DecimalMode.PRECISE, TemporalPrecisionMode.ADAPTIVE, JdbcValueConverters.BigIntUnsignedMode.LONG, CommonConnectorConfig.BinaryHandlingMode.BYTES, - x ->x, CommonConnectorConfig.EventConvertingFailureHandlingMode.WARN); + x ->x, CommonConnectorConfig.EventConvertingFailureHandlingMode.WARN, connectorConfig.getServiceRegistry()); String convertedDataType = null; diff --git a/sink-connector-lightweight/src/main/resources/release.properties b/sink-connector-lightweight/src/main/resources/release.properties new file mode 100644 index 000000000..e69de29bb diff --git a/sink-connector-lightweight/src/test/java/com/altinity/clickhouse/debezium/embedded/ClickHouseDebeziumEmbeddedPostgresDecoderBufsDockerIT.java b/sink-connector-lightweight/src/test/java/com/altinity/clickhouse/debezium/embedded/ClickHouseDebeziumEmbeddedPostgresDecoderBufsDockerIT.java index b6a694a7f..a9f044582 100644 --- a/sink-connector-lightweight/src/test/java/com/altinity/clickhouse/debezium/embedded/ClickHouseDebeziumEmbeddedPostgresDecoderBufsDockerIT.java +++ b/sink-connector-lightweight/src/test/java/com/altinity/clickhouse/debezium/embedded/ClickHouseDebeziumEmbeddedPostgresDecoderBufsDockerIT.java @@ -81,9 +81,7 @@ public void testDecoderBufsPlugin() throws Exception { try { engine.set(new DebeziumChangeEventCapture()); - engine.get().setup(getProperties(), new SourceRecordParserService(), - new MySQLDDLParserService(new ClickHouseSinkConnectorConfig(new HashMap<>()), - "employees"), false); + engine.get().setup(getProperties(), new SourceRecordParserService(), false); } catch (Exception e) { throw new RuntimeException(e); } diff --git a/sink-connector-lightweight/src/test/java/com/altinity/clickhouse/debezium/embedded/ClickHouseDebeziumEmbeddedPostgresPgoutputDockerIT.java b/sink-connector-lightweight/src/test/java/com/altinity/clickhouse/debezium/embedded/ClickHouseDebeziumEmbeddedPostgresPgoutputDockerIT.java index d09e6da0c..5df4eebec 100644 --- a/sink-connector-lightweight/src/test/java/com/altinity/clickhouse/debezium/embedded/ClickHouseDebeziumEmbeddedPostgresPgoutputDockerIT.java +++ b/sink-connector-lightweight/src/test/java/com/altinity/clickhouse/debezium/embedded/ClickHouseDebeziumEmbeddedPostgresPgoutputDockerIT.java @@ -84,8 +84,7 @@ public void testPgOutputPlugin() throws Exception { try { engine.set(new DebeziumChangeEventCapture()); - engine.get().setup(getProperties(), new SourceRecordParserService(), - new MySQLDDLParserService(new ClickHouseSinkConnectorConfig(new HashMap<>()), "system"), false); + engine.get().setup(getProperties(), new SourceRecordParserService(), false); } catch (Exception e) { throw new RuntimeException(e); } diff --git a/sink-connector-lightweight/src/test/java/com/altinity/clickhouse/debezium/embedded/ITCommon.java b/sink-connector-lightweight/src/test/java/com/altinity/clickhouse/debezium/embedded/ITCommon.java index cdb081ef4..dcc5b5063 100644 --- a/sink-connector-lightweight/src/test/java/com/altinity/clickhouse/debezium/embedded/ITCommon.java +++ b/sink-connector-lightweight/src/test/java/com/altinity/clickhouse/debezium/embedded/ITCommon.java @@ -29,22 +29,73 @@ static public Connection connectToMySQL(MySQLContainer mySqlContainer) { return conn; } - // Function to connect to Postgres. - static public Connection connectToPostgreSQL(PostgreSQLContainer postgreSQLContainer) { + static public Connection connectToMySQL(String host, String port, String databaseName, String userName, String password) { Connection conn = null; try { - String connectionUrl = String.format("jdbc:postgresql://%s:%s/%s?user=%s&password=%s", postgreSQLContainer.getHost(), - postgreSQLContainer.getFirstMappedPort(), - postgreSQLContainer.getDatabaseName(), postgreSQLContainer.getUsername(), postgreSQLContainer.getPassword()); + String connectionUrl = String.format("jdbc:mysql://%s:%s/%s?user=%s&password=%s", host, port, + databaseName, userName, password); conn = DriverManager.getConnection(connectionUrl); + } catch (SQLException ex) { + // handle any errors } + return conn; } + // Function to connect to Postgres. + static public Connection connectToPostgreSQL(PostgreSQLContainer postgreSQLContainer) throws SQLException { + Connection conn = null; + + String connectionUrl = String.format("jdbc:postgresql://%s:%s/%s?user=%s&password=%s", postgreSQLContainer.getHost(), + postgreSQLContainer.getFirstMappedPort(), + postgreSQLContainer.getDatabaseName(), postgreSQLContainer.getUsername(), postgreSQLContainer.getPassword()); + conn = DriverManager.getConnection(connectionUrl); + + return conn; + } + + static public Properties getDebeziumProperties(String mySQLHost, String mySQLPort, ClickHouseContainer clickHouseContainer) throws Exception { + + // Start the debezium embedded application. + + Properties defaultProps = new Properties(); + Properties defaultProperties = PropertiesHelper.getProperties("config.properties"); + + defaultProps.putAll(defaultProperties); + Properties fileProps = new ConfigLoader().load("config.yml"); + defaultProps.putAll(fileProps); + + defaultProps.setProperty("database.hostname", mySQLHost); + defaultProps.setProperty("database.port", String.valueOf(mySQLPort)); + defaultProps.setProperty("database.user", "root"); + defaultProps.setProperty("database.password", "adminpass"); + + defaultProps.setProperty("clickhouse.server.url", clickHouseContainer.getHost()); + defaultProps.setProperty("clickhouse.server.port", String.valueOf(clickHouseContainer.getFirstMappedPort())); + defaultProps.setProperty("clickhouse.server.user", clickHouseContainer.getUsername()); + defaultProps.setProperty("clickhouse.server.password", clickHouseContainer.getPassword()); + + defaultProps.setProperty("offset.storage.jdbc.url", String.format("jdbc:clickhouse://%s:%s", + clickHouseContainer.getHost(), clickHouseContainer.getFirstMappedPort())); + + defaultProps.setProperty("schema.history.internal.jdbc.url", String.format("jdbc:clickhouse://%s:%s", + clickHouseContainer.getHost(), clickHouseContainer.getFirstMappedPort())); + + defaultProps.setProperty("offset.storage.jdbc.url", String.format("jdbc:clickhouse://%s:%s", + clickHouseContainer.getHost(), clickHouseContainer.getFirstMappedPort())); + + defaultProps.setProperty("schema.history.internal.jdbc.url", String.format("jdbc:clickhouse://%s:%s", + clickHouseContainer.getHost(), clickHouseContainer.getFirstMappedPort())); + + + return defaultProps; + + } + static public Properties getDebeziumProperties(MySQLContainer mySqlContainer, ClickHouseContainer clickHouseContainer) throws Exception { // Start the debezium embedded application. @@ -65,6 +116,7 @@ static public Properties getDebeziumProperties(MySQLContainer mySqlContainer, Cl defaultProps.setProperty("clickhouse.server.port", String.valueOf(clickHouseContainer.getFirstMappedPort())); defaultProps.setProperty("clickhouse.server.user", clickHouseContainer.getUsername()); defaultProps.setProperty("clickhouse.server.password", clickHouseContainer.getPassword()); + //defaultProps.setProperty("ddl.retry", "true"); defaultProps.setProperty("offset.storage.jdbc.url", String.format("jdbc:clickhouse://%s:%s", clickHouseContainer.getHost(), clickHouseContainer.getFirstMappedPort())); diff --git a/sink-connector-lightweight/src/test/java/com/altinity/clickhouse/debezium/embedded/MariaDBIT.java b/sink-connector-lightweight/src/test/java/com/altinity/clickhouse/debezium/embedded/MariaDBIT.java new file mode 100644 index 000000000..97f1b9148 --- /dev/null +++ b/sink-connector-lightweight/src/test/java/com/altinity/clickhouse/debezium/embedded/MariaDBIT.java @@ -0,0 +1,160 @@ +package com.altinity.clickhouse.debezium.embedded; + +import com.altinity.clickhouse.debezium.embedded.cdc.DebeziumChangeEventCapture; +import com.altinity.clickhouse.debezium.embedded.ddl.parser.MySQLDDLParserService; +import com.altinity.clickhouse.debezium.embedded.parser.SourceRecordParserService; +import com.altinity.clickhouse.sink.connector.ClickHouseSinkConnectorConfig; +import com.altinity.clickhouse.sink.connector.db.BaseDbWriter; +import com.clickhouse.jdbc.ClickHouseConnection; +import org.apache.log4j.BasicConfigurator; +import org.junit.Assert; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.testcontainers.clickhouse.ClickHouseContainer; +import org.testcontainers.containers.MariaDBContainer; +import org.testcontainers.containers.MySQLContainer; +import org.testcontainers.containers.wait.strategy.HttpWaitStrategy; +import org.testcontainers.junit.jupiter.Testcontainers; +import org.testcontainers.utility.DockerImageName; +import org.testcontainers.utility.MountableFile; + +import java.sql.Connection; +import java.sql.ResultSet; +import java.util.HashMap; +import java.util.Properties; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.atomic.AtomicReference; + +/** + * Integration test to validate support for replication of multiple databases. + */ +@Testcontainers +@DisplayName("Integration Test that validates basic replication of MariaDB databases in single threaded mode") +public class MariaDBIT +{ + + protected MariaDBContainer mySqlContainer; + static ClickHouseContainer clickHouseContainer; + + @BeforeEach + public void startContainers() throws InterruptedException { + mySqlContainer = (MariaDBContainer) new MariaDBContainer() + .withDatabaseName("employees").withUsername("adminuser").withPassword("adminpass") + // .withInitScript("data_types.sql") + .withCopyFileToContainer( + MountableFile.forClasspathResource("my.cnf"), // Adjust this to your resource file + "/etc/mysql/my.cnf" + ) + .withExtraHost("mysql-server", "0.0.0.0") + .waitingFor(new HttpWaitStrategy().forPort(3306)); + + BasicConfigurator.configure(); + mySqlContainer.start(); + Thread.sleep(15000); + } + + static { + clickHouseContainer = new ClickHouseContainer(DockerImageName.parse("clickhouse/clickhouse-server:latest") + .asCompatibleSubstituteFor("clickhouse")) + .withInitScript("init_clickhouse_it.sql") + .withUsername("ch_user") + .withPassword("password") + .withExposedPorts(8123); + + clickHouseContainer.start(); + } + + @DisplayName("Integration Test that validates replication of MariaDB databases in single.threaded mode") + @Test + public void testMultipleDatabases() throws Exception { + + AtomicReference engine = new AtomicReference<>(); + + Properties props = ITCommon.getDebeziumProperties(mySqlContainer.getHost(), + String.valueOf(mySqlContainer.getFirstMappedPort()), clickHouseContainer); + // Set the list of databases captured. + props.put("database.whitelist", "employees,test_db,test_db2"); + props.put("database.include.list", "employees,test_db,test_db2"); + props.put("single.threaded", true); + + ExecutorService executorService = Executors.newFixedThreadPool(1); + executorService.execute(() -> { + try { + + engine.set(new DebeziumChangeEventCapture()); + engine.get().setup(props, new SourceRecordParserService(),false); + } catch (Exception e) { + throw new RuntimeException(e); + } + }); + + Thread.sleep(30000); + Connection conn = ITCommon.connectToMySQL(mySqlContainer.getHost(), String.valueOf(mySqlContainer.getFirstMappedPort()), + mySqlContainer.getDatabaseName(), mySqlContainer.getUsername(), mySqlContainer.getPassword()); + + //conn.createStatement().execute("CREATE DATABASE test_db2"); + Thread.sleep(5000); + // Create a new database + conn.createStatement().execute("CREATE TABLE employees.audience (" + + "id int unsigned NOT NULL AUTO_INCREMENT, " + + "client_id int unsigned NOT NULL, " + + "list_id int unsigned NOT NULL, " + + "status tinyint NOT NULL, " + + "email varchar(200) CHARACTER SET utf16 COLLATE utf16_unicode_ci NOT NULL, " + + "custom_properties JSON, " + + "source tinyint unsigned NOT NULL DEFAULT '0', " + + "created_date datetime DEFAULT NULL, " + + "modified_date datetime DEFAULT NULL, " + + "property_update_date datetime DEFAULT NULL, " + + "PRIMARY KEY (id), " + + "KEY cid_email (client_id,email), " + + "KEY cid (client_id,list_id,status), " + + "KEY contact_created (created_date), " + + "KEY idx_email (email)" + + ") ENGINE=InnoDB CHARSET=utf16 COLLATE=utf16_unicode_ci"); + + + Thread.sleep(5000); + // Insert a new row. + conn.createStatement().execute("INSERT INTO employees.audience (client_id, list_id, status, email, custom_properties, source, created_date, modified_date, property_update_date)" + + " VALUES (1, 100, 1, 'example@example.com', '{\"name\": \"John\", \"age\": 30}', 1, '2024-05-13 12:00:00', '2024-05-13 12:00:00', '2024-05-13 12:00:00')"); + + Thread.sleep(10000); + conn.close(); + + // Create connection to clickhouse and validate if the tables are replicated. + String jdbcUrl = BaseDbWriter.getConnectionString(clickHouseContainer.getHost(), clickHouseContainer.getFirstMappedPort(), + "system"); + ClickHouseConnection chConn = BaseDbWriter.createConnection(jdbcUrl, "Client_1", + clickHouseContainer.getUsername(), clickHouseContainer.getPassword(), new ClickHouseSinkConnectorConfig(new HashMap<>())); + + BaseDbWriter writer = new BaseDbWriter(clickHouseContainer.getHost(), clickHouseContainer.getFirstMappedPort(), + "system", clickHouseContainer.getUsername(), clickHouseContainer.getPassword(), null, chConn); + // query clickhouse connection and get data for test_table1 and test_table2 + + + ResultSet rs = writer.executeQueryWithResultSet("SELECT * FROM employees.audience"); + // Validate the data + boolean recordFound = false; + while(rs.next()) { + recordFound = true; + assert rs.getInt("id") == 1; + //assert rs.getString("name").equalsIgnoreCase("test"); + } + Assert.assertTrue(recordFound); + + + + if(engine.get() != null) { + engine.get().stop(); + } + // Files.deleteIfExists(tmpFilePath); + executorService.shutdown(); + + writer.getConnection().close(); + + + } +} diff --git a/sink-connector-lightweight/src/test/java/com/altinity/clickhouse/debezium/embedded/MySQLGenerateColumnsTest.java b/sink-connector-lightweight/src/test/java/com/altinity/clickhouse/debezium/embedded/MySQLGenerateColumnsTest.java index cb7348279..6444b11a9 100644 --- a/sink-connector-lightweight/src/test/java/com/altinity/clickhouse/debezium/embedded/MySQLGenerateColumnsTest.java +++ b/sink-connector-lightweight/src/test/java/com/altinity/clickhouse/debezium/embedded/MySQLGenerateColumnsTest.java @@ -75,9 +75,7 @@ public void testMySQLGeneratedColumns() throws Exception { Properties props = getDebeziumProperties(mySqlContainer, clickHouseContainer); engine.set(new DebeziumChangeEventCapture()); - engine.get().setup(props, new SourceRecordParserService(), - new MySQLDDLParserService(new ClickHouseSinkConnectorConfig(new HashMap<>()), - "employees"), false); + engine.get().setup(props, new SourceRecordParserService(), false); } catch (Exception e) { throw new RuntimeException(e); } diff --git a/sink-connector-lightweight/src/test/java/com/altinity/clickhouse/debezium/embedded/MySQLJsonIT.java b/sink-connector-lightweight/src/test/java/com/altinity/clickhouse/debezium/embedded/MySQLJsonIT.java index ccf9409ea..6712ae23f 100644 --- a/sink-connector-lightweight/src/test/java/com/altinity/clickhouse/debezium/embedded/MySQLJsonIT.java +++ b/sink-connector-lightweight/src/test/java/com/altinity/clickhouse/debezium/embedded/MySQLJsonIT.java @@ -78,8 +78,7 @@ public void testMultipleDatabases() throws Exception { try { engine.set(new DebeziumChangeEventCapture()); - engine.get().setup(props, new SourceRecordParserService(), - new MySQLDDLParserService(new ClickHouseSinkConnectorConfig(new HashMap<>()), "test_db"),false); + engine.get().setup(props, new SourceRecordParserService(), false); } catch (Exception e) { throw new RuntimeException(e); } diff --git a/sink-connector-lightweight/src/test/java/com/altinity/clickhouse/debezium/embedded/OffsetManagementIT.java b/sink-connector-lightweight/src/test/java/com/altinity/clickhouse/debezium/embedded/OffsetManagementIT.java index 0c89e3020..c8419da2d 100644 --- a/sink-connector-lightweight/src/test/java/com/altinity/clickhouse/debezium/embedded/OffsetManagementIT.java +++ b/sink-connector-lightweight/src/test/java/com/altinity/clickhouse/debezium/embedded/OffsetManagementIT.java @@ -93,8 +93,7 @@ public void testAutoCreateTable(String clickHouseServerVersion) throws Exception try { engine.set(new DebeziumChangeEventCapture()); - engine.get().setup(ITCommon.getDebeziumPropertiesForSchemaOnly(mySqlContainer, clickHouseContainer), new SourceRecordParserService(), - new MySQLDDLParserService(new ClickHouseSinkConnectorConfig(new HashMap<>()), "datatypes"),false); + engine.get().setup(ITCommon.getDebeziumPropertiesForSchemaOnly(mySqlContainer, clickHouseContainer), new SourceRecordParserService(), false); } catch (Exception e) { throw new RuntimeException(e); } diff --git a/sink-connector-lightweight/src/test/java/com/altinity/clickhouse/debezium/embedded/PostgresInitialDockerIT.java b/sink-connector-lightweight/src/test/java/com/altinity/clickhouse/debezium/embedded/PostgresInitialDockerIT.java index 4189bb5a5..96ac528fc 100644 --- a/sink-connector-lightweight/src/test/java/com/altinity/clickhouse/debezium/embedded/PostgresInitialDockerIT.java +++ b/sink-connector-lightweight/src/test/java/com/altinity/clickhouse/debezium/embedded/PostgresInitialDockerIT.java @@ -82,8 +82,7 @@ public void testDecoderBufsPlugin() throws Exception { try { engine.set(new DebeziumChangeEventCapture()); - engine.get().setup(getProperties(), new SourceRecordParserService(), - new MySQLDDLParserService(new ClickHouseSinkConnectorConfig(new HashMap<>()), "employees"), false); + engine.get().setup(getProperties(), new SourceRecordParserService(), false); } catch (Exception e) { throw new RuntimeException(e); } diff --git a/sink-connector-lightweight/src/test/java/com/altinity/clickhouse/debezium/embedded/PostgresInitialDockerWKeeperMapStorageIT.java b/sink-connector-lightweight/src/test/java/com/altinity/clickhouse/debezium/embedded/PostgresInitialDockerWKeeperMapStorageIT.java new file mode 100644 index 000000000..45ce26625 --- /dev/null +++ b/sink-connector-lightweight/src/test/java/com/altinity/clickhouse/debezium/embedded/PostgresInitialDockerWKeeperMapStorageIT.java @@ -0,0 +1,173 @@ +package com.altinity.clickhouse.debezium.embedded; + +import com.altinity.clickhouse.debezium.embedded.cdc.DebeziumChangeEventCapture; +import com.altinity.clickhouse.debezium.embedded.cdc.DebeziumOffsetStorage; +import com.altinity.clickhouse.debezium.embedded.ddl.parser.MySQLDDLParserService; +import com.altinity.clickhouse.debezium.embedded.parser.SourceRecordParserService; +import com.altinity.clickhouse.sink.connector.ClickHouseSinkConnectorConfig; +import com.altinity.clickhouse.sink.connector.db.BaseDbWriter; +import com.clickhouse.jdbc.ClickHouseConnection; +import org.junit.Assert; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.testcontainers.Testcontainers; +import org.testcontainers.clickhouse.ClickHouseContainer; +import org.testcontainers.containers.*; +import org.testcontainers.containers.wait.strategy.HttpWaitStrategy; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.utility.DockerImageName; + +import java.sql.ResultSet; +import java.util.HashMap; +import java.util.Map; +import java.util.Properties; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.atomic.AtomicReference; + +import static com.altinity.clickhouse.debezium.embedded.PostgresProperties.getDefaultProperties; + +public class PostgresInitialDockerWKeeperMapStorageIT { + + static ClickHouseContainer clickHouseContainer; + + static GenericContainer zookeeperContainer = new GenericContainer(DockerImageName.parse("zookeeper:3.6.2")) + .withExposedPorts(2181).withAccessToHost(true); + + + @BeforeEach + public void startContainers() throws InterruptedException { + + Network network = Network.newNetwork(); + zookeeperContainer.withNetwork(network).withNetworkAliases("zookeeper"); + zookeeperContainer.start(); + + Thread.sleep(15000); + + clickHouseContainer = new ClickHouseContainer(DockerImageName.parse("clickhouse/clickhouse-server:22.3") + .asCompatibleSubstituteFor("clickhouse")) + .withInitScript("init_clickhouse_it.sql") + .withUsername("ch_user") + .withPassword("password") + .withClasspathResourceMapping("config_replicated.xml", "/etc/clickhouse-server/config.d/config.xml", BindMode.READ_ONLY) + .withClasspathResourceMapping("macros.xml", "/etc/clickhouse-server/config.d/macros.xml", BindMode.READ_ONLY) + .withExposedPorts(8123) + .waitingFor(new HttpWaitStrategy().forPort(zookeeperContainer.getFirstMappedPort())); + clickHouseContainer.withNetwork(network).withNetworkAliases("clickhouse"); + clickHouseContainer.start(); + } + + + public static DockerImageName myImage = DockerImageName.parse("debezium/postgres:15-alpine").asCompatibleSubstituteFor("postgres"); + + @Container + public static PostgreSQLContainer postgreSQLContainer = (PostgreSQLContainer) new PostgreSQLContainer(myImage) + .withInitScript("init_postgres.sql") + .withDatabaseName("public") + .withUsername("root") + .withPassword("root") + .withExposedPorts(5432) + .withCommand("postgres -c wal_level=logical") + .withNetworkAliases("postgres").withAccessToHost(true); + + public Properties getProperties() throws Exception { + + Properties properties = getDefaultProperties(postgreSQLContainer, clickHouseContainer); + properties.put("plugin.name", "pgoutput"); + properties.put("plugin.path", "/"); + properties.put("table.include.list", "public.tm"); + properties.put("slot.max.retries", "6"); + properties.put("slot.retry.delay.ms", "5000"); + properties.put("database.allowPublicKeyRetrieval", "true"); + properties.put("table.include.list", "public.tm,public.tm2,public.redata"); + properties.put("offset.storage.jdbc.offset.table.ddl", "CREATE TABLE if not exists %s on cluster '{cluster}' (id String, offset_key String, offset_val String, record_insert_ts DateTime, record_insert_seq UInt64) ENGINE = KeeperMap('/asc_offsets201',10) PRIMARY KEY offset_key"); + properties.put("offset.storage.jdbc.offset.table.delete", "select 1"); + properties.put("skipped.operations","none"); + properties.put("disable.drop.truncate", "false"); + return properties; + } + + @Test + @DisplayName("Integration Test - Validates PostgreSQL replication when the plugin is set to DecoderBufs") + public void testDecoderBufsPlugin() throws Exception { + Network network = Network.newNetwork(); + + postgreSQLContainer.withNetwork(network).start(); + clickHouseContainer.withNetwork(network).start(); + Thread.sleep(10000); + + Testcontainers.exposeHostPorts(postgreSQLContainer.getFirstMappedPort()); + AtomicReference engine = new AtomicReference<>(); + + ExecutorService executorService = Executors.newFixedThreadPool(1); + executorService.execute(() -> { + try { + + engine.set(new DebeziumChangeEventCapture()); + engine.get().setup(getProperties(), new SourceRecordParserService(), false); + } catch (Exception e) { + throw new RuntimeException(e); + } + }); + + Thread.sleep(10000);// + Thread.sleep(50000); + + String jdbcUrl = BaseDbWriter.getConnectionString(clickHouseContainer.getHost(), clickHouseContainer.getFirstMappedPort(), + "public"); + ClickHouseConnection chConn = BaseDbWriter.createConnection(jdbcUrl, "Client_1", + clickHouseContainer.getUsername(), clickHouseContainer.getPassword(), new ClickHouseSinkConnectorConfig(new HashMap<>())); + + BaseDbWriter writer = new BaseDbWriter(clickHouseContainer.getHost(), clickHouseContainer.getFirstMappedPort(), + "public", clickHouseContainer.getUsername(), clickHouseContainer.getPassword(), null, chConn); + Map tmColumns = writer.getColumnsDataTypesForTable("tm"); + Assert.assertTrue(tmColumns.size() == 22); + Assert.assertTrue(tmColumns.get("id").equalsIgnoreCase("UUID")); + Assert.assertTrue(tmColumns.get("secid").equalsIgnoreCase("Nullable(UUID)")); + //Assert.assertTrue(tmColumns.get("am").equalsIgnoreCase("Nullable(Decimal(21,5))")); + Assert.assertTrue(tmColumns.get("created").equalsIgnoreCase("Nullable(DateTime64(6))")); + + + int tmCount = 0; + ResultSet chRs = writer.getConnection().prepareStatement("select count(*) from tm").executeQuery(); + while(chRs.next()) { + tmCount = chRs.getInt(1); + } + + // Get the columns in re_data. + Map reDataColumns = writer.getColumnsDataTypesForTable("redata"); + + Assert.assertTrue(reDataColumns.get("amount").equalsIgnoreCase("Decimal(64, 18)")); + Assert.assertTrue(reDataColumns.get("total_amount").equalsIgnoreCase("Decimal(21, 5)")); + Assert.assertTrue(tmCount == 2); + + String offsetValue = new DebeziumOffsetStorage().getDebeziumStorageStatusQuery(getProperties(), writer); + + // Parse offsetvalue json and check the keys + Assert.assertTrue(offsetValue.contains("last_snapshot_record")); + Assert.assertTrue(offsetValue.contains("lsn")); + Assert.assertTrue(offsetValue.contains("txId")); + Assert.assertTrue(offsetValue.contains("ts_usec")); + Assert.assertTrue(offsetValue.contains("snapshot")); + + // Connect to postgreSQL and issue a truncate table command. + ITCommon.connectToPostgreSQL(postgreSQLContainer).prepareStatement("truncate table public.tm").execute(); + Thread.sleep(15000); + + // Check if the clickhouse table is empty. + chRs = writer.getConnection().prepareStatement("select count(*) from tm").executeQuery(); + while(chRs.next()) { + tmCount = chRs.getInt(1); + } + + //Assert.assertTrue(tmCount == 0); + + if(engine.get() != null) { + engine.get().stop(); + } + // Files.deleteIfExists(tmpFilePath); + executorService.shutdown(); + + } +} diff --git a/sink-connector-lightweight/src/test/java/com/altinity/clickhouse/debezium/embedded/PostgresPgoutputMultipleSchemaIT.java b/sink-connector-lightweight/src/test/java/com/altinity/clickhouse/debezium/embedded/PostgresPgoutputMultipleSchemaIT.java index 87e78b7b5..2ea19ea19 100644 --- a/sink-connector-lightweight/src/test/java/com/altinity/clickhouse/debezium/embedded/PostgresPgoutputMultipleSchemaIT.java +++ b/sink-connector-lightweight/src/test/java/com/altinity/clickhouse/debezium/embedded/PostgresPgoutputMultipleSchemaIT.java @@ -65,12 +65,13 @@ public Properties getProperties() throws Exception { properties.put("slot.retry.delay.ms", "5000" ); properties.put("database.allowPublicKeyRetrieval", "true" ); properties.put("schema.include.list", "public,public2"); - properties.put("table.include.list", "public.tm,public2.tm2" ); + properties.put("table.include.list", "public.tm,public2.tm2,public.people" ); + properties.put("column.exclude.list", "public.people.full_name_mat"); return properties; } @Test - @DisplayName("Integration Test - Validates postgresql replication works with multiple schemas") + @DisplayName("Integration Test - Validates postgresql replication works with multiple schemas and ignoring ALIAS columns in ClickHouse") public void testMultipleSchemaReplication() throws Exception { Network network = Network.newNetwork(); @@ -86,8 +87,7 @@ public void testMultipleSchemaReplication() throws Exception { try { engine.set(new DebeziumChangeEventCapture()); - engine.get().setup(getProperties(), new SourceRecordParserService(), - new MySQLDDLParserService(new ClickHouseSinkConnectorConfig(new HashMap<>()), "system"), false); + engine.get().setup(getProperties(), new SourceRecordParserService(), false); } catch (Exception e) { throw new RuntimeException(e); } @@ -135,6 +135,32 @@ public void testMultipleSchemaReplication() throws Exception { } Assert.assertTrue(tm2Count == 1); + // Create a connection to postgresql and create a new table. + Connection postgresConn2 = ITCommon.connectToPostgreSQL(postgreSQLContainer); + postgresConn2.createStatement().execute("CREATE TABLE public.people( height_cm numeric PRIMARY KEY, height_in numeric GENERATED ALWAYS AS (height_cm / 2.54) STORED)"); + + Thread.sleep(10000); + // insert new records into the new table. + postgresConn2.createStatement().execute("insert into public.people (height_cm) values (180)"); + Thread.sleep(10000); + + // ClickHouse, add ALIAS column to public.people + conn.createStatement().execute("ALTER TABLE public.people ADD COLUMN full_name String ALIAS concat('John', ' ', 'Doe');"); + Thread.sleep(10000); + + // Add MATERIALIZED column to public.people + conn.createStatement().execute("ALTER TABLE public.people ADD COLUMN full_name_mat String MATERIALIZED toString(height_cm)"); + postgresConn2.createStatement().execute("insert into public.people (height_cm) values (200)"); + Thread.sleep(20000); + + // Check if public.people has 2 records. + int peopleCount = 0; + ResultSet chRs3 = writer.getConnection().prepareStatement("select count(*) from public.people").executeQuery(); + while(chRs3.next()) { + peopleCount = chRs3.getInt(1); + } + Assert.assertTrue(peopleCount == 2); + if(engine.get() != null) { engine.get().stop(); } diff --git a/sink-connector-lightweight/src/test/java/com/altinity/clickhouse/debezium/embedded/ReplicatedRMTClickHouse22TIT.java b/sink-connector-lightweight/src/test/java/com/altinity/clickhouse/debezium/embedded/ReplicatedRMTClickHouse22TIT.java index 08ea89ba8..70fae4413 100644 --- a/sink-connector-lightweight/src/test/java/com/altinity/clickhouse/debezium/embedded/ReplicatedRMTClickHouse22TIT.java +++ b/sink-connector-lightweight/src/test/java/com/altinity/clickhouse/debezium/embedded/ReplicatedRMTClickHouse22TIT.java @@ -91,8 +91,7 @@ public void testReplicatedRMTAutoCreate(String clickHouseServerVersion) throws E try { engine.set(new DebeziumChangeEventCapture()); - engine.get().setup(props, new SourceRecordParserService(), - new MySQLDDLParserService(new ClickHouseSinkConnectorConfig(new HashMap<>()), "employees"), false); + engine.get().setup(props, new SourceRecordParserService(), false); } catch (Exception e) { throw new RuntimeException(e); } diff --git a/sink-connector-lightweight/src/test/java/com/altinity/clickhouse/debezium/embedded/ReplicatedRMTDDLClickHouse22TIT.java b/sink-connector-lightweight/src/test/java/com/altinity/clickhouse/debezium/embedded/ReplicatedRMTDDLClickHouse22TIT.java index 21e839d84..2ec6a53b1 100644 --- a/sink-connector-lightweight/src/test/java/com/altinity/clickhouse/debezium/embedded/ReplicatedRMTDDLClickHouse22TIT.java +++ b/sink-connector-lightweight/src/test/java/com/altinity/clickhouse/debezium/embedded/ReplicatedRMTDDLClickHouse22TIT.java @@ -93,8 +93,7 @@ public void testReplicatedRMTAutoCreate(String clickHouseServerVersion) throws E try { engine.set(new DebeziumChangeEventCapture()); - engine.get().setup(props, new SourceRecordParserService(), - new MySQLDDLParserService(new ClickHouseSinkConnectorConfig(new HashMap<>()), "employees"), false); + engine.get().setup(props, new SourceRecordParserService(), false); } catch (Exception e) { throw new RuntimeException(e); } diff --git a/sink-connector-lightweight/src/test/java/com/altinity/clickhouse/debezium/embedded/ReplicatedRMTDDLIT.java b/sink-connector-lightweight/src/test/java/com/altinity/clickhouse/debezium/embedded/ReplicatedRMTDDLIT.java index fc5278dd8..f0fe12e1b 100644 --- a/sink-connector-lightweight/src/test/java/com/altinity/clickhouse/debezium/embedded/ReplicatedRMTDDLIT.java +++ b/sink-connector-lightweight/src/test/java/com/altinity/clickhouse/debezium/embedded/ReplicatedRMTDDLIT.java @@ -96,8 +96,7 @@ public void testReplicatedRMTAutoCreate(String clickHouseServerVersion) throws E try { engine.set(new DebeziumChangeEventCapture()); - engine.get().setup(props, new SourceRecordParserService(), - new MySQLDDLParserService(new ClickHouseSinkConnectorConfig(new HashMap<>()), "employees"), false); + engine.get().setup(props, new SourceRecordParserService(), false); } catch (Exception e) { throw new RuntimeException(e); } diff --git a/sink-connector-lightweight/src/test/java/com/altinity/clickhouse/debezium/embedded/ReplicatedRMTIT.java b/sink-connector-lightweight/src/test/java/com/altinity/clickhouse/debezium/embedded/ReplicatedRMTIT.java index 65a385561..6cb2c1e9c 100644 --- a/sink-connector-lightweight/src/test/java/com/altinity/clickhouse/debezium/embedded/ReplicatedRMTIT.java +++ b/sink-connector-lightweight/src/test/java/com/altinity/clickhouse/debezium/embedded/ReplicatedRMTIT.java @@ -95,9 +95,7 @@ public void testReplicatedRMTAutoCreate(String clickHouseServerVersion) throws E try { engine.set(new DebeziumChangeEventCapture()); - engine.get().setup(props, new SourceRecordParserService(), - new MySQLDDLParserService(new ClickHouseSinkConnectorConfig(new HashMap<>()), - "employees"), false); + engine.get().setup(props, new SourceRecordParserService(), false); } catch (Exception e) { throw new RuntimeException(e); } diff --git a/sink-connector-lightweight/src/test/java/com/altinity/clickhouse/debezium/embedded/api/DebeziumEmbeddedRestApiIT.java b/sink-connector-lightweight/src/test/java/com/altinity/clickhouse/debezium/embedded/api/DebeziumEmbeddedRestApiIT.java new file mode 100644 index 000000000..7a1e76ab1 --- /dev/null +++ b/sink-connector-lightweight/src/test/java/com/altinity/clickhouse/debezium/embedded/api/DebeziumEmbeddedRestApiIT.java @@ -0,0 +1,144 @@ +package com.altinity.clickhouse.debezium.embedded.api; + +import com.altinity.clickhouse.debezium.embedded.AppInjector; +import com.altinity.clickhouse.debezium.embedded.ITCommon; +import com.altinity.clickhouse.debezium.embedded.cdc.DebeziumChangeEventCapture; +import com.altinity.clickhouse.debezium.embedded.common.PropertiesHelper; +import com.altinity.clickhouse.debezium.embedded.ddl.parser.MySQLDDLParserService; +import com.altinity.clickhouse.debezium.embedded.parser.SourceRecordParserService; +import com.altinity.clickhouse.sink.connector.ClickHouseSinkConnectorConfig; +import com.clickhouse.client.internal.apache.hc.client5.http.classic.methods.HttpGet; +import com.clickhouse.client.internal.apache.hc.client5.http.classic.methods.HttpUriRequest; +import com.clickhouse.client.internal.apache.hc.client5.http.impl.classic.HttpClientBuilder; +import com.clickhouse.client.internal.apache.hc.core5.http.HttpResponse; +import com.google.inject.Guice; +import io.javalin.http.HttpStatus; +import org.apache.log4j.BasicConfigurator; +import org.junit.Assert; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.testcontainers.clickhouse.ClickHouseContainer; +import org.testcontainers.containers.MySQLContainer; +import org.testcontainers.containers.wait.strategy.HttpWaitStrategy; +import org.testcontainers.utility.DockerImageName; + +import java.sql.Connection; +import java.util.HashMap; +import java.util.Properties; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.atomic.AtomicReference; +import io.javalin.testtools.JavalinTest; + + +@Disabled +public class DebeziumEmbeddedRestApiIT { + + protected MySQLContainer mySqlContainer; + static ClickHouseContainer clickHouseContainer; + + @BeforeEach + public void startContainers() throws InterruptedException { + mySqlContainer = new MySQLContainer<>(DockerImageName.parse("docker.io/bitnami/mysql:8.0.36") + .asCompatibleSubstituteFor("mysql")) + .withDatabaseName("employees").withUsername("root").withPassword("adminpass") + // .withInitScript("data_types.sql") + .withExtraHost("mysql-server", "0.0.0.0") + .waitingFor(new HttpWaitStrategy().forPort(3306)); + + BasicConfigurator.configure(); + mySqlContainer.start(); + // clickHouseContainer.start(); + Thread.sleep(15000); + } + + @AfterEach + public void stopContainers() { + if(mySqlContainer != null && mySqlContainer.isRunning()) { + mySqlContainer.stop();; + } + if(clickHouseContainer != null && clickHouseContainer.isRunning()) { + clickHouseContainer.stop(); + } + + } + + static { + clickHouseContainer = new org.testcontainers.clickhouse.ClickHouseContainer(DockerImageName.parse("clickhouse/clickhouse-server:latest") + .asCompatibleSubstituteFor("clickhouse")) + .withInitScript("init_clickhouse_it.sql") + .withUsername("ch_user") + .withPassword("password") + .withExposedPorts(8123); + + clickHouseContainer.start(); + } + @ParameterizedTest + @CsvSource({ + "clickhouse/clickhouse-server:latest" + }) + @DisplayName("Test that validates that the REST API is working.") + public void testRESTAPI(String clickHouseServerVersion) throws Exception { + + Thread.sleep(5000); + + Connection conn = ITCommon.connectToMySQL(mySqlContainer); + conn.prepareStatement("create table `newtable`(col1 varchar(255), col2 int, col3 int)").execute(); + + Thread.sleep(10000); + + + AtomicReference engine = new AtomicReference<>(); + ExecutorService executorService = Executors.newFixedThreadPool(1); + Properties props = ITCommon.getDebeziumPropertiesForSchemaOnly(mySqlContainer, clickHouseContainer); + + executorService.execute(() -> { + try { + + engine.set(new DebeziumChangeEventCapture()); + engine.get().setup(ITCommon.getDebeziumPropertiesForSchemaOnly(mySqlContainer, clickHouseContainer), new SourceRecordParserService() + ,false); + } catch (Exception e) { + throw new RuntimeException(e); + } + }); + + + try { + DebeziumEmbeddedRestApi.startRestApi(props, Guice.createInjector(new AppInjector()), engine.get(), props); + } catch(Exception e) { + System.out.println("Error starting REST API" + e.toString()); + } + + Thread.sleep(10000); + conn.prepareStatement("insert into `newtable` values('test', 1, 2)").execute(); + conn.close(); + + Thread.sleep(10000); + + DebeziumChangeEventCapture dec = engine.get(); + long getStoredRecordTs = dec.getLatestRecordTimestamp(new ClickHouseSinkConnectorConfig(PropertiesHelper.toMap(props)), props); + + Assert.assertTrue(getStoredRecordTs > 0); + + // Given + HttpUriRequest request = new HttpGet( "http://localhost:7000/status" ); + + // When + HttpResponse httpResponse = HttpClientBuilder.create().build().execute( request ); + + // Then + Assert.assertTrue(httpResponse.getCode() == HttpStatus.OK.getCode()); + + if(engine.get() != null) { + engine.get().stop(); + } + // Files.deleteIfExists(tmpFilePath); + executorService.shutdown(); + + } +} diff --git a/sink-connector-lightweight/src/test/java/com/altinity/clickhouse/debezium/embedded/cdc/BatchRetryOnFailureIT.java b/sink-connector-lightweight/src/test/java/com/altinity/clickhouse/debezium/embedded/cdc/BatchRetryOnFailureIT.java index ce1aacd5f..9e729d3ce 100644 --- a/sink-connector-lightweight/src/test/java/com/altinity/clickhouse/debezium/embedded/cdc/BatchRetryOnFailureIT.java +++ b/sink-connector-lightweight/src/test/java/com/altinity/clickhouse/debezium/embedded/cdc/BatchRetryOnFailureIT.java @@ -14,6 +14,7 @@ import org.apache.log4j.BasicConfigurator; import org.junit.Assert; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.testcontainers.clickhouse.ClickHouseContainer; @@ -32,6 +33,7 @@ import static com.altinity.clickhouse.debezium.embedded.ITCommon.getDebeziumProperties; +@Disabled @Testcontainers @DisplayName("Test that validates if the sink connector retries batches on ClickHouse failure") public class BatchRetryOnFailureIT { @@ -73,8 +75,7 @@ public void testBatchRetryOnCHFailure() throws Exception { ExecutorService executorService = Executors.newFixedThreadPool(1); executorService.execute(() -> { try { - clickHouseDebeziumEmbeddedApplication.start(injector.getInstance(DebeziumRecordParserService.class), - injector.getInstance(DDLParserService.class), props, false); + clickHouseDebeziumEmbeddedApplication.start(injector.getInstance(DebeziumRecordParserService.class) , props, false); DebeziumEmbeddedRestApi.startRestApi(props, injector, clickHouseDebeziumEmbeddedApplication.getDebeziumEventCapture() , new Properties()); } catch (Exception e) { diff --git a/sink-connector-lightweight/src/test/java/com/altinity/clickhouse/debezium/embedded/cdc/ClickHouseDelayedStartIT.java b/sink-connector-lightweight/src/test/java/com/altinity/clickhouse/debezium/embedded/cdc/ClickHouseDelayedStartIT.java index bdc18d15d..d0d1e7105 100644 --- a/sink-connector-lightweight/src/test/java/com/altinity/clickhouse/debezium/embedded/cdc/ClickHouseDelayedStartIT.java +++ b/sink-connector-lightweight/src/test/java/com/altinity/clickhouse/debezium/embedded/cdc/ClickHouseDelayedStartIT.java @@ -76,8 +76,7 @@ public void testClickHouseDelayedStart() throws Exception { ExecutorService executorService = Executors.newFixedThreadPool(1); executorService.execute(() -> { try { - clickHouseDebeziumEmbeddedApplication.start(injector.getInstance(DebeziumRecordParserService.class), - injector.getInstance(DDLParserService.class), props, false); + clickHouseDebeziumEmbeddedApplication.start(injector.getInstance(DebeziumRecordParserService.class), props, false); DebeziumEmbeddedRestApi.startRestApi(props, injector, clickHouseDebeziumEmbeddedApplication.getDebeziumEventCapture() , new Properties()); } catch (Exception e) { @@ -136,8 +135,7 @@ public void debeziumStorageView() throws Exception { ExecutorService executorService = Executors.newFixedThreadPool(1); executorService.execute(() -> { try { - clickHouseDebeziumEmbeddedApplication.start(injector.getInstance(DebeziumRecordParserService.class), - injector.getInstance(DDLParserService.class), props, false); + clickHouseDebeziumEmbeddedApplication.start(injector.getInstance(DebeziumRecordParserService.class), props, false); DebeziumEmbeddedRestApi.startRestApi(props, injector, clickHouseDebeziumEmbeddedApplication.getDebeziumEventCapture() , new Properties()); } catch (Exception e) { diff --git a/sink-connector-lightweight/src/test/java/com/altinity/clickhouse/debezium/embedded/cdc/DatabaseOverrideIT.java b/sink-connector-lightweight/src/test/java/com/altinity/clickhouse/debezium/embedded/cdc/DatabaseOverrideIT.java index c711c9ff0..5ab95eb1b 100644 --- a/sink-connector-lightweight/src/test/java/com/altinity/clickhouse/debezium/embedded/cdc/DatabaseOverrideIT.java +++ b/sink-connector-lightweight/src/test/java/com/altinity/clickhouse/debezium/embedded/cdc/DatabaseOverrideIT.java @@ -86,8 +86,7 @@ public void testDatabaseOverride() throws Exception { ExecutorService executorService = Executors.newFixedThreadPool(1); executorService.execute(() -> { try { - clickHouseDebeziumEmbeddedApplication.start(injector.getInstance(DebeziumRecordParserService.class), - injector.getInstance(DDLParserService.class), props, false); + clickHouseDebeziumEmbeddedApplication.start(injector.getInstance(DebeziumRecordParserService.class), props, false); DebeziumEmbeddedRestApi.startRestApi(props, injector, clickHouseDebeziumEmbeddedApplication.getDebeziumEventCapture() , new Properties()); } catch (Exception e) { @@ -114,6 +113,7 @@ public void testDatabaseOverride() throws Exception { conn.prepareStatement("create table customers.custtable(col1 varchar(255) not null, col2 int, col3 int, primary key(col1))").execute(); conn.prepareStatement("insert into customers.custtable values('a', 1, 1)").execute(); + Thread.sleep(10000); // Validate in Clickhouse the last record written is 29999 @@ -148,6 +148,9 @@ public void testDatabaseOverride() throws Exception { assertTrue(customersCol2 == 1); + Thread.sleep(10000); + // Execute the query in MySQL to rename table. + clickHouseDebeziumEmbeddedApplication.getDebeziumEventCapture().engine.close(); diff --git a/sink-connector-lightweight/src/test/java/com/altinity/clickhouse/debezium/embedded/cdc/DatabaseOverrideInitialIT.java b/sink-connector-lightweight/src/test/java/com/altinity/clickhouse/debezium/embedded/cdc/DatabaseOverrideInitialIT.java new file mode 100644 index 000000000..465268279 --- /dev/null +++ b/sink-connector-lightweight/src/test/java/com/altinity/clickhouse/debezium/embedded/cdc/DatabaseOverrideInitialIT.java @@ -0,0 +1,140 @@ +package com.altinity.clickhouse.debezium.embedded.cdc; + +import com.altinity.clickhouse.debezium.embedded.AppInjector; +import com.altinity.clickhouse.debezium.embedded.ClickHouseDebeziumEmbeddedApplication; +import com.altinity.clickhouse.debezium.embedded.ITCommon; +import com.altinity.clickhouse.debezium.embedded.api.DebeziumEmbeddedRestApi; +import com.altinity.clickhouse.debezium.embedded.ddl.parser.DDLParserService; +import com.altinity.clickhouse.debezium.embedded.parser.DebeziumRecordParserService; +import com.altinity.clickhouse.sink.connector.ClickHouseSinkConnectorConfig; +import com.altinity.clickhouse.sink.connector.db.BaseDbWriter; +import com.clickhouse.jdbc.ClickHouseConnection; +import com.google.inject.Guice; +import com.google.inject.Injector; +import org.apache.log4j.BasicConfigurator; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.testcontainers.clickhouse.ClickHouseContainer; +import org.testcontainers.containers.MySQLContainer; +import org.testcontainers.containers.wait.strategy.HttpWaitStrategy; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.utility.DockerImageName; + +import java.sql.Connection; +import java.sql.ResultSet; +import java.util.HashMap; +import java.util.Properties; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +import static com.altinity.clickhouse.debezium.embedded.ITCommon.getDebeziumProperties; +import static org.junit.Assert.assertTrue; + +public class DatabaseOverrideInitialIT { + + private static final Logger log = LoggerFactory.getLogger(DatabaseOverrideInitialIT.class); + + + protected MySQLContainer mySqlContainer; + + @Container + public static ClickHouseContainer clickHouseContainer = new ClickHouseContainer(DockerImageName.parse("clickhouse/clickhouse-server:latest") + .asCompatibleSubstituteFor("clickhouse")) + //.withInitScript("init_database_override_initial.sql") + // .withCopyFileToContainer(MountableFile.forClasspathResource("config.xml"), "/etc/clickhouse-server/config.d/config.xml") + .withUsername("ch_user") + .withPassword("password") + .withExposedPorts(8123); + + @BeforeEach + public void startContainers() throws InterruptedException { + mySqlContainer = new MySQLContainer<>(DockerImageName.parse("docker.io/bitnami/mysql:8.0.36") + .asCompatibleSubstituteFor("mysql")) + .withDatabaseName("employees").withUsername("root").withPassword("adminpass") + .withInitScript("mysql_database_override_initial.sql") +// .withInitScript("15k_tables_mysql.sql") + .withExtraHost("mysql-server", "0.0.0.0") + .waitingFor(new HttpWaitStrategy().forPort(3306)); + + BasicConfigurator.configure(); + mySqlContainer.start(); + clickHouseContainer.start(); + Thread.sleep(35000); + } + + + @DisplayName("Test that validates overriding database name in ClickHouse") + @Test + public void testDatabaseOverride() throws Exception { + + Injector injector = Guice.createInjector(new AppInjector()); + + Properties props = getDebeziumProperties(mySqlContainer, clickHouseContainer); + props.setProperty("snapshot.mode", "initial"); + props.setProperty("schema.history.internal.store.only.captured.tables.ddl", "true"); + props.setProperty("schema.history.internal.store.only.captured.databases.ddl", "true"); + props.setProperty("clickhouse.database.override.map", "employees:employees2, products:productsnew"); + props.setProperty("database.include.list", "employees, products, customers"); + + // Override clickhouse server timezone. + ClickHouseDebeziumEmbeddedApplication clickHouseDebeziumEmbeddedApplication = new ClickHouseDebeziumEmbeddedApplication(); + + + ExecutorService executorService = Executors.newFixedThreadPool(1); + executorService.execute(() -> { + try { + clickHouseDebeziumEmbeddedApplication.start(injector.getInstance(DebeziumRecordParserService.class), props, false); + DebeziumEmbeddedRestApi.startRestApi(props, injector, clickHouseDebeziumEmbeddedApplication.getDebeziumEventCapture() + , new Properties()); + } catch (Exception e) { + throw new RuntimeException(e); + } + + }); + + Thread.sleep(25000); + + Thread.sleep(10000); + + // Validate in Clickhouse the last record written is 29999 + String jdbcUrl = BaseDbWriter.getConnectionString(clickHouseContainer.getHost(), clickHouseContainer.getFirstMappedPort(), + "system"); + ClickHouseConnection chConn = BaseDbWriter.createConnection(jdbcUrl, "Client_1", + clickHouseContainer.getUsername(), clickHouseContainer.getPassword(), new ClickHouseSinkConnectorConfig(new HashMap<>())); + BaseDbWriter writer = new BaseDbWriter(clickHouseContainer.getHost(), clickHouseContainer.getFirstMappedPort(), + "employees", clickHouseContainer.getUsername(), clickHouseContainer.getPassword(), null, chConn); + + long col2 = 0L; + ResultSet version1Result = writer.executeQueryWithResultSet("select col2 from employees2.newtable final where col1 = 'a'"); + while(version1Result.next()) { + col2 = version1Result.getLong("col2"); + } + Thread.sleep(10000); + assertTrue(col2 == 1); + + long productsCol2 = 0L; + ResultSet productsVersionResult = writer.executeQueryWithResultSet("select col2 from productsnew.prodtable final where col1 = 'a'"); + while(productsVersionResult.next()) { + productsCol2 = productsVersionResult.getLong("col2"); + } + assertTrue(productsCol2 == 1); + Thread.sleep(10000); + + long customersCol2 = 0L; + ResultSet customersVersionResult = writer.executeQueryWithResultSet("select col2 from customers.custtable final where col1 = 'a'"); + while(customersVersionResult.next()) { + customersCol2 = customersVersionResult.getLong("col2"); + } + assertTrue(customersCol2 == 1); + + + + clickHouseDebeziumEmbeddedApplication.getDebeziumEventCapture().engine.close(); + + // Files.deleteIfExists(tmpFilePath); + executorService.shutdown(); + } +} diff --git a/sink-connector-lightweight/src/test/java/com/altinity/clickhouse/debezium/embedded/cdc/DatabaseOverrideRRMTIT.java b/sink-connector-lightweight/src/test/java/com/altinity/clickhouse/debezium/embedded/cdc/DatabaseOverrideRRMTIT.java new file mode 100644 index 000000000..182429f34 --- /dev/null +++ b/sink-connector-lightweight/src/test/java/com/altinity/clickhouse/debezium/embedded/cdc/DatabaseOverrideRRMTIT.java @@ -0,0 +1,215 @@ +package com.altinity.clickhouse.debezium.embedded.cdc; + +import com.altinity.clickhouse.debezium.embedded.AppInjector; +import com.altinity.clickhouse.debezium.embedded.ClickHouseDebeziumEmbeddedApplication; +import com.altinity.clickhouse.debezium.embedded.ITCommon; +import com.altinity.clickhouse.debezium.embedded.api.DebeziumEmbeddedRestApi; +import com.altinity.clickhouse.debezium.embedded.parser.DebeziumRecordParserService; +import com.altinity.clickhouse.sink.connector.ClickHouseSinkConnectorConfig; +import com.altinity.clickhouse.sink.connector.ClickHouseSinkConnectorConfigVariables; +import com.altinity.clickhouse.sink.connector.db.BaseDbWriter; +import com.clickhouse.jdbc.ClickHouseConnection; +import com.google.inject.Guice; +import com.google.inject.Injector; +import org.apache.log4j.BasicConfigurator; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.testcontainers.clickhouse.ClickHouseContainer; +import org.testcontainers.containers.BindMode; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.MySQLContainer; +import org.testcontainers.containers.Network; +import org.testcontainers.containers.wait.strategy.HttpWaitStrategy; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.utility.DockerImageName; + +import java.sql.Connection; +import java.sql.ResultSet; +import java.util.HashMap; +import java.util.Properties; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +import static com.altinity.clickhouse.debezium.embedded.ITCommon.getDebeziumProperties; +import static org.junit.Assert.assertTrue; + +public class DatabaseOverrideRRMTIT { + + private static final Logger log = LoggerFactory.getLogger(DatabaseOverrideRRMTIT.class); + + + protected MySQLContainer mySqlContainer; + static ClickHouseContainer clickHouseContainer; + + static GenericContainer zookeeperContainer = new GenericContainer(DockerImageName.parse("zookeeper:3.6.2")) + .withExposedPorts(2181).withAccessToHost(true); + + @BeforeEach + public void startContainers() throws InterruptedException { + + Network network = Network.newNetwork(); + zookeeperContainer.withNetwork(network).withNetworkAliases("zookeeper"); + zookeeperContainer.start(); + mySqlContainer = new MySQLContainer<>(DockerImageName.parse("docker.io/bitnami/mysql:8.0.36") + .asCompatibleSubstituteFor("mysql")) + .withDatabaseName("employees").withUsername("root").withPassword("adminpass") +// .withInitScript("15k_tables_mysql.sql") + .withExtraHost("mysql-server", "0.0.0.0") + .waitingFor(new HttpWaitStrategy().forPort(3306)); + + clickHouseContainer = new ClickHouseContainer(DockerImageName.parse("clickhouse/clickhouse-server:latest") + .asCompatibleSubstituteFor("clickhouse")) + .withInitScript("init_clickhouse_schema_only_column_timezone.sql") + // .withCopyFileToContainer(MountableFile.forClasspathResource("config.xml"), "/etc/clickhouse-server/config.d/config.xml") + .withUsername("ch_user") + .withPassword("password") + .withClasspathResourceMapping("config_replicated.xml", "/etc/clickhouse-server/config.d/config.xml", BindMode.READ_ONLY) + .withClasspathResourceMapping("macros.xml", "/etc/clickhouse-server/config.d/macros.xml", BindMode.READ_ONLY) + .withExposedPorts(8123) + .waitingFor(new HttpWaitStrategy().forPort(zookeeperContainer.getFirstMappedPort())); + clickHouseContainer.withNetwork(network).withNetworkAliases("clickhouse"); + clickHouseContainer.start(); + + BasicConfigurator.configure(); + mySqlContainer.start(); + clickHouseContainer.start(); + Thread.sleep(35000); + } + + + @DisplayName("Test that validates overriding database name in ClickHouse for ReplicatedReplacingMergeTree(RRMT)") + @Test + public void testDatabaseOverride() throws Exception { + + String jdbcUrl = BaseDbWriter.getConnectionString(clickHouseContainer.getHost(), clickHouseContainer.getFirstMappedPort(), + "system"); + ClickHouseConnection chConn = BaseDbWriter.createConnection(jdbcUrl, "Client_1", + clickHouseContainer.getUsername(), clickHouseContainer.getPassword(), new ClickHouseSinkConnectorConfig(new HashMap<>())); + BaseDbWriter writer = new BaseDbWriter(clickHouseContainer.getHost(), clickHouseContainer.getFirstMappedPort(), + "employees", clickHouseContainer.getUsername(), clickHouseContainer.getPassword(), null, chConn); + + writer.executeQuery("CREATE DATABASE employees2"); + writer.executeQuery("CREATE DATABASE productsnew"); + + Thread.sleep(10000); + Injector injector = Guice.createInjector(new AppInjector()); + + Properties props = getDebeziumProperties(mySqlContainer, clickHouseContainer); + props.setProperty("snapshot.mode", "schema_only"); + props.setProperty("schema.history.internal.store.only.captured.tables.ddl", "true"); + props.setProperty("schema.history.internal.store.only.captured.databases.ddl", "true"); + props.setProperty("clickhouse.database.override.map", "employees:employees2, products:productsnew"); + props.setProperty("database.include.list", "employees, products, customers"); + props.setProperty(ClickHouseSinkConnectorConfigVariables.AUTO_CREATE_TABLES_REPLICATED.toString(), "true"); + props.setProperty(ClickHouseSinkConnectorConfigVariables.AUTO_CREATE_TABLES.toString(), "true"); + props.setProperty("ddl.retry", "true"); + // Override clickhouse server timezone. + ClickHouseDebeziumEmbeddedApplication clickHouseDebeziumEmbeddedApplication = new ClickHouseDebeziumEmbeddedApplication(); + + + ExecutorService executorService = Executors.newFixedThreadPool(1); + executorService.execute(() -> { + try { + clickHouseDebeziumEmbeddedApplication.start(injector.getInstance(DebeziumRecordParserService.class), props, false); + DebeziumEmbeddedRestApi.startRestApi(props, injector, clickHouseDebeziumEmbeddedApplication.getDebeziumEventCapture() + , new Properties()); + } catch (Exception e) { + throw new RuntimeException(e); + } + + }); + + Thread.sleep(25000); + + // Employees table + Connection conn = ITCommon.connectToMySQL(mySqlContainer); + conn.prepareStatement("create table `newtable`(col1 varchar(255) not null, col2 int, col3 int, primary key(col1))").execute(); + + // Insert a new row in the table + conn.prepareStatement("insert into newtable values('a', 1, 1)").execute(); + + + conn.prepareStatement("create database products").execute(); + conn.prepareStatement("create table products.prodtable(col1 varchar(255) not null, col2 int, col3 int, primary key(col1))").execute(); + conn.prepareStatement("insert into products.prodtable values('a', 1, 1)").execute(); + + conn.prepareStatement("create database customers").execute(); + conn.prepareStatement("create table customers.custtable(col1 varchar(255) not null, col2 int, col3 int, primary key(col1))").execute(); + conn.prepareStatement("insert into customers.custtable values('a', 1, 1)").execute(); + + + Thread.sleep(10000); + + // Validate in Clickhouse the last record written is 29999 + + + long col2 = 0L; + ResultSet version1Result = writer.executeQueryWithResultSet("select col2 from employees2.newtable final where col1 = 'a'"); + while(version1Result.next()) { + col2 = version1Result.getLong("col2"); + } + Thread.sleep(10000); + assertTrue(col2 == 1); + + long productsCol2 = 0L; + ResultSet productsVersionResult = writer.executeQueryWithResultSet("select col2 from productsnew.prodtable final where col1 = 'a'"); + while(productsVersionResult.next()) { + productsCol2 = productsVersionResult.getLong("col2"); + } + assertTrue(productsCol2 == 1); + Thread.sleep(10000); + + long customersCol2 = 0L; + ResultSet customersVersionResult = writer.executeQueryWithResultSet("select col2 from customers.custtable final where col1 = 'a'"); + while(customersVersionResult.next()) { + customersCol2 = customersVersionResult.getLong("col2"); + } + assertTrue(customersCol2 == 1); + + + Thread.sleep(10000); + // Execute the query in MySQL to rename table. + conn.prepareStatement("use products").execute(); + conn.prepareStatement("rename table prodtable to prodtable2").execute(); + Thread.sleep(10000); +// ResultSet customersVersionResult2 = writer.executeQueryWithResultSet("select col2 from customers.custtable2 final where col1 = 'a'"); +// while(customersVersionResult2.next()) { +// customersCol2 = customersVersionResult2.getLong("col2"); +// } +// assertTrue(customersCol2 == 2); + + // validate that the table prodtaable2 is present in clickhouse + ResultSet chRs = writer.executeQueryWithResultSet("select * from productsnew.prodtable2"); + boolean recordFound = false; + while(chRs.next()) { + recordFound = true; + assert chRs.getString("col1").equalsIgnoreCase("a"); + //assert rs.getString("name").equalsIgnoreCase("test"); + } + + assertTrue(recordFound); + + + // Execute mysql to rename from prodtabl2 to prodtable3 without database prefix. + conn.prepareStatement("rename table prodtable2 to prodtable3").execute(); + + Thread.sleep(10000); + // Validate on CH that the table prodtable3 is present. + chRs = writer.executeQueryWithResultSet("select * from productsnew.prodtable3"); + boolean prod3RecordFound = false; + while(chRs.next()) { + prod3RecordFound = true; + assert chRs.getString("col1").equalsIgnoreCase("a"); + //assert rs.getString("name").equalsIgnoreCase("test"); + } + assertTrue(prod3RecordFound); + clickHouseDebeziumEmbeddedApplication.getDebeziumEventCapture().engine.close(); + + conn.close(); + // Files.deleteIfExists( tmpFilePath); + executorService.shutdown(); + } +} diff --git a/sink-connector-lightweight/src/test/java/com/altinity/clickhouse/debezium/embedded/cdc/Debezium15KTablesLoadIT.java b/sink-connector-lightweight/src/test/java/com/altinity/clickhouse/debezium/embedded/cdc/Debezium15KTablesLoadIT.java index 6e5948729..0fffe2464 100644 --- a/sink-connector-lightweight/src/test/java/com/altinity/clickhouse/debezium/embedded/cdc/Debezium15KTablesLoadIT.java +++ b/sink-connector-lightweight/src/test/java/com/altinity/clickhouse/debezium/embedded/cdc/Debezium15KTablesLoadIT.java @@ -78,8 +78,7 @@ public void testLoadingTablesInSchemaOnlyMode() throws Exception { ExecutorService executorService = Executors.newFixedThreadPool(1); executorService.execute(() -> { try { - clickHouseDebeziumEmbeddedApplication.start(injector.getInstance(DebeziumRecordParserService.class), - injector.getInstance(DDLParserService.class), props, false); + clickHouseDebeziumEmbeddedApplication.start(injector.getInstance(DebeziumRecordParserService.class), props, false); DebeziumEmbeddedRestApi.startRestApi(props, injector, clickHouseDebeziumEmbeddedApplication.getDebeziumEventCapture() , new Properties()); } catch (Exception e) { diff --git a/sink-connector-lightweight/src/test/java/com/altinity/clickhouse/debezium/embedded/cdc/DebeziumChangeEventCaptureIT.java b/sink-connector-lightweight/src/test/java/com/altinity/clickhouse/debezium/embedded/cdc/DebeziumChangeEventCaptureIT.java index c8a5bf35c..f3c8f2853 100644 --- a/sink-connector-lightweight/src/test/java/com/altinity/clickhouse/debezium/embedded/cdc/DebeziumChangeEventCaptureIT.java +++ b/sink-connector-lightweight/src/test/java/com/altinity/clickhouse/debezium/embedded/cdc/DebeziumChangeEventCaptureIT.java @@ -48,6 +48,7 @@ public class DebeziumChangeEventCaptureIT{ public void testDeleteOffsetStorageRow2() { //System.out.println("Delete offset"); DebeziumChangeEventCapture dec = new DebeziumChangeEventCapture(); + try { Properties props = getDebeziumProperties(mySqlContainer, clickHouseContainer); props.setProperty("name", "altinity_sink_connector"); @@ -122,8 +123,7 @@ public void testIncrementingSequenceNumbers() throws Exception { ExecutorService executorService = Executors.newFixedThreadPool(1); executorService.execute(() -> { try { - clickHouseDebeziumEmbeddedApplication.start(injector.getInstance(DebeziumRecordParserService.class), - injector.getInstance(DDLParserService.class), props, false); + clickHouseDebeziumEmbeddedApplication.start(injector.getInstance(DebeziumRecordParserService.class) , props, false); DebeziumEmbeddedRestApi.startRestApi(props, injector, clickHouseDebeziumEmbeddedApplication.getDebeziumEventCapture() , new Properties()); } catch (Exception e) { diff --git a/sink-connector-lightweight/src/test/java/com/altinity/clickhouse/debezium/embedded/cdc/DebeziumChangeEventCaptureTest.java b/sink-connector-lightweight/src/test/java/com/altinity/clickhouse/debezium/embedded/cdc/DebeziumChangeEventCaptureTest.java index b25fdba68..1c26bce8f 100644 --- a/sink-connector-lightweight/src/test/java/com/altinity/clickhouse/debezium/embedded/cdc/DebeziumChangeEventCaptureTest.java +++ b/sink-connector-lightweight/src/test/java/com/altinity/clickhouse/debezium/embedded/cdc/DebeziumChangeEventCaptureTest.java @@ -28,13 +28,17 @@ public void testUpdateBingLogInformation() throws ParseException { } @Test - @DisplayName("Unit test to check if the LSN record is updated properly") + @DisplayName("Unit test to check if the LSN record is updated properly when provided in string format and long format") public void testUpdateLsn() throws ParseException { String record = "{\"transaction_id\":null,\"lsn_proc\":27485360,\"messageType\":\"UPDATE\",\"lsn\":27485360,\"txId\":743,\"ts_usec\":1687876724804733}"; - String updatedRecord = new DebeziumOffsetStorage().updateLsnInformation(record, 1232323L); + String updatedRecord = new DebeziumOffsetStorage().updateLsnInformation(record, "0/1A38FA0"); + + assertTrue(updatedRecord.equalsIgnoreCase("{\"transaction_id\":null,\"lsn_proc\":27496352,\"messageType\":\"UPDATE\",\"lsn\":27496352,\"txId\":743,\"ts_usec\":1687876724804733}")); + + String updatedRecordLong = new DebeziumOffsetStorage().updateLsnInformation(record, "27496352"); + assertTrue(updatedRecordLong.equalsIgnoreCase("{\"transaction_id\":null,\"lsn_proc\":27496352,\"messageType\":\"UPDATE\",\"lsn\":27496352,\"txId\":743,\"ts_usec\":1687876724804733}")); - assertTrue(updatedRecord.equalsIgnoreCase("{\"transaction_id\":null,\"lsn_proc\":1232323,\"messageType\":\"UPDATE\",\"lsn\":1232323,\"txId\":743,\"ts_usec\":1687876724804733}")); } diff --git a/sink-connector-lightweight/src/test/java/com/altinity/clickhouse/debezium/embedded/cdc/DebeziumStorageViewIT.java b/sink-connector-lightweight/src/test/java/com/altinity/clickhouse/debezium/embedded/cdc/DebeziumStorageViewIT.java index 368aec496..dc5066fb5 100644 --- a/sink-connector-lightweight/src/test/java/com/altinity/clickhouse/debezium/embedded/cdc/DebeziumStorageViewIT.java +++ b/sink-connector-lightweight/src/test/java/com/altinity/clickhouse/debezium/embedded/cdc/DebeziumStorageViewIT.java @@ -73,8 +73,7 @@ public void debeziumStorageView() throws Exception { ExecutorService executorService = Executors.newFixedThreadPool(1); executorService.execute(() -> { try { - clickHouseDebeziumEmbeddedApplication.start(injector.getInstance(DebeziumRecordParserService.class), - injector.getInstance(DDLParserService.class), props, false); + clickHouseDebeziumEmbeddedApplication.start(injector.getInstance(DebeziumRecordParserService.class), props, false); DebeziumEmbeddedRestApi.startRestApi(props, injector, clickHouseDebeziumEmbeddedApplication.getDebeziumEventCapture() , new Properties()); } catch (Exception e) { diff --git a/sink-connector-lightweight/src/test/java/com/altinity/clickhouse/debezium/embedded/cdc/DestinationDBColumnMissingIT.java b/sink-connector-lightweight/src/test/java/com/altinity/clickhouse/debezium/embedded/cdc/DestinationDBColumnMissingIT.java index 3f4c8d73e..87d347f42 100644 --- a/sink-connector-lightweight/src/test/java/com/altinity/clickhouse/debezium/embedded/cdc/DestinationDBColumnMissingIT.java +++ b/sink-connector-lightweight/src/test/java/com/altinity/clickhouse/debezium/embedded/cdc/DestinationDBColumnMissingIT.java @@ -85,8 +85,7 @@ public void testColumnMismatch() throws Exception { ExecutorService executorService = Executors.newFixedThreadPool(1); executorService.execute(() -> { try { - clickHouseDebeziumEmbeddedApplication.start(injector.getInstance(DebeziumRecordParserService.class), - injector.getInstance(DDLParserService.class), props, false); + clickHouseDebeziumEmbeddedApplication.start(injector.getInstance(DebeziumRecordParserService.class) , props, false); DebeziumEmbeddedRestApi.startRestApi(props, injector, clickHouseDebeziumEmbeddedApplication.getDebeziumEventCapture() , new Properties()); } catch (Exception e) { diff --git a/sink-connector-lightweight/src/test/java/com/altinity/clickhouse/debezium/embedded/cdc/MultipleUpdatesWSameTimestampIT.java b/sink-connector-lightweight/src/test/java/com/altinity/clickhouse/debezium/embedded/cdc/MultipleUpdatesWSameTimestampIT.java index 14b8f3170..f2a47165e 100644 --- a/sink-connector-lightweight/src/test/java/com/altinity/clickhouse/debezium/embedded/cdc/MultipleUpdatesWSameTimestampIT.java +++ b/sink-connector-lightweight/src/test/java/com/altinity/clickhouse/debezium/embedded/cdc/MultipleUpdatesWSameTimestampIT.java @@ -92,8 +92,7 @@ public void testIncrementingSequenceNumberWithUpdates() throws Exception { ExecutorService executorService = Executors.newFixedThreadPool(1); executorService.execute(() -> { try { - clickHouseDebeziumEmbeddedApplication.start(injector.getInstance(DebeziumRecordParserService.class), - injector.getInstance(DDLParserService.class), props, false); + clickHouseDebeziumEmbeddedApplication.start(injector.getInstance(DebeziumRecordParserService.class) , props, false); DebeziumEmbeddedRestApi.startRestApi(props, injector, clickHouseDebeziumEmbeddedApplication.getDebeziumEventCapture() , new Properties()); } catch (Exception e) { diff --git a/sink-connector-lightweight/src/test/java/com/altinity/clickhouse/debezium/embedded/cdc/SourceDBColumnMissingIT.java b/sink-connector-lightweight/src/test/java/com/altinity/clickhouse/debezium/embedded/cdc/SourceDBColumnMissingIT.java index 6ef0bd20a..98234cd8a 100644 --- a/sink-connector-lightweight/src/test/java/com/altinity/clickhouse/debezium/embedded/cdc/SourceDBColumnMissingIT.java +++ b/sink-connector-lightweight/src/test/java/com/altinity/clickhouse/debezium/embedded/cdc/SourceDBColumnMissingIT.java @@ -84,7 +84,7 @@ public void testColumnMismatch() throws Exception { executorService.execute(() -> { try { clickHouseDebeziumEmbeddedApplication.start(injector.getInstance(DebeziumRecordParserService.class), - injector.getInstance(DDLParserService.class), props, false); + props, false); DebeziumEmbeddedRestApi.startRestApi(props, injector, clickHouseDebeziumEmbeddedApplication.getDebeziumEventCapture() , new Properties()); } catch (Exception e) { diff --git a/sink-connector-lightweight/src/test/java/com/altinity/clickhouse/debezium/embedded/client/SinkConnectorClientRestAPITest.java b/sink-connector-lightweight/src/test/java/com/altinity/clickhouse/debezium/embedded/client/SinkConnectorClientRestAPITest.java index a82932f36..207340903 100644 --- a/sink-connector-lightweight/src/test/java/com/altinity/clickhouse/debezium/embedded/client/SinkConnectorClientRestAPITest.java +++ b/sink-connector-lightweight/src/test/java/com/altinity/clickhouse/debezium/embedded/client/SinkConnectorClientRestAPITest.java @@ -81,8 +81,7 @@ public void testRestClient() throws Exception { ExecutorService executorService = Executors.newFixedThreadPool(1); executorService.execute(() -> { try { - clickHouseDebeziumEmbeddedApplication.start(injector.getInstance(DebeziumRecordParserService.class), - injector.getInstance(DDLParserService.class), props, false); + clickHouseDebeziumEmbeddedApplication.start(injector.getInstance(DebeziumRecordParserService.class), props, false); DebeziumEmbeddedRestApi.startRestApi(props, injector, clickHouseDebeziumEmbeddedApplication.getDebeziumEventCapture() , new Properties()); } catch (Exception e) { diff --git a/sink-connector-lightweight/src/test/java/com/altinity/clickhouse/debezium/embedded/ddl/parser/AlterTableAddColumnIT.java b/sink-connector-lightweight/src/test/java/com/altinity/clickhouse/debezium/embedded/ddl/parser/AlterTableAddColumnIT.java index dc968d75f..66d77a177 100644 --- a/sink-connector-lightweight/src/test/java/com/altinity/clickhouse/debezium/embedded/ddl/parser/AlterTableAddColumnIT.java +++ b/sink-connector-lightweight/src/test/java/com/altinity/clickhouse/debezium/embedded/ddl/parser/AlterTableAddColumnIT.java @@ -1,8 +1,10 @@ package com.altinity.clickhouse.debezium.embedded.ddl.parser; import com.altinity.clickhouse.debezium.embedded.cdc.DebeziumChangeEventCapture; +import com.altinity.clickhouse.debezium.embedded.config.SinkConnectorLightWeightConfig; import com.altinity.clickhouse.debezium.embedded.parser.SourceRecordParserService; import com.altinity.clickhouse.sink.connector.ClickHouseSinkConnectorConfig; +import com.altinity.clickhouse.sink.connector.ClickHouseSinkConnectorConfigVariables; import com.altinity.clickhouse.sink.connector.db.BaseDbWriter; import com.clickhouse.jdbc.ClickHouseConnection; import org.apache.log4j.BasicConfigurator; @@ -18,6 +20,7 @@ import java.sql.Connection; import java.util.HashMap; import java.util.Map; +import java.util.Properties; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.atomic.AtomicReference; @@ -50,10 +53,12 @@ public void testAddColumn() throws Exception { executorService.execute(() -> { try { + Properties properties = getDebeziumProperties(); + // Add ddl.retry to true + //properties.put(SinkConnectorLightWeightConfig.DDL_RETRY, "true"); + engine.set(new DebeziumChangeEventCapture()); - engine.get().setup(getDebeziumProperties(), new SourceRecordParserService(), - new MySQLDDLParserService(new ClickHouseSinkConnectorConfig(new HashMap<>()), - "employees"), false); + engine.get().setup(properties, new SourceRecordParserService(), false); } catch (Exception e) { throw new RuntimeException(e); } @@ -92,18 +97,18 @@ public void testAddColumn() throws Exception { Map addTestColumns = writer.getColumnsDataTypesForTable("add_test"); // Validate all ship_class columns. - Assert.assertTrue(shipClassColumns.get("ship_spec").equalsIgnoreCase("Nullable(String)")); - Assert.assertTrue(shipClassColumns.get("somecol").equalsIgnoreCase("Nullable(Int32)")); + Assert.assertTrue(shipClassColumns.get("ship_spec").equalsIgnoreCase("String")); + Assert.assertTrue(shipClassColumns.get("somecol").equalsIgnoreCase("Int32")); Assert.assertTrue(shipClassColumns.get("newcol").equalsIgnoreCase("Nullable(Bool)")); Assert.assertTrue(shipClassColumns.get("customer_address").equalsIgnoreCase("String")); Assert.assertTrue(shipClassColumns.get("customer_name").equalsIgnoreCase("Nullable(String)")); // Validate all add_test columns. - Assert.assertTrue(addTestColumns.get("col8").equalsIgnoreCase("Nullable(String)")); + Assert.assertTrue(addTestColumns.get("col8").equalsIgnoreCase("String")); Assert.assertTrue(addTestColumns.get("col2").equalsIgnoreCase("Nullable(Int32)")); Assert.assertTrue(addTestColumns.get("col3").equalsIgnoreCase("Nullable(Int32)")); - Assert.assertTrue(addTestColumns.get("col5").equalsIgnoreCase("Nullable(String)")); - Assert.assertTrue(addTestColumns.get("col6").equalsIgnoreCase("Nullable(String)")); + Assert.assertTrue(addTestColumns.get("col5").equalsIgnoreCase("String")); + Assert.assertTrue(addTestColumns.get("col6").equalsIgnoreCase("String")); if(engine.get() != null) { engine.get().stop(); diff --git a/sink-connector-lightweight/src/test/java/com/altinity/clickhouse/debezium/embedded/ddl/parser/AlterTableChangeColumnIT.java b/sink-connector-lightweight/src/test/java/com/altinity/clickhouse/debezium/embedded/ddl/parser/AlterTableChangeColumnIT.java index 799bb662a..33bbf1740 100644 --- a/sink-connector-lightweight/src/test/java/com/altinity/clickhouse/debezium/embedded/ddl/parser/AlterTableChangeColumnIT.java +++ b/sink-connector-lightweight/src/test/java/com/altinity/clickhouse/debezium/embedded/ddl/parser/AlterTableChangeColumnIT.java @@ -48,8 +48,7 @@ public void testChangeColumn() throws Exception { executorService.execute(() -> { try { engine.set(new DebeziumChangeEventCapture()); - engine.get().setup(getDebeziumProperties(), new SourceRecordParserService(), - new MySQLDDLParserService(new ClickHouseSinkConnectorConfig(new HashMap<>()), "employees"), false); + engine.get().setup(getDebeziumProperties(), new SourceRecordParserService(), false); } catch (Exception e) { throw new RuntimeException(e); } @@ -84,14 +83,14 @@ public void testChangeColumn() throws Exception { Thread.sleep(10000); // Validate all ship_class columns. - Assert.assertTrue(shipClassColumns.get("class_name_new").equalsIgnoreCase("Int32")); - Assert.assertTrue(shipClassColumns.get("tonange_new").equalsIgnoreCase("Decimal(10, 10)")); + Assert.assertTrue(shipClassColumns.get("class_name_new").equalsIgnoreCase("Nullable(Int32)")); + Assert.assertTrue(shipClassColumns.get("tonange_new").equalsIgnoreCase("Nullable(Decimal(10, 10))")); Assert.assertTrue(shipClassColumns.get("max_length").equalsIgnoreCase("Nullable(Decimal(10, 2))")); // Files.deleteIfExists(tmpFilePath); - Assert.assertTrue(addTestColumns.get("new_col3_name").equalsIgnoreCase("Int32")); - Assert.assertTrue(addTestColumns.get("col1_new").equalsIgnoreCase("Int32")); - Assert.assertTrue(addTestColumns.get("new_col2_name").equalsIgnoreCase("Int32")); + Assert.assertTrue(addTestColumns.get("new_col3_name").equalsIgnoreCase("Nullable(Int32)")); + Assert.assertTrue(addTestColumns.get("col1_new").equalsIgnoreCase("Nullable(Int32)")); + Assert.assertTrue(addTestColumns.get("new_col2_name").equalsIgnoreCase("Nullable(Int32)")); if(engine.get() != null) { diff --git a/sink-connector-lightweight/src/test/java/com/altinity/clickhouse/debezium/embedded/ddl/parser/AlterTableModifyColumnIT.java b/sink-connector-lightweight/src/test/java/com/altinity/clickhouse/debezium/embedded/ddl/parser/AlterTableModifyColumnIT.java index efb67010f..746139f18 100644 --- a/sink-connector-lightweight/src/test/java/com/altinity/clickhouse/debezium/embedded/ddl/parser/AlterTableModifyColumnIT.java +++ b/sink-connector-lightweight/src/test/java/com/altinity/clickhouse/debezium/embedded/ddl/parser/AlterTableModifyColumnIT.java @@ -48,9 +48,7 @@ public void testModifyColumn() throws Exception { executorService.execute(() -> { try { engine.set(new DebeziumChangeEventCapture()); - engine.get().setup(getDebeziumProperties(), new SourceRecordParserService(), - new MySQLDDLParserService(new ClickHouseSinkConnectorConfig(new HashMap<>()), - "employees"), false); + engine.get().setup(getDebeziumProperties(), new SourceRecordParserService(), false); } catch (Exception e) { throw new RuntimeException(e); } @@ -78,12 +76,16 @@ public void testModifyColumn() throws Exception { Map shipClassColumns = writer.getColumnsDataTypesForTable("ship_class"); Map addTestColumns = writer.getColumnsDataTypesForTable("add_test"); - Assert.assertTrue(shipClassColumns.get("class_name").equalsIgnoreCase("Int32")); - Assert.assertTrue(shipClassColumns.get("tonange").equalsIgnoreCase("Decimal(10, 10)")); + Assert.assertTrue(shipClassColumns.get("class_name").equalsIgnoreCase("Nullable(Int32)")); + Assert.assertTrue(shipClassColumns.get("tonange").equalsIgnoreCase("Nullable(Decimal(10, 10))")); - Assert.assertTrue(addTestColumns.get("col1").equalsIgnoreCase("Int32")); - Assert.assertTrue(addTestColumns.get("col2").equalsIgnoreCase("Int32")); - Assert.assertTrue(addTestColumns.get("col3").equalsIgnoreCase("Int32")); + Assert.assertTrue(addTestColumns.get("col1").equalsIgnoreCase("Nullable(Int32)")); + Assert.assertTrue(addTestColumns.get("col2").equalsIgnoreCase("Nullable(Int32)")); + Assert.assertTrue(addTestColumns.get("col3").equalsIgnoreCase("Nullable(Int32)")); + + // Validate logic of adding Nullable based on the existing schema. + conn.prepareStatement("alter table office modify column office_code int").execute(); + Thread.sleep(10000); if(engine.get() != null) { engine.get().stop(); diff --git a/sink-connector-lightweight/src/test/java/com/altinity/clickhouse/debezium/embedded/ddl/parser/AutoCreateTableIT.java b/sink-connector-lightweight/src/test/java/com/altinity/clickhouse/debezium/embedded/ddl/parser/AutoCreateTableIT.java index 898f1bb47..149930ee9 100644 --- a/sink-connector-lightweight/src/test/java/com/altinity/clickhouse/debezium/embedded/ddl/parser/AutoCreateTableIT.java +++ b/sink-connector-lightweight/src/test/java/com/altinity/clickhouse/debezium/embedded/ddl/parser/AutoCreateTableIT.java @@ -78,9 +78,7 @@ public void testAutoCreateTable(String clickHouseServerVersion) throws Exception try { engine.set(new DebeziumChangeEventCapture()); - engine.get().setup(ITCommon.getDebeziumProperties(mySqlContainer, clickHouseContainer), new SourceRecordParserService(), - new MySQLDDLParserService(new ClickHouseSinkConnectorConfig(new HashMap<>()), - "employees"),false); + engine.get().setup(ITCommon.getDebeziumProperties(mySqlContainer, clickHouseContainer), new SourceRecordParserService(), false); } catch (Exception e) { throw new RuntimeException(e); } diff --git a/sink-connector-lightweight/src/test/java/com/altinity/clickhouse/debezium/embedded/ddl/parser/CreateTableDataTypesIT.java b/sink-connector-lightweight/src/test/java/com/altinity/clickhouse/debezium/embedded/ddl/parser/CreateTableDataTypesIT.java index 20c230d3c..c739ff918 100644 --- a/sink-connector-lightweight/src/test/java/com/altinity/clickhouse/debezium/embedded/ddl/parser/CreateTableDataTypesIT.java +++ b/sink-connector-lightweight/src/test/java/com/altinity/clickhouse/debezium/embedded/ddl/parser/CreateTableDataTypesIT.java @@ -1,5 +1,6 @@ package com.altinity.clickhouse.debezium.embedded.ddl.parser; +import com.altinity.clickhouse.debezium.embedded.ITCommon; import com.altinity.clickhouse.debezium.embedded.cdc.DebeziumChangeEventCapture; import com.altinity.clickhouse.debezium.embedded.parser.SourceRecordParserService; import com.altinity.clickhouse.sink.connector.ClickHouseSinkConnectorConfig; @@ -25,7 +26,8 @@ import java.util.concurrent.atomic.AtomicReference; @Testcontainers -@DisplayName("Integration test that tests replication of data types and validates datetime, date limits with no timezone values set on CH and MySQL") +@DisplayName("Integration test that tests replication of data types and validates datetime," + + " date limits with no timezone values and MySQL Point Data typek set on CH and MySQL") public class CreateTableDataTypesIT extends DDLBaseIT { @BeforeEach @@ -55,9 +57,7 @@ public void testCreateTable() throws Exception { props.setProperty("database.include.list", "datatypes"); engine.set(new DebeziumChangeEventCapture()); - engine.get().setup(getDebeziumProperties(), new SourceRecordParserService(), - new MySQLDDLParserService(new ClickHouseSinkConnectorConfig(new HashMap<>()), - "employees"), false); + engine.get().setup(getDebeziumProperties(), new SourceRecordParserService() , false); } catch (Exception e) { throw new RuntimeException(e); } @@ -262,6 +262,51 @@ public void testCreateTable() throws Exception { break; } + // validate POINT data type + // Create a new table with POINT data type + // Crate a new table on MySQL with POINT data type + String createTableWithPoint = "CREATE TABLE employees.point_table (id int not null PRIMARY KEY, c1 int, c2 int, c3a POINT, c3b POINT, f1 float(10), f2 decimal(8,4))"; + ITCommon.connectToMySQL(mySqlContainer).createStatement().execute(createTableWithPoint); + + // Sleep for 10 seconds to allow the table to be replicated + Thread.sleep(10000); + + // Insert a new row into the table + ITCommon.connectToMySQL(mySqlContainer).createStatement().execute("INSERT INTO employees.point_table (id, c1, c2, c3a, c3b, f1, f2) values (1, 123, 456, POINT(1.0,2.0), POINT(3.0,4.0), 100.20, 100.20)"); + + Thread.sleep(10000); + ResultSet rs = writer.executeQueryWithResultSet("select * from employees.point_table"); + boolean pointResultValidated = false; + while(rs.next()) { + pointResultValidated = true; + String c3a = rs.getString("c3a"); + String c3b = rs.getString("c3b"); + Assert.assertTrue(c3a.equalsIgnoreCase("(1.0,2.0)")); + Assert.assertTrue(c3b.equalsIgnoreCase("(3.0,4.0)")); + } + Assert.assertTrue(pointResultValidated); + String createTableWithGeometry = "CREATE TABLE employees.locations ( id INT not null AUTO_INCREMENT PRIMARY KEY, name VARCHAR(100), location GEOMETRY)"; + ITCommon.connectToMySQL(mySqlContainer).createStatement().execute(createTableWithGeometry); + Thread.sleep(10000); + + // Insert a new row into the table + ITCommon.connectToMySQL(mySqlContainer).createStatement().execute("INSERT INTO locations (name, location)\n" + + "VALUES ('Route', ST_GeomFromText('LINESTRING(0 0, 1 1, 2 2)'));\n"); + // Validate the row inserted to locations table. + Thread.sleep(10000); + + + ResultSet rs2 = ITCommon.connectToMySQL(mySqlContainer).createStatement().executeQuery("SELECT ST_AsText(location) as location FROM employees.locations"); + boolean geometryResultValidated = false; + + while(rs2.next()) { + geometryResultValidated = true; + String c3a = rs2.getString("location"); + Assert.assertTrue(c3a.equalsIgnoreCase("LINESTRING(0 0,1 1,2 2)")); + } + Assert.assertTrue(geometryResultValidated); + + if(engine.get() != null) { engine.get().stop(); } diff --git a/sink-connector-lightweight/src/test/java/com/altinity/clickhouse/debezium/embedded/ddl/parser/CreateTableDataTypesTimeZoneIT.java b/sink-connector-lightweight/src/test/java/com/altinity/clickhouse/debezium/embedded/ddl/parser/CreateTableDataTypesTimeZoneIT.java index 93bdc1dc6..aa7418596 100644 --- a/sink-connector-lightweight/src/test/java/com/altinity/clickhouse/debezium/embedded/ddl/parser/CreateTableDataTypesTimeZoneIT.java +++ b/sink-connector-lightweight/src/test/java/com/altinity/clickhouse/debezium/embedded/ddl/parser/CreateTableDataTypesTimeZoneIT.java @@ -69,8 +69,8 @@ public void testCreateTable() throws Exception { props.setProperty("database.include.list", "datatypes"); engine.set(new DebeziumChangeEventCapture()); - engine.get().setup(ITCommon.getDebeziumProperties(mySqlContainer, clickHouseContainer), new SourceRecordParserService(), - new MySQLDDLParserService(new ClickHouseSinkConnectorConfig(new HashMap<>()), "datatypes"), false); + engine.get().setup(ITCommon.getDebeziumProperties(mySqlContainer, clickHouseContainer), new SourceRecordParserService() + , false); } catch (Exception e) { throw new RuntimeException(e); } diff --git a/sink-connector-lightweight/src/test/java/com/altinity/clickhouse/debezium/embedded/ddl/parser/DDLBaseIT.java b/sink-connector-lightweight/src/test/java/com/altinity/clickhouse/debezium/embedded/ddl/parser/DDLBaseIT.java index 51409f8e0..412c94828 100644 --- a/sink-connector-lightweight/src/test/java/com/altinity/clickhouse/debezium/embedded/ddl/parser/DDLBaseIT.java +++ b/sink-connector-lightweight/src/test/java/com/altinity/clickhouse/debezium/embedded/ddl/parser/DDLBaseIT.java @@ -2,6 +2,7 @@ import com.altinity.clickhouse.debezium.embedded.ITCommon; +import com.altinity.clickhouse.debezium.embedded.config.SinkConnectorLightWeightConfig; import org.apache.log4j.BasicConfigurator; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; @@ -60,8 +61,11 @@ protected Connection connectToMySQL() { protected Properties getDebeziumProperties() throws Exception { - return ITCommon.getDebeziumProperties(mySqlContainer, clickHouseContainer); + Properties props = ITCommon.getDebeziumProperties(mySqlContainer, clickHouseContainer); + props.put(SinkConnectorLightWeightConfig.DDL_RETRY, "true"); + + return props; } } diff --git a/sink-connector-lightweight/src/test/java/com/altinity/clickhouse/debezium/embedded/ddl/parser/DateTimeWithTimeZoneColumnSchemaOnlyIT.java b/sink-connector-lightweight/src/test/java/com/altinity/clickhouse/debezium/embedded/ddl/parser/DateTimeWithTimeZoneColumnSchemaOnlyIT.java index 785145afa..a9f3ac7e7 100644 --- a/sink-connector-lightweight/src/test/java/com/altinity/clickhouse/debezium/embedded/ddl/parser/DateTimeWithTimeZoneColumnSchemaOnlyIT.java +++ b/sink-connector-lightweight/src/test/java/com/altinity/clickhouse/debezium/embedded/ddl/parser/DateTimeWithTimeZoneColumnSchemaOnlyIT.java @@ -109,8 +109,7 @@ public void testSchemaOnlyMode() throws Exception { try { engine.set(new DebeziumChangeEventCapture()); - engine.get().setup(getDebeziumProperties(), new SourceRecordParserService(), - new MySQLDDLParserService(new ClickHouseSinkConnectorConfig(new HashMap<>()), "employees"), false); + engine.get().setup(getDebeziumProperties(), new SourceRecordParserService(), false); } catch (Exception e) { throw new RuntimeException(e); } diff --git a/sink-connector-lightweight/src/test/java/com/altinity/clickhouse/debezium/embedded/ddl/parser/DateTimeWithTimeZoneIT.java b/sink-connector-lightweight/src/test/java/com/altinity/clickhouse/debezium/embedded/ddl/parser/DateTimeWithTimeZoneIT.java index 599f82372..5c6ec0249 100644 --- a/sink-connector-lightweight/src/test/java/com/altinity/clickhouse/debezium/embedded/ddl/parser/DateTimeWithTimeZoneIT.java +++ b/sink-connector-lightweight/src/test/java/com/altinity/clickhouse/debezium/embedded/ddl/parser/DateTimeWithTimeZoneIT.java @@ -70,8 +70,7 @@ public void testCreateTable() throws Exception { props.setProperty("database.include.list", "datatypes"); engine.set(new DebeziumChangeEventCapture()); - engine.get().setup(ITCommon.getDebeziumProperties(mySqlContainer, clickHouseContainer), new SourceRecordParserService(), - new MySQLDDLParserService(new ClickHouseSinkConnectorConfig(new HashMap<>()), "datatypes"), false); + engine.get().setup(ITCommon.getDebeziumProperties(mySqlContainer, clickHouseContainer), new SourceRecordParserService(), false); } catch (Exception e) { throw new RuntimeException(e); } diff --git a/sink-connector-lightweight/src/test/java/com/altinity/clickhouse/debezium/embedded/ddl/parser/DateTimeWithTimeZoneSchemaOnlyIT.java b/sink-connector-lightweight/src/test/java/com/altinity/clickhouse/debezium/embedded/ddl/parser/DateTimeWithTimeZoneSchemaOnlyIT.java index 6ade4e240..27230aa48 100644 --- a/sink-connector-lightweight/src/test/java/com/altinity/clickhouse/debezium/embedded/ddl/parser/DateTimeWithTimeZoneSchemaOnlyIT.java +++ b/sink-connector-lightweight/src/test/java/com/altinity/clickhouse/debezium/embedded/ddl/parser/DateTimeWithTimeZoneSchemaOnlyIT.java @@ -72,8 +72,7 @@ public void testCreateTable() throws Exception { props.setProperty("database.include.list", "datatypes"); engine.set(new DebeziumChangeEventCapture()); - engine.get().setup(getDebeziumProperties(), new SourceRecordParserService(), - new MySQLDDLParserService(new ClickHouseSinkConnectorConfig(new HashMap<>()), "datatypes"), false); + engine.get().setup(getDebeziumProperties(), new SourceRecordParserService(), false); } catch (Exception e) { throw new RuntimeException(e); } diff --git a/sink-connector-lightweight/src/test/java/com/altinity/clickhouse/debezium/embedded/ddl/parser/DateTimeWithUserProvidedDifferentTimeZoneIT.java b/sink-connector-lightweight/src/test/java/com/altinity/clickhouse/debezium/embedded/ddl/parser/DateTimeWithUserProvidedDifferentTimeZoneIT.java index 34985b83f..887b04d62 100644 --- a/sink-connector-lightweight/src/test/java/com/altinity/clickhouse/debezium/embedded/ddl/parser/DateTimeWithUserProvidedDifferentTimeZoneIT.java +++ b/sink-connector-lightweight/src/test/java/com/altinity/clickhouse/debezium/embedded/ddl/parser/DateTimeWithUserProvidedDifferentTimeZoneIT.java @@ -70,9 +70,7 @@ public void testCreateTable() throws Exception { try { engine.set(new DebeziumChangeEventCapture()); - engine.get().setup(getDebeziumProperties(), new SourceRecordParserService(), - new MySQLDDLParserService(new ClickHouseSinkConnectorConfig(new HashMap<>()), - "datatypes"), false); + engine.get().setup(getDebeziumProperties(), new SourceRecordParserService(), false); } catch (Exception e) { throw new RuntimeException(e); } diff --git a/sink-connector-lightweight/src/test/java/com/altinity/clickhouse/debezium/embedded/ddl/parser/DateTimeWithUserProvidedTimeZoneSchemaOnlyIT.java b/sink-connector-lightweight/src/test/java/com/altinity/clickhouse/debezium/embedded/ddl/parser/DateTimeWithUserProvidedTimeZoneSchemaOnlyIT.java index cb3bf551e..0920e706a 100644 --- a/sink-connector-lightweight/src/test/java/com/altinity/clickhouse/debezium/embedded/ddl/parser/DateTimeWithUserProvidedTimeZoneSchemaOnlyIT.java +++ b/sink-connector-lightweight/src/test/java/com/altinity/clickhouse/debezium/embedded/ddl/parser/DateTimeWithUserProvidedTimeZoneSchemaOnlyIT.java @@ -74,8 +74,7 @@ public void testCreateTable() throws Exception { props.setProperty("database.include.list", "datatypes"); engine.set(new DebeziumChangeEventCapture()); - engine.get().setup(getDebeziumProperties(), new SourceRecordParserService(), - new MySQLDDLParserService(new ClickHouseSinkConnectorConfig(new HashMap<>()), "datatypes"), false); + engine.get().setup(getDebeziumProperties(), new SourceRecordParserService(), false); } catch (Exception e) { throw new RuntimeException(e); } diff --git a/sink-connector-lightweight/src/test/java/com/altinity/clickhouse/debezium/embedded/ddl/parser/EmployeesDBIT.java b/sink-connector-lightweight/src/test/java/com/altinity/clickhouse/debezium/embedded/ddl/parser/EmployeesDBIT.java index 760e452d8..e5d8d654e 100644 --- a/sink-connector-lightweight/src/test/java/com/altinity/clickhouse/debezium/embedded/ddl/parser/EmployeesDBIT.java +++ b/sink-connector-lightweight/src/test/java/com/altinity/clickhouse/debezium/embedded/ddl/parser/EmployeesDBIT.java @@ -62,9 +62,7 @@ public void testEmployeesDB() throws Exception { executorService.execute(() -> { try { engine.set(new DebeziumChangeEventCapture()); - engine.get().setup(getDebeziumProperties(), new SourceRecordParserService(), - new MySQLDDLParserService(new ClickHouseSinkConnectorConfig(new HashMap<>()), - "employees"), false); + engine.get().setup(getDebeziumProperties(), new SourceRecordParserService(), false); } catch (Exception e) { throw new RuntimeException(e); } diff --git a/sink-connector-lightweight/src/test/java/com/altinity/clickhouse/debezium/embedded/ddl/parser/IsDeletedColumnsIT.java b/sink-connector-lightweight/src/test/java/com/altinity/clickhouse/debezium/embedded/ddl/parser/IsDeletedColumnsIT.java index 43fcd20ce..28382c53b 100644 --- a/sink-connector-lightweight/src/test/java/com/altinity/clickhouse/debezium/embedded/ddl/parser/IsDeletedColumnsIT.java +++ b/sink-connector-lightweight/src/test/java/com/altinity/clickhouse/debezium/embedded/ddl/parser/IsDeletedColumnsIT.java @@ -74,8 +74,8 @@ public void testIsDeleted(String clickHouseServerVersion) throws Exception { try { engine.set(new DebeziumChangeEventCapture()); - engine.get().setup(ITCommon.getDebeziumProperties(mySqlContainer, clickHouseContainer), new SourceRecordParserService(), - new MySQLDDLParserService(new ClickHouseSinkConnectorConfig(new HashMap<>()), "datatypes"),false); + engine.get().setup(ITCommon.getDebeziumProperties(mySqlContainer, clickHouseContainer), new SourceRecordParserService() + ,false); } catch (Exception e) { throw new RuntimeException(e); } diff --git a/sink-connector-lightweight/src/test/java/com/altinity/clickhouse/debezium/embedded/ddl/parser/MultipleDatabaseIT.java b/sink-connector-lightweight/src/test/java/com/altinity/clickhouse/debezium/embedded/ddl/parser/MultipleDatabaseIT.java index 0126b6293..07db78f4f 100644 --- a/sink-connector-lightweight/src/test/java/com/altinity/clickhouse/debezium/embedded/ddl/parser/MultipleDatabaseIT.java +++ b/sink-connector-lightweight/src/test/java/com/altinity/clickhouse/debezium/embedded/ddl/parser/MultipleDatabaseIT.java @@ -78,8 +78,7 @@ public void testMultipleDatabases() throws Exception { try { engine.set(new DebeziumChangeEventCapture()); - engine.get().setup(props, new SourceRecordParserService(), - new MySQLDDLParserService(new ClickHouseSinkConnectorConfig(new HashMap<>()), "test_db"),false); + engine.get().setup(props, new SourceRecordParserService(), false); } catch (Exception e) { throw new RuntimeException(e); } diff --git a/sink-connector-lightweight/src/test/java/com/altinity/clickhouse/debezium/embedded/ddl/parser/MySqlDDLParserListenerImplTest.java b/sink-connector-lightweight/src/test/java/com/altinity/clickhouse/debezium/embedded/ddl/parser/MySqlDDLParserListenerImplTest.java index 7e1d5b56f..e4d479774 100644 --- a/sink-connector-lightweight/src/test/java/com/altinity/clickhouse/debezium/embedded/ddl/parser/MySqlDDLParserListenerImplTest.java +++ b/sink-connector-lightweight/src/test/java/com/altinity/clickhouse/debezium/embedded/ddl/parser/MySqlDDLParserListenerImplTest.java @@ -7,12 +7,14 @@ import org.apache.logging.log4j.Logger; import org.junit.Assert; import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.CsvSource; import java.util.HashMap; +import java.util.Map; import java.util.concurrent.atomic.AtomicBoolean; @@ -213,6 +215,25 @@ public void testAutoCreateTableWithCHTimezoneUpperCaseDateTime() { log.info("Create table " + clickHouseQuery); } + @Test + public void testCreateTableWithReplicatedReplacingMergeTree() { + + StringBuffer clickHouseQuery = new StringBuffer(); + String createDB = "CREATE TABLE IF NOT EXISTS mysql1.`table_7220f7bd_8c8c_11ef_94db_67ff65f7711d` (id INT NOT NULL,col1 varchar(255), col2 int, PRIMARY KEY (id)) ENGINE = InnoDB))"; + + // Set ClickHouse sink connector config to set replicated tables. + Map config = new HashMap<>(); + config.put(ClickHouseSinkConnectorConfigVariables.AUTO_CREATE_TABLES_REPLICATED.toString(), "true"); + config.put(ClickHouseSinkConnectorConfigVariables.CLICKHOUSE_DATABASE_OVERRIDE_MAP.toString(), "mysql1:ch1"); + + ClickHouseSinkConnectorConfig clickHouseSinkConnectorConfig = new ClickHouseSinkConnectorConfig(config); + MySQLDDLParserService mySQLDDLParserService = new MySQLDDLParserService(clickHouseSinkConnectorConfig, "ch1"); + mySQLDDLParserService.parseSql(createDB, "Persons", clickHouseQuery); + log.info("Create table " + clickHouseQuery); + + Assert.assertTrue(clickHouseQuery.toString().equalsIgnoreCase("CREATE TABLE if not exists ch1.`table_7220f7bd_8c8c_11ef_94db_67ff65f7711d` ON CLUSTER `{cluster}`(id Int32 NOT NULL ,col1 Nullable(String),col2 Nullable(Int32),`_version` UInt64,`is_deleted` UInt8)Engine=ReplicatedReplacingMergeTree(_version, is_deleted) ORDER BY (id)")); + + } @Test public void testCreateTableAutoIncrement() { StringBuffer clickHouseQuery = new StringBuffer(); @@ -240,6 +261,7 @@ public void testCreateTable() { log.info("Create table " + clickHouseQuery); } + @Test public void testCreateTableWithNulLFields() { StringBuffer clickHouseQuery = new StringBuffer(); @@ -394,6 +416,14 @@ public void testAddColumnWithoutExplicitNull() { Assert.assertTrue(clickHouseQuery.toString().equalsIgnoreCase(expectedClickHouseQuery)); } + @Test + public void testAlterTableModifyColumn() { + StringBuffer clickHouseQuery = new StringBuffer(); + String alterTableModifyColumn = "ALTER TABLE employees.add_test MODIFY COLUMN col1 INT;"; + mySQLDDLParserService.parseSql(alterTableModifyColumn, "add_test", clickHouseQuery); + + Assert.assertTrue(clickHouseQuery.toString().equalsIgnoreCase("ALTER TABLE employees.add_test MODIFY COLUMN col1 Int32")); + } @Test @@ -507,13 +537,22 @@ public void testAddConstraints() { String sql = "alter table t2 add constraint t2_pk_constraint primary key (1c), alter column `_` set default 1;\n"; mySQLDDLParserService.parseSql(sql, "t2", clickHouseQuery); - StringBuffer clickHouseQuery2 = new StringBuffer(); String checkConstraintSql = "ALTER TABLE orders ADD CONSTRAINT check_revenue_positive CHECK (revenue >= 0);"; mySQLDDLParserService.parseSql(checkConstraintSql, " ", clickHouseQuery2); } + @Test + public void testDropContraints() { + StringBuffer clickhouseQuery = new StringBuffer(); + + String dropConstraintsSql = "alter table employees drop CONSTRAINT employees_ibfk_2"; + mySQLDDLParserService.parseSql(dropConstraintsSql, "employees", clickhouseQuery); + + Assert.assertTrue(clickhouseQuery.toString().equalsIgnoreCase("ALTER TABLE employees.employees DROP CONSTRAINT employees_ibfk_2")); + } + @Test public void testAddConstraintsWithAnd() { StringBuffer clickHouseQuery = new StringBuffer(); @@ -530,7 +569,7 @@ public void testAddPrimaryKey() { String sql = "alter table table1 add primary key (id)"; mySQLDDLParserService.parseSql(sql, "table1", clickHouseQuery); - + // Assert.assertTrue(clickHouseQuery.toString().equalsIgnoreCase("alter table employees.table1 add primary key (id)")); } @Test @@ -583,6 +622,20 @@ public void renameTable() { Assert.assertTrue(clickHouseQuery.toString().equalsIgnoreCase("rename table employees.add_test to employees.add_test_old")); } + @Test + public void testRenameTableWithDatabaseOverride() { + StringBuffer clickHouseQuery = new StringBuffer(); + + HashMap props = new HashMap<>(); + props.put(ClickHouseSinkConnectorConfigVariables.CLICKHOUSE_DATABASE_OVERRIDE_MAP.toString(), "employees:employees2, products:productsnew"); + ClickHouseSinkConnectorConfig config = new ClickHouseSinkConnectorConfig(props); + MySQLDDLParserService mySQLDDLParserService = new MySQLDDLParserService(config, "employees2"); + + String sql = "rename table employees.add_test to employees.add_test_old"; + + mySQLDDLParserService.parseSql(sql, "table1", clickHouseQuery); + Assert.assertTrue(clickHouseQuery.toString().equalsIgnoreCase("rename table employees2.add_test to employees2.add_test_old")); + } @Test public void testAddIndex() { StringBuffer clickHouseQuery = new StringBuffer(); @@ -659,7 +712,7 @@ public void renameMultipleTables() { String sql = "rename /* gh-ost */ table `trade_prod`.`enriched_trade` to `trade_prod`.`_enriched_trade_del`, `trade_prod`.`_enriched_trade_gho` to `trade_prod`.`enriched_trade`\n"; mySQLDDLParserService.parseSql(sql, "", clickHouseQuery); - Assert.assertTrue(clickHouseQuery.toString().equalsIgnoreCase("RENAME TABLE `trade_prod`.`enriched_trade` to `trade_prod`.`_enriched_trade_del`,`trade_prod`.`_enriched_trade_gho` to `trade_prod`.`enriched_trade`")); + Assert.assertTrue(clickHouseQuery.toString().equalsIgnoreCase("RENAME TABLE employees.`enriched_trade` to employees.`_enriched_trade_del`,employees.`_enriched_trade_gho` to employees.`enriched_trade`")); } @Test public void alterTableRenameTable() { @@ -819,6 +872,36 @@ public void testRenameIsDeletedColumn() { Assert.assertTrue(clickHouseQuery2.toString().equalsIgnoreCase( "CREATE TABLE employees.city(id Int32 NOT NULL ,Name Nullable(String),is_deleted Nullable(Int16),`_version` UInt64,`__is_deleted` UInt8) Engine=ReplacingMergeTree(_version,__is_deleted) ORDER BY (id)")); } + + @Test + @Disabled + public void testPartitionedByRangeTable() { + String sql = "CREATE TABLE `city` (\n" + + " `ID` int NOT NULL AUTO_INCREMENT,\n" + + " `Name` char(35) COLLATE utf8mb4_general_ci NOT NULL DEFAULT '',\n" + + " `CountryCode` char(3) COLLATE utf8mb4_general_ci NOT NULL DEFAULT '',\n" + + " `District` char(20) COLLATE utf8mb4_general_ci NOT NULL DEFAULT '',\n" + + " `Population` int NOT NULL DEFAULT '0',\n" + + " `is_deleted` tinyint(1) DEFAULT '0',\n" + + " PRIMARY KEY (`ID`)\n" + + ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci PARTITION BY RANGE(`ID`)\n" + + "(PARTITION p0 VALUES LESS THAN (1000),\n" + + " PARTITION p1 VALUES LESS THAN (2000),\n" + + " PARTITION p2 VALUES LESS THAN (3000),\n" + + " PARTITION p3 VALUES LESS THAN (4000),\n" + + " PARTITION p4 VALUES LESS THAN (5000),\n" + + " PARTITION p5 VALUES LESS THAN (6000),\n" + + " PARTITION p6 VALUES LESS THAN (7000),\n" + + " PARTITION p7 VALUES LESS THAN (8000),\n" + + " PARTITION p8 VALUES LESS THAN (9000),\n" + + " PARTITION p9 VALUES LESS THAN (10000));"; + + StringBuffer clickHouseQuery = new StringBuffer(); + mySQLDDLParserService.parseSql(sql, "employees", clickHouseQuery); + + Assert.assertTrue(clickHouseQuery.toString().equalsIgnoreCase( + "CREATE TABLE employees.`city`(`ID` Int32 NOT NULL ,`Name` String NOT NULL ,`CountryCode` String NOT NULL ,`District` String NOT NULL ,`Population` Int32 NOT NULL ,`is_deleted` Nullable(Int16),`_version` UInt64,`__is_deleted` UInt8) Engine=ReplacingMergeTree(_version,__is_deleted) ORDER BY (`ID`) PARTITION BY ID")); + } // @Test // public void deleteData() { // String sql = "DELETE FROM Customers WHERE CustomerName='Alfreds Futterkiste'"; diff --git a/sink-connector-lightweight/src/test/java/com/altinity/clickhouse/debezium/embedded/ddl/parser/TableOperationsIT.java b/sink-connector-lightweight/src/test/java/com/altinity/clickhouse/debezium/embedded/ddl/parser/TableOperationsIT.java index b72d2f8dd..2678f81fe 100644 --- a/sink-connector-lightweight/src/test/java/com/altinity/clickhouse/debezium/embedded/ddl/parser/TableOperationsIT.java +++ b/sink-connector-lightweight/src/test/java/com/altinity/clickhouse/debezium/embedded/ddl/parser/TableOperationsIT.java @@ -2,6 +2,7 @@ import com.altinity.clickhouse.debezium.embedded.ITCommon; import com.altinity.clickhouse.debezium.embedded.cdc.DebeziumChangeEventCapture; +import com.altinity.clickhouse.debezium.embedded.common.PropertiesHelper; import com.altinity.clickhouse.debezium.embedded.parser.SourceRecordParserService; import com.altinity.clickhouse.sink.connector.ClickHouseSinkConnectorConfig; import com.altinity.clickhouse.sink.connector.db.BaseDbWriter; @@ -13,6 +14,7 @@ import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.ValueSource; import org.testcontainers.clickhouse.ClickHouseContainer; import org.testcontainers.containers.MySQLContainer; import org.testcontainers.containers.wait.strategy.HttpWaitStrategy; @@ -22,6 +24,7 @@ import java.sql.Connection; import java.util.HashMap; import java.util.Map; +import java.util.Properties; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.atomic.AtomicReference; @@ -60,22 +63,27 @@ public void startContainers() throws InterruptedException { clickHouseContainer.start(); } @ParameterizedTest - @CsvSource({ - "clickhouse/clickhouse-server:latest", - "clickhouse/clickhouse-server:22.3" - }) + @ValueSource(booleans = { + false, + true} + ) @DisplayName("Test that validates DDL(Create, ALTER, RENAME)") - public void testTableOperations(String clickHouseServerVersion) throws Exception { + public void testTableOperations(boolean databaseOverride) throws Exception { AtomicReference engine = new AtomicReference<>(); + Properties props = ITCommon.getDebeziumProperties(mySqlContainer, clickHouseContainer); + if(databaseOverride) { + props.setProperty("clickhouse.database.override.map", "employees:ch_employees, datatypes:ch_datatypes, public:ch_public, project:ch_project"); + } + DebeziumChangeEventCapture debeziumChangeEventCapture = new DebeziumChangeEventCapture(); ExecutorService executorService = Executors.newFixedThreadPool(1); executorService.execute(() -> { try { - engine.set(new DebeziumChangeEventCapture()); - engine.get().setup(ITCommon.getDebeziumProperties(mySqlContainer, clickHouseContainer), new SourceRecordParserService(), - new MySQLDDLParserService(new ClickHouseSinkConnectorConfig(new HashMap<>()), "employees"),false); + engine.set(debeziumChangeEventCapture); + ClickHouseSinkConnectorConfig config = new ClickHouseSinkConnectorConfig(PropertiesHelper.toMap(props)); + engine.get().setup(props, new SourceRecordParserService(),false); } catch (Exception e) { throw new RuntimeException(e); } @@ -100,11 +108,15 @@ public void testTableOperations(String clickHouseServerVersion) throws Exception "PARTITIONS 6;").execute(); conn.prepareStatement("create table copied_table like new_table").execute(); conn.prepareStatement("CREATE TABLE rcx ( a INT not null, b INT, c CHAR(3) not null, d INT not null) PARTITION BY RANGE COLUMNS(a,d,c) ( PARTITION p0 VALUES LESS THAN (5,10,'ggg'));").execute(); + + // insert a new row to new_table + conn.prepareStatement("insert into new_table values('a', 1, 1)").execute(); + Thread.sleep(10000); conn.prepareStatement("\n" + - "CREATE TABLE contacts (id INT AUTO_INCREMENT PRIMARY KEY,\n" + + "CREATE TABLE contacts (id INT NOT NULL AUTO_INCREMENT PRIMARY KEY,\n" + "first_name VARCHAR(50) NOT NULL,\n" + "last_name VARCHAR(50) NOT NULL,\n" + "fullname varchar(101) GENERATED ALWAYS AS (CONCAT(first_name,' ',last_name)),\n" + @@ -131,24 +143,7 @@ public void testTableOperations(String clickHouseServerVersion) throws Exception // Validate table created with partitions. String membersResult = writer.executeQuery("show create table members"); - - if(clickHouseServerVersion.contains("latest")) { - Assert.assertTrue(membersResult.equalsIgnoreCase("CREATE TABLE employees.members\n" + - "(\n" + - " `firstname` String,\n" + - " `lastname` String,\n" + - " `username` String,\n" + - " `email` Nullable(String),\n" + - " `joined` Date32,\n" + - " `_version` UInt64,\n" + - " `is_deleted` UInt8\n" + - ")\n" + - "ENGINE = ReplacingMergeTree(_version, is_deleted)\n" + - "PARTITION BY joined\n" + - "ORDER BY tuple()\n" + - "SETTINGS index_granularity = 8192")); - } else { - Assert.assertTrue(membersResult.equalsIgnoreCase("CREATE TABLE employees.members\n" + + Assert.assertTrue(membersResult.equalsIgnoreCase("CREATE TABLE employees.members\n" + "(\n" + " `firstname` String,\n" + " `lastname` String,\n" + @@ -162,12 +157,10 @@ public void testTableOperations(String clickHouseServerVersion) throws Exception "PARTITION BY joined\n" + "ORDER BY tuple()\n" + "SETTINGS index_granularity = 8192")); - } String rcxResult = writer.executeQuery("show create table rcx"); - if(clickHouseServerVersion.contains("latest")) { - Assert.assertTrue(rcxResult.equalsIgnoreCase("CREATE TABLE employees.rcx\n" + + Assert.assertTrue(rcxResult.equalsIgnoreCase("CREATE TABLE employees.rcx\n" + "(\n" + " `a` Int32,\n" + " `b` Nullable(Int32),\n" + @@ -180,8 +173,10 @@ public void testTableOperations(String clickHouseServerVersion) throws Exception "PARTITION BY (a, d, c)\n" + "ORDER BY tuple()\n" + "SETTINGS index_granularity = 8192")); - } + Thread.sleep(10000); + // Delete offset table. + debeziumChangeEventCapture.deleteOffsets(props); if(engine.get() != null) { engine.get().stop(); diff --git a/sink-connector-lightweight/src/test/java/com/altinity/clickhouse/debezium/embedded/ddl/parser/TruncateTableIT.java b/sink-connector-lightweight/src/test/java/com/altinity/clickhouse/debezium/embedded/ddl/parser/TruncateTableIT.java index 626d68b2b..62518cabe 100644 --- a/sink-connector-lightweight/src/test/java/com/altinity/clickhouse/debezium/embedded/ddl/parser/TruncateTableIT.java +++ b/sink-connector-lightweight/src/test/java/com/altinity/clickhouse/debezium/embedded/ddl/parser/TruncateTableIT.java @@ -71,8 +71,7 @@ public void testIsDeleted() throws Exception { try { engine.set(new DebeziumChangeEventCapture()); - engine.get().setup(ITCommon.getDebeziumProperties(mySqlContainer, clickHouseContainer), new SourceRecordParserService(), - new MySQLDDLParserService(new ClickHouseSinkConnectorConfig(new HashMap<>()), "datatypes"),false); + engine.get().setup(ITCommon.getDebeziumProperties(mySqlContainer, clickHouseContainer), new SourceRecordParserService() ,false); } catch (Exception e) { throw new RuntimeException(e); } diff --git a/sink-connector-lightweight/src/test/resources/alter_ddl_modify_column.sql b/sink-connector-lightweight/src/test/resources/alter_ddl_modify_column.sql index 8721ea5f2..e8d1a00b0 100644 --- a/sink-connector-lightweight/src/test/resources/alter_ddl_modify_column.sql +++ b/sink-connector-lightweight/src/test/resources/alter_ddl_modify_column.sql @@ -1,6 +1,9 @@ create table ship_class(id int, class_name varchar(100), tonange decimal(10,2), max_length decimal(10,2), start_build year, end_build year(4), max_guns_size int); create table add_test(col1 varchar(255), col2 int, col3 int); +create table office(office_id INT PRIMARY KEY, office_name VARCHAR(50) NOT NULL, office_address VARCHAR(255) NOT NULL, office_code int DEFAULT NULL); + + --insert into ship_class values(1, "test_class", 20.2, 20.2, 1997, 1997, 1998); --insert into ship_class values(2, "test_class", 20.2, 20.2, 1997, 1997, 1998); -- | MODIFY [COLUMN] col_name column_definition diff --git a/sink-connector-lightweight/src/test/resources/my.cnf b/sink-connector-lightweight/src/test/resources/my.cnf new file mode 100644 index 000000000..0e745dabe --- /dev/null +++ b/sink-connector-lightweight/src/test/resources/my.cnf @@ -0,0 +1,43 @@ +# For advice on how to change settings please see +# http://dev.mysql.com/doc/refman/8.2/en/server-configuration-defaults.html + +# -------------------------------------------------------------------------------------------- +# This section specifies 5.5 and cross-version common configurations +# -------------------------------------------------------------------------------------------- +[mysqld] +# +# Remove leading # and set to the amount of RAM for the most important data +# cache in MySQL. Start at 70% of total RAM for dedicated server, else 10%. +# innodb_buffer_pool_size = 128M +# +# Remove leading # to turn on a very important data integrity option: logging +# changes to the binary log between backups. +# log_bin +# +# Remove leading # to set options mainly useful for reporting servers. +# The server defaults are faster for transactions and fast SELECTs. +# Adjust sizes as needed, experiment to find the optimal values. +# join_buffer_size = 128M +# sort_buffer_size = 2M +# read_rnd_buffer_size = 2M +skip-host-cache +skip-name-resolve +#datadir=/var/lib/mysql +#socket=/var/lib/mysql/mysql.sock +#secure-file-priv=/var/lib/mysql-files +user=mysql + +# Disabling symbolic-links is recommended to prevent assorted security risks +symbolic-links=0 + +#log-error=/var/log/mysqld.log +#pid-file=/var/run/mysqld/mysqld.pid + +# Enable binary replication log and set the prefix, expiration, and log format. +# The prefix is arbitrary, expiration can be short for integration tests but would +# be longer on a production system. Row-level info is required for ingest to work. +# Server ID is required, but this will vary on production systems +server-id = 112233 +log_bin = mysql-bin +binlog_format = row +log_bin_compress = off \ No newline at end of file diff --git a/sink-connector-lightweight/src/test/resources/mysql_database_override_initial.sql b/sink-connector-lightweight/src/test/resources/mysql_database_override_initial.sql new file mode 100644 index 000000000..0abbaee4b --- /dev/null +++ b/sink-connector-lightweight/src/test/resources/mysql_database_override_initial.sql @@ -0,0 +1,10 @@ +create table `newtable`(col1 varchar(255) not null, col2 int, col3 int, primary key(col1)); +insert into newtable values('a', 1, 1); + +create database products; +create table products.prodtable(col1 varchar(255) not null, col2 int, col3 int, primary key(col1)); + +insert into products.prodtable values('a', 1, 1); +create database customers; +create table customers.custtable(col1 varchar(255) not null, col2 int, col3 int, primary key(col1)); +insert into customers.custtable values('a', 1, 1); \ No newline at end of file diff --git a/sink-connector-lightweight/tests/integration/configs/clickhouse/config.xml b/sink-connector-lightweight/tests/integration/configs/clickhouse/config.xml index b7d072453..1abae988f 100644 --- a/sink-connector-lightweight/tests/integration/configs/clickhouse/config.xml +++ b/sink-connector-lightweight/tests/integration/configs/clickhouse/config.xml @@ -69,7 +69,7 @@ 300 - 100 + 250 diff --git a/sink-connector-lightweight/tests/integration/helpers/cluster.py b/sink-connector-lightweight/tests/integration/helpers/cluster.py index 717def898..fd6063c88 100755 --- a/sink-connector-lightweight/tests/integration/helpers/cluster.py +++ b/sink-connector-lightweight/tests/integration/helpers/cluster.py @@ -596,14 +596,15 @@ def up(self, timeout=30 * 60): "IMAGE_DEPENDENCY_PROXY", "" ) self.environ["COMPOSE_HTTP_TIMEOUT"] = "300" - self.environ[ - "CLICKHOUSE_TESTS_SERVER_BIN_PATH" - ] = self.clickhouse_binary_path - self.environ[ - "CLICKHOUSE_TESTS_ODBC_BRIDGE_BIN_PATH" - ] = self.clickhouse_odbc_bridge_binary_path or os.path.join( - os.path.dirname(self.clickhouse_binary_path), - "clickhouse-odbc-bridge", + self.environ["CLICKHOUSE_TESTS_SERVER_BIN_PATH"] = ( + self.clickhouse_binary_path + ) + self.environ["CLICKHOUSE_TESTS_ODBC_BRIDGE_BIN_PATH"] = ( + self.clickhouse_odbc_bridge_binary_path + or os.path.join( + os.path.dirname(self.clickhouse_binary_path), + "clickhouse-odbc-bridge", + ) ) self.environ["CLICKHOUSE_TESTS_DIR"] = self.configs_dir diff --git a/sink-connector-lightweight/tests/integration/helpers/default_config.py b/sink-connector-lightweight/tests/integration/helpers/default_config.py index fd873999f..af92e9329 100644 --- a/sink-connector-lightweight/tests/integration/helpers/default_config.py +++ b/sink-connector-lightweight/tests/integration/helpers/default_config.py @@ -47,4 +47,5 @@ "database.serverTimezone": "UTC", "clickhouse.datetime.timezone": "UTC", "auto.create.tables": "true", + "ddl.retry": "true", } diff --git a/sink-connector-lightweight/tests/integration/regression_auto.py b/sink-connector-lightweight/tests/integration/regression_auto.py index 39dec83a9..b765dff15 100755 --- a/sink-connector-lightweight/tests/integration/regression_auto.py +++ b/sink-connector-lightweight/tests/integration/regression_auto.py @@ -311,10 +311,10 @@ def regression( run=load("tests.schema_only", "module"), ) Feature( - run=load("tests.sink_cli_commands", "module"), + run=load("tests.multiple_databases", "module"), ) Feature( - run=load("tests.multiple_databases", "module"), + run=load("tests.sink_cli_commands", "module"), ) diff --git a/sink-connector-lightweight/tests/integration/regression_auto_replicated.py b/sink-connector-lightweight/tests/integration/regression_auto_replicated.py index 7757ab5ff..6b423fbb9 100755 --- a/sink-connector-lightweight/tests/integration/regression_auto_replicated.py +++ b/sink-connector-lightweight/tests/integration/regression_auto_replicated.py @@ -137,6 +137,10 @@ Skip, "Test requires fixing.", ), + "/mysql to clickhouse replication/auto replicated table creation/multiple databases/source destination overrides": ( + Skip, + "https://github.com/Altinity/clickhouse-sink-connector/issues/874.", + ), } xflags = {} diff --git a/sink-connector-lightweight/tests/integration/requirements.txt b/sink-connector-lightweight/tests/integration/requirements.txt index fdac85095..7177ca226 100644 --- a/sink-connector-lightweight/tests/integration/requirements.txt +++ b/sink-connector-lightweight/tests/integration/requirements.txt @@ -1,4 +1,4 @@ -testflows==2.1.5 +testflows==2.4.10 python-dateutil==2.9.0 numpy==1.26.4 pyarrow==16.1.0 diff --git a/sink-connector-lightweight/tests/integration/tests/alter.py b/sink-connector-lightweight/tests/integration/tests/alter.py index 7cf6b234d..467f6675d 100644 --- a/sink-connector-lightweight/tests/integration/tests/alter.py +++ b/sink-connector-lightweight/tests/integration/tests/alter.py @@ -85,7 +85,7 @@ def add_column(self, node=None): delay=5, )( f"DESC test.{table_name} FORMAT CSV", - message='"new_col","Nullable(String)"', + message='"new_col","String"', ) @@ -232,7 +232,7 @@ def add_column_first_after(self, node=None): delay=5, )( f"DESC test.{table_name} FORMAT CSV", - message='"new_col","Nullable(String)","","","","",""\n"second_col","Nullable(String)","","","","",""\n"id","Int32","","","","",""', + message='"new_col","String","","","","",""\n"second_col","String","","","","",""\n"id","Int32","","","","",""', ) diff --git a/sink-connector-lightweight/tests/integration/tests/compound_alters.py b/sink-connector-lightweight/tests/integration/tests/compound_alters.py index 82b83c1da..0c03fe14a 100644 --- a/sink-connector-lightweight/tests/integration/tests/compound_alters.py +++ b/sink-connector-lightweight/tests/integration/tests/compound_alters.py @@ -80,7 +80,7 @@ def add_change_column(self, node=None): delay=5, )( f"DESC test.{table_name} FORMAT CSV", - message='"new_col","Nullable(String)","","","","",""\n"x2","Nullable(String)"', + message='"new_col","String","","","","",""\n"x2","Nullable(String)"', ) @@ -125,7 +125,7 @@ def change_add_column(self, node=None): delay=5, )( f"DESC test.{table_name} FORMAT CSV", - message='"new_col","Nullable(String)","","","","",""\n"x2","Nullable(String)"', + message='"new_col","String","","","","",""\n"x2","Nullable(String)"', ) @@ -169,7 +169,7 @@ def add_modify_column(self, node=None): delay=5, )( f"DESC test.{table_name} FORMAT CSV", - message='"new_col","Nullable(String)","","","","",""\n"x","Nullable(String)"', + message='"new_col","String","","","","",""\n"x","Nullable(String)"', ) @@ -214,7 +214,7 @@ def modify_add_column(self, node=None): delay=5, )( f"DESC test.{table_name} FORMAT CSV", - message='"new_col","Nullable(String)","","","","",""\n"x","Nullable(String)"', + message='"new_col","String","","","","",""\n"x","Nullable(String)"', ) @@ -258,7 +258,7 @@ def add_rename_column(self, node=None): delay=5, )( f"DESC test.{table_name} FORMAT CSV", - message='"new_col","Nullable(String)","","","","",""\n"x2"', + message='"new_col","String","","","","",""\n"x2"', ) @@ -305,8 +305,8 @@ def multiple_add_column(self, node=None): delay=5, )( f"DESC test.{table_name} FORMAT CSV", - message='"new_col3","Nullable(String)","","","","",""\n"new_col2","Nullable(String)","",""' - ',"","",""\n"new_col1","Nullable(String)"', + message='"new_col3","String","","","","",""\n"new_col2","String","",""' + ',"","",""\n"new_col1","String"', ) @@ -355,8 +355,8 @@ def multiple_modify_column(self, node=None): delay=5, )( f"DESC test.{table_name} FORMAT CSV", - message='"new_col3","Nullable(String)","","","","",""\n"new_col2","Nullable(String)","",""' - ',"","",""\n"new_col1","Nullable(String)"', + message='"new_col3","String","","","","",""\n"new_col2","String","",""' + ',"","",""\n"new_col1","String"', ) with And( @@ -427,8 +427,8 @@ def multiple_change_column(self, node=None): delay=5, )( f"DESC test.{table_name} FORMAT CSV", - message='"new_col3","Nullable(String)","","","","",""\n"new_col2","Nullable(String)","",""' - ',"","",""\n"new_col1","Nullable(String)"', + message='"new_col3","String","","","","",""\n"new_col2","String","",""' + ',"","",""\n"new_col1","String"', ) with And( @@ -499,8 +499,8 @@ def multiple_drop_column(self, node=None): delay=5, )( f"DESC test.{table_name} FORMAT CSV", - message='"new_col3","Nullable(String)","","","","",""\n"new_col2","Nullable(String)","",""' - ',"","",""\n"new_col1","Nullable(String)"', + message='"new_col3","String","","","","",""\n"new_col2","String","",""' + ',"","",""\n"new_col1","String"', ) with And( diff --git a/sink-connector-lightweight/tests/integration/tests/is_deleted.py b/sink-connector-lightweight/tests/integration/tests/is_deleted.py index b35e722aa..4ba56fd0c 100644 --- a/sink-connector-lightweight/tests/integration/tests/is_deleted.py +++ b/sink-connector-lightweight/tests/integration/tests/is_deleted.py @@ -16,9 +16,9 @@ def create_table_with_is_deleted( clickhouse_node = self.context.clickhouse_node if not backticks: - columns = "col1 varchar(255), col2 int, " + columns = r"col1 varchar(255), col2 int, " else: - columns = "\`col1\` varchar(255), \`col2\` int, " + columns = r"\`col1\` varchar(255), \`col2\` int, " with By( f"creating a {table_name} table with is_deleted column and {datatype} datatype" @@ -196,7 +196,7 @@ def column_with_is_deleted_backticks(self): table_name = "tb_" + getuid() with Given(f"I create the {table_name} table and populate it with data"): - create_table_with_is_deleted(table_name=table_name, column="\`is_deleted\`") + create_table_with_is_deleted(table_name=table_name, column=r"\`is_deleted\`") with Then("I check that the data was inserted correctly into the ClickHouse table"): for retry in retries(timeout=40, delay=1): diff --git a/sink-connector-lightweight/tests/integration/tests/multiple_databases.py b/sink-connector-lightweight/tests/integration/tests/multiple_databases.py index 9fe2dc1ae..b8f10b42e 100644 --- a/sink-connector-lightweight/tests/integration/tests/multiple_databases.py +++ b/sink-connector-lightweight/tests/integration/tests/multiple_databases.py @@ -14,8 +14,6 @@ change_column, modify_column, drop_column, - add_primary_key, - drop_primary_key, ) @@ -592,26 +590,6 @@ def drop_column_on_a_database(self, database): check_column(table_name=table_name, database=database, column_name="") -@TestScenario -@Requirements( - RQ_SRS_030_ClickHouse_MySQLToClickHouseReplication_PrimaryKey_Simple("1.0") -) -def add_primary_key_on_a_database(self, database): - """Check that the primary key is added to the table when we add a primary key on a database.""" - table_name = f"table_{getuid()}" - column = "col1" - - with Given("I create a table on multiple databases"): - create_table_and_insert_values(table_name=table_name, database_name=database) - - with When("I add a primary key on the table"): - drop_primary_key(table_name=table_name, database=database) - add_primary_key(table_name=table_name, database=database, column_name=column) - - with Then("I check that the primary key was added to the table"): - check_column(table_name=table_name, database=database, column_name=column) - - @TestOutline def check_different_database_names(self, database_map): """Check that the tables are replicated when we have source and destination databases with different names.""" @@ -641,10 +619,10 @@ def different_database_names(self): def different_database_names_with_source_backticks(self): """Check that the tables are replicated when we have source and destination databases with different names and source database name contains backticks.""" database_map = { - "\`mysql1\`": "ch1", - "\`mysql2\`": "ch2", - "\`mysql3\`": "ch3", - "\`mysql4\`": "ch4", + r"\`mysql1\`": r"ch1", + r"\`mysql2\`": r"ch2", + r"\`mysql3\`": r"ch3", + r"\`mysql4\`": r"ch4", } check_different_database_names(database_map=database_map) @@ -653,10 +631,10 @@ def different_database_names_with_source_backticks(self): def different_database_names_with_destination_backticks(self): """Check that the tables are replicated when we have source and destination databases with different names and destination database name contains backticks.""" database_map = { - "mysql1": "\`ch1\`", - "mysql2": "\`ch2\`", - "mysql3": "\`ch3\`", - "mysql4": "\`ch4\`", + r"mysql1": r"\`ch1\`", + r"mysql2": r"\`ch2\`", + r"mysql3": r"\`ch3\`", + r"mysql4": r"\`ch4\`", } check_different_database_names(database_map=database_map) @@ -665,10 +643,10 @@ def different_database_names_with_destination_backticks(self): def different_database_names_with_backticks(self): """Check that the tables are replicated when we have source and destination databases with the same names and they contain backticks.""" database_map = { - "\`mysql1\`": "\`ch1\`", - "\`mysql2\`": "\`ch2\`", - "\`mysql3\`": "\`ch3\`", - "\`mysql4\`": "\`ch4\`", + r"\`mysql1\`": r"\`ch1\`", + r"\`mysql2\`": r"\`ch2\`", + r"\`mysql3\`": r"\`ch3\`", + r"\`mysql4\`": r"\`ch4\`", } check_different_database_names(database_map=database_map) @@ -755,7 +733,6 @@ def check_alters_on_different_databases(self): change_column_on_a_database, modify_column_on_a_database, drop_column_on_a_database, - add_primary_key_on_a_database, ] check_alters( diff --git a/sink-connector-lightweight/tests/integration/tests/steps/clickhouse.py b/sink-connector-lightweight/tests/integration/tests/steps/clickhouse.py index 96558e9d6..0f4555b28 100644 --- a/sink-connector-lightweight/tests/integration/tests/steps/clickhouse.py +++ b/sink-connector-lightweight/tests/integration/tests/steps/clickhouse.py @@ -17,9 +17,15 @@ def drop_database(self, database_name=None, node=None): @TestStep(Then) def check_column( - self, table_name, column_name, node=None, column_type=None, database=None + self, + table_name, + column_name, + node=None, + column_type=None, + database=None, + is_primary_key=False, ): - """Check if column exists in ClickHouse table.""" + """Check if column exists in ClickHouse table and optionally verify if it is the primary key.""" if database is None: database = "test" @@ -51,6 +57,14 @@ def check_column( assert column.output.strip() == expected_output, error() + if is_primary_key: + primary_key = node.query( + f"SELECT is_in_primary_key FROM system.columns WHERE database = '{database}' AND table = '{table_name}' AND name = '{column_name}' LIMIT 1 FORMAT TabSeparated" + ) + assert primary_key.output.strip() == 1, error( + f"Column {column_name} is not a primary key" + ) + @TestStep(Given) def create_clickhouse_database(self, name=None, node=None): diff --git a/sink-connector-lightweight/tests/integration/tests/steps/mysql.py b/sink-connector-lightweight/tests/integration/tests/steps/mysql.py index e451522e1..689deebdd 100644 --- a/sink-connector-lightweight/tests/integration/tests/steps/mysql.py +++ b/sink-connector-lightweight/tests/integration/tests/steps/mysql.py @@ -317,7 +317,7 @@ def insert(self, table_name, values, node=None, database_name=None): node = self.context.cluster.node("mysql-master") with When("I insert data into MySQL table"): - node.query(f"INSERT INTO {database_name}.\`{table_name}\` VALUES ({values});") + node.query(rf"INSERT INTO {database_name}.\`{table_name}\` VALUES ({values});") @TestStep(Given) diff --git a/sink-connector/pom.xml b/sink-connector/pom.xml index 16fb6dc68..1c8d99a09 100644 --- a/sink-connector/pom.xml +++ b/sink-connector/pom.xml @@ -42,6 +42,7 @@ 11 1.19.1 5.2.1 + 0.6.0 @@ -149,7 +150,7 @@ maven-surefire-plugin 2.22.2 - 0 + 0 false **/*IT.java @@ -308,7 +309,7 @@ io.debezium debezium-core - 2.7.0.Beta2 + 2.7.2.Final @@ -334,7 +335,7 @@ com.clickhouse clickhouse-jdbc - 0.6.0 + ${clickhouse.jdbc.version} http @@ -421,6 +422,12 @@ compile + + org.locationtech.jts + jts-core + 1.18.2 + + org.testcontainers testcontainers @@ -448,7 +455,7 @@ com.clickhouse clickhouse-jdbc - 0.6.0 + ${clickhouse.jdbc.version} compile diff --git a/sink-connector/src/main/java/com/altinity/clickhouse/sink/connector/ClickHouseSinkConnectorConfig.java b/sink-connector/src/main/java/com/altinity/clickhouse/sink/connector/ClickHouseSinkConnectorConfig.java index 0335c1257..11a9a0bd7 100644 --- a/sink-connector/src/main/java/com/altinity/clickhouse/sink/connector/ClickHouseSinkConnectorConfig.java +++ b/sink-connector/src/main/java/com/altinity/clickhouse/sink/connector/ClickHouseSinkConnectorConfig.java @@ -450,6 +450,16 @@ static ConfigDef newConfigDef() { 6, ConfigDef.Width.NONE, ClickHouseSinkConnectorConfigVariables.MAX_QUEUE_SIZE.toString()) + .define( + ClickHouseSinkConnectorConfigVariables.SINGLE_THREADED.toString(), + Type.BOOLEAN, + false, + Importance.HIGH, + "Single threaded mode", + CONFIG_GROUP_CONNECTOR_CONFIG, + 6, + ConfigDef.Width.NONE, + ClickHouseSinkConnectorConfigVariables.SINGLE_THREADED.toString()) .define( ClickHouseSinkConnectorConfigVariables.REPLICA_STATUS_VIEW.toString(), Type.STRING, diff --git a/sink-connector/src/main/java/com/altinity/clickhouse/sink/connector/ClickHouseSinkConnectorConfigVariables.java b/sink-connector/src/main/java/com/altinity/clickhouse/sink/connector/ClickHouseSinkConnectorConfigVariables.java index 20001e092..b4fddc77c 100644 --- a/sink-connector/src/main/java/com/altinity/clickhouse/sink/connector/ClickHouseSinkConnectorConfigVariables.java +++ b/sink-connector/src/main/java/com/altinity/clickhouse/sink/connector/ClickHouseSinkConnectorConfigVariables.java @@ -73,7 +73,9 @@ public enum ClickHouseSinkConnectorConfigVariables { JDBC_PARAMETERS("clickhouse.jdbc.params"), REPLICA_STATUS_VIEW("replica.status.view"), - MAX_QUEUE_SIZE("sink.connector.max.queue.size"); + MAX_QUEUE_SIZE("sink.connector.max.queue.size"), + + SINGLE_THREADED("single.threaded"); private String label; diff --git a/sink-connector/src/main/java/com/altinity/clickhouse/sink/connector/common/Utils.java b/sink-connector/src/main/java/com/altinity/clickhouse/sink/connector/common/Utils.java index 5489c9503..4851a1fd0 100644 --- a/sink-connector/src/main/java/com/altinity/clickhouse/sink/connector/common/Utils.java +++ b/sink-connector/src/main/java/com/altinity/clickhouse/sink/connector/common/Utils.java @@ -58,15 +58,16 @@ public static Map parseSourceToDestinationDatabaseMap(String inp String srcDatabase = tt[0].trim(); String dstDatabase = tt[1].trim(); - if (!isValidDatabaseName(srcDatabase)) { - LOGGER.error( - Logging.logMessage( - "database name{} should have at least 2 " - + "characters, start with _a-zA-Z, and only contains " - + "_$a-zA-z0-9", - srcDatabase)); - isInvalid = true; - } + // Disable validation of source database. +// if (!isValidDatabaseName(srcDatabase)) { +// LOGGER.error( +// Logging.logMessage( +// "database name{} should have at least 2 " +// + "characters, start with _a-zA-Z, and only contains " +// + "_$a-zA-z0-9", +// srcDatabase)); +// isInvalid = true; +// } if (!isValidDatabaseName(dstDatabase)) { LOGGER.error( @@ -185,7 +186,11 @@ public static boolean isValidDatabaseName(String dbName) { // Check the remaining characters for (int i = 1; i < dbName.length(); i++) { char ch = dbName.charAt(i); - if (!(Character.isLetterOrDigit(ch) || ch == '_' || ch == '.')) { + // If character is a underscore, continue + if(ch == '_') { + continue; + } + if (!(Character.isLetterOrDigit(ch) || ch == '.')) { return false; } } diff --git a/sink-connector/src/main/java/com/altinity/clickhouse/sink/connector/converters/ClickHouseDataTypeMapper.java b/sink-connector/src/main/java/com/altinity/clickhouse/sink/connector/converters/ClickHouseDataTypeMapper.java index 79bf2218d..415895659 100644 --- a/sink-connector/src/main/java/com/altinity/clickhouse/sink/connector/converters/ClickHouseDataTypeMapper.java +++ b/sink-connector/src/main/java/com/altinity/clickhouse/sink/connector/converters/ClickHouseDataTypeMapper.java @@ -4,11 +4,14 @@ import com.altinity.clickhouse.sink.connector.ClickHouseSinkConnectorConfigVariables; import com.clickhouse.data.ClickHouseDataType; import com.clickhouse.data.value.ClickHouseDoubleValue; +import com.clickhouse.data.value.ClickHouseGeoPolygonValue; +import com.clickhouse.data.value.ClickHouseGeoPointValue; import com.google.common.io.BaseEncoding; import io.debezium.data.*; import io.debezium.data.Enum; import io.debezium.data.EnumSet; import io.debezium.data.geometry.Geometry; +import io.debezium.data.geometry.Point; import io.debezium.time.*; import io.debezium.time.Date; import org.apache.commons.lang3.tuple.MutablePair; @@ -16,6 +19,12 @@ import org.apache.kafka.connect.data.Schema; import org.apache.kafka.connect.data.Struct; +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.LinearRing; +import org.locationtech.jts.geom.Polygon; +import org.locationtech.jts.io.ParseException; +import org.locationtech.jts.io.WKBReader; + import java.math.BigDecimal; import java.math.BigInteger; import java.nio.ByteBuffer; @@ -87,7 +96,10 @@ public class ClickHouseDataTypeMapper { dataTypesMap.put(new MutablePair<>(Schema.STRING_SCHEMA.type(), EnumSet.LOGICAL_NAME), ClickHouseDataType.String); // Geometry -> Geometry - dataTypesMap.put(new MutablePair<>(Schema.Type.STRUCT, Geometry.LOGICAL_NAME), ClickHouseDataType.String); + dataTypesMap.put(new MutablePair<>(Schema.Type.STRUCT, Geometry.LOGICAL_NAME), ClickHouseDataType.Polygon); + + // Point -> Point + dataTypesMap.put(new MutablePair<>(Schema.Type.STRUCT, Point.LOGICAL_NAME), ClickHouseDataType.Point); // PostgreSQL UUID -> UUID dataTypesMap.put(new MutablePair<>(Schema.Type.STRING, Uuid.LOGICAL_NAME), ClickHouseDataType.UUID); @@ -224,18 +236,93 @@ else if (value instanceof Long) { } } - } else if (type == Schema.Type.STRUCT && schemaName.equalsIgnoreCase(Geometry.LOGICAL_NAME)) { - // Geometry + } else if (type == Schema.Type.STRUCT && schemaName.equalsIgnoreCase(Geometry.LOGICAL_NAME)) { + // Handle Geometry type (e.g., Polygon) if (value instanceof Struct) { Struct geometryValue = (Struct) value; Object wkbValue = geometryValue.get("wkb"); - if(wkbValue != null) { - ps.setString(index, BaseEncoding.base16().lowerCase().encode(((ByteBuffer) wkbValue).array())); + + byte[] wkbBytes; + if (wkbValue instanceof byte[]) { + wkbBytes = (byte[]) wkbValue; + } else if (wkbValue instanceof ByteBuffer) { + ByteBuffer byteBuffer = (ByteBuffer) wkbValue; + wkbBytes = new byte[byteBuffer.remaining()]; + byteBuffer.get(wkbBytes); + } else { + // Set an empty polygon if WKB value is not available + ps.setObject(index, ClickHouseGeoPolygonValue.ofEmpty()); + return true; + } + + // Parse the WKB bytes using JTS WKBReader + WKBReader wkbReader = new WKBReader(); + org.locationtech.jts.geom.Geometry geometry; + try { + geometry = wkbReader.read(wkbBytes); + } catch (ParseException e) { + // If parsing fails, insert an empty polygon + ps.setObject(index, ClickHouseGeoPolygonValue.ofEmpty()); + return true; + } + + // Check if geometry is a Polygon + if (geometry instanceof Polygon) { + Polygon polygon = (Polygon) geometry; + + // Convert the polygon into double[][][] format + List rings = new ArrayList<>(); + + // Exterior ring + Coordinate[] exteriorCoords = polygon.getExteriorRing().getCoordinates(); + double[][] exteriorPoints = new double[exteriorCoords.length][2]; + for (int i = 0; i < exteriorCoords.length; i++) { + exteriorPoints[i][0] = exteriorCoords[i].getX(); + exteriorPoints[i][1] = exteriorCoords[i].getY(); + } + rings.add(exteriorPoints); + + // Interior rings (holes), if any + int numInteriorRings = polygon.getNumInteriorRing(); + for (int i = 0; i < numInteriorRings; i++) { + Coordinate[] interiorCoords = polygon.getInteriorRingN(i).getCoordinates(); + double[][] interiorPoints = new double[interiorCoords.length][2]; + for (int j = 0; j < interiorCoords.length; j++) { + interiorPoints[j][0] = interiorCoords[j].getX(); + interiorPoints[j][1] = interiorCoords[j].getY(); + } + rings.add(interiorPoints); + } + + // Convert the list of rings to double[][][] + double[][][] polygonCoordinates = rings.toArray(new double[rings.size()][][]); + + // Create ClickHouseGeoPolygonValue + ClickHouseGeoPolygonValue geoPolygonValue = ClickHouseGeoPolygonValue.of(polygonCoordinates); + + // Set the string into PreparedStatement + ps.setObject(index, geoPolygonValue); } else { - ps.setString(index, ""); + // If geometry is not a Polygon, insert an empty polygon + ps.setObject(index, ClickHouseGeoPolygonValue.ofEmpty()); } } else { - ps.setString(index, ""); + // If value is not a Struct, insert an empty polygon + ps.setString(index, ClickHouseGeoPolygonValue.ofEmpty().asString()); + } + } else if (type == Schema.Type.STRUCT && schemaName.equalsIgnoreCase(Point.LOGICAL_NAME)) { + // Handle Point type (ClickHouse expects (longitude, latitude)) + if (value instanceof Struct) { + Struct pointValue = (Struct) value; + Object xValue = pointValue.get("x"); + Object yValue = pointValue.get("y"); + + double[] point = {(Double) xValue, (Double) yValue}; + + ps.setObject(index, ClickHouseGeoPointValue.of(point)); + } else { + // If the value is not a valid Struct for a Point, set an empty point + ps.setObject(index, ClickHouseGeoPointValue.ofOrigin()); } } else if (type == Schema.Type.STRUCT && schemaName.equalsIgnoreCase(VariableScaleDecimal.LOGICAL_NAME)) { if (value instanceof Struct) { diff --git a/sink-connector/src/main/java/com/altinity/clickhouse/sink/connector/db/DBMetadata.java b/sink-connector/src/main/java/com/altinity/clickhouse/sink/connector/db/DBMetadata.java index fbccff1ea..e24977722 100644 --- a/sink-connector/src/main/java/com/altinity/clickhouse/sink/connector/db/DBMetadata.java +++ b/sink-connector/src/main/java/com/altinity/clickhouse/sink/connector/db/DBMetadata.java @@ -1,6 +1,8 @@ package com.altinity.clickhouse.sink.connector.db; import static com.altinity.clickhouse.sink.connector.db.ClickHouseDbConstants.CHECK_DB_EXISTS_SQL; + +import com.altinity.clickhouse.sink.connector.ClickHouseSinkConnectorConfig; import com.clickhouse.jdbc.ClickHouseConnection; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.tuple.MutablePair; @@ -12,9 +14,8 @@ import java.sql.SQLException; import java.sql.Statement; import java.time.ZoneId; -import java.util.LinkedHashMap; -import java.util.Map; -import java.util.TimeZone; +import java.util.*; + public class DBMetadata { @@ -272,16 +273,44 @@ public boolean checkIfNewReplacingMergeTree(String currentClickHouseVersion) thr } + /** + * Function to get the column name and isNullable as key/value pair. + */ + public Map getColumnsIsNullableForTable(String tableName, + ClickHouseConnection conn, + String database) throws SQLException { + Map columnsIsNullable = new HashMap<>(); + // Execute the following query to get the column name and isNullable as key/value pair. + String query = String.format("SELECT name AS column_name, type LIKE 'Nullable(%%' AS is_nullable FROM system.columns WHERE (table = '%s') AND (database = '%s')", tableName, database); + try (Statement stmt = conn.createStatement()) { + ResultSet rs = stmt.executeQuery(query); + while (rs.next()) { + String columnName = rs.getString("column_name"); + boolean isNullable = rs.getBoolean("is_nullable"); + columnsIsNullable.put(columnName, isNullable); + } + } + + return columnsIsNullable; + } + /** * Function that uses the DatabaseMetaData JDBC functionality * to get the column name and column data type as key/value pair. */ public Map getColumnsDataTypesForTable(String tableName, ClickHouseConnection conn, - String database) { + String database, + ClickHouseSinkConnectorConfig config) { + Set aliasColumns = new HashSet<>(); + try { + aliasColumns = new DBMetadata().getAliasAndMaterializedColumnsForTableAndDatabase(tableName, database, conn); + } catch(Exception e) { + log.error("Error getting alias columns", e); + } LinkedHashMap result = new LinkedHashMap<>(); try { if (conn == null) { @@ -308,6 +337,10 @@ public Map getColumnsDataTypesForTable(String tableName, if(isGeneratedColumn != null && isGeneratedColumn.equalsIgnoreCase("YES")) { continue; } + if(aliasColumns.contains(columnName)) { + log.debug("Skipping alias column: " + columnName); + continue; + } result.put(columnName, typeName); } } catch (SQLException sq) { @@ -329,4 +362,29 @@ public ZoneId getServerTimeZone(ClickHouseConnection conn) { return result; } + + /** + * Function to get the column names which are + * @return + */ + public Set getAliasAndMaterializedColumnsForTableAndDatabase(String tableName, String databaseName, + ClickHouseConnection conn) throws SQLException { + + Set aliasColumns = new HashSet<>(); + String query = "SELECT name FROM system.columns WHERE (table = '%s') AND (database = '%s') and " + + "(default_kind='ALIAS' or default_kind='MATERIALIZED')"; + String formattedQuery = String.format(query, tableName, databaseName); + + // Execute query + ResultSet rs = conn.createStatement().executeQuery(formattedQuery); + + // Get the list of columns from rs. + if(rs != null) { + while (rs.next()) { + String response = rs.getString(1); + aliasColumns.add(response); + } + } + return aliasColumns; + } } diff --git a/sink-connector/src/main/java/com/altinity/clickhouse/sink/connector/db/DbKafkaOffsetWriter.java b/sink-connector/src/main/java/com/altinity/clickhouse/sink/connector/db/DbKafkaOffsetWriter.java index b57020209..98b9dc5c7 100644 --- a/sink-connector/src/main/java/com/altinity/clickhouse/sink/connector/db/DbKafkaOffsetWriter.java +++ b/sink-connector/src/main/java/com/altinity/clickhouse/sink/connector/db/DbKafkaOffsetWriter.java @@ -37,7 +37,7 @@ public DbKafkaOffsetWriter( createOffsetTable(); this.columnNamesToDataTypesMap = new DBMetadata().getColumnsDataTypesForTable(tableName, this.getConnection(), - database); + database, config); this.query = new QueryFormatter().getInsertQueryUsingInputFunction(tableName, columnNamesToDataTypesMap); } diff --git a/sink-connector/src/main/java/com/altinity/clickhouse/sink/connector/db/DbWriter.java b/sink-connector/src/main/java/com/altinity/clickhouse/sink/connector/db/DbWriter.java index 3fd0a12ef..457532268 100644 --- a/sink-connector/src/main/java/com/altinity/clickhouse/sink/connector/db/DbWriter.java +++ b/sink-connector/src/main/java/com/altinity/clickhouse/sink/connector/db/DbWriter.java @@ -82,17 +82,13 @@ public DbWriter( try { if (this.conn != null) { // Order of the column names and the data type has to match. - this.columnNameToDataTypeMap = new DBMetadata().getColumnsDataTypesForTable(tableName, this.conn, database); + this.columnNameToDataTypeMap = new DBMetadata().getColumnsDataTypesForTable(tableName, this.conn, + database, config); } - DBMetadata metadata = new DBMetadata(); - try { - if (false == metadata.checkIfDatabaseExists(this.conn, database)) { - new ClickHouseCreateDatabase().createNewDatabase(this.conn, database); - } - } catch(Exception e) { - log.error("Error creating Database: " + database); - } + + createOffsetSchemaHistoryDatabase(); + MutablePair response = metadata.getTableEngine(this.conn, database, tableName); this.engine = response.getLeft(); @@ -130,7 +126,7 @@ public DbWriter( log.error("********* AUTO CREATE DISABLED, Table does not exist, please enable it by setting auto.create.tables=true"); } - this.columnNameToDataTypeMap = new DBMetadata().getColumnsDataTypesForTable(tableName, this.conn, database); + this.columnNameToDataTypeMap = new DBMetadata().getColumnsDataTypesForTable(tableName, this.conn, database, config); response = metadata.getTableEngine(this.conn, database, tableName); this.engine = response.getLeft(); } @@ -158,8 +154,42 @@ public DbWriter( } } + // Create offset/schema history storage database. + public void createOffsetSchemaHistoryDatabase() { + DBMetadata metadata = new DBMetadata(); + try { + if (false == metadata.checkIfDatabaseExists(this.conn, database)) { + new ClickHouseCreateDatabase().createNewDatabase(this.conn, database); + } + } catch(Exception e) { + + int maxRetries = 0; + final int MAX_RETRIES = 5; + log.error("Error creating Database: " + database); + + // Keep retrying to createNewDatabase until Max number of retries is reached. + boolean createDatabaseFailed = false; + while(maxRetries++ > MAX_RETRIES) { + try { + Thread.sleep(maxRetries * 5000); + if (false == metadata.checkIfDatabaseExists(this.conn, database)) { + new ClickHouseCreateDatabase().createNewDatabase(this.conn, database); + createDatabaseFailed = true; + break; + } + } catch (Exception ex) { + log.error("Retry Number: " + maxRetries + "of" + MAX_RETRIES + " Error creating Database: " + database); + } + } + // if maxRetries exceeded, throw runtime exception. + if(createDatabaseFailed == false) { + throw new RuntimeException("Error creating Database: " + database); + } + } + } + public void updateColumnNameToDataTypeMap() throws SQLException { - this.columnNameToDataTypeMap = new DBMetadata().getColumnsDataTypesForTable(tableName, this.conn, database); + this.columnNameToDataTypeMap = new DBMetadata().getColumnsDataTypesForTable(tableName, this.conn, database, config); MutablePair response = new DBMetadata().getTableEngine(this.conn, database, tableName); this.engine = response.getLeft(); } diff --git a/sink-connector/src/main/java/com/altinity/clickhouse/sink/connector/db/batch/GroupInsertQueryWithBatchRecords.java b/sink-connector/src/main/java/com/altinity/clickhouse/sink/connector/db/batch/GroupInsertQueryWithBatchRecords.java index 69f1dafbe..d652920e7 100644 --- a/sink-connector/src/main/java/com/altinity/clickhouse/sink/connector/db/batch/GroupInsertQueryWithBatchRecords.java +++ b/sink-connector/src/main/java/com/altinity/clickhouse/sink/connector/db/batch/GroupInsertQueryWithBatchRecords.java @@ -47,7 +47,9 @@ public boolean groupQueryWithRecords(List records, Iterator iterator = records.iterator(); while (iterator.hasNext()) { ClickHouseStruct record = (ClickHouseStruct) iterator.next(); - updatePartitionOffsetMap(partitionToOffsetMap, record.getKafkaPartition(), record.getTopic(), record.getKafkaOffset()); + if(record != null && record.getKafkaPartition() != null && record.getTopic() != null ) { + updatePartitionOffsetMap(partitionToOffsetMap, record.getKafkaPartition(), record.getTopic(), record.getKafkaOffset()); + } boolean enableSchemaEvolution = config.getBoolean(ClickHouseSinkConnectorConfigVariables.ENABLE_SCHEMA_EVOLUTION.toString()); if(CdcRecordState.CDC_RECORD_STATE_BEFORE == getCdcSectionBasedOnOperation(record.getCdcOperation())) { @@ -56,13 +58,13 @@ public boolean groupQueryWithRecords(List records, if(enableSchemaEvolution) { try { new ClickHouseAlterTable().alterTable(record.getAfterStruct().schema().fields(), tableName, connection, columnNameToDataTypeMap); - columnNameToDataTypeMap = new DBMetadata().getColumnsDataTypesForTable(tableName, connection, databaseName); + columnNameToDataTypeMap = new DBMetadata().getColumnsDataTypesForTable(tableName, connection, databaseName, config); } catch(Exception e) { log.error("**** ERROR ALTER TABLE: " + tableName, e); } } - + columnNameToDataTypeMap = new DBMetadata().getColumnsDataTypesForTable(tableName, connection, databaseName, config ); result = updateQueryToRecordsMap(record, record.getAfterModifiedFields(), queryToRecordsMap, tableName, config, columnNameToDataTypeMap); } else if(CdcRecordState.CDC_RECORD_STATE_BOTH == getCdcSectionBasedOnOperation(record.getCdcOperation())) { if(record.getBeforeModifiedFields() != null) { diff --git a/sink-connector/src/main/java/com/altinity/clickhouse/sink/connector/executor/ClickHouseBatchRunnable.java b/sink-connector/src/main/java/com/altinity/clickhouse/sink/connector/executor/ClickHouseBatchRunnable.java index 006a6eec5..7d199b2fc 100644 --- a/sink-connector/src/main/java/com/altinity/clickhouse/sink/connector/executor/ClickHouseBatchRunnable.java +++ b/sink-connector/src/main/java/com/altinity/clickhouse/sink/connector/executor/ClickHouseBatchRunnable.java @@ -152,6 +152,7 @@ public void run() { if(currentBatch == null) { // No records in the queue. continue; + //Thread.sleep(config.getLong(ClickHouseSinkConnectorConfigVariables.BUFFER_FLUSH_TIME.toString())); } } else { log.debug("***** RETRYING the same batch again"); @@ -198,6 +199,7 @@ public void run() { currentBatch = null; } } + Thread.sleep(config.getLong(ClickHouseSinkConnectorConfigVariables.BUFFER_FLUSH_TIME.toString())); //acknowledgeRecords(batch); ///// ***** END PROCESSING BATCH ************************** diff --git a/sink-connector/src/main/java/com/altinity/clickhouse/sink/connector/executor/ClickHouseBatchWriter.java b/sink-connector/src/main/java/com/altinity/clickhouse/sink/connector/executor/ClickHouseBatchWriter.java new file mode 100644 index 000000000..ce2f163e5 --- /dev/null +++ b/sink-connector/src/main/java/com/altinity/clickhouse/sink/connector/executor/ClickHouseBatchWriter.java @@ -0,0 +1,348 @@ +package com.altinity.clickhouse.sink.connector.executor; + +import com.altinity.clickhouse.sink.connector.ClickHouseSinkConnectorConfig; +import com.altinity.clickhouse.sink.connector.ClickHouseSinkConnectorConfigVariables; +import com.altinity.clickhouse.sink.connector.common.Metrics; +import com.altinity.clickhouse.sink.connector.common.Utils; +import com.altinity.clickhouse.sink.connector.db.BaseDbWriter; +import com.altinity.clickhouse.sink.connector.db.DBMetadata; +import com.altinity.clickhouse.sink.connector.db.DbKafkaOffsetWriter; +import com.altinity.clickhouse.sink.connector.db.DbWriter; +import com.altinity.clickhouse.sink.connector.db.batch.GroupInsertQueryWithBatchRecords; +import com.altinity.clickhouse.sink.connector.db.batch.PreparedStatementExecutor; +import com.altinity.clickhouse.sink.connector.model.BlockMetaData; +import com.altinity.clickhouse.sink.connector.model.ClickHouseStruct; +import com.altinity.clickhouse.sink.connector.model.DBCredentials; +import com.clickhouse.jdbc.ClickHouseConnection; +import org.apache.commons.lang3.tuple.MutablePair; +import org.apache.kafka.common.TopicPartition; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import java.sql.SQLException; +import java.time.ZoneId; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.LinkedBlockingQueue; + +public class ClickHouseBatchWriter { + + private final ClickHouseSinkConnectorConfig config; + + // Connection that will be used to create + // the debezium storage database. + private ClickHouseConnection systemConnection; + + // For insert batch the database connection has to be the same. + // Create a map of database name to ClickHouseConnection. + private Map databaseToConnectionMap = new HashMap<>(); + + private static final Logger log = LogManager.getLogger(ClickHouseBatchWriter.class); + + // Map of topic names to table names. + private final Map topic2TableMap; + + // Map of topic name to CLickHouseConnection instance(DbWriter) + private Map topicToDbWriterMap; + + + private DBCredentials dbCredentials; + private Map databaseOverrideMap = new HashMap<>(); + + public ClickHouseBatchWriter( + ClickHouseSinkConnectorConfig config, + Map topic2TableMap) { + this.config = config; + if (topic2TableMap == null) { + this.topic2TableMap = new HashMap(); + } else { + this.topic2TableMap = topic2TableMap; + } + + //this.queryToRecordsMap = new HashMap<>(); + this.topicToDbWriterMap = new HashMap<>(); + //this.topicToRecordsMap = new HashMap<>(); + + this.dbCredentials = parseDBConfiguration(); + this.systemConnection = createConnection(); + + + try { + this.databaseOverrideMap = Utils.parseSourceToDestinationDatabaseMap(this.config. + getString(ClickHouseSinkConnectorConfigVariables.CLICKHOUSE_DATABASE_OVERRIDE_MAP.toString())); + } catch (Exception e) { + log.error("Error parsing database override map" + e); + } + } + + private ClickHouseConnection createConnection() { + String jdbcUrl = BaseDbWriter.getConnectionString(this.dbCredentials.getHostName(), + this.dbCredentials.getPort(), "system"); + + return BaseDbWriter.createConnection(jdbcUrl, "Sink Connector Lightweight", this.dbCredentials.getUserName(), + this.dbCredentials.getPassword(), config); + } + + // Function to check if we have already stored a ClickHouseConnection + // in the databaseToConnectionMap. + private ClickHouseConnection getClickHouseConnection(String databaseName) { + if (this.databaseToConnectionMap.containsKey(databaseName)) { + return this.databaseToConnectionMap.get(databaseName); + } + + String jdbcUrl = BaseDbWriter.getConnectionString(this.dbCredentials.getHostName(), + this.dbCredentials.getPort(), databaseName); + + ClickHouseConnection conn = BaseDbWriter.createConnection(jdbcUrl, "Sink Connector Lightweight", + this.dbCredentials.getUserName(), this.dbCredentials.getPassword(), config); + + this.databaseToConnectionMap.put(databaseName, conn); + return conn; + } + + private DBCredentials parseDBConfiguration() { + DBCredentials dbCredentials = new DBCredentials(); + + dbCredentials.setHostName(config.getString(ClickHouseSinkConnectorConfigVariables.CLICKHOUSE_URL.toString())); + dbCredentials.setPort(config.getInt(ClickHouseSinkConnectorConfigVariables.CLICKHOUSE_PORT.toString())); + dbCredentials.setUserName(config.getString(ClickHouseSinkConnectorConfigVariables.CLICKHOUSE_USER.toString())); + dbCredentials.setPassword(config.getString(ClickHouseSinkConnectorConfigVariables.CLICKHOUSE_PASS.toString())); + + return dbCredentials; + } + + /** + * Function to persist records in a single thread. + * @param records + */ + public void persistRecords(List records) { + + + log.info("****** Thread: " + Thread.currentThread().getName() + " Batch Size: " + records.size() + " ******"); + // Group records by topic name. + // Create a new map of topic name to list of records. + try { + Map> topicToRecordsMap = new ConcurrentHashMap<>(); + records.forEach(record -> { + String topicName = record.getTopic(); + // If the topic name is not present, create a new list and add the record. + if (topicToRecordsMap.containsKey(topicName) == false) { + List recordsList = new ArrayList<>(); + recordsList.add(record); + topicToRecordsMap.put(topicName, recordsList); + } else { + // If the topic name is present, add the record to the list. + List recordsList = topicToRecordsMap.get(topicName); + recordsList.add(record); + topicToRecordsMap.put(topicName, recordsList); + } + }); + boolean result = true; + // For each topic, process the records. + // topic name syntax is server.database.table + for (Map.Entry> entry : topicToRecordsMap.entrySet()) { + result = processRecordsByTopic(entry.getKey(), entry.getValue()); + if (result == false) { + log.error("Error processing records for topic: " + entry.getKey()); + break; + } + } + + // acknowledge the records. + if (result) { + log.info("****** Acknowledging records ******"); + records.forEach(record -> { + try { + record.getCommitter().markProcessed(record.getSourceRecord()); + } catch (InterruptedException e) { + //throw new RuntimeException(e); + log.error("Error marking records as processed"+ e); + } + + if(record.isLastRecordInBatch()) { + try { + record.getCommitter().markBatchFinished(); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + }); + } + } catch(Exception e) { + log.error("Error persisting records to ClickHouse" + e); + } + } + + /** + * Function to retrieve table name from topic name + * + * @param topicName + * @return Table Name + */ + public String getTableFromTopic(String topicName) { + String tableName = null; + + if (this.topic2TableMap.containsKey(topicName) == false) { + tableName = Utils.getTableNameFromTopic(topicName); + this.topic2TableMap.put(topicName, tableName); + } else { + tableName = this.topic2TableMap.get(topicName); + } + + return tableName; + } + + public DbWriter getDbWriterForTable(String topicName, String tableName, String databaseName, + ClickHouseStruct record, ClickHouseConnection connection) { + DbWriter writer = null; + + if (this.topicToDbWriterMap.containsKey(topicName)) { + writer = this.topicToDbWriterMap.get(topicName); + return writer; + } + + writer = new DbWriter(this.dbCredentials.getHostName(), this.dbCredentials.getPort(), + databaseName, tableName, this.dbCredentials.getUserName(), + this.dbCredentials.getPassword(), this.config, record, connection); + this.topicToDbWriterMap.put(topicName, writer); + return writer; + } + + /** + * Function to return ClickHouse server timezone. + * @return + */ + public ZoneId getServerTimeZone(ClickHouseSinkConnectorConfig config) { + + String userProvidedTimeZone = config.getString(ClickHouseSinkConnectorConfigVariables + .CLICKHOUSE_DATETIME_TIMEZONE.toString()); + // Validate if timezone string is valid. + ZoneId userProvidedTimeZoneId = null; + try { + if(!userProvidedTimeZone.isEmpty()) { + userProvidedTimeZoneId = ZoneId.of(userProvidedTimeZone); + } + } catch (Exception e){ + log.error("**** Error parsing user provided timezone:"+ userProvidedTimeZone + e.toString()); + } + + if(userProvidedTimeZoneId != null) { + return userProvidedTimeZoneId; + } + return new DBMetadata().getServerTimeZone(this.systemConnection); + } + /** + * Function to process records + * + * @param topicName + * @param records + */ + private boolean processRecordsByTopic(String topicName, List records) throws Exception { + + boolean result = false; + //The user parameter will override the topic mapping to table. + String tableName = getTableFromTopic(topicName); + // Note: getting records.get(0) is safe as the topic name is same for all records. + ClickHouseStruct firstRecord = records.get(0); + + String databaseName = firstRecord.getDatabase(); + + // Check if user has overridden the database name. + if(this.databaseOverrideMap.containsKey(firstRecord.getDatabase())) + databaseName = this.databaseOverrideMap.get(firstRecord.getDatabase()); + + ClickHouseConnection databaseConn = getClickHouseConnection(databaseName); + + DbWriter writer = getDbWriterForTable(topicName, tableName, databaseName, firstRecord, databaseConn); + PreparedStatementExecutor preparedStatementExecutor = new + PreparedStatementExecutor(writer.getReplacingMergeTreeDeleteColumn(), + writer.isReplacingMergeTreeWithIsDeletedColumn(), writer.getSignColumn(), writer.getVersionColumn(), + writer.getDatabaseName(), getServerTimeZone(this.config)); + + + if(writer == null || writer.wasTableMetaDataRetrieved() == false) { + log.error(String.format("*** TABLE METADATA not retrieved for Database(%s), table(%s) retrying", + writer.getDatabaseName(), writer.getTableName())); + if(writer == null) { + writer = getDbWriterForTable(topicName, tableName, databaseName, firstRecord, databaseConn); + } + if(writer.wasTableMetaDataRetrieved() == false) + writer.updateColumnNameToDataTypeMap(); + + if(writer == null || writer.wasTableMetaDataRetrieved() == false ) { + log.error(String.format("*** TABLE METADATA not retrieved for Database(%s), table(%s), " + + "retrying on next attempt", writer.getDatabaseName(), writer.getTableName())); + return false; + } + } + // Step 1: The Batch Insert with preparedStatement in JDBC + // works by forming the Query and then adding records to the Batch. + // This step creates a Map of Query -> Records(List of ClickHouseStruct) + Map>, List> queryToRecordsMap = new HashMap<>(); + Map partitionToOffsetMap = new HashMap<>(); + result = new GroupInsertQueryWithBatchRecords().groupQueryWithRecords(records, queryToRecordsMap, + partitionToOffsetMap, this.config,tableName, writer.getDatabaseName(), writer.getConnection(), + writer.getColumnNameToDataTypeMap()); + + BlockMetaData bmd = new BlockMetaData(); + long maxBufferSize = this.config.getLong(ClickHouseSinkConnectorConfigVariables.BUFFER_MAX_RECORDS.toString()); + + // Step 2: Create a PreparedStatement and add the records to the batch. + // In DBWriter, the queryToRecordsMap is converted to PreparedStatement and added to the batch. + // The batch is then executed and the records are flushed to ClickHouse. + result = flushRecordsToClickHouse(topicName, writer, queryToRecordsMap, bmd, maxBufferSize, preparedStatementExecutor); + + if(result) { + // Remove the entry. + queryToRecordsMap.remove(topicName); + } + + if (this.config.getBoolean(ClickHouseSinkConnectorConfigVariables.ENABLE_KAFKA_OFFSET.toString())) { + log.info("***** KAFKA OFFSET MANAGEMENT ENABLED *****"); + DbKafkaOffsetWriter dbKafkaOffsetWriter = new DbKafkaOffsetWriter(dbCredentials.getHostName(), + dbCredentials.getPort(), dbCredentials.getDatabase(), + "topic_offset_metadata", dbCredentials.getUserName(), dbCredentials.getPassword(), + this.config, databaseConn); + try { + dbKafkaOffsetWriter.insertTopicOffsetMetadata(partitionToOffsetMap); + } catch (SQLException e) { + log.error("Error persisting offsets to CH", e); + } + } + + return result; + } + + /** + * Function that flushes records to ClickHouse if + * there are minimum records or if the flush timeout has reached. + * @param writer + * @param queryToRecordsMap + * @return + */ + private boolean flushRecordsToClickHouse(String topicName, DbWriter writer, + Map>, + List> queryToRecordsMap, BlockMetaData bmd, + long maxBufferSize, PreparedStatementExecutor preparedStatementExecutor) throws Exception { + + boolean result = false; + + synchronized (queryToRecordsMap) { + result = preparedStatementExecutor.addToPreparedStatementBatch(topicName, queryToRecordsMap, bmd, config, writer.getConnection(), + writer.getTableName(), writer.getColumnNameToDataTypeMap(), writer.getEngine()); + + } + try { + Metrics.updateMetrics(bmd); + } catch(Exception e) { + log.error("****** Error updating Metrics ******"); + } + //result = true; + + return result; + } + +} diff --git a/sink-connector/src/main/java/com/altinity/clickhouse/sink/connector/executor/DebeziumOffsetManagement.java b/sink-connector/src/main/java/com/altinity/clickhouse/sink/connector/executor/DebeziumOffsetManagement.java index 060b16d03..b4d219102 100644 --- a/sink-connector/src/main/java/com/altinity/clickhouse/sink/connector/executor/DebeziumOffsetManagement.java +++ b/sink-connector/src/main/java/com/altinity/clickhouse/sink/connector/executor/DebeziumOffsetManagement.java @@ -1,7 +1,10 @@ package com.altinity.clickhouse.sink.connector.executor; import com.altinity.clickhouse.sink.connector.model.ClickHouseStruct; +import io.debezium.engine.ChangeEvent; +import io.debezium.engine.DebeziumEngine; import org.apache.commons.lang3.tuple.Pair; +import org.apache.kafka.connect.source.SourceRecord; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -154,4 +157,18 @@ static synchronized void acknowledgeRecords(List batch) throws Pair pair = calculateMinMaxTimestampFromBatch(batch); inFlightBatches.remove(pair); } + + public static synchronized void acknowledgeRecords(DebeziumEngine.RecordCommitter> + recordCommitter, ChangeEvent sourceRecord, + boolean lastRecordInBatch) + throws InterruptedException { + + if (sourceRecord != null) { + recordCommitter.markProcessed(sourceRecord); + + if(lastRecordInBatch == true) { + recordCommitter.markBatchFinished(); + } + } + } } diff --git a/sink-connector/src/test/java/com/altinity/clickhouse/sink/connector/UtilsTest.java b/sink-connector/src/test/java/com/altinity/clickhouse/sink/connector/UtilsTest.java index d1af8621d..c68fb3bac 100644 --- a/sink-connector/src/test/java/com/altinity/clickhouse/sink/connector/UtilsTest.java +++ b/sink-connector/src/test/java/com/altinity/clickhouse/sink/connector/UtilsTest.java @@ -53,5 +53,18 @@ public void testIsValidDatabase() { Assert.assertTrue(resultWithSpecialCharacters); } + + @Test + public void testParseSourceToDestinationDatabaseMap() throws Exception { + String sourceToDestination = "src_db1:dst_db1, src_db2:dst_db2,src-db2:src_db2"; + Map result = Utils.parseSourceToDestinationDatabaseMap(sourceToDestination); + + Map expectedHashMap = new HashMap(); + expectedHashMap.put("src_db1", "dst_db1"); + expectedHashMap.put("src_db2", "dst_db2"); + expectedHashMap.put("src-db2", "src_db2"); + + Assert.assertEquals(result, expectedHashMap); + } } diff --git a/sink-connector/src/test/java/com/altinity/clickhouse/sink/connector/db/DBMetadataTest.java b/sink-connector/src/test/java/com/altinity/clickhouse/sink/connector/db/DBMetadataTest.java index 0a08f8319..2a463596a 100644 --- a/sink-connector/src/test/java/com/altinity/clickhouse/sink/connector/db/DBMetadataTest.java +++ b/sink-connector/src/test/java/com/altinity/clickhouse/sink/connector/db/DBMetadataTest.java @@ -16,6 +16,8 @@ import java.sql.SQLException; import java.time.ZoneId; import java.util.HashMap; +import java.util.Map; +import java.util.Set; @Testcontainers @@ -82,6 +84,10 @@ public void testCheckIfDatabaseExists() throws SQLException { boolean result2 = new DBMetadata().checkIfDatabaseExists(writer.getConnection(), "newdb"); Assert.assertFalse(result2); + Map isNullableList = new DBMetadata().getColumnsIsNullableForTable(tableName, writer.getConnection(), "default"); + isNullableList.get("_offset").equals(true); + isNullableList.get("hire_date").equals(false); + } @Test @@ -149,4 +155,28 @@ public void getTestGetServerTimeZone() { Assert.assertTrue(serverTimeZone.toString().equalsIgnoreCase("America/Chicago")); } + + @Test + public void getAliasAndMaterializedColumnsList() throws SQLException { + String dbHostName = clickHouseContainer.getHost(); + Integer port = clickHouseContainer.getFirstMappedPort(); + String database = "default"; + String userName = clickHouseContainer.getUsername(); + String password = clickHouseContainer.getPassword(); + String tableName = "employees"; + + String jdbcUrl = BaseDbWriter.getConnectionString(dbHostName, port, database); + ClickHouseConnection conn = DbWriter.createConnection(jdbcUrl, "client_1", userName, password, new ClickHouseSinkConnectorConfig(new HashMap<>())); + Set aliasColumns = new DBMetadata().getAliasAndMaterializedColumnsForTableAndDatabase("people", "employees2", conn); + + Assert.assertTrue(aliasColumns.size() == 2); + + + // Check for a table with no alias columns. + Set tmAliasColumns = new DBMetadata().getAliasAndMaterializedColumnsForTableAndDatabase("tm", "public", conn); + Assert.assertTrue(tmAliasColumns.size() == 0); + // Check for a table with no alias columns. + Set employeeMaterializedColumns = new DBMetadata().getAliasAndMaterializedColumnsForTableAndDatabase("employee_materialized", "employees2", conn); + Assert.assertTrue(employeeMaterializedColumns.size() == 1); + } } diff --git a/sink-connector/src/test/java/com/altinity/clickhouse/sink/connector/db/DbWriterTest.java b/sink-connector/src/test/java/com/altinity/clickhouse/sink/connector/db/DbWriterTest.java index 32d86013a..757453ff1 100644 --- a/sink-connector/src/test/java/com/altinity/clickhouse/sink/connector/db/DbWriterTest.java +++ b/sink-connector/src/test/java/com/altinity/clickhouse/sink/connector/db/DbWriterTest.java @@ -222,22 +222,23 @@ public static List getSampleRecords() { @Test public void testGroupRecords() { - String hostName = "remoteClickHouse"; - Integer port = 8123; - String database = "test"; - String userName = "root"; - String password = "root"; + String dbHostName = clickHouseContainer.getHost(); + Integer port = clickHouseContainer.getFirstMappedPort(); + String database = "default"; + String userName = clickHouseContainer.getUsername(); + String password = clickHouseContainer.getPassword(); String tableName = "employees"; - String connectionUrl = writer.getConnectionString(hostName, port, database); + + String connectionUrl = writer.getConnectionString(dbHostName, port, database); Properties properties = new Properties(); properties.setProperty("client_name", "Test_1"); ClickHouseSinkConnectorConfig config= new ClickHouseSinkConnectorConfig(new HashMap<>()); - String jdbcUrl = BaseDbWriter.getConnectionString(hostName, port, database); - ClickHouseConnection conn = DbWriter.createConnection(jdbcUrl, "client_1", userName, password, config); - DbWriter dbWriter = new DbWriter(hostName, port, database, tableName, userName, password, config, null, conn); + //String jdbcUrl = BaseDbWriter.getConnectionString(hostName, port, database); + ClickHouseConnection conn = DbWriter.createConnection(connectionUrl, "client_1", userName, password, config); + DbWriter dbWriter = new DbWriter(dbHostName, port, database, tableName, userName, password, config, null, conn); Map>, List> queryToRecordsMap = new HashMap<>(); diff --git a/sink-connector/src/test/resources/init_clickhouse.sql b/sink-connector/src/test/resources/init_clickhouse.sql index 9ed7470c1..739b165d2 100644 --- a/sink-connector/src/test/resources/init_clickhouse.sql +++ b/sink-connector/src/test/resources/init_clickhouse.sql @@ -157,4 +157,32 @@ create table employees2.ma_users ) engine = MergeTree() ORDER BY date - SETTINGS index_granularity = 8192; \ No newline at end of file + SETTINGS index_granularity = 8192; + + +CREATE TABLE employees2.people +( + `height_cm` Decimal(64, 18), + `_version` UInt64, + `_sign` UInt8, + `full_name` String ALIAS concat('John', ' ', 'Doe'), + `full_name2` String ALIAS concat('Alice', ' ', 'W'), +) +ENGINE = ReplacingMergeTree(_version, _sign) +PRIMARY KEY height_cm +ORDER BY height_cm +SETTINGS index_granularity = 8192; + +-- CREATE TABLE with ClickHouse Materialized columns. +CREATE TABLE employees2.employee_materialized ( + `id` UInt64, + `name` String, + `age` Nullable(UInt8), + `salary` Nullable(UInt32), + `_sign` UInt8, + `full_name` String MATERIALIZED concat('John', ' ', 'Doe') + ) +ENGINE = MergeTree() +PRIMARY KEY (id) +ORDER BY (id) +SETTINGS index_granularity = 8192; \ No newline at end of file diff --git a/sink-connector/tests/integration/env/clickhouse-sink-connector-kafka-service.yml b/sink-connector/tests/integration/env/clickhouse-sink-connector-kafka-service.yml new file mode 100644 index 000000000..3a6379621 --- /dev/null +++ b/sink-connector/tests/integration/env/clickhouse-sink-connector-kafka-service.yml @@ -0,0 +1,22 @@ +version: "2.3" + +services: + clickhouse-sink-connector-kafka: + hostname: clickhouse-sink-connector-kafka + image: ${SINK_CONNECTOR_IMAGE} + restart: "no" + expose: + - "8083" + - "5005" + - "39999" + environment: + - BOOTSTRAP_SERVERS=kafka:9092 + - GROUP_ID=2 + - CONFIG_STORAGE_TOPIC=config-storage-topic-sink + - OFFSET_STORAGE_TOPIC=offset-storage-topic-sink + - STATUS_STORAGE_TOPIC=status-storage-topic-sink + - LOG_LEVEL=INFO + - JAVA_DEBUG_PORT=*:5005 + - DEFAULT_JAVA_DEBUG_PORT=*:5005 + - KAFKA_DEBUG=true + - JMX_PORT=39999 \ No newline at end of file diff --git a/sink-connector/tests/integration/env/debezium-service.yml b/sink-connector/tests/integration/env/debezium-service.yml new file mode 100644 index 000000000..0b74a6e2e --- /dev/null +++ b/sink-connector/tests/integration/env/debezium-service.yml @@ -0,0 +1,23 @@ +version: "2.3" + +services: + debezium: + container_name: debezium + hostname: debezium + build: + context: ../../../docker/debezium_jmx + args: + DEBEZIUM_VERSION: 2.1.0.Alpha1 + restart: "no" + expose: + - "8083" + - "1976" + environment: + - BOOTSTRAP_SERVERS=kafka:9092 + - GROUP_ID=1 + - CONFIG_STORAGE_TOPIC=config-storage-topic-debezium + - OFFSET_STORAGE_TOPIC=offset-storage-topic-debezium + - STATUS_STORAGE_TOPIC=status-storage-topic-debezium + - LOG_LEVEL=INFO + - KEY_CONVERTER=io.confluent.connect.avro.AvroConverter + - VALUE_CONVERTER=io.confluent.connect.avro.AvroConverter \ No newline at end of file diff --git a/sink-connector/tests/integration/env/docker-compose.yml b/sink-connector/tests/integration/env/docker-compose.yml index 7c6733b67..6cf4167d0 100644 --- a/sink-connector/tests/integration/env/docker-compose.yml +++ b/sink-connector/tests/integration/env/docker-compose.yml @@ -3,26 +3,9 @@ version: "2.3" services: mysql-master: - container_name: mysql-master - image: docker.io/bitnami/mysql:8.0.36 - restart: "no" - expose: - - "3306" - environment: - - MYSQL_ROOT_PASSWORD=root - - MYSQL_DATABASE=test - - MYSQL_REPLICATION_MODE=master - - MYSQL_REPLICATION_USER=repl_user - - ALLOW_EMPTY_PASSWORD=yes - volumes: - - ./mysqld.cnf:/opt/bitnami/mysql/conf/my_custom.cnf - - ../sql/init_mysql.sql:/docker-entrypoint-initdb.d/init_mysql.sql - - "${CLICKHOUSE_TESTS_DIR}/_instances/share_folder:/tmp/share_folder" - healthcheck: - test: [ 'CMD', '/opt/bitnami/scripts/mysql/healthcheck.sh' ] - interval: 15s - timeout: 5s - retries: 6 + extends: + file: mysql-master-service.yml + service: mysql-master schemaregistry: @@ -36,70 +19,25 @@ services: - SCHEMA_REGISTRY_HOST_NAME=schemaregistry - SCHEMA_REGISTRY_LISTENERS=http://schemaregistry:8081 - SCHEMA_REGISTRY_DEBUG=true - depends_on: - kafka debezium: - container_name: debezium - hostname: debezium -# image: debezium/connect:1.9.5.Final - build: - context: ../../../docker/debezium_jmx - args: - DEBEZIUM_VERSION: 2.1.0.Alpha1 - restart: "no" - expose: - - "8083" - - "1976" - environment: - - BOOTSTRAP_SERVERS=kafka:9092 - - GROUP_ID=1 - - CONFIG_STORAGE_TOPIC=config-storage-topic-debezium - - OFFSET_STORAGE_TOPIC=offset-storage-topic-debezium - - STATUS_STORAGE_TOPIC=status-storage-topic-debezium - - LOG_LEVEL=INFO - - KEY_CONVERTER=io.confluent.connect.avro.AvroConverter - - VALUE_CONVERTER=io.confluent.connect.avro.AvroConverter + extends: + file: debezium-service.yml + service: debezium depends_on: - kafka kafka: - container_name: kafka - hostname: kafka - image: vectorized/redpanda - restart: "no" - expose: - - "19092" - command: - - redpanda - - start - - --overprovisioned - - --kafka-addr - - DOCKER_NETWORK://0.0.0.0:9092,LOCALHOST_NETWORK://0.0.0.0:19092 - - --advertise-kafka-addr - - DOCKER_NETWORK://kafka:9092,LOCALHOST_NETWORK://127.0.0.1:19092 - - sink: - container_name: sink - hostname: sink - image: ${SINK_CONNECTOR_IMAGE} - restart: "no" - expose: - - "8083" - - "5005" - - "39999" - environment: - - BOOTSTRAP_SERVERS=kafka:9092 - - GROUP_ID=2 - - CONFIG_STORAGE_TOPIC=config-storage-topic-sink - - OFFSET_STORAGE_TOPIC=offset-storage-topic-sink - - STATUS_STORAGE_TOPIC=status-storage-topic-sink - - LOG_LEVEL=INFO - - JAVA_DEBUG_PORT=*:5005 - - DEFAULT_JAVA_DEBUG_PORT=*:5005 - - KAFKA_DEBUG=true - - JMX_PORT=39999 + extends: + file: kafka-service.yml + service: kafka + + clickhouse-sink-connector-kafka: + extends: + file: clickhouse-sink-connector-kafka-service.yml + service: clickhouse-sink-connector-kafka depends_on: - kafka @@ -114,10 +52,6 @@ services: file: clickhouse-service.yml service: clickhouse hostname: clickhouse -# environment: -# - CLICKHOUSE_USER=1000 -# - CLICKHOUSE_PASSWORD=1000 -# - CLICKHOUSE_DB=test ulimits: nofile: soft: 262144 @@ -186,7 +120,7 @@ services: condition: service_healthy # dummy service which does nothing, but allows to postpone - # 'docker-compose up -d' till all dependecies will go healthy + # 'docker-compose up -d' till all dependencies will go healthy all_services_ready: image: hello-world depends_on: @@ -200,4 +134,6 @@ services: condition: service_healthy zookeeper: condition: service_healthy + mysql-master: + condition: service_healthy diff --git a/sink-connector/tests/integration/env/kafka-service.yml b/sink-connector/tests/integration/env/kafka-service.yml new file mode 100644 index 000000000..f8b3d24d4 --- /dev/null +++ b/sink-connector/tests/integration/env/kafka-service.yml @@ -0,0 +1,18 @@ +version: "2.3" + +services: + kafka: + container_name: kafka + hostname: kafka + image: vectorized/redpanda + restart: "no" + expose: + - "19092" + command: + - redpanda + - start + - --overprovisioned + - --kafka-addr + - DOCKER_NETWORK://0.0.0.0:9092,LOCALHOST_NETWORK://0.0.0.0:19092 + - --advertise-kafka-addr + - DOCKER_NETWORK://kafka:9092,LOCALHOST_NETWORK://127.0.0.1:19092 \ No newline at end of file diff --git a/sink-connector/tests/integration/env/mysql-master-service.yml b/sink-connector/tests/integration/env/mysql-master-service.yml new file mode 100644 index 000000000..13d92b52f --- /dev/null +++ b/sink-connector/tests/integration/env/mysql-master-service.yml @@ -0,0 +1,24 @@ +version: "2.3" + +services: + mysql-master: + container_name: mysql-master + image: docker.io/bitnami/mysql:8.0.36 + restart: "no" + expose: + - "3306" + environment: + - MYSQL_ROOT_PASSWORD=root + - MYSQL_DATABASE=test + - MYSQL_REPLICATION_MODE=master + - MYSQL_REPLICATION_USER=repl_user + - ALLOW_EMPTY_PASSWORD=yes + volumes: + - ./mysqld.cnf:/opt/bitnami/mysql/conf/my_custom.cnf + - ../sql/init_mysql.sql:/docker-entrypoint-initdb.d/init_mysql.sql + - "${CLICKHOUSE_TESTS_DIR}/_instances/share_folder:/tmp/share_folder" + healthcheck: + test: [ 'CMD', '/opt/bitnami/scripts/mysql/healthcheck.sh' ] + interval: 15s + timeout: 5s + retries: 6 \ No newline at end of file diff --git a/sink-connector/tests/integration/helpers/cluster.py b/sink-connector/tests/integration/helpers/cluster.py index fe2560ce8..f99a6b7b6 100755 --- a/sink-connector/tests/integration/helpers/cluster.py +++ b/sink-connector/tests/integration/helpers/cluster.py @@ -884,7 +884,7 @@ def query( if "Exception:" in r.output: if raise_on_exception: raise QueryRuntimeException(r.output) - assert False, error(r.output) + assert False, f"{error(r.output)}\nfor query {sql}" elif "ERROR" in r.output: if raise_on_exception: raise QueryRuntimeException(r.output) diff --git a/sink-connector/tests/integration/helpers/common.py b/sink-connector/tests/integration/helpers/common.py index dd480c55c..ff214b0de 100644 --- a/sink-connector/tests/integration/helpers/common.py +++ b/sink-connector/tests/integration/helpers/common.py @@ -590,7 +590,7 @@ def set_envs_on_node(self, envs, node=None): node.command(f"unset {key}", exitcode=0) -from helpers.cluster import Cluster +from integration.helpers.cluster import Cluster @TestStep(Given) diff --git a/sink-connector/tests/integration/regression.py b/sink-connector/tests/integration/regression.py index 3d01cb4bd..35b4dea08 100755 --- a/sink-connector/tests/integration/regression.py +++ b/sink-connector/tests/integration/regression.py @@ -6,14 +6,13 @@ from testflows.core import * - append_path(sys.path, "..") +from integration.tests.steps.clickhouse import create_clickhouse_database from integration.helpers.argparser import argparser from integration.helpers.common import check_clickhouse_version from integration.helpers.common import create_cluster from integration.requirements.requirements import * -from integration.tests.steps.steps_global import * xfails = { "schema changes/table recreation with different datatypes": [ @@ -41,6 +40,11 @@ "types/date time/*": [(Fail, "difference between timezones, tests need rework")], "types/integer types/*": [(Fail, "requires investigation")], } + +ffails = { + "/regression/multiple databases": (Skip, "Work in progress") +} + xflags = {} @@ -48,7 +52,8 @@ @ArgumentParser(argparser) @XFails(xfails) @XFlags(xflags) -@Name("mysql to clickhouse replication") +@FFails(ffails) +@Name("regression") @Requirements( RQ_SRS_030_ClickHouse_MySQLToClickHouseReplication("1.0"), RQ_SRS_030_ClickHouse_MySQLToClickHouseReplication_Consistency_Select("1.0"), @@ -74,7 +79,7 @@ def regression( "clickhouse": ("clickhouse", "clickhouse1", "clickhouse2", "clickhouse3"), "bash-tools": ("bash-tools",), "schemaregistry": ("schemaregistry",), - "sink": ("sink",), + "clickhouse-sink-connector-kafka": ("clickhouse-sink-connector-kafka",), "zookeeper": ("zookeeper",), } @@ -108,21 +113,17 @@ def regression( self.context.node = cluster.node("clickhouse1") with And("I create test database in ClickHouse"): - create_database(name="test") - - modules = [ - "autocreate", - "insert", - "delete", - "truncate", - "deduplication", - "primary_keys", - "virtual_columns", - "columns_inconsistency", - ] - for module in modules: - Feature(run=load(f"tests.{module}", "module")) - + create_clickhouse_database(name="test") + + Feature(run=load("tests.autocreate", "feature")) + Feature(run=load("tests.insert", "feature")) + Feature(run=load("tests.delete", "feature")) + Feature(run=load("tests.truncate", "feature")) + Feature(run=load("tests.deduplication", "feature")) + Feature(run=load("tests.primary_keys", "feature")) + Feature(run=load("tests.columns_inconsistency", "feature")) + Feature(run=load("tests.types", "feature")) + Feature(run=load("tests.multiple_databases", "feature")) if __name__ == "__main__": diff --git a/sink-connector/tests/integration/requirements.txt b/sink-connector/tests/integration/requirements.txt index fdac85095..7177ca226 100644 --- a/sink-connector/tests/integration/requirements.txt +++ b/sink-connector/tests/integration/requirements.txt @@ -1,4 +1,4 @@ -testflows==2.1.5 +testflows==2.4.10 python-dateutil==2.9.0 numpy==1.26.4 pyarrow==16.1.0 diff --git a/sink-connector/tests/integration/tests/autocreate.py b/sink-connector/tests/integration/tests/autocreate.py index 1f4b83898..a64757588 100644 --- a/sink-connector/tests/integration/tests/autocreate.py +++ b/sink-connector/tests/integration/tests/autocreate.py @@ -1,10 +1,17 @@ +from integration.requirements.requirements import * +from integration.tests.steps.configurations import * from integration.tests.steps.sql import * -from integration.tests.steps.service_settings_steps import * -from integration.tests.steps.statements import * +from integration.tests.steps.datatypes import * @TestOutline -def create_all_data_types(self, mysql_columns, clickhouse_columns, clickhouse_table): +def create_all_data_types( + self, + mysql_columns, + clickhouse_columns, + clickhouse_table, + auto_create_replicated=False, +): """Check auto-creation of replicated MySQL table which contains all supported data types. """ @@ -13,7 +20,9 @@ def create_all_data_types(self, mysql_columns, clickhouse_columns, clickhouse_ta mysql = self.context.cluster.node("mysql-master") init_sink_connector( - auto_create_tables=clickhouse_table[0], topics=f"SERVER5432.test.{table_name}" + auto_create_tables=clickhouse_table[0], + topics=f"SERVER5432.test.{table_name}", + auto_create_replicated_tables=auto_create_replicated, ) with Given( @@ -43,10 +52,11 @@ def create_all_data_types(self, mysql_columns, clickhouse_columns, clickhouse_ta clickhouse_table=clickhouse_table, statement="count(*)", with_final=True, + replicated=auto_create_replicated, ) -@TestFeature +@TestScenario def create_all_data_types_null_table( self, mysql_columns=all_mysql_datatypes, @@ -65,7 +75,27 @@ def create_all_data_types_null_table( ) -@TestFeature +@TestScenario +def create_all_data_types_null_table_replicated( + self, + mysql_columns=all_mysql_datatypes, + clickhouse_columns=all_ch_datatypes, +): + """Check all availabe methods and tables creation of replicated MySQL to Ch table that + contains all supported "NULL" data types. + """ + + for clickhouse_table in available_clickhouse_tables: + with Example({clickhouse_table}, flags=TE): + create_all_data_types( + mysql_columns=mysql_columns, + clickhouse_columns=clickhouse_columns, + clickhouse_table=clickhouse_table, + auto_create_replicated=True, + ) + + +@TestScenario def create_all_data_types_not_null_table_manual( self, mysql_columns=all_nullable_mysql_datatypes, @@ -83,22 +113,37 @@ def create_all_data_types_not_null_table_manual( ) -@TestModule +@TestScenario +def create_all_data_types_not_null_table_manual_replicated( + self, + mysql_columns=all_nullable_mysql_datatypes, + clickhouse_columns=all_nullable_ch_datatypes, +): + """Check all availabe methods and tables creation of replicated MySQL to CH table + which contains all supported "NOT NULL" data types. + """ + for clickhouse_table in available_clickhouse_tables: + with Example({clickhouse_table}, flags=TE): + create_all_data_types( + mysql_columns=mysql_columns, + clickhouse_columns=clickhouse_columns, + clickhouse_table=clickhouse_table, + auto_create_replicated=True, + ) + + +@TestFeature @Requirements( RQ_SRS_030_ClickHouse_MySQLToClickHouseReplication_TableSchemaCreation_AutoCreate( "1.0" ) ) @Name("autocreate") -def module(self): +def feature(self): """Verify correct replication of all supported MySQL data types.""" with Given("I enable debezium and sink connectors after kafka starts up"): init_debezium_connector() - with Pool(1) as executor: - try: - for feature in loads(current_module(), Feature): - Feature(test=feature, parallel=True, executor=executor)() - finally: - join() + for scenario in loads(current_module(), Scenario): + scenario() diff --git a/sink-connector/tests/integration/tests/columns_inconsistency.py b/sink-connector/tests/integration/tests/columns_inconsistency.py index 30af74513..20e256b6d 100644 --- a/sink-connector/tests/integration/tests/columns_inconsistency.py +++ b/sink-connector/tests/integration/tests/columns_inconsistency.py @@ -1,6 +1,7 @@ +from integration.requirements.requirements import * +from integration.tests.steps.configurations import * from integration.tests.steps.sql import * -from integration.tests.steps.statements import * -from integration.tests.steps.service_settings_steps import * +from integration.tests.steps.datatypes import * @TestOutline @@ -38,7 +39,7 @@ def mysql_to_clickhouse_insert( ) -@TestFeature +@TestScenario def more_columns( self, input="(2,7,777)", @@ -59,7 +60,7 @@ def more_columns( ) -@TestFeature +@TestScenario def less_columns( self, input="(2,7,777)", @@ -80,7 +81,7 @@ def less_columns( ) -@TestFeature +@TestScenario def equal_columns_different_names( self, input="(2,7,777)", @@ -101,7 +102,7 @@ def equal_columns_different_names( ) -@TestFeature +@TestScenario def equal_columns_some_different_names( self, input="(2,7,777)", @@ -122,20 +123,16 @@ def equal_columns_some_different_names( ) -@TestModule +@TestFeature @Requirements( RQ_SRS_030_ClickHouse_MySQLToClickHouseReplication_ColumnsInconsistency("1.0") ) @Name("columns inconsistency") -def module(self): +def feature(self): """Check for different columns inconsistency.""" with Given("I enable debezium and sink connectors after kafka starts up"): init_debezium_connector() - with Pool(1) as executor: - try: - for feature in loads(current_module(), Feature): - Feature(test=feature, parallel=True, executor=executor)() - finally: - join() + for scenario in loads(current_module(), Scenario): + scenario() diff --git a/sink-connector/tests/integration/tests/consistency.py b/sink-connector/tests/integration/tests/consistency.py index f3644d4df..10aeaf86d 100644 --- a/sink-connector/tests/integration/tests/consistency.py +++ b/sink-connector/tests/integration/tests/consistency.py @@ -1,8 +1,10 @@ -import time from itertools import combinations + from testflows.connect import Shell + +from integration.requirements.requirements import * +from integration.tests.steps.configurations import * from integration.tests.steps.sql import * -from integration.tests.steps.service_settings_steps import * @TestOutline diff --git a/sink-connector/tests/integration/tests/deduplication.py b/sink-connector/tests/integration/tests/deduplication.py index 73343dc41..b4910cf19 100644 --- a/sink-connector/tests/integration/tests/deduplication.py +++ b/sink-connector/tests/integration/tests/deduplication.py @@ -1,6 +1,7 @@ +from integration.requirements.requirements import * +from integration.tests.steps.configurations import * from integration.tests.steps.sql import * -from integration.tests.steps.statements import * -from integration.tests.steps.service_settings_steps import * +from integration.tests.steps.datatypes import * @TestOutline @@ -44,11 +45,11 @@ def deduplication( statement="count(*)", clickhouse_table=clickhouse_table, with_final=True, - timeout=50, + timeout=140, ) -@TestFeature +@TestScenario def deduplication_on_big_insert(self): """Check MySQL to Clickhouse connection for non-duplication data on 10 000 inserts.""" for clickhouse_table in available_clickhouse_tables: @@ -58,7 +59,7 @@ def deduplication_on_big_insert(self): ) -@TestFeature +@TestScenario def deduplication_on_many_inserts(self): """Check MySQL to Clickhouse connection for non-duplication data on big inserts.""" for clickhouse_table in available_clickhouse_tables: @@ -68,21 +69,17 @@ def deduplication_on_many_inserts(self): ) -@TestModule +@TestFeature @Requirements( RQ_SRS_030_ClickHouse_MySQLToClickHouseReplication_Consistency_Deduplication("1.0") ) @Name("deduplication") -def module(self): +def feature(self): """MySql to ClickHouse replication tests to check for non-duplication data on big inserts.""" with Given("I enable debezium and sink connectors after kafka starts up"): init_debezium_connector() - with Pool(1) as executor: - try: - for feature in loads(current_module(), Feature): - Feature(test=feature, parallel=True, executor=executor)() - finally: - join() + for scenario in loads(current_module(), Scenario): + scenario() diff --git a/sink-connector/tests/integration/tests/delete.py b/sink-connector/tests/integration/tests/delete.py index e83749f59..b5a12ae83 100644 --- a/sink-connector/tests/integration/tests/delete.py +++ b/sink-connector/tests/integration/tests/delete.py @@ -1,6 +1,7 @@ +from integration.requirements.requirements import * +from integration.tests.steps.configurations import * from integration.tests.steps.sql import * -from integration.tests.steps.statements import * -from integration.tests.steps.service_settings_steps import * +from integration.tests.steps.datatypes import * @TestOutline @@ -43,7 +44,7 @@ def delete( ) -@TestFeature +@TestScenario def no_primary_key(self): """Check for `DELETE` with no primary key without InnoDB engine.""" for clickhouse_table in available_clickhouse_tables: @@ -57,7 +58,7 @@ def no_primary_key(self): ) -@TestFeature +@TestScenario def no_primary_key_innodb(self): """Check for `DELETE` with no primary key with InnoDB engine.""" for clickhouse_table in available_clickhouse_tables: @@ -85,7 +86,7 @@ def simple_primary_key(self): ) -@TestFeature +@TestScenario def simple_primary_key_innodb(self): """Check for `DELETE` with simple primary key with InnoDB engine.""" for clickhouse_table in available_clickhouse_tables: @@ -99,7 +100,7 @@ def simple_primary_key_innodb(self): ) -@TestFeature +@TestScenario def complex_primary_key(self): """Check for `DELETE` with complex primary key without engine InnoDB.""" for clickhouse_table in available_clickhouse_tables: @@ -113,7 +114,7 @@ def complex_primary_key(self): ) -@TestFeature +@TestScenario def complex_primary_key_innodb(self): """Check for `DELETE` with complex primary key with engine InnoDB.""" for clickhouse_table in available_clickhouse_tables: @@ -127,18 +128,14 @@ def complex_primary_key_innodb(self): ) -@TestModule +@TestFeature @Requirements(RQ_SRS_030_ClickHouse_MySQLToClickHouseReplication_Queries_Deletes("1.0")) @Name("delete") -def module(self): +def feature(self): """MySql to ClickHouse replication delete tests to test `DELETE` queries.""" with Given("I enable debezium connector after kafka starts up"): init_debezium_connector() - with Pool(1) as executor: - try: - for feature in loads(current_module(), Feature): - Feature(test=feature, parallel=True, executor=executor)() - finally: - join() + for scenario in loads(current_module(), Scenario): + scenario() diff --git a/sink-connector/tests/integration/tests/insert.py b/sink-connector/tests/integration/tests/insert.py index 372e18757..ddccb47ad 100644 --- a/sink-connector/tests/integration/tests/insert.py +++ b/sink-connector/tests/integration/tests/insert.py @@ -1,6 +1,7 @@ +from integration.requirements.requirements import * +from integration.tests.steps.configurations import * from integration.tests.steps.sql import * -from integration.tests.steps.statements import * -from integration.tests.steps.service_settings_steps import * +from integration.tests.steps.datatypes import * @TestOutline @@ -37,7 +38,7 @@ def mysql_to_clickhouse_inserts( ) -@TestFeature +@TestScenario def null_default_insert( self, input="(DEFAULT,5,DEFAULT)", @@ -57,7 +58,7 @@ def null_default_insert( ) -@TestFeature +@TestScenario def null_default_insert_2( self, input="(DEFAULT,5,333)", @@ -77,7 +78,7 @@ def null_default_insert_2( ) -@TestFeature +@TestScenario def select_insert( self, input="((select 2),7,DEFAULT)", @@ -97,7 +98,7 @@ def select_insert( ) -@TestFeature +@TestScenario def select_insert_2( self, input="((select 2),7,DEFAULT)", @@ -117,19 +118,15 @@ def select_insert_2( ) -@TestModule +@TestFeature @Requirements(RQ_SRS_030_ClickHouse_MySQLToClickHouseReplication_Queries_Inserts("1.0")) @Name("insert") -def module(self): +def feature(self): """Different `INSERT` tests section.""" # xfail("") with Given("I enable debezium and sink connectors after kafka starts up"): init_debezium_connector() - with Pool(1) as executor: - try: - for feature in loads(current_module(), Feature): - Feature(test=feature, parallel=True, executor=executor)() - finally: - join() + for scenario in loads(current_module(), Scenario): + scenario() diff --git a/sink-connector/tests/integration/tests/multiple_databases.py b/sink-connector/tests/integration/tests/multiple_databases.py new file mode 100644 index 000000000..ee06b8132 --- /dev/null +++ b/sink-connector/tests/integration/tests/multiple_databases.py @@ -0,0 +1,110 @@ +from testflows.core import * + +from integration.helpers.common import getuid +from integration.tests.replication import replication +from integration.tests.steps.configurations import init_sink_connector_auto_created +from integration.tests.steps.mysql.mysql import ( + create_mysql_database, + generate_special_case_names, + create_sample_table, +) +from integration.tests.steps.clickhouse import ( + create_clickhouse_database, + check_if_table_was_created, +) + + +@TestStep(Given) +def create_source_database(self, database_name): + """Create a MySQL database.""" + create_mysql_database(database_name=database_name) + + +@TestStep(Given) +def create_destination_database(self, database_name): + """Create a ClickHouse database.""" + create_clickhouse_database(name=database_name) + + +@TestStep(Given) +def create_source_and_destination_databases(self, database_name=None): + """Create databases where MySQL database is source and ClickHouse database is destination.""" + if database_name is None: + database_name = "test" + + with By(f"creating a ClickHouse database {database_name}"): + create_clickhouse_database(name=database_name) + + with And(f"creating a a MySQL database {database_name}"): + create_mysql_database(database_name=database_name) + + +@TestScenario +def check_database_creation(self): + """Check if the databases are created.""" + databases = generate_special_case_names( + self.context.number_of_databases, max_length=63 + ) + table_name = f"table_{getuid()}" + + with Given("I create databases with different combinations in names"): + for database in databases: + create_source_and_destination_databases(database_name=database) + + with And("I create a sample table on each of these databases"): + for database in databases: + with By("initializing sink connector with the database"): + init_sink_connector_auto_created( + topics=f"SERVER5432.{database}.{table_name}" + ) + with And("creating a sample table on that database"): + create_sample_table(database=database, table_name=table_name) + + with Then("I validate that the table was created on each of these databases"): + for database in databases: + check_if_table_was_created(database_name=database, table_name=table_name) + + +@TestScenario +def create_databases_with_special_names(self): + """Create databases with special names on source and destination manually.""" + update_config = { + "auto.create.tables": "true", + "auto.create.tables.replicated": "false", + } + databases = generate_special_case_names( + self.context.number_of_databases, max_length=63 + ) + with Given("I create databases with special characters in the name"): + for database in databases: + create_source_and_destination_databases(database_name=rf"\`{database}\`") + + Check(test=replication)(databases=databases, update=update_config) + + +@TestScenario +def auto_create_databases_with_special_names(self): + """Auto create databases with special characters in the name on source and wait and check it's created on destination.""" + databases = generate_special_case_names( + self.context.number_of_databases, max_length=63 + ) + update_config = { + "auto.create.tables": "true", + "auto.create.tables.replicated": "false", + } + with Given("I create databases with special characters in the name"): + for database in databases: + create_source_database(database_name=rf"\`{database}\`") + + Check(test=replication)(databases=databases, update=update_config) + + +@TestFeature +@Name("multiple databases") +def feature(self, number_of_databases=100): + """Validate sink connector with multiple databases.""" + self.context.number_of_databases = number_of_databases + + Scenario(run=check_database_creation) + Scenario(run=create_databases_with_special_names) + Scenario(run=auto_create_databases_with_special_names) diff --git a/sink-connector/tests/integration/tests/multiple_tables.py b/sink-connector/tests/integration/tests/multiple_tables.py index e110b0431..1e8c4fff3 100644 --- a/sink-connector/tests/integration/tests/multiple_tables.py +++ b/sink-connector/tests/integration/tests/multiple_tables.py @@ -1,6 +1,7 @@ +from integration.requirements.requirements import * +from integration.tests.steps.configurations import * from integration.tests.steps.sql import * -from integration.tests.steps.statements import * -from integration.tests.steps.service_settings_steps import * +from integration.tests.steps.datatypes import * @TestOutline diff --git a/sink-connector/tests/integration/tests/partition_limits.py b/sink-connector/tests/integration/tests/partition_limits.py index 4a27e213c..4e1f70efa 100644 --- a/sink-connector/tests/integration/tests/partition_limits.py +++ b/sink-connector/tests/integration/tests/partition_limits.py @@ -1,6 +1,7 @@ +from integration.requirements.requirements import * +from integration.tests.steps.configurations import * from integration.tests.steps.sql import * -from integration.tests.steps.statements import * -from integration.tests.steps.service_settings_steps import * +from integration.tests.steps.datatypes import * @TestOutline @@ -57,7 +58,7 @@ def partition_limits( retry( clickhouse.query, - timeout=50, + timeout=140, delay=1, )( f"SELECT count() FROM test.{table_name} FINAL where _sign !=-1 FORMAT CSV", diff --git a/sink-connector/tests/integration/tests/primary_keys.py b/sink-connector/tests/integration/tests/primary_keys.py index 804689668..788744a05 100644 --- a/sink-connector/tests/integration/tests/primary_keys.py +++ b/sink-connector/tests/integration/tests/primary_keys.py @@ -1,6 +1,7 @@ +from integration.requirements.requirements import * +from integration.tests.steps.configurations import * from integration.tests.steps.sql import * -from integration.tests.steps.statements import * -from integration.tests.steps.service_settings_steps import * +from integration.tests.steps.datatypes import * @TestOutline @@ -51,7 +52,7 @@ def check_different_primary_keys( ) -@TestFeature +@TestScenario @Requirements( RQ_SRS_030_ClickHouse_MySQLToClickHouseReplication_PrimaryKey_Simple("1.0") ) @@ -70,7 +71,7 @@ def simple_primary_key(self): ) -@TestFeature +@TestScenario @Requirements( RQ_SRS_030_ClickHouse_MySQLToClickHouseReplication_PrimaryKey_Composite("1.0") ) @@ -89,7 +90,7 @@ def composite_primary_key(self): ) -@TestFeature +@TestScenario @Requirements(RQ_SRS_030_ClickHouse_MySQLToClickHouseReplication_PrimaryKey_No("1.0")) def no_primary_key(self): """Check replicating MySQl table without any primary key.""" @@ -107,17 +108,13 @@ def no_primary_key(self): ) -@TestModule +@TestFeature @Name("primary keys") -def module(self): +def feature(self): """MySql to ClickHouse replication simple and composite primary keys tests.""" with Given("I enable debezium and sink connectors after kafka starts up"): init_debezium_connector() - with Pool(1) as executor: - try: - for feature in loads(current_module(), Feature): - Feature(test=feature, parallel=True, executor=executor)() - finally: - join() + for scenario in loads(current_module(), Scenario): + scenario() diff --git a/sink-connector/tests/integration/tests/replication.py b/sink-connector/tests/integration/tests/replication.py new file mode 100644 index 000000000..4c9c8471f --- /dev/null +++ b/sink-connector/tests/integration/tests/replication.py @@ -0,0 +1,370 @@ +from testflows.core import * + +from integration.helpers.common import getuid +from integration.tests.steps.clickhouse import ( + check_if_table_was_created, + validate_data_in_clickhouse_table, + check_column, + select, + get_random_value_from_column, +) +from integration.tests.steps.configurations import ( + init_sink_connector, + init_debezium_connector, +) +from integration.tests.steps.datatypes import ( + all_mysql_datatypes_dict, +) +from integration.tests.steps.mysql.alters import ( + add_column, + change_column, + modify_column, + drop_column, + drop_primary_key, + add_primary_key, +) +from integration.tests.steps.mysql.deletes import delete_all_records, delete +from integration.tests.steps.mysql.mysql import ( + create_mysql_table, + insert, + generate_sample_mysql_value, + generate_special_case_names, +) +from integration.tests.steps.mysql.updates import update + + +@TestOutline +def auto_create_table( + self, + column_datatype="VARCHAR(255)", + column_name="name", + node=None, + table_name=None, + replicate=False, + validate_values=True, + multiple_inserts=False, +): + """Check that tables created on the source database are replicated on the destination.""" + databases = self.context.databases + update_sink_config = self.context.update_sink_config + + if type(databases) is not list: + databases = [databases] + + if table_name is None: + table_name = "table_" + getuid() + + if node is None: + node = self.context.cluster.node("mysql-master") + for database in databases: + with Given("I initialize sink connector for the given database and table"): + init_sink_connector( + auto_create_tables=True, + topics=f"SERVER5432.{database}.{table_name}", + auto_create_replicated_tables=replicate, + update=update_sink_config, + ) + + with And("I create a table on the source database"): + create_mysql_table( + table_name=table_name, + database_name=database, + mysql_node=node, + columns=f"{column_name} {column_datatype}", + ) + + with When("I insert values into the table"): + if not multiple_inserts: + table_values = f"{generate_sample_mysql_value('INT')},{generate_sample_mysql_value(column_datatype)}" + insert( + table_name=table_name, values=table_values, database_name=database + ) + else: + for _ in range(10): + insert( + table_name=table_name, + values=f"{generate_sample_mysql_value('INT')}, {generate_sample_mysql_value(column_datatype)}", + database_name=database, + ) + if not validate_values: + table_values = "" + + with Then("I check that the table is replicated on the destination database"): + with Check(f"table with {column_datatype} was replicated"): + check_if_table_was_created( + database_name=database, table_name=table_name + ) + if validate_values: + validate_data_in_clickhouse_table( + table_name=table_name, + expected_output=table_values.replace("'", ""), + database_name=database, + statement=f"id, {column_name}", + ) + + +@TestStep(Given) +def auto_create_with_multiple_inserts(self, table_name=None): + """Create a table with multiple inserts.""" + auto_create_table( + table_name=table_name, + multiple_inserts=True, + validate_values=False, + ) + + +@TestScenario +def check_auto_creation_all_datatypes(self, table_name=None): + """Check that tables created on the source database are replicated on the destination.""" + for name, datatype in all_mysql_datatypes_dict.items(): + ( + Check( + test=auto_create_table, + name=f"auto table creation with {datatype} datatype", + )( + column_name=name, + column_datatype=datatype, + table_name=table_name, + ) + ) + + +@TestScenario +def auto_creation_different_table_names(self): + """Check that tables with all datatypes are replicated on the destination table when tables have names with special cases.""" + table_names = generate_special_case_names(self.context.number_of_tables) + + for table_name in table_names: + Check( + test=auto_create_table, + name=f"auto table creation with {table_name} table name", + )( + table_name=table_name, + ) + + +@TestScenario +def add_column_on_source(self, database): + """Check that the column is added on the table when we add a column on a database.""" + table_name = f"table_{getuid()}" + column = "new_col" + + with Given("I create a table on multiple databases"): + auto_create_table(table_name=table_name) + + for database in self.context.databases: + with When("I add a column on the table"): + add_column(table_name=table_name, column_name=column, database=database) + + with Then("I check that the column was added on the table"): + check_column(table_name=table_name, column_name=column, database=database) + + +@TestScenario +def change_column_on_source(self, database): + """Check that the column is changed on the table when we change a column on a database.""" + table_name = f"table_{getuid()}" + column = "col1" + new_column = "new_col" + new_column_type = "varchar(255)" + + with Given("I create a table on multiple databases"): + auto_create_table(table_name=table_name) + + for database in self.context.databases: + with When("I change a column on the table"): + change_column( + table_name=table_name, + database=database, + column_name=column, + new_column_name=new_column, + new_column_type=new_column_type, + ) + + with Then("I check that the column was changed on the table"): + check_column( + table_name=table_name, column_name=new_column, database=database + ) + + +@TestScenario +def modify_column_on_source(self, database): + """Check that the column is modified on the table when we modify a column on a database.""" + table_name = f"table_{getuid()}" + column = "col1" + new_column_type = "varchar(255)" + + with Given("I create a table on multiple databases"): + auto_create_table(table_name=table_name) + + for database in self.context.databases: + with When("I modify a column on the table"): + modify_column( + table_name=table_name, + database=database, + column_name=column, + new_column_type=new_column_type, + ) + + with Then("I check that the column was modified on the table"): + check_column( + table_name=table_name, + database=database, + column_name=column, + column_type=new_column_type, + ) + + +@TestScenario +def drop_column_on_source(self, database): + """Check that the column is dropped from the table when we drop a column on a database.""" + table_name = f"table_{getuid()}" + column = "col1" + + with Given("I create a table on multiple databases"): + auto_create_table(table_name=table_name) + + for database in self.context.databases: + with When("I drop a column on the table"): + drop_column(table_name=table_name, database=database, column_name=column) + + with Then("I check that the column was dropped from the table"): + check_column(table_name=table_name, database=database, column_name="") + + +@TestScenario +def add_primary_key_on_a_database(self): + """Check that the primary key is added to the table when we add a primary key on a database.""" + table_name = f"table_{getuid()}" + column = "col1" + + with Given("I create a table on multiple databases"): + auto_create_table(table_name=table_name) + + for database in self.context.databases: + with When("I add a primary key on the table"): + drop_primary_key(table_name=table_name, database=database) + add_primary_key( + table_name=table_name, database=database, column_name=column + ) + + with Then("I check that the primary key was added to the table"): + check_column(table_name=table_name, database=database, column_name=column) + + +@TestScenario +def delete_all_records_from_source(self): + """Check that records are deleted from the destination table when we delete all columns on the source.""" + table_name = f"table_{getuid()}" + + with Given("I create a table on multiple databases"): + auto_create_with_multiple_inserts(table_name=table_name) + + for database in self.context.databases: + with When("I delete columns on the source"): + delete_all_records(table_name=table_name, database=database) + + with Then("I check that the primary key was added to the table"): + select(table_name=table_name, database=database, manual_output="") + + +@TestScenario +def delete_specific_records(self): + """Check that records are deleted from the destination table when we execute DELETE WHERE on source table.""" + table_name = f"table_{getuid()}" + + with Given("I create a table on multiple databases"): + auto_create_with_multiple_inserts(table_name=table_name) + + for database in self.context.databases: + with When("I delete columns on the source"): + delete(table_name=table_name, database=database, condition="WHERE id > 0") + + with Then("I check that the primary key was added to the table"): + select(table_name=table_name, database=database, manual_output="") + + +@TestScenario +def update_record_on_source(self): + """Check that the record is updated on the destination table when we update a record on the source.""" + table_name = f"table_{getuid()}" + + with Given("I create a table on multiple databases"): + auto_create_with_multiple_inserts(table_name=table_name) + + for database in self.context.databases: + with When("I update a record on the source"): + random_value = get_random_value_from_column( + database=database, table_name=table_name, column_name="id" + ) + + update( + table_name=table_name, + database=database, + set="id 5", + condition=f"id = {random_value}", + ) + + with Then("I check that the primary key was added to the table"): + select( + table_name=table_name, + database=database, + where=f"id = {random_value}", + manual_output=random_value, + ) + + +@TestSuite +def table_creation(self): + """Check that tables created on the source database are correctly replicated on the destination.""" + Scenario(run=check_auto_creation_all_datatypes) + Scenario(run=auto_creation_different_table_names) + + +@TestSuite +def alters(self): + """Check that alter statements performed on the source are replicated to the destination.""" + databases = self.context.databases + + for database in databases: + Scenario(test=add_column_on_source)(database=database) + Scenario(test=change_column_on_source)(database=database) + Scenario(test=modify_column_on_source)(database=database) + Scenario(test=drop_column_on_source)(database=database) + Scenario(test=add_primary_key_on_a_database)(database=database) + + +@TestSuite +def deletes(self): + """Check that deletes are replicated to the destination.""" + databases = self.context.databases + + for database in databases: + Scenario(test=delete_all_records_from_source)(database=database) + Scenario(test=delete_specific_records)(database=database) + + +@TestSuite +def updates(self): + """Check that updates are replicated to the destination.""" + Scenario(run=update_record_on_source) + + +@TestFeature +@Name("replication") +def replication(self, number_of_tables=20, databases: list = None, update: dict = None): + """Check that actions performed on the source database are replicated on the destination database.""" + + self.context.number_of_tables = number_of_tables + self.context.update_sink_config = update + + if databases is None: + self.context.databases = ["test"] + else: + self.context.databases = databases + + with Given("I enable debezium and sink connectors after kafka starts up"): + init_debezium_connector() + + for suite in loads(current_module(), Suite): + suite() diff --git a/sink-connector/tests/integration/tests/sanity.py b/sink-connector/tests/integration/tests/sanity.py index 5ee5cc962..b59eb2606 100644 --- a/sink-connector/tests/integration/tests/sanity.py +++ b/sink-connector/tests/integration/tests/sanity.py @@ -1,6 +1,6 @@ +from integration.tests.steps.configurations import * from integration.tests.steps.sql import * -from integration.tests.steps.statements import * -from integration.tests.steps.service_settings_steps import * +from integration.tests.steps.datatypes import * @TestOutline diff --git a/sink-connector/tests/integration/tests/schema_changes.py b/sink-connector/tests/integration/tests/schema_changes.py index c38d56c8d..747edbd4b 100644 --- a/sink-connector/tests/integration/tests/schema_changes.py +++ b/sink-connector/tests/integration/tests/schema_changes.py @@ -1,6 +1,6 @@ +from integration.tests.steps.configurations import * from integration.tests.steps.sql import * -from integration.tests.steps.statements import * -from integration.tests.steps.service_settings_steps import * +from integration.tests.steps.datatypes import * @TestOutline diff --git a/sink-connector/tests/integration/tests/steps/clickhouse.py b/sink-connector/tests/integration/tests/steps/clickhouse.py new file mode 100644 index 000000000..29cbef374 --- /dev/null +++ b/sink-connector/tests/integration/tests/steps/clickhouse.py @@ -0,0 +1,347 @@ +from integration.helpers.common import * +from integration.tests.steps.datatypes import mysql_to_clickhouse_datatypes_mapping + + +@TestStep(Then) +def drop_database(self, database_name=None, node=None, cluster=None): + """Drop ClickHouse database.""" + + if cluster is None: + cluster = "replicated_cluster" + + if database_name is None: + database_name = "test" + + if node is None: + node = self.context.cluster.node("clickhouse") + with By("executing drop database query"): + node.query( + rf"DROP DATABASE IF EXISTS \`{database_name}\` ON CLUSTER {cluster};" + ) + + +@TestStep +def select_column_type(self, node, database, table_name, column_name): + """Check column type in ClickHouse table.""" + with By(f"checking column type for {column_name}"): + node.query( + f"SELECT type FROM system.columns WHERE table = '{table_name}' AND name = '{column_name}' AND database = '{database}' FORMAT TabSeparated" + ).output.strip() + + +@TestStep +def validate_column_type(self, mysq_column_type): + """Validate column type in ClickHouse table.""" + with By(f"validating column type"): + assert ( + mysql_to_clickhouse_datatypes_mapping[mysq_column_type]["mysql"] + == mysql_to_clickhouse_datatypes_mapping[mysq_column_type]["clickhouse"] + ), f"expected {mysql_to_clickhouse_datatypes_mapping[mysq_column_type]['mysql']} but got {mysql_to_clickhouse_datatypes_mapping[mysq_column_type]['clickhouse']}" + + +@TestStep(Then) +def check_column( + self, table_name, column_name, node=None, column_type=None, database=None +): + """Check if column exists in ClickHouse table.""" + + if database is None: + database = "test" + + if column_type is not None: + if "varchar" in column_type: + column_type = "String" + + if node is None: + node = self.context.cluster.node("clickhouse") + + if column_type is None: + select = "name" + else: + select = "name, type" + + with By(f"checking if {column_name} exists in {table_name}"): + for retry in retries(timeout=25, delay=1): + with retry: + column = node.query( + f"SELECT {select} FROM system.columns WHERE table = '{table_name}' AND name = '{column_name}' AND database = '{database}' FORMAT TabSeparated" + ) + + expected_output = ( + column_name + if column_type is None + else f"{column_name} {column_type}" + ) + + assert ( + column.output.strip() == expected_output + ), f"expected {expected_output} but got {column.output.strip()}" + + +@TestStep(Given) +def create_clickhouse_database(self, name=None, node=None): + """Create ClickHouse database.""" + if name is None: + name = "test" + + if node is None: + node = self.context.cluster.node("clickhouse") + + try: + with By(f"adding {name} database if not exists"): + drop_database(database_name=name) + + node.query( + rf"CREATE DATABASE IF NOT EXISTS \`{name}\` ON CLUSTER replicated_cluster" + ) + yield + finally: + with Finally(f"I delete {name} database if exists"): + drop_database(database_name=name) + + +@TestStep(Then) +def select( + self, + manual_output=None, + table_name=None, + statement=None, + database=None, + node=None, + with_final=False, + with_optimize=False, + sign_column="_sign", + timeout=300, + where=None, +): + """SELECT statement in ClickHouse with an option to use FINAL or loop SELECT + OPTIMIZE TABLE default simple 'SELECT'""" + if node is None: + node = self.context.cluster.node("clickhouse") + if table_name is None: + table_name = "users" + if statement is None: + statement = "*" + if database is None: + database = "test" + + mysql = self.context.cluster.node("mysql-master") + mysql_output = mysql.query(f"select {statement} from {table_name}").output.strip()[ + 90: + ] + + if manual_output is None: + manual_output = mysql_output + + if with_final: + retry( + node.query, + timeout=timeout, + delay=10, + )( + f"SELECT {statement} FROM {database}.{table_name} FINAL", + message=f"{manual_output}", + ) + elif with_optimize: + for attempt in retries(count=10, timeout=100, delay=5): + with attempt: + node.query(f"OPTIMIZE TABLE {database}.{table_name} FINAL DEDUPLICATE") + + node.query( + f"SELECT {statement} FROM {database}.{table_name} where {sign_column} !=-1 FORMAT CSV", + message=f"{manual_output}", + ) + else: + r = "SELECT {statement} FROM {database}.{table_name}" + + if where: + r += f" WHERE {where}" + + r += " FORMAT CSV" + + retry( + node.query, + timeout=timeout, + delay=10, + )( + r, + message=f"{manual_output}", + ) + + +@TestStep(Then) +def check_if_table_was_created( + self, + table_name, + database_name=None, + node=None, + timeout=40, + message=1, + replicated=False, +): + """Check if table was created in ClickHouse.""" + if database_name is None: + database_name = "test" + + if node is None: + node = self.context.cluster.node("clickhouse") + + if replicated: + for node in self.context.cluster.nodes["clickhouse"]: + retry(self.context.cluster.node(node).query, timeout=timeout, delay=3)( + rf"EXISTS \`{database_name}\`.\`{table_name}\`", message=f"{message}" + ) + else: + retry(node.query, timeout=timeout, delay=3)( + rf"EXISTS \`{database_name}\`.\`{table_name}\`", message=f"{message}" + ) + + +@TestStep(Then) +def validate_data_in_clickhouse_table( + self, + table_name, + expected_output, + statement="*", + node=None, + database_name=None, + timeout=40, + replicated=False, +): + """Validate data in ClickHouse table.""" + + if database_name is None: + database_name = "test" + + if node is None: + node = self.context.cluster.node("clickhouse") + + if replicated: + for node in self.context.cluster.nodes["clickhouse"]: + for retry in retries(timeout=timeout, delay=1): + with retry: + data = ( + self.context.cluster.node(node) + .query( + f"SELECT {statement} FROM {database_name}.{table_name} ORDER BY tuple(*) FORMAT CSV" + ) + .output.strip() + .replace('"', "") + ) + + assert ( + data == expected_output + ), f"Expected: {expected_output}, Actual: {data}" + else: + for retry in retries(timeout=timeout, delay=1): + with retry: + data = ( + node.query( + f"SELECT {statement} FROM {database_name}.{table_name} ORDER BY tuple(*) FORMAT CSV" + ) + .output.strip() + .replace('"', "") + ) + + assert ( + data == expected_output + ), f"Expected: {expected_output}, Actual: {data}" + + +@TestStep(Then) +def validate_rows_number( + self, table_name, expected_rows, node=None, database_name=None +): + """Validate number of rows in ClickHouse table.""" + + if database_name is None: + database_name = "test" + + if node is None: + node = self.context.cluster.node("clickhouse") + + for retry in retries(timeout=40): + with retry: + data = node.query( + f"SELECT count(*) FROM {database_name}.{table_name} ORDER BY tuple(*) FORMAT CSV" + ) + assert data.output.strip().replace('"', "") == expected_rows, error() + + +@TestStep(Then) +def verify_table_creation_in_clickhouse( + self, + table_name, + statement, + clickhouse_table_engine=(""), + timeout=300, + clickhouse_node=None, + database_name=None, + manual_output=None, + with_final=False, + with_optimize=False, + replicated=False, +): + """ + Verify the creation of tables on all ClickHouse nodes where they are expected, and ensure data consistency with MySQL. + """ + + if clickhouse_node is None: + clickhouse_node = self.context.cluster.node("clickhouse") + + if database_name is None: + database_name = "test" + + if replicated: + with Then("I check table creation on few nodes"): + for node in self.context.cluster.nodes["clickhouse"]: + retry(self.context.cluster.node(node).query, timeout=timeout, delay=3)( + f"EXISTS {database_name}.{table_name}", message=f"{message}" + ) + else: + with Then("I check table creation"): + retry(clickhouse_node.query, timeout=100, delay=3)( + "SHOW TABLES FROM test", message=f"{table_name}" + ) + + with Then("I check that ClickHouse table has same number of rows as MySQL table"): + select( + table_name=table_name, + manual_output=manual_output, + statement=statement, + with_final=with_final, + with_optimize=with_optimize, + timeout=timeout, + ) + if clickhouse_table_engine.startswith("Replicated"): + with Then( + "I check that ClickHouse table has same number of rows as MySQL table on the replica node if it is " + "replicted table" + ): + select( + table_name=table_name, + manual_output=manual_output, + statement=statement, + node=self.context.cluster.node("clickhouse1"), + with_final=with_final, + with_optimize=with_optimize, + timeout=timeout, + ) + + +@TestStep(When) +def get_random_value_from_column( + self, table_name, column_name, database=None, node=None +): + """Get random value from ClickHouse table column.""" + if database is None: + database = "test" + + if node is None: + node = self.context.cluster.node("clickhouse") + + with By(f"getting random value from {column_name}"): + random_value = node.query( + rf"SELECT {column_name} FROM {database}.\`{table_name}\` ORDER BY rand() LIMIT 1" + ) + + return random_value diff --git a/sink-connector/tests/integration/tests/steps/service_settings_steps.py b/sink-connector/tests/integration/tests/steps/configurations.py similarity index 56% rename from sink-connector/tests/integration/tests/steps/service_settings_steps.py rename to sink-connector/tests/integration/tests/steps/configurations.py index 5230ac8d6..6102314d1 100644 --- a/sink-connector/tests/integration/tests/steps/service_settings_steps.py +++ b/sink-connector/tests/integration/tests/steps/configurations.py @@ -1,4 +1,4 @@ -from integration.requirements.requirements import * +import json from integration.helpers.common import * @@ -7,73 +7,68 @@ def init_sink_connector( self, node=None, - auto_create_tables="auto", + url="clickhouse-sink-connector-kafka", + auto_create_tables=True, auto_create_replicated_tables=False, - topics="SERVER5432.sbtest.sbtest1,SERVER5432.test.users1,SERVER5432.test.users2,SERVER5432.test.users3, SERVER5432.test.users", + topics="SERVER5432.sbtest.sbtest1,SERVER5432.test.users1,SERVER5432.test.users2,SERVER5432.test.users3, " + "SERVER5432.test.users", + update=None, ): """ - Initialize sink connector. + Initialize sink connector with custom configuration. """ if node is None: node = self.context.cluster.node("bash-tools") - if auto_create_replicated_tables: - auto_create_replicated_tables = "true" - else: - auto_create_replicated_tables = "false" - if auto_create_tables == "auto": - auto_create_tables_local = "true" - else: - auto_create_tables_local = "false" + auto_create_tables = True + elif auto_create_tables == "manual": + auto_create_tables = False - # "topics": "SERVER5432.test.users", - sink_settings_transfer_command_confluent = ( - """cat </dev/null | jq ." ) +@TestStep(Given) +def init_sink_connector_auto_created(self, topics, node=None, update=None): + """Initialize sink connector with auto created tables.""" + init_sink_connector( + auto_create_tables=True, + topics=topics, + auto_create_replicated_tables=False, + node=node, + update=update, + ) + + +@TestStep(Given) +def init_sink_connector_manual_created(self, topics, node=None, update=None): + """Initialize sink connector with manual created tables.""" + init_sink_connector( + auto_create_tables=False, + topics=topics, + auto_create_replicated_tables=False, + node=node, + update=update, + ) + + +@TestStep(Given) +def init_sink_connector_auto_created_replicated(self, topics, node=None, update=None): + """Initialize sink connector with auto created replicated tables.""" + init_sink_connector( + auto_create_tables=True, + topics=topics, + auto_create_replicated_tables=True, + node=node, + update=update, + ) + + +@TestStep(Given) +def init_sink_connector_manual_created_replicated(self, topics, node=None, update=None): + """Initialize sink connector with manual created replicated tables.""" + init_sink_connector( + auto_create_tables=False, + topics=topics, + auto_create_replicated_tables=True, + node=node, + update=update, + ) + + @TestStep(Given) def init_debezium_connector(self, node=None): """ @@ -105,50 +148,6 @@ def init_debezium_connector(self, node=None): if node is None: node = self.context.cluster.node("bash-tools") - debezium_settings_transfer_command_apicurio = """cat <?@[]^_{|}~""" + + def generate_table_name(length): + """Generate a random table name of a given length.""" + allowed_chars = ( + string.ascii_letters + + string.digits + + special_chars + # + utf8_chars + + chinese_characters + # + punctuation + ) + return "".join(random.choice(allowed_chars) for _ in range(length)) + + table_names = set() + + # Add reserved keywords + table_names.update(reserved_keywords) + + base_name = generate_table_name(random.randint(1, max_length)).lower() + case_variation_name = "".join( + random.choice([char.upper(), char.lower()]) for char in base_name + ) + + table_names.add(base_name) + table_names.add(case_variation_name) + + while len(table_names) < num_names: + length = random.randint(1, max_length) + name = generate_table_name(length) + + # Ensure the name does not start with a digit + if name[0] in string.digits: + name = "_" + name + + table_names.add(f"{name}") + + return list(table_names) + + +def generate_sample_mysql_value(data_type): + """Generate a sample MySQL value for the provided datatype.""" + if data_type.startswith("DECIMAL"): + precision, scale = map( + int, data_type[data_type.index("(") + 1 : data_type.index(")")].split(",") + ) + number = round( + random.uniform(-(10 ** (precision - scale)), 10 ** (precision - scale)), + scale, + ) + return str(number) + elif data_type.startswith("DOUBLE"): + # Adjusting the range to avoid overflow, staying within a reasonable limit + return f"'{str(random.uniform(-1.7e307, 1.7e307))}'" + elif data_type == "DATE NOT NULL": + return f'\'{(datetime.today() - timedelta(days=random.randint(1, 365))).strftime("%Y-%m-%d")}\'' + elif data_type.startswith("DATETIME"): + return f'\'{(datetime.now() - timedelta(days=random.randint(1, 365))).strftime("%Y-%m-%d %H:%M:%S.%f")[:19]}\'' + elif data_type.startswith("TIME"): + if "6" in data_type: + return f'\'{(datetime.now()).strftime("%H:%M:%S.%f")[: 8 + 3]}\'' + else: + return f'\'{(datetime.now()).strftime("%H:%M:%S")}\'' + elif "INT" in data_type: + if "TINYINT" in data_type: + return str( + random.randint(0, 255) + if "UNSIGNED" in data_type + else random.randint(-128, 127) + ) + elif "SMALLINT" in data_type: + return str( + random.randint(0, 65535) + if "UNSIGNED" in data_type + else random.randint(-32768, 32767) + ) + elif "MEDIUMINT" in data_type: + return str( + random.randint(0, 16777215) + if "UNSIGNED" in data_type + else random.randint(-8388608, 8388607) + ) + elif "BIGINT" in data_type: + return str( + random.randint(0, 2**63 - 1) + if "UNSIGNED" in data_type + else random.randint(-(2**63), 2**63 - 1) + ) + else: # INT + return f'\'{str(random.randint(0, 4294967295) if "UNSIGNED" in data_type else random.randint(-2147483648, 2147483647))}\'' + elif ( + data_type.startswith("CHAR") + or data_type.startswith("VARCHAR") + or data_type.startswith("TEXT") + ): + return "'SampleText'" + elif data_type.startswith("BLOB"): + return "'SampleBinaryData'" + elif data_type.endswith("BLOB NOT NULL"): + return "'SampleBinaryData'" + elif data_type.startswith("BINARY") or data_type.startswith("VARBINARY"): + return "'a'" + else: + return "UnknownType" + + +@TestStep(Given) +def create_mysql_database(self, node=None, database_name=None): + """Creation of MySQL database.""" + if node is None: + node = self.context.cluster.node("mysql-master") + + if database_name is None: + database_name = "test" + + try: + with Given(f"I create MySQL database {database_name}"): + node.query(rf"DROP DATABASE IF EXISTS \`{database_name}\`;") + node.query(rf"CREATE DATABASE IF NOT EXISTS \`{database_name}\`;") + yield + finally: + with Finally(f"I delete MySQL database {database_name}"): + node.query(rf"DROP DATABASE IF EXISTS \`{database_name}\`;") + + +@TestStep(Given) +def create_mysql_table( + self, + table_name, + columns, + mysql_node=None, + clickhouse_node=None, + database_name=None, + primary_key="id", + engine=True, + partition_by_mysql=False, +): + """Create MySQL table that will be auto created in ClickHouse.""" + + if database_name is None: + database_name = "test" + + if mysql_node is None: + mysql_node = self.context.cluster.node("mysql-master") + + if clickhouse_node is None: + clickhouse_node = self.context.cluster.node("clickhouse") + + try: + key = "" + if primary_key is not None: + key = f"{primary_key} INT NOT NULL," + + with Given(f"I create MySQL table", description=name): + query = rf"CREATE TABLE IF NOT EXISTS \`{database_name}\`.\`{table_name}\` ({key}{columns}" + + if primary_key is not None: + query += f", PRIMARY KEY ({primary_key}))" + else: + query += ")" + + if engine: + query += f" ENGINE = InnoDB" + + if partition_by_mysql: + query += f", PARTITION BY {partition_by_mysql}" + + query += ";" + + mysql_node.query(query) + + yield + finally: + with Finally( + "I clean up by deleting MySQL to ClickHouse replicated table", + description={name}, + ): + mysql_node.query( + rf"DROP TABLE IF EXISTS \`{database_name}\`.\`{table_name}\`;" + ) + clickhouse_node.query( + rf"DROP TABLE IF EXISTS \`{database_name}\`.\`{table_name}\` ON CLUSTER replicated_cluster;" + ) + + +@TestStep(Given) +def create_sample_table(self, table_name, database=None, node=None): + """Create a sample table in MySQL.""" + if node is None: + node = self.context.cluster.node("mysql-master") + + if database is None: + database = "test" + + with By(f"creating a sample table {table_name} in MySQL"): + create_mysql_table( + table_name=table_name, + database_name=database, + mysql_node=node, + columns=f"name VARCHAR(255)", + ) + + with And("Inserting sample data"): + insert( + table_name=table_name, values="1,'test'", node=node, database_name=database + ) + + +@TestStep +def create_mysql_to_clickhouse_replicated_table( + self, + name, + mysql_columns, + clickhouse_table_engine, + database_name=None, + clickhouse_columns=None, + mysql_node=None, + clickhouse_node=None, + version_column="_version", + sign_column="_sign", + primary_key="id", + partition_by=None, + engine=True, + partition_by_mysql=False, +): + """Create MySQL to ClickHouse replicated table.""" + if database_name is None: + database_name = "test" + + if mysql_node is None: + mysql_node = self.context.cluster.node("mysql-master") + + if clickhouse_node is None: + clickhouse_node = self.context.cluster.node("clickhouse") + + try: + with Given(f"I create MySQL table", description=name): + query = f"CREATE TABLE IF NOT EXISTS {database_name}.{name} (id INT NOT NULL,{mysql_columns}" + + if primary_key is not None: + query += f", PRIMARY KEY ({primary_key}))" + else: + query += ")" + + if engine: + query += f" ENGINE = InnoDB" + + if partition_by_mysql: + query += f", PARTITION BY {partition_by_mysql}" + + query += ";" + + mysql_node.query(query) + + yield + finally: + with Finally( + "I clean up by deleting MySQL to CH replicated table", description={name} + ): + mysql_node.query(f"DROP TABLE IF EXISTS {database_name}.{name};") + clickhouse_node.query( + f"DROP TABLE IF EXISTS {database_name}.{name} ON CLUSTER replicated_cluster;" + ) + time.sleep(5) + + +@TestStep(Given) +def create_table_with_no_primary_key(self, table_name, clickhouse_table_engine): + """Create MySQL table without primary key.""" + + with By(f"creating a {table_name} table without primary key"): + create_mysql_to_clickhouse_replicated_table( + name=f"{table_name}_no_primary_key", + mysql_columns="x INT NOT NULL", + clickhouse_columns="x Int32", + clickhouse_table_engine=clickhouse_table_engine, + primary_key=None, + ) + + +@TestStep(Given) +def create_table_with_no_engine(self, table_name, clickhouse_table_engine): + """Create MySQL table without engine.""" + + with By(f"creating a {table_name} table without engine"): + create_mysql_to_clickhouse_replicated_table( + name=f"{table_name}_no_engine", + mysql_columns="x INT NOT NULL", + clickhouse_columns="x Int32", + clickhouse_table_engine=clickhouse_table_engine, + engine=False, + ) + + +@TestStep(Given) +def create_table_with_primary_key_and_engine(self, table_name, clickhouse_table_engine): + """Create MySQL table with primary key and with engine.""" + + with By(f"creating a {table_name} table with primary key and with engine"): + create_mysql_to_clickhouse_replicated_table( + name=f"{table_name}", + mysql_columns="x INT NOT NULL", + clickhouse_columns="x Int32", + clickhouse_table_engine=clickhouse_table_engine, + ) + + +@TestStep(Given) +def create_table_with_no_engine_and_no_primary_key( + self, table_name, clickhouse_table_engine +): + """Create MySQL table without engine and without primary key.""" + + with By(f"creating a {table_name} table without engine and without primary key"): + create_mysql_to_clickhouse_replicated_table( + name=f"{table_name}_no_engine_no_primary_key", + mysql_columns="x INT NOT NULL", + clickhouse_columns="x Int32", + clickhouse_table_engine=clickhouse_table_engine, + primary_key=None, + engine=False, + ) + + +@TestStep(Given) +def create_tables(self, table_name, clickhouse_table_engine="ReplacingMergeTree"): + """Create different types of replicated tables.""" + + with Given("I set the table names"): + tables_list = [ + f"{table_name}", + f"{table_name}_no_primary_key", + f"{table_name}_no_engine", + f"{table_name}_no_engine_no_primary_key", + ] + + with And( + "I create MySQL to ClickHouse replicated table with primary key and with engine" + ): + create_table_with_primary_key_and_engine( + table_name=table_name, clickhouse_table_engine=clickhouse_table_engine + ) + + with And( + "I create MySQL to ClickHouse replicated table without primary key and with engine" + ): + create_table_with_no_primary_key( + table_name=table_name, clickhouse_table_engine=clickhouse_table_engine + ) + + with And( + "I create MySQL to ClickHouse replicated table with primary key and without engine" + ): + create_table_with_no_engine( + table_name=table_name, clickhouse_table_engine=clickhouse_table_engine + ) + + with And( + "I create MySQL to ClickHouse replicated table without primary key and without engine" + ): + create_table_with_no_engine_and_no_primary_key( + table_name=table_name, clickhouse_table_engine=clickhouse_table_engine + ) + + return tables_list + + +@TestStep(When) +def insert(self, table_name, values, node=None, database_name=None): + """Insert data into MySQL table.""" + if database_name is None: + database_name = "test" + + if node is None: + node = self.context.cluster.node("mysql-master") + + with When("I insert data into MySQL table"): + node.query( + rf"INSERT INTO \`{database_name}\`.\`{table_name}\` VALUES ({values});" + ) + + +@TestStep(Given) +def insert_precondition_rows( + self, + first_insert_id, + last_insert_id, + table_name, + insert_values=None, + node=None, +): + """Insert some controlled interval of ID's in MySQL table.""" + if insert_values is None: + insert_values = "({x},2,'a','b')" + + if node is None: + node = self.context.cluster.node("mysql-master") + + with Given( + f"I insert {first_insert_id - last_insert_id} rows of data in MySQL table" + ): + for i in range(first_insert_id, last_insert_id + 1): + node.query(f"INSERT INTO `{table_name}` VALUES {insert_values}".format(x=i)) + + +@TestStep(When) +def complex_insert( + self, + table_name, + values, + start_id=1, + start_value=1, + node=None, + partitions=101, + parts_per_partition=1, + block_size=1, + exitcode=True, +): + """Insert data having specified number of partitions and parts.""" + if node is None: + node = self.context.cluster.node("mysql-master") + + x = start_id + y = start_value + + insert_values_1 = ",".join( + f"{values[0]}".format(x=x, y=y) + for x in range(start_id, partitions + start_id) + for y in range(start_value, block_size * parts_per_partition + start_value) + ) + + if exitcode: + retry( + node.query, + timeout=300, + delay=10, + )(f"INSERT INTO {table_name} VALUES {insert_values_1}", exitcode=0) + else: + retry( + node.query, + timeout=300, + delay=10, + )(f"INSERT INTO {table_name} VALUES {insert_values_1}") + + +@TestStep(When) +def delete_rows( + self, + table_name, + first_delete_id=None, + last_delete_id=None, + condition=None, + row_delete=False, + multiple=False, + no_checks=False, + check=False, + delay=False, +): + """ + Test step to delete rows from MySQL table. + """ + mysql = self.context.cluster.node("mysql-master") + + if row_delete: + if multiple: + command = ";".join( + [f"DELETE FROM {table_name} WHERE {i}" for i in condition] + ) + else: + command = f"DELETE FROM {table_name} WHERE {condition}" + r = mysql.query(command, no_checks=no_checks) + if check: + for attempt in retries(delay=0.1, timeout=30): + with attempt: + with Then("I check rows are deleted"): + check_result = mysql.query( + f"SELECT count() FROM {table_name} WHERE {condition}" + ) + assert check_result.output == "0", error() + if delay: + time.sleep(delay) + return r + else: + with Given( + f"I delete {last_delete_id - first_delete_id} rows of data in MySQL table" + ): + for i in range(first_delete_id, last_delete_id): + mysql.query(f"DELETE FROM {table_name} WHERE id={i}") + + +@TestStep(When) +def update( + self, + table_name, + first_update_id=None, + last_update_id=None, + condition=None, + row_update=False, + multiple=False, + no_checks=False, + check=False, + delay=False, +): + """Update query step for MySQL table.""" + mysql = self.context.cluster.node("mysql-master") + + if row_update: + if multiple: + command = ";".join( + [f"UPDATE {table_name} SET x=x+10000 WHERE {i}" for i in condition] + ) + else: + command = f"UPDATE {table_name} SET x=x+10000 WHERE {condition}" + r = mysql.query(command, no_checks=no_checks) + if check: + for attempt in retries(delay=0.1, timeout=30): + with attempt: + with Then("I check rows are deleted"): + check_result = mysql.query( + f"SELECT count() FROM {table_name} WHERE {condition}" + ) + assert check_result.output == "0", error() + if delay: + time.sleep(delay) + return r + else: + with Given( + f"I update {last_update_id - first_update_id} rows of data in MySQL table" + ): + for i in range(first_update_id, last_update_id): + mysql.query(f"UPDATE {table_name} SET k=k+5 WHERE id={i};") + + +@TestStep(When) +def concurrent_queries( + self, + table_name, + first_insert_number, + last_insert_number, + first_insert_id, + last_insert_id, + first_delete_id, + last_delete_id, + first_update_id, + last_update_id, +): + """Insert, update, delete for concurrent queries.""" + + with Given("I insert block of precondition rows"): + insert_precondition_rows( + table_name=table_name, + first_insert_id=first_insert_number, + last_insert_id=last_insert_number, + ) + + with When("I start concurrently insert, update and delete queries in MySQL table"): + By( + "inserting data in MySQL table", + test=insert_precondition_rows, + parallel=True, + )( + first_insert_id=first_insert_id, + last_insert_id=last_insert_id, + table_name=table_name, + ) + By( + "deleting data in MySQL table", + test=delete_rows, + parallel=True, + )( + first_delete_id=first_delete_id, + last_delete_id=last_delete_id, + table_name=table_name, + ) + By( + "updating data in MySQL table", + test=update, + parallel=True, + )( + first_update_id=first_update_id, + last_update_id=last_update_id, + table_name=table_name, + ) diff --git a/sink-connector/tests/integration/tests/steps/mysql/updates.py b/sink-connector/tests/integration/tests/steps/mysql/updates.py new file mode 100644 index 000000000..ac9de2f16 --- /dev/null +++ b/sink-connector/tests/integration/tests/steps/mysql/updates.py @@ -0,0 +1,16 @@ +from integration.helpers.common import * + + +@TestStep(Given) +def update(self, table_name, database=None, node=None, condition=None, set=None): + """Update records in MySQL table.""" + if database is None: + database = "test" + + if node is None: + node = self.context.cluster.node("mysql-master") + + query = rf"UPDATE {database}.{table_name} SET {set} WHERE {condition};" + + with By("executing UPDATE query"): + node.query(query) diff --git a/sink-connector/tests/integration/tests/steps/sql.py b/sink-connector/tests/integration/tests/steps/sql.py index 49e94dedd..1b47f1738 100644 --- a/sink-connector/tests/integration/tests/steps/sql.py +++ b/sink-connector/tests/integration/tests/steps/sql.py @@ -1,4 +1,5 @@ -from integration.requirements.requirements import * +import random +import string from integration.helpers.common import * @@ -69,7 +70,7 @@ def create_clickhouse_table( ) -@TestStep +@TestStep(Given) def create_mysql_to_clickhouse_replicated_table( self, name, @@ -225,7 +226,7 @@ def select( with_optimize=False, sign_column="_sign", timeout=100, - order_by=None + order_by=None, ): """SELECT with an option to either with FINAL or loop SELECT + OPTIMIZE TABLE default simple 'SELECT' :param insert: expected insert data if None compare with MySQL table @@ -292,11 +293,15 @@ def complex_check_creation_and_select( table_name, clickhouse_table, statement, - timeout=50, + timeout=140, + message=1, + clickhouse_node=None, + database_name=None, manual_output=None, with_final=False, with_optimize=False, order_by=None, + replicated=False, ): """ Check for table creation on all clickhouse nodes where it is expected and select data consistency with MySql @@ -309,29 +314,22 @@ def complex_check_creation_and_select( :param with_optimize: :return: """ - clickhouse = self.context.cluster.node("clickhouse") - clickhouse1 = self.context.cluster.node("clickhouse1") - clickhouse2 = self.context.cluster.node("clickhouse2") - clickhouse3 = self.context.cluster.node("clickhouse3") - mysql = self.context.cluster.node("mysql-master") + if clickhouse_node is None: + clickhouse_node = self.context.cluster.node("clickhouse") + + if database_name is None: + database_name = "test" - if clickhouse_table[1].startswith("Replicated"): + if replicated: with Then("I check table creation on few nodes"): - retry(clickhouse.query, timeout=30, delay=3)( - "SHOW TABLES FROM test", message=f"{table_name}" - ) - retry(clickhouse1.query, timeout=30, delay=3)( - "SHOW TABLES FROM test", message=f"{table_name}" - ) - retry(clickhouse2.query, timeout=30, delay=3)( - "SHOW TABLES FROM test", message=f"{table_name}" - ) - retry(clickhouse3.query, timeout=30, delay=3)( - "SHOW TABLES FROM test", message=f"{table_name}" - ) + for node in self.context.cluster.nodes["clickhouse"]: + retry(self.context.cluster.node(node).query, timeout=timeout, delay=3)( + f"EXISTS {database_name}.{table_name}", message=f"{message}" + ) + else: with Then("I check table creation"): - retry(clickhouse.query, timeout=30, delay=3)( + retry(clickhouse_node.query, timeout=30, delay=3)( "SHOW TABLES FROM test", message=f"{table_name}" ) diff --git a/sink-connector/tests/integration/tests/steps/statements.py b/sink-connector/tests/integration/tests/steps/statements.py deleted file mode 100644 index 403d0ea0d..000000000 --- a/sink-connector/tests/integration/tests/steps/statements.py +++ /dev/null @@ -1,112 +0,0 @@ -available_clickhouse_tables = [ - ("auto", "ReplacingMergeTree"), - ("manual", "ReplacingMergeTree"), -] - -all_nullable_mysql_datatypes = ( - f"D4 DECIMAL(2,1), D5 DECIMAL(30, 10)," - f" Doublex DOUBLE," - f" x_date DATE," - f"x_datetime6 DATETIME(6)," - f"x_time TIME," - f"x_time6 TIME(6)," - f"Intmin INT, Intmax INT," - f"UIntmin INT UNSIGNED, UIntmax INT UNSIGNED," - f"BIGIntmin BIGINT,BIGIntmax BIGINT," - f"UBIGIntmin BIGINT UNSIGNED,UBIGIntmax BIGINT UNSIGNED," - f"TIntmin TINYINT,TIntmax TINYINT," - f"UTIntmin TINYINT UNSIGNED,UTIntmax TINYINT UNSIGNED," - f"SIntmin SMALLINT,SIntmax SMALLINT," - f"USIntmin SMALLINT UNSIGNED,USIntmax SMALLINT UNSIGNED," - f"MIntmin MEDIUMINT,MIntmax MEDIUMINT," - f"UMIntmin MEDIUMINT UNSIGNED,UMIntmax MEDIUMINT UNSIGNED," - f" x_char CHAR," - f" x_text TEXT," - f" x_varchar VARCHAR(4)," - f" x_Blob BLOB," - f" x_Mediumblob MEDIUMBLOB," - f" x_Longblob LONGBLOB," - f" x_binary BINARY," - f" x_varbinary VARBINARY(4)" -) - -all_nullable_ch_datatypes = ( - f" D4 Nullable(DECIMAL(2,1)), D5 Nullable(DECIMAL(30, 10))," - f" Doublex Nullable(Float64)," - f" x_date Nullable(Date)," - f" x_datetime6 Nullable(String)," - f" x_time Nullable(String)," - f" x_time6 Nullable(String)," - f" Intmin Nullable(Int32), Intmax Nullable(Int32)," - f" UIntmin Nullable(UInt32), UIntmax Nullable(UInt32)," - f" BIGIntmin Nullable(UInt64), BIGIntmax Nullable(UInt64)," - f" UBIGIntmin Nullable(UInt64), UBIGIntmax Nullable(UInt64)," - f" TIntmin Nullable(Int8), TIntmax Nullable(Int8)," - f" UTIntmin Nullable(UInt8), UTIntmax Nullable(UInt8)," - f" SIntmin Nullable(Int16), SIntmax Nullable(Int16)," - f" USIntmin Nullable(UInt16), USIntmax Nullable(UInt16)," - f" MIntmin Nullable(Int32), MIntmax Nullable(Int32)," - f" UMIntmin Nullable(UInt32), UMIntmax Nullable(UInt32)," - f" x_char LowCardinality(Nullable(String))," - f" x_text Nullable(String)," - f" x_varchar Nullable(String)," - f" x_Blob Nullable(String)," - f" x_Mediumblob Nullable(String)," - f" x_Longblob Nullable(String)," - f" x_binary Nullable(String)," - f" x_varbinary Nullable(String)" -) - -all_mysql_datatypes = ( - f"D4 DECIMAL(2,1) NOT NULL, D5 DECIMAL(30, 10) NOT NULL," - f" Doublex DOUBLE NOT NULL," - f" x_date DATE NOT NULL," - f"x_datetime6 DATETIME(6) NOT NULL," - f"x_time TIME NOT NULL," - f"x_time6 TIME(6) NOT NULL," - f"Intmin INT NOT NULL, Intmax INT NOT NULL," - f"UIntmin INT UNSIGNED NOT NULL, UIntmax INT UNSIGNED NOT NULL," - f"BIGIntmin BIGINT NOT NULL,BIGIntmax BIGINT NOT NULL," - f"UBIGIntmin BIGINT UNSIGNED NOT NULL,UBIGIntmax BIGINT UNSIGNED NOT NULL," - f"TIntmin TINYINT NOT NULL,TIntmax TINYINT NOT NULL," - f"UTIntmin TINYINT UNSIGNED NOT NULL,UTIntmax TINYINT UNSIGNED NOT NULL," - f"SIntmin SMALLINT NOT NULL,SIntmax SMALLINT NOT NULL," - f"USIntmin SMALLINT UNSIGNED NOT NULL,USIntmax SMALLINT UNSIGNED NOT NULL," - f"MIntmin MEDIUMINT NOT NULL,MIntmax MEDIUMINT NOT NULL," - f"UMIntmin MEDIUMINT UNSIGNED NOT NULL,UMIntmax MEDIUMINT UNSIGNED NOT NULL," - f" x_char CHAR NOT NULL," - f" x_text TEXT NOT NULL," - f" x_varchar VARCHAR(4) NOT NULL," - f" x_Blob BLOB NOT NULL," - f" x_Mediumblob MEDIUMBLOB NOT NULL," - f" x_Longblob LONGBLOB NOT NULL," - f" x_binary BINARY NOT NULL," - f" x_varbinary VARBINARY(4) NOT NULL" -) - -all_ch_datatypes = ( - f" D4 DECIMAL(2,1), D5 DECIMAL(30, 10)," - f" Doublex Float64," - f" x_date Date," - f" x_datetime6 String," - f" x_time String," - f" x_time6 String," - f" Intmin Int32, Intmax Int32," - f" UIntmin UInt32, UIntmax UInt32," - f" BIGIntmin UInt64, BIGIntmax UInt64," - f" UBIGIntmin UInt64, UBIGIntmax UInt64," - f" TIntmin Int8, TIntmax Int8," - f" UTIntmin UInt8, UTIntmax UInt8," - f" SIntmin Int16, SIntmax Int16," - f" USIntmin UInt16, USIntmax UInt16," - f" MIntmin Int32, MIntmax Int32," - f" UMIntmin UInt32, UMIntmax UInt32," - f" x_char LowCardinality(String)," - f" x_text String," - f" x_varchar String," - f" x_Blob String," - f" x_Mediumblob String," - f" x_Longblob String," - f" x_binary String," - f" x_varbinary String" -) diff --git a/sink-connector/tests/integration/tests/steps/steps_global.py b/sink-connector/tests/integration/tests/steps/steps_global.py deleted file mode 100644 index faebf2d92..000000000 --- a/sink-connector/tests/integration/tests/steps/steps_global.py +++ /dev/null @@ -1,20 +0,0 @@ -from integration.helpers.common import * - - -@TestStep(Given) -def create_database(self, name="test", node=None): - """Create ClickHouse database.""" - if node is None: - node = self.context.cluster.node("clickhouse") - - try: - with By(f"adding {name} database if not exists"): - node.query( - f"CREATE DATABASE IF NOT EXISTS {name} ON CLUSTER replicated_cluster" - ) - yield - finally: - with Finally(f"I delete {name} database if exists"): - node.query( - f"DROP DATABASE IF EXISTS {name} ON CLUSTER replicated_cluster;" - ) diff --git a/sink-connector/tests/integration/tests/sysbench.py b/sink-connector/tests/integration/tests/sysbench.py index 2c1c1758f..b321e274b 100644 --- a/sink-connector/tests/integration/tests/sysbench.py +++ b/sink-connector/tests/integration/tests/sysbench.py @@ -1,6 +1,6 @@ from datetime import datetime -from integration.tests.steps.sql import * -from integration.tests.steps.service_settings_steps import * + +from integration.tests.steps.configurations import * @TestScenario diff --git a/sink-connector/tests/integration/tests/truncate.py b/sink-connector/tests/integration/tests/truncate.py index d0720964a..694597d7e 100644 --- a/sink-connector/tests/integration/tests/truncate.py +++ b/sink-connector/tests/integration/tests/truncate.py @@ -1,6 +1,6 @@ +from integration.tests.steps.configurations import * from integration.tests.steps.sql import * -from integration.tests.steps.statements import * -from integration.tests.steps.service_settings_steps import * +from integration.tests.steps.datatypes import * @TestOutline @@ -50,7 +50,7 @@ def truncate( ) -@TestFeature +@TestScenario def no_primary_key(self): """Check for `DELETE` with no primary key without InnoDB engine.""" for clickhouse_table in available_clickhouse_tables: @@ -64,7 +64,7 @@ def no_primary_key(self): ) -@TestFeature +@TestScenario def no_primary_key_innodb(self): """Check for `DELETE` with no primary key with InnoDB engine.""" for clickhouse_table in available_clickhouse_tables: @@ -78,7 +78,7 @@ def no_primary_key_innodb(self): ) -@TestFeature +@TestScenario def simple_primary_key(self): """Check for `DELETE` with simple primary key without InnoDB engine.""" for clickhouse_table in available_clickhouse_tables: @@ -92,7 +92,7 @@ def simple_primary_key(self): ) -@TestFeature +@TestScenario def simple_primary_key_innodb(self): """Check for `DELETE` with simple primary key with InnoDB engine.""" for clickhouse_table in available_clickhouse_tables: @@ -106,7 +106,7 @@ def simple_primary_key_innodb(self): ) -@TestFeature +@TestScenario def complex_primary_key(self): """Check for `DELETE` with complex primary key without engine InnoDB.""" for clickhouse_table in available_clickhouse_tables: @@ -120,7 +120,7 @@ def complex_primary_key(self): ) -@TestFeature +@TestScenario def complex_primary_key_innodb(self): """Check for `DELETE` with complex primary key with engine InnoDB.""" for clickhouse_table in available_clickhouse_tables: @@ -134,16 +134,12 @@ def complex_primary_key_innodb(self): ) -@TestModule +@TestFeature @Name("truncate") -def module(self): +def feature(self): """'ALTER TRUNCATE' query tests.""" with Given("I enable debezium connector after kafka starts up"): init_debezium_connector() - with Pool(1) as executor: - try: - for feature in loads(current_module(), Feature): - Feature(test=feature, parallel=True, executor=executor)() - finally: - join() + for scenario in loads(current_module(), Scenario): + scenario() diff --git a/sink-connector/tests/integration/tests/types.py b/sink-connector/tests/integration/tests/types.py index 4e89a7a1a..d326667a4 100644 --- a/sink-connector/tests/integration/tests/types.py +++ b/sink-connector/tests/integration/tests/types.py @@ -1,6 +1,7 @@ +from integration.requirements.requirements import * +from integration.tests.steps.configurations import * from integration.tests.steps.sql import * -from integration.tests.steps.statements import * -from integration.tests.steps.service_settings_steps import * +from integration.tests.steps.datatypes import * @TestOutline @@ -47,7 +48,7 @@ def check_datatype_replication( ) -@TestOutline(Feature) +@TestOutline(Scenario) @Examples( "mysql_type ch_type values ch_values nullable", [ @@ -82,7 +83,7 @@ def decimal(self, mysql_type, ch_type, values, ch_values, nullable): ) -@TestOutline(Feature) +@TestOutline(Scenario) @Examples( "mysql_type ch_type values ch_values nullable", [ @@ -111,7 +112,7 @@ def date_time(self, mysql_type, ch_type, values, ch_values, nullable): ) -@TestOutline(Feature) +@TestOutline(Scenario) # @Repeat(3) @Examples( "mysql_type ch_type values ch_values nullable", @@ -178,7 +179,7 @@ def integer_types(self, mysql_type, ch_type, values, ch_values, nullable): ) -@TestOutline(Feature) +@TestOutline(Scenario) @Examples( "mysql_type ch_type values ch_values nullable", [ @@ -207,7 +208,7 @@ def string(self, mysql_type, ch_type, values, ch_values, nullable): ) -@TestOutline(Feature) +@TestOutline(Scenario) @Examples( "mysql_type ch_type values ch_values nullable", [ @@ -243,7 +244,7 @@ def blob(self, mysql_type, ch_type, values, ch_values, nullable): ) -@TestOutline(Feature) +@TestOutline(Scenario) @Examples( "mysql_type ch_type values ch_values nullable", [ @@ -271,7 +272,7 @@ def binary(self, mysql_type, ch_type, values, ch_values, nullable): ) -@TestOutline(Feature) +@TestOutline(Scenario) @Examples( "mysql_type ch_type values ch_values nullable", [ @@ -296,12 +297,12 @@ def enum(self, mysql_type, ch_type, values, ch_values, nullable): ) -@TestModule +@TestFeature @Requirements( RQ_SRS_030_ClickHouse_MySQLToClickHouseReplication_DataTypes_Nullable("1.0") ) @Name("types") -def module(self): +def feature(self): """Verify correct replication of all supported MySQL data types.""" with Given("I enable debezium and sink connectors after kafka starts up"): @@ -309,7 +310,7 @@ def module(self): with Pool(1) as executor: try: - for feature in loads(current_module(), Feature): - Feature(test=feature, parallel=True, executor=executor)() + for scenario in loads(current_module(), Scenario): + Feature(test=scenario, parallel=True, executor=executor)() finally: join() diff --git a/sink-connector/tests/integration/tests/update.py b/sink-connector/tests/integration/tests/update.py index 5f6aa822d..718b821b8 100644 --- a/sink-connector/tests/integration/tests/update.py +++ b/sink-connector/tests/integration/tests/update.py @@ -1,6 +1,7 @@ +from integration.requirements.requirements import * +from integration.tests.steps.configurations import * from integration.tests.steps.sql import * -from integration.tests.steps.statements import * -from integration.tests.steps.service_settings_steps import * +from integration.tests.steps.datatypes import * @TestOutline diff --git a/sink-connector/tests/integration/tests/virtual_columns.py b/sink-connector/tests/integration/tests/virtual_columns.py index ece24f2be..75048e20d 100644 --- a/sink-connector/tests/integration/tests/virtual_columns.py +++ b/sink-connector/tests/integration/tests/virtual_columns.py @@ -1,6 +1,7 @@ +from integration.requirements.requirements import * +from integration.tests.steps.configurations import * from integration.tests.steps.sql import * -from integration.tests.steps.statements import * -from integration.tests.steps.service_settings_steps import * +from integration.tests.steps.datatypes import * @TestOutline @@ -54,7 +55,7 @@ def virtual_column_names( ) -@TestFeature +@TestScenario def virtual_column_names_default(self): """Check correctness of default virtual column names.""" for clickhouse_table in available_clickhouse_tables: @@ -63,7 +64,7 @@ def virtual_column_names_default(self): virtual_column_names(clickhouse_table=clickhouse_table) -@TestFeature +@TestScenario @Requirements( RQ_SRS_030_ClickHouse_MySQLToClickHouseReplication_MySQLStorageEngines_ReplicatedReplacingMergeTree_DifferentVersionColumnNames( "1.0" @@ -81,17 +82,13 @@ def virtual_column_names_replicated_random(self): ) -@TestModule +@TestFeature @Name("virtual columns") -def module(self): +def feature(self): """Section to check behavior of virtual columns.""" with Given("I enable debezium and sink connectors after kafka starts up"): init_debezium_connector() - with Pool(1) as executor: - try: - for feature in loads(current_module(), Feature): - Feature(test=feature, parallel=True, executor=executor)() - finally: - join() + for scenario in loads(current_module(), Scenario): + scenario()