From 7f858af4b409fc89d656b710f4be853697df9746 Mon Sep 17 00:00:00 2001 From: Pramod Varma Date: Mon, 7 Jun 2021 21:33:41 +0530 Subject: [PATCH 001/222] Create LICENSE --- LICENSE | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/LICENSE b/LICENSE index da7e8f9fa1..367155fc74 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ -The MIT License (MIT) +MIT License -Copyright (c) 2015 EkStep +Copyright (c) 2021 Project Sunbird Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -19,4 +19,3 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - From 7cdc698abd96b1f3c3d3551427851c048804d991 Mon Sep 17 00:00:00 2001 From: Jayaprakash8887 Date: Tue, 1 Mar 2022 16:57:13 +0530 Subject: [PATCH 002/222] Issue #SB-28066 feat: reverting Content Auto-creator Flink Job for release-4.7.0 --- .../roles/flink-jobs-deploy/defaults/main.yml | 13 ---- .../helm_charts/datapipeline_jobs/values.j2 | 66 ------------------- 2 files changed, 79 deletions(-) diff --git a/kubernetes/ansible/roles/flink-jobs-deploy/defaults/main.yml b/kubernetes/ansible/roles/flink-jobs-deploy/defaults/main.yml index cd89360f1c..cfb9eaa6fd 100644 --- a/kubernetes/ansible/roles/flink-jobs-deploy/defaults/main.yml +++ b/kubernetes/ansible/roles/flink-jobs-deploy/defaults/main.yml @@ -195,13 +195,6 @@ flink_job_names: taskmanager_memory: 1024m taskslots: 1 cpu_requests: 0.3 - content-auto-creator: - job_class_name: 'org.sunbird.job.contentautocreator.task.ContentAutoCreatorStreamTask' - replica: 1 - jobmanager_memory: 1024m - taskmanager_memory: 1024m - taskslots: 1 - cpu_requests: 0.3 audit-event-generator: job_class_name: 'org.sunbird.job.auditevent.task.AuditEventGeneratorStreamTask' replica: 1 @@ -276,12 +269,6 @@ audit_history_indexer_parallelism: 1 auto_creator_v2_consumer_parallelism: 1 auto_creator_v2_parallelism: 1 - -### Content Auto Creator Related Vars -content_auto_creator_consumer_parallelism: 1 -content_auto_creator_parallelism: 1 -auto_creator_g_service_acct_cred: "{{ auto_creator_gservice_acct_cred | default('') }}" - ### MVC Indexer Related Vars mvc_indexer_consumer_parallelism: 1 mvc_indexer_parallelism: 1 diff --git a/kubernetes/helm_charts/datapipeline_jobs/values.j2 b/kubernetes/helm_charts/datapipeline_jobs/values.j2 index 4ed503e38b..7ba2174cf4 100644 --- a/kubernetes/helm_charts/datapipeline_jobs/values.j2 +++ b/kubernetes/helm_charts/datapipeline_jobs/values.j2 @@ -528,72 +528,6 @@ auto-creator-v2: taskmanager.memory.network.fraction: 0.1 -content-auto-creator: - content-auto-creator: |+ - include file("/data/flink/conf/base-config.conf") - kafka { - input.topic = "{{ env_name }}.auto.creation.job.request" - groupId = "{{ env_name }}-content-auto-creator-group" - failed.topic = "{{ env_name }}.auto.creation.job.request.failed" - } - - task { - consumer.parallelism = {{ content_auto_creator_consumer_parallelism }} - parallelism = {{ content_auto_creator_parallelism }} - window.time = 60 - } - - redis { - database { - relationCache.id = 10 - collectionCache.id = 5 - } - } - - service { - content_service.basePath = "{{ kp_content_service_base_url }}" - search.basePath = "{{ kp_search_service_base_url }}" - lms.basePath = "{{ lms_service_base_url }}" - learning_service.basePath = "{{ kp_learning_service_base_url }}" - } - - cloud_storage_type="{{ cloud_store }}" - azure_storage_key="{{ sunbird_public_storage_account_name }}" - azure_storage_secret="{{ sunbird_public_storage_account_key }}" - azure_storage_container="{{ azure_public_container }}" - - content_auto_creator { - actions=auto-create - allowed_object_types=["Content"] - allowed_content_stages=["create","upload","review","publish"] - content_mandatory_fields=["name","code","mimeType","primaryCategory","artifactUrl","lastPublishedBy"] - content_props_to_removed=["identifier","downloadUrl","variants","createdOn","collections","children","lastUpdatedOn","SYS_INTERNAL_LAST_UPDATED_ON","versionKey","s3Key","status","pkgVersion","toc_url","mimeTypesCount","contentTypesCount","leafNodesCount","childNodes","prevState","lastPublishedOn","flagReasons","compatibilityLevel","size","publishChecklist","publishComment","lastPublishedBy","rejectReasons","rejectComment","badgeAssertions","leafNodes","sYS_INTERNAL_LAST_UPDATED_ON","previewUrl","channel","objectType","visibility","version","pragma","prevStatus","streamingUrl","idealScreenSize","contentDisposition","lastStatusChangedOn","idealScreenDensity","lastSubmittedOn","publishError","flaggedBy","flags","lastFlaggedOn","publisher","lastUpdatedBy","lastSubmittedBy","uploadError","lockKey","publish_type","reviewError","totalCompressedSize","origin","originData","importError","questions"] - bulk_upload_mime_types=["video/mp4"] - artifact_upload_max_size=52428800 - content_create_props=["name","code","mimeType","contentType","framework","processId","primaryCategory"] - artifact_upload_allowed_source=[] - g_service_acct_cred="{{ auto_creator_g_service_acct_cred }}" - gdrive.application_name=drive-download - initial_backoff_delay=120000 - maximum_backoff_delay=1200000 - increment_backoff_delay=2 - api_call_delay=1 - maxIteration=1 - } - - search_exists_fields=["originData"] - search_fields=["identifier","mimeType","pkgVersion","channel","status","origin","originData","artifactUrl"] - - - flink-conf: |+ - jobmanager.memory.flink.size: {{ flink_job_names['content-auto-creator'].jobmanager_memory }} - taskmanager.memory.flink.size: {{ flink_job_names['content-auto-creator'].taskmanager_memory }} - taskmanager.numberOfTaskSlots: {{ flink_job_names['content-auto-creator'].taskslots }} - parallelism.default: 1 - jobmanager.execution.failover-strategy: region - taskmanager.memory.network.fraction: 0.1 - - audit-event-generator: audit-event-generator: |+ include file("/data/flink/conf/base-config.conf") From 55a4f19ba6ffc8e064ac655491b93f3acd363f9c Mon Sep 17 00:00:00 2001 From: pritha-tarento Date: Fri, 25 Mar 2022 13:44:40 +0530 Subject: [PATCH 003/222] Issue #SB-28732 : env enable_rc_certificate --- kubernetes/ansible/roles/flink-jobs-deploy/defaults/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kubernetes/ansible/roles/flink-jobs-deploy/defaults/main.yml b/kubernetes/ansible/roles/flink-jobs-deploy/defaults/main.yml index 057b2d6c0f..c17a1079a4 100644 --- a/kubernetes/ansible/roles/flink-jobs-deploy/defaults/main.yml +++ b/kubernetes/ansible/roles/flink-jobs-deploy/defaults/main.yml @@ -119,7 +119,7 @@ middleware_assessment_aggregator_table: "assessment_aggregator" collection_certificate_generator_consumer_parallelism: 1 collection_certificate_generator_parallelism: 1 collection_certificate_generator_enable_suppress_exception: false -collection_certificate_generator_enable_rc_certificate: true +collection_certificate_generator_enable_rc_certificate: "{{ enable_rc_certificate | default(true)}}" registry_sunbird_keyspace: "sunbird" cert_registry_table: "cert_registry" From eb71dc57bcaeb2592e803a5d7180ff4b744f0bbf Mon Sep 17 00:00:00 2001 From: pritha-tarento Date: Mon, 28 Mar 2022 15:12:04 +0530 Subject: [PATCH 004/222] Issue #SB-28732 :set env rc impl enabled as false --- kubernetes/ansible/roles/flink-jobs-deploy/defaults/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kubernetes/ansible/roles/flink-jobs-deploy/defaults/main.yml b/kubernetes/ansible/roles/flink-jobs-deploy/defaults/main.yml index c17a1079a4..777f8c8615 100644 --- a/kubernetes/ansible/roles/flink-jobs-deploy/defaults/main.yml +++ b/kubernetes/ansible/roles/flink-jobs-deploy/defaults/main.yml @@ -119,7 +119,7 @@ middleware_assessment_aggregator_table: "assessment_aggregator" collection_certificate_generator_consumer_parallelism: 1 collection_certificate_generator_parallelism: 1 collection_certificate_generator_enable_suppress_exception: false -collection_certificate_generator_enable_rc_certificate: "{{ enable_rc_certificate | default(true)}}" +collection_certificate_generator_enable_rc_certificate: false registry_sunbird_keyspace: "sunbird" cert_registry_table: "cert_registry" From a9ea2cea9bd840e724a9b2cfb3ee6fe636bd851e Mon Sep 17 00:00:00 2001 From: Jayaprakash8887 Date: Mon, 4 Apr 2022 13:04:54 +0530 Subject: [PATCH 005/222] Issue #SB-24965 feat: Updating Local Setup details --- .../helm_charts/datapipeline_jobs/values.j2 | 86 ++++++++++++------- 1 file changed, 55 insertions(+), 31 deletions(-) diff --git a/kubernetes/helm_charts/datapipeline_jobs/values.j2 b/kubernetes/helm_charts/datapipeline_jobs/values.j2 index 31b1118a3c..b6f8eb711b 100644 --- a/kubernetes/helm_charts/datapipeline_jobs/values.j2 +++ b/kubernetes/helm_charts/datapipeline_jobs/values.j2 @@ -810,37 +810,61 @@ content-publish: table = "content_data" tmp_file_location = "/tmp" objectType = ["Content", "ContentImage","Collection","CollectionImage"] - mimeType = ["application/pdf", "video/avi", "video/mpeg", "video/quicktime", "video/3gpp", "video/mpeg", "video/mp4", "video/ogg", "video/webm", "application/vnd.ekstep.html-archive","application/vnd.ekstep.ecml-archive","application/vnd.ekstep.content-collection" - "application/vnd.ekstep.ecml-archive", - "application/vnd.ekstep.html-archive", - "application/vnd.android.package-archive", - "application/vnd.ekstep.content-archive", - "application/octet-stream", - "application/json", - "application/javascript", - "application/xml", - "text/plain", - "text/html", - "text/javascript", - "text/xml", - "text/css", - "image/jpeg", "image/jpg", "image/png", "image/tiff", "image/bmp", "image/gif", "image/svg+xml", - "image/x-quicktime", - "video/avi", "video/mpeg", "video/quicktime", "video/3gpp", "video/mpeg", "video/mp4", "video/ogg", "video/webm", - "video/msvideo", - "video/x-msvideo", - "video/x-qtc", - "video/x-mpeg", - "audio/mp3", "audio/mp4", "audio/mpeg", "audio/ogg", "audio/webm", "audio/x-wav", "audio/wav", - "audio/mpeg3", - "audio/x-mpeg-3", - "audio/vorbis", - "application/x-font-ttf", - "application/pdf", "application/epub", "application/msword", - "application/vnd.ekstep.h5p-archive", - "application/vnd.ekstep.plugin-archive", - "video/x-youtube", "video/youtube", - "text/x-url"] + mimeType = ["application/pdf", + "application/vnd.ekstep.ecml-archive", + "application/vnd.ekstep.html-archive", + "application/vnd.android.package-archive", + "application/vnd.ekstep.content-archive", + "application/epub", + "application/msword", + "application/vnd.ekstep.h5p-archive", + "video/webm", + "video/mp4", + "application/vnd.ekstep.content-collection", + "video/quicktime", + "application/octet-stream", + "application/json", + "application/javascript", + "application/xml", + "text/plain", + "text/html", + "text/javascript", + "text/xml", + "text/css", + "image/jpeg", + "image/jpg", + "image/png", + "image/tiff", + "image/bmp", + "image/gif", + "image/svg+xml", + "image/x-quicktime", + "video/avi", + "video/mpeg", + "video/quicktime", + "video/3gpp", + "video/mp4", + "video/ogg", + "video/webm", + "video/msvideo", + "video/x-msvideo", + "video/x-qtc", + "video/x-mpeg", + "audio/mp3", + "audio/mp4", + "audio/mpeg", + "audio/ogg", + "audio/webm", + "audio/x-wav", + "audio/wav", + "audio/mpeg3", + "audio/x-mpeg-3", + "audio/vorbis", + "application/x-font-ttf", + "application/vnd.ekstep.plugin-archive", + "video/x-youtube", + "video/youtube", + "text/x-url"] asset_download_duration = "60 seconds" stream { enabled = {{ content_stream_enabled | lower }} From f463208f141774df394dcf31cb1736ebea2a4ee1 Mon Sep 17 00:00:00 2001 From: Anil Gupta Date: Wed, 6 Apr 2022 09:44:11 +0530 Subject: [PATCH 006/222] Issue #SB-28381 chore: Removed the qrcode-image-generator and aut-creator from samza job distribution list. --- platform-jobs/samza/distribution/pom.xml | 28 ++++++++++++------------ 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/platform-jobs/samza/distribution/pom.xml b/platform-jobs/samza/distribution/pom.xml index 968b50ffef..ab285c244d 100644 --- a/platform-jobs/samza/distribution/pom.xml +++ b/platform-jobs/samza/distribution/pom.xml @@ -9,13 +9,13 @@ pom Distribution - - org.sunbird - qrcode-image-generator - 0.0.31 - tar.gz - distribution - + + + + + + + org.sunbird publish-pipeline @@ -30,13 +30,13 @@ tar.gz distribution - - org.sunbird - auto-creator - 0.0.39 - tar.gz - distribution - + + + + + + + org.sunbird mvc-processor-indexer From 4bf4bd8e02713115809aa2e6cde9d952fa7eedbc Mon Sep 17 00:00:00 2001 From: Jayaprakash8887 Date: Mon, 11 Apr 2022 13:42:45 +0530 Subject: [PATCH 007/222] Issue #SB-24965 feat: Updating Local Setup details --- README.md | 145 ++++++++++++++++++ img.png | Bin 0 -> 170476 bytes img_1.png | Bin 0 -> 60946 bytes img_2.png | Bin 0 -> 151225 bytes img_3.png | Bin 0 -> 82280 bytes .../src/main/resources/application.conf | 14 +- 6 files changed, 157 insertions(+), 2 deletions(-) create mode 100644 img.png create mode 100644 img_1.png create mode 100644 img_2.png create mode 100644 img_3.png diff --git a/README.md b/README.md index d9860997d2..d111ea54e6 100644 --- a/README.md +++ b/README.md @@ -12,3 +12,148 @@ The platform contains of the following projects: * platform-modules - Functional/Pedagogy modules to support game based learning and API +## learning-service local setup +This readme file contains the instruction to set up and run the learnin-service in local machine. + +### System Requirements: + +### Prerequisites: +* Java 11 is required for docker container setups +* Java 8 and Tomcat v9 is required for local service setup via IntelliJ + +### Prepare folders for database data and logs + +```shell +mkdir -p ~/sunbird-dbs/neo4j ~/sunbird-dbs/cassandra ~/sunbird-dbs/redis ~/sunbird-dbs/es ~/sunbird-dbs/kafka +export sunbird_dbs_path=~/sunbird-dbs +``` + + +### Elasticsearch database setup in docker: +```shell +docker run --name sunbird_es -d -p 9200:9200 -p 9300:9300 \ +-v $sunbird_dbs_path/es/data:/usr/share/elasticsearch/data \ +-v $sunbird_dbs_path/es/logs://usr/share/elasticsearch/logs \ +-v $sunbird_dbs_path/es/backups:/opt/elasticsearch/backup \ + -e "discovery.type=single-node" docker.elastic.co/elasticsearch/elasticsearch:6.8.22 + +``` +> --name - Name your container (avoids generic id) +> +> -p - Specify container ports to expose +> +> Using the -p option with ports 7474 and 7687 allows us to expose and listen for traffic on both the HTTP and Bolt ports. Having the HTTP port means we can connect to our database with Neo4j Browser, and the Bolt port means efficient and type-safe communication requests between other layers and the database. +> +> -d - This detaches the container to run in the background, meaning we can access the container separately and see into all of its processes. +> +> -v - The next several lines start with the -v option. These lines define volumes we want to bind in our local directory structure so we can access certain files locally. +> +> --env - Set config as environment variables for Neo4j database +> + + +### Neo4j database setup in docker: +1. First, we need to get the neo4j image from docker hub using the following command. +```shell +docker pull neo4j:3.3.0 +``` +2. We need to create the neo4j instance, By using the below command we can create the same and run in a container. +```shell +docker run --name sunbird_neo4j -p7474:7474 -p7687:7687 -d \ + -v $sunbird_dbs_path/neo4j/data:/var/lib/neo4j/data \ +-v $sunbird_dbs_path/neo4j/logs:/var/lib/neo4j/logs \ +-v $sunbird_dbs_path/neo4j/plugins:/var/lib/neo4j/plugins \ +--env NEO4J_dbms_connector_https_advertised__address="localhost:7473" \ +--env NEO4J_dbms_connector_http_advertised__address="localhost:7474" \ +--env NEO4J_dbms_connector_bolt_advertised__address="localhost:7687" \ +--env NEO4J_AUTH=none \ +neo4j:3.3.0 +``` +> --name - Name your container (avoids generic id) +> +> -p - Specify container ports to expose +> +> Using the -p option with ports 7474 and 7687 allows us to expose and listen for traffic on both the HTTP and Bolt ports. Having the HTTP port means we can connect to our database with Neo4j Browser, and the Bolt port means efficient and type-safe communication requests between other layers and the database. +> +> -d - This detaches the container to run in the background, meaning we can access the container separately and see into all of its processes. +> +> -v - The next several lines start with the -v option. These lines define volumes we want to bind in our local directory structure so we can access certain files locally. +> +> --env - Set config as environment variables for Neo4j database +> +> Using Docker on Windows will also need a couple of additional configurations because the default 0.0.0.0 address that is resolved with the above command does not translate to localhost in Windows. We need to add environment variables to our command above to set the advertised addresses. +> +> By default, Neo4j requires authentication and requires us to first login with neo4j/neo4j and set a new password. We will skip this password reset by initializing the authentication none when we create the Docker container using the --env NEO4J_AUTH=none. + +3. Load seed data to neo4j using the instructions provided in the [link](master-data/loading-seed-data.md#loading-seed-data-to-neo4j-database) + +4. Verify whether neo4j is running or not by accessing neo4j browser(http://localhost:7474/browser). + +5. To SSH to neo4j docker container, run the below command. +```shell +docker exec -it sunbird_neo4j bash +``` + +### Redis database setup in docker: +1. we need to get the redis image from docker hub using the below command. +```shell +docker pull redis:6.0.8 +``` +2. We need to create the redis instance, By using the below command we can create the same and run in a container. +```shell +docker run --name sunbird_redis -d -p 6379:6379 redis:6.0.8 +``` +3. To SSH to redis docker container, run the below command +```shell +docker exec -it sunbird_redis bash +``` +### cassandra database setup in docker: +1. we need to get the cassandra image and can be done using the below command. +```shell +docker pull cassandra:3.11.8 +``` +2. We need to create the cassandra instance, By using the below command we can create the same and run in a container. +```shell +docker run --name sunbird_cassandra -d -p 9042:9042 \ +-v $sunbird_dbs_path/cassandra/data:/var/lib/cassandra \ +-v $sunbird_dbs_path/cassandra/logs:/opt/cassandra/logs \ +-v $sunbird_dbs_path/cassandra/backups:/mnt/backups \ +--network bridge cassandra:3.11.8 +``` +For network, we can use the existing network or create a new network using the following command and use it. +```shell +docker network create sunbird_db_network +``` +3. To start cassandra cypher shell run the below command. +```shell +docker exec -it sunbird_cassandra cqlsh +``` +4. To ssh to cassandra docker container, run the below command. +```shell +docker exec -it sunbird_cassandra /bin/bash +``` +5. Load seed data to cassandra using the instructions provided in the [link](master-data/loading-seed-data.md#loading-seed-data-to-cassandra-database) + + +### Steps to start learning-service in debug or development mode using IntelliJ: +1. Navigate to downloaded repository folder and run below command in terminal. +```shell +export JAVA_HOME = #JDK1.8 folder location +mvn clean install -DskipTests +``` +2. Open the project in IntelliJ. +3. Add 'Smart Tomcat' plugin. (File -> Settings -> plugin -> Search for 'Smart Tomcat' -> install) +4. Configure the tomcat server using 'Smart Tomcat' plugin under 'Add configuration'. +![img.png](img.png) +5. Give the 'name' as 'learning-service'. +6. Specify the Tomcat v9 server location in 'Tomcat Server'. +7. Specify the absolute path of the 'webapp' folder (under ../sunbird-learning-platform/platform-modules/service/src/main/webapp). +8. Mention '/learning-service' as the context path. Click on 'Apply'. +9. Click on 'File' -> 'Project Structure'. Check if Java version is set to jdk1.8 in both 'Project' and 'Modules'. +![img_1.png](img_1.png) +![img_2.png](img_2.png) +10. Click on 'File' -> Settings -> Build, Execution, Deployment -> compiler -> scala compiler -> scala compiler server. Check if Java version is set to jdk1.8. +![img_3.png](img_3.png) +11. Service configuration file is available at '../sunbird-learning-platform/platform-modules/service/src/main/resources/application.conf'. Update config file to connect to local databases. +12. Start Redis, neo4j, cassandra and ElasticSearch docker containers. +13. Start the 'learning-service' configuration in IntelliJ. Verify the health of learning service by trying to connect to 'http://localhost:8080/learning-service/health'. diff --git a/img.png b/img.png new file mode 100644 index 0000000000000000000000000000000000000000..b0fc37c93b1131850e4ff71814af66283787e3e4 GIT binary patch literal 170476 zcmce-bx_prA1`Xrt#pS9N_T^_g0x7tfW*=b3o62blt{OLlyuk9DN9H$xxmsa4a)+{ z!sV;KIOm>wXU>_qbDw`cv!9t~pLqA{_5MU?X{Zq4)8gN`bBE~FOGTYKcd+1h?%bEb z#lC&xt?OoT=gx;auN39p_?qo5f_&(qmLh>Sf*My(-QdS_RcvIBC0=}D_CIJ1;duP~ z?px9aFF#>1zqs>2?klsu3qh%wxceH6b@uHVqgoZ4Vaz*2(@jagwx$}a79U$5JF1q_ z*pUkcH@p3XCP5E7Z`5pO=IGVI%{6cMe+LJj%*l3P_NV{83BMO2Cq?$3p?!xxoSpSQ z1D_fDNb$epZVz+Tf2VSMpze9}pP6J>q#?flnaG-Z$BI<*zca~S5W*w=JM~`p|Hh&} zeU2_i+hSdu(~ri}kELXNH-*8ctyC8N*LJ?fS#~J9t7GF)OQ18^5;{*Rma8##>n@*X zn+#!Wm)9U}R^dO33;f;wPo@=Hdlq~GIzA+UU)B^(h1LEWqUSrj@Fu9t*oe>SstbzK zixb(v3m4A3pv0a#{Jv+Ie!sf`c=`*+WtF7;f*a;*E_pCPaud!pWZM0!z$hVEFSi_t zpA~FcNG~lMl~J*@PlfqiK+YyRo^&L89}`TnSsJ$|()U8rOR9Nu+l7B(*B7j5x!S%i z@Xx;X|K73=4Q|YJ<40WwT;0!(ylsV<2VqqoATQ_`Yo(n$EY2%qVF6@7C9718QjA>I z4Ox)zd<<(=@oHsD{H$wdyYtRyF8|T8+>kl z-eI;c%vCQL77@d<5{mlazjPsbs%NkPT@1owa+pRh@u7rs3%~~4dF2gfViEyid?#0& z=O_Im>q`MSqt)ebU4vj#{BYT8;R0hWghLjl&SekD#|Uz#BGq-f=ApjECV2qX5K&OlM;OyvyHcU+-WAws*dcPtlK{Tk@mI9)QSD(%(bm zT9H#x8D5wDR6Otqo=o86DN)qZOAHALggl$Me$Z|#Rb{McPm*<0gFsRy>g5zvPd-dA z(#tmQtYFx=!svj2X2Ziw%Ro>w?0SPD!EiH?I0))b54NuerV%#>$ORsFxHxWL#l&47 zpv4{EwM6reYy7fy%esERwiB!SXZu-{e;!-KC(~-2)M7w-;q`oulAkJ&)PVCbbD1e3 zmmm@C!mw9**0NPRH}>`|qjnLMSSmJR)37f_RatXy zO|md#u-c_rPLNt~2dV({+oKwbNO+xtyG+rvpZdvJN=Xm5-!pnqCafCk{2^$9ck%Ok)kTF?)kUH~Hy$}R zdkR|3GwT&lu;1@ouiT%<_a6L0z=yOR`;a69bO|n5_>a@PqzSV=f;RPV})s|Fc?#x}=t~TrcyT*|L_> zX0zg!9wVP8JU57Ro;hm^^d9gBd7XOMdw?DxaN?3r)flL{Bw?G$zQ0DPU^(PtaMmJF zkz2js-{3>jtR7q$2FXG>8hSA}aB?X$7+fldJN$%{dQHL4)asek&g9uEug|#yPnzpU zE=haZPvo=xLH^tMF3Z7Ysfw*dh0cV$!CRKCKT${VRsN7>x~E43UUQ)H4L!BlOMeKs zpjdwjbABDGtd$2Y*C{CR=@lJ?H#j4F`jdeN^A|ty;MvWH$qC;qfq&wU|HBAWu>QoK z_%b%Jj#7KC8&YT{g-Uwz^>QsQmMbS-|8BA{`Q^EKN;CJ0_9{j{U%)HnJk_9I<$P76 z;t4euQy0lpi~73js~`-X%+;Yn;AH`x-P8*NbzjhNj8Qu<&`IHLvVt#ibMv<;S3O4P zt-0>b^{F-ZVnM0SR4KXbW4lo@W^FZ=;$US@w)LZEP=mc;K_%U2YngsUUFNZ81G=|Y z1i^i>Au>{oko}T#fQh9I!)#i$*r189j;5PsF9?EU{JI{7O~tI(ELvNvc%!{8!y#UC z1?~B}sE7Qml=qkXb?r|;zCgsMeAzd|aGag5OvlwcXNZ=i?iY2wrs0vJH%b@+oD(dy z0VB~P!D8tywZJ%Qojv-Vr1*bXhGJ; z-2ELsJ+ad5y`>UC0pgeK$nt&v_3u|9R27B_Lo!$C%@wdWJGBX|)m71vRyS`JJ5Wn# z!7KzB8ZK7QaFD0H*xqpXO@Gg8t-B3%6gc(ZNE6|;Q!JrL^H0o|j_5(Rme~JwgWJbr z6?_x6M3fL(x1e`eOTojJr1F`EebycMuhB@S8lwW=Wyx{Vcw)KH`OzHXGW6r=n<9PX zAOLb4@aD_<&%VQIuQaK%V-n}Neeb#=>WjD_pe$`wY-ZV*P{>jdvOOd@`SQ2PwwOqx z>pjkj7W={x&r!5eP+83o2}t|-w7K|~;D&NFVmOgYJ1)VLz@CAhdN78u!MrfGaUSO< zQ_TDNl?hZ^1jIqQpYMcI6yi{DLsM{)B$7E_y}*BkXre@Vjde75h3#DMTxQmH&o29{ zAD7=lSW{oeth%zatgAjp{Qmvh!Y2sn(}Kh}-!0pzCs_ea^Y;N(Td2(Wj6wKr<(kCs zCG>_~0O?JxQU7hY=Q2ySI0rE;_vh7-`tbS0F6ne5;2!C|t92B$B@mPDhABR%ML_0x z}5^WR8+9*gigUR*M6FL zPlI_zpP!eMjs9N!m<9@Lz6OZ31(E4GNlH8x}ur+eipL^cVyW8ET87!MIzs9}!;woM=OT{IB-D`EGT{0?h zPeN=7iD%XXeJ{&Ht5m%p(r&czjCO=PrAr}ISt z6@Ohu-a($v;q<%}$KCZMrYmBP%p?-qB-atj!^B+ZRe?V=H~3z;&(Yu;5NL&R4f@t) zCM>Y1%jP6_rCYP^Zn7<%9`8yL?e`?><@xEOrJLFip1N|{)6}3ypR$j$I4-R*qSp== zsPqCJwZ+YbBUuM&Uh7g-uGQx$HUGKD84$RryMv%&wc?;XnkkwgerL+Mq=ncgm?v!U z+`w*zXiw%CjFIiDl<@X>g5BH>9d2&AbxwVyq7XgApcc7GQk@?!&l8AO zUOx!t%WSBvE25dGFdAi07jI(hRT;ddAhGA>QmzAVIqMPZOacwUr<<0xpr~IUU(~Sq zit)szHs^HNj_B7_|KH<;quR@mlqHv_~Og(Xn&J5p2{hlfeq8*;woWVVWT!3l9 zuJ|`x&a6F=h2sWrv)uruQGIpNn@N+xtc{a|zq#B?U^|p0d7krc(A+}z3eiRmYC{9> z6>5m&DHEMO=Q{-+-D!bZD}tm3l>hh^D{SHxu@%wm4(f9zonIG;-uJ z&BHEZP8xlW$akiIr(ZvvYuSJkDDF#+Oe1U>jvTu{;D(EL7qO;wd#c0VepH#iBV|tv~!eG+b16<8(1*WQbyy4 zjX44Gkb}d8XlG9m1|^$f1I;J261SC4K!s#f6*rWv z-TUy@{CVu2vDlJHWHp?XJXtR<2Pwp(j_I(B0+f-Mp(&sGrzHXaonZgX%KUQuhJM$! zd`gNZNzE}{XI9&G(?^A}K&JMKJ~98foymHt2%y%|C*4-mA*NxWPFK>j#e7ZR*s;}y zoT)uDxnX~0BazY59Q^URCMidCFf6!e))o*j=vvxJQD^hyRxNthXPH(tcTDRk=YVVW zXxZL=m)59IPJlyZK85gm2|<@80%N;?T^T=|iZo@{GXo0MLik?+1?IV89jg7{sfo1_0olS-X57QE@X_Tk>WTd zlJY@{IY79(E5rHPewNeNUG`O8?Jc&$v}Qq>Zd4Z6qzXGhCE7baFdk_jFBpUhl8Mz7 zUN4et|6(5eIO)i33GLc8c$-vu{5Ppwll)tDuK5J}t&;ev5GhkIfn%cg1%!X^7zC;7 zdMDFIkx4hNF<);V31t`liJ}8Oe;(ljqp(g7=}?a6Wj$kk%9DBqOeThIWH*=WO{IB- z;>&sc(f?EJwejmzD625I<3wR`&~5)krbM~SpDO&A@ozDHTq6tbts>YGdmH&>+$!Pi zbYANww8#sCgVry-(UTd!UT^bJwNiw?pIl!9q-*fy^vEGjV zf2;q$YbxG7O-(lQ$NSf>e;(x2*?!)^eE$U#!a4ZN1wA9MqZ&?m|EjVrX`UgX+wjT2t1pv=BBX0bS)!}fVUo)ftS<|qOxl*u zikkVpZ08fD)K6EsRuST?37Glm#AGj2M4Q}ainqQV5U7s5uP(D6D<7ORzZS_}=OTj- z4Ewu*0RSmebY8b@%CM1wN?zi{QPB=vZ(Z0MUiqR{_r<5sm5=m11A8M1sGl^e>TVJc zT8fC{YxNYW=C1{*R8hcGyDAH_{@z^I_s|-4ky01Lkj;>%HC zW?>^9gppN0xi=-~_nS1(X{prOkKH;=sn4s0WD#(fc&SrLW!`T3-moy&=wi2pizNPP zkKglTC>zR=4f-OO-s6T||0Y6iHRUV~?D#-9?pUDh_8L$FD<(^P0Bc7e%|ru^Yx&dR z%lip|3m9njBaH>ntS3Wn=bT^l_v(@v5zS?TQeWDTXo?My|=Uxj#!;LYO| z&MQSZ*VlJ03ID=Wad)#q&W$q%h6d%rLe4{2B-WXFl1-riRjJ+QQYYj*=j@7jJWZd7 zP6S5Q2hdL+q?$(ZRh+WUzi2$R*t@Q*nlz7A&^WoG=|QIl$D~@Wh>Q8TJhGjTa?TJB zJO1{N{Ox33ehHJ^Ah834Zll;`KM{Ezld1UDmU7$!Uwhn@GEdDmr9gvCSdpiK9h=D7 zmMdd;I(4%IC-BPC5H-Ao_ruNrrH-*D9C5L>tHLo1JBuC~5dO{7zrfT#mEGmaCrn}} zPKku-TbO}A(IOC~8E+IX_U7!~F&%u|FyU2l)00lqFqd0$(?^_|2gCFiK-?!>8b8D| zG*uu2kHd@wGv&{IjM%5>C$|06jdi&i6U9XQ8;6rNl)^cZF#p8;?RU^p)T}PeG*rUe zn}l92XwxfdwA{)Ah^Na&r4>h>qnoUqHTOe|w4~I7RF~6uMrT@FTq-I65wr4X!C5UF z?u)Zo_eBfPn)R6+4o{F!hP2Z#VYg z4#teEQ+_N12)#MK_WLUIJF!mGp?<9-7c3IzZ9H)rCF%vYYajdcES=J6Qh%1D>Q>jI*orKYG$&!c2EVjHP5b?v_F zJ*cet85Qalwg@|Enzna_+KYR~py#H|Q2j5%wx z4}EdJf5A~Xrlhc*N%<9&V}FFp$_7XcRevB`h+ z3L@K>QXBAUL<<&urz+Vn^qe_xO@JN*$zti7fPU@r)hf8N7Yy4vnebF$qdNx>rrjei z*;s`D$33(?=G-d2awHqxl)sHV2P8DD_+Nmi{i&$6TX^trxe2A0;BU*Nt*nXr1EXrJ=>kyw_6q^RJO`_VxQ zI(&DJxcr1)VyrF=WICk5%!Dg$e(&3wK4lut*9YL&tOu2~V*AT%wDGx{MArfRzSH=@ zuQ&mUs2+(Sg3Y+L{uU@Q#Fwpq`X!A$Q-7U>l^ulw`(RvZR>V-Q5&2^@^(W&|B$6{L z>mxIhk3MFWYF4b_2YvF1-jfQ?D8l73f+Ca z#BhfFW8K;Nloyfhr#7Nse9Y5F*gqNeZ9VZn1+L_(BdFroHb8pM5kjjPHP!=9Zkk#b zoHIKH^`EixTbuRgTNzW+gnd$ToN1a5>ppWIzm(o9-13jwb%MJ`Jh_T;pbxPJ5Tz4r ze*Kx$u#1YARs;k{%@M(sjN>(5chpLRf1&2T)d)#7`Sfar3HGvn$y!2vkp{QCzGeoN zCyijst~z|TR+^j*G8a+O00RxBY(i~rc^XPi$#Osv@2hd_C^hrZCVwu|x?*(`bMv+ykS^GuF zsLYYJGsBe2`vrcv6#P2~!i=m(&>IDggQc#nCMQT+1fY*$y|tjQV~S+;*m3)ub~{}; zrvJWg@HlvB-Y~>B7RrZVhHcY6{1#E8nkw8N+xYNr4Bd;~WDYugpfe%x)heDG!Act6 zwp(C)oe`vR@yV%r{_Xt=I^ni!yv8zhz)Ou+3g6U^$v%f)IFPo5PiV+}qYwPGcV-v< zFt#9q!RbujIE(QI1`yd>T&D+4;%aq)j+9bjqugd&JVd`)Fhs zH^FqNYx+f-;zwtECqh{8f%EMh^{G{Z!r;Y0DHBH6{Ww+5*?F1l&`+t<@j6bpSs!kPH z=3oo^NI9k~PT*LESfgvK^NEuSx5bY_iYL!-axg3X%%YqAR`X&>2i~lPK<)#{OkgX{ zSbv`dj8W0gfbYyEvm1U%K^}(9R$I+fkWVlmMH+RV0y6_+l+VYE1T?81BP=#o9e=pc zN-%y54i;G2(aK)^y20EE@DvS4`4S0|_HeofOj~4$hdxl3{8w6i)$KvbOA>6s9-KNw zkTdPtB!TCKH$tpDKq4eOsE|I%y9No@6AyX~UaP zmQ|gUd;|D_q?^&KDnnUh$wnr8)j0($^d7N+v_zZpVEGJ-z66qIT0O=(q!lwhjg$(G z#?$~OcVhTz8edy$XvDtJyTW`{{xb5-!VHiWZdqdtH(8rX80!uEc|EoxS;0trz4$5@ z<**uYH`fD6oFhi)Iy{ADCfSUDM{cR0dOzzPMuy!0y-4HpF)^-f8+9gmV_My{WL zEL(JXF?83F=8qIPHTyZ5bc^*{G`URZW!cbUdI4bDMbUBUqtX4*o>V_U5gv+ek=FyQ zXKRZW&fQL}Ol;EswngT;m^Q~>K*2zo>*_)O1)ZvWZ*SSD3i1_8vN)xfDq&G@Rar7jHC<@(N>+j4W7 ze^(}6&N=={V&>u>`wWXwSzgLMg&kz}{Uf({bks66+ zKFf!)YWezM*-ilHH7g0*zmvVSWawgOvvv^}uP>^&W!bQU$9 z-*L-4zOY(3qK*JoKOu3nT-`aRnTh3@Ta~7g3w+7goIGT3!2Ri*AbF!@JZ0rpOxv{Y z*HX|u9d5|xz0QwB@MwBtHk&oz?qhly(;7bBOqFKaEHb!>x!wrUjb77J2D*rI#B*6Q zsxy}3wE9|~{c<`van*+IM221o=r{uodT3YBgA{~W()>>b)p<8Kcs!up=hlyyb+5_f z)wrComrRJDRfQ4%;=+%qWU-I3G$6imX=D25Xb#b=eXqNh9JK- zrs!T(I>?T8);8aCDakiY^=@$0N1~G->!T-`8xVwObvFFBXMd>^RHQ(%jAT=CJW6s9 zv6o3x%ML;Csmg{2d*4{TAu2EKHk~;L*L>gGnyDV;GsVo##_+1RSIoy8AD)QG9;^G1 zIjru)jbw&g|5ou!?~YrPuGq~B5Hr{RJhWgnnb8(XQG<-}jYYV74Mpv$G0{I)S?Y=z zhrKd!ko*Ggl3%0H&)HWb8F>@IH?)!y)(?qGuK*V1aI7}DT()5RH(jZ}NwO!_$K>&w z)ecWctXtcyA5lL;dc*X0GP{wF>Qr0dALCRp#a3J7`u(E=L7rob6}Opm8S%S!>m0yJ z(%w42R#OfB$q4<(JL53l2tcY^%v+RV3EavDC(E2}5;3r&3gaK273I_8b}_3^(P?Ev+ydUWDK#6PABa;8XR+GKi@ zZug`4WE#Gd?Qxg#4qIHwW915Y(bxdu4f`>~J;gS;Url`O=BY~7*Q?K%n05^%n539rRd?bqT{RH7Bh3FhL<$+OSMKt?=d*Jlv(@j%Q(~5VDpOLSs7pn z?)!*oKxa38D<)*MMxY}RGbY9ziz|`_d@eZ+zh|0Fm;^`B<5Xy3+q`l|(nlw`FhGh( z62FHXscB+;ixLUzo&nQ7ms7{PxsI+(naM(kWkz9}eQ|&Ao{t+b=-tXtDGfr@fiy?M zqd_Cny6`wKY8N?D(OFv)9i9K|^sPZw^78(9(^72t+-liNa@2^WP4|zOC#`u-sJU?= zXA;jm8T8?PW?iR>majjxV2SaJ2*AqAo;coGU#sl8qW}&5X@Ucl&8wHK)$(lDNvy7ubl{Pz-f@kMyM{qOnSRc83?l~dRXeByqB8xN84tT z1MYIdCHz^H(%2bc4)ugst3sy#xN3vVe_VA|EQoXf#jY;9#EWzEspU_0d^S9{m`n=0MQlwowVr+`&TuE^&y=(YwND+PDg_7R|Q4IH5Un4oqzdr={r0jzQYWg51Xz+ zm>cAtCI^3At8ZlGMe+Rtc&M0pS3Ddv*3y0ZxwO>j9ThasF_389N0S+LUHMBT1k7|g z0FipW=Dib_>}NE9mWu@stt!GOQ%!;@)-G+_FI3_dZ1vaHuU?rYYr9Kp-t(g~jY~d> z`VS~Tf4g>WM}+)!D*tQlQmcvEYP#1x3R%Y{I(?=k&&rzn!s6duJ|sB*AGiPft8Vyz zMIEKF&lqq!OZ zO-T1^gBW;80?eY!!lxVho1!8pSl!u54^^4~_8t>}l^63cd-Q zs@ixvEM)FCZ{E$CHwB<=@}eV9<7||bj#LBc$E9W#K0}n;@9b=Zo(Y42H)`jQ1Q~wK z1TvfT^-vX|@K|{#4ZJ}_ESs!G)+Uan+e_Y3MS!mOL*lI={a)s}YBKdqJFU3Kqim6d z)=Z(#**#Rf+iaWJSwA;m?YrZSGRu6mBGq%pKCu}_P;=B%rDS>Nc6=wIV1vs$|8%K3%GRkR1-HhDV|76uo zHkJKhD@SoxGeRpOIMUuK%LGOz*O`MG8`YK5T@@aw`NGI3P4&w7^`jF-8owr$hz6+O z=$Z`uzmeW&bQW>^=Q|NCz8kJTy|c3SB)^*o#`cO~mQRWXd7pQl?%qp!J6%~>8&xIg zqX%EIN=soM9DrQNe?H9B@N2FBZ&w_>`qe1uXKvb_6%$`54Dsa@Tf1Y{c&?{@xc`NN zGbs+?rx(F(^9fR2NbH(w##Sa zeX);76ez#(rpFogf$;uGC#9cRtv;HPT%d0^*j%Gno<9G4dC>Uz@vy-4vO4K^ycJ{O zRl{V_pbJ}G%OZ8PVao>x=zG<=3S!bs=19rsS1j$&0L5EH+ErNthCvKi! zSV+gpyBQ-271T3$s48*k>gZ2HR{(G-WK63== z^)73h`<~L(Nd(?rpzK4IKm*;Ve9cIvz=tTtzQ~iVvo@A^h`+*v-AG7Dim`z_aMkVe zXO-E_ze>PS1LcEJcF9IDn=RnE5UjYwEXdnx@W`{VPK^>pt8%@9 z>jPN-ky2pDdAB;Xez&J!Ukt--XW^zB5gb)Le0QZ=2ESPGCdR;)k$qzAg5~8-N>6=r z^wUyqE(?1MaoW8jeKIqAyhueO=bRjpmoY407rDU4#0=&yjy}-XgxDq!JAE%g5ew|l zk~-fUvC>wtbD!9x-H`y(ra}|_HH3|pxE4!Gf*F*gR|kq*e=HR6U^0<+WDz6!nuvw+-q+HC1gBz1V^Y{ol9Tf88?6gCk;=EB#TP<<#&FFcq!_ z3z#*RofzGPIngIA@A7TR&|Vn>O|~p5{m%N9WuKju6qjG!N_lIp+Hbkopw^$Qae2cn zmX7ME@i!fAh}PDq9Fn~H-v6$0AgIGGwt`n&P8Z5-f&H&{-~6O=*~R; zF!=+VXFN|BaIB+Eao@S)9>|0@V9(fTunO=Kko^i3d*ewt3$#rz6N`4O8#RJt>?^+0 zA!eojtR#8EN@wIoVgpp;pv_Rw0SBT!b~h*3u<3qrEvme4zxh^cS~fZ%YO%$Nm^{|7 z>+*!1^m&7LE`BOhaX<#7h$dn0o8L13%Ohq9*{9B?F}LM5Q$lgD`GYpI3c&5uc=y&M zSEgFr%5I3CpLtI$2MuY(tfUIaKY3B08#kS+NLmo*y=AspBTXS zI%}&wbLz25Nk)Au*Ivjp>>0eItXD^*Ftvw24`_Y$zTKs7_nGm`b}^VV^hdK<)U&+> z0e~gIo*3m!^j$^|rp!Wkq({XkP!l!02XNxj%&VAN!4_+nFKti<#X(!}PcX#vZU&m~ zx;cgrw_sabL;pA|Axw=r+Z^<2)~BRq_5H8fAdpAPb;n7;!LKz}?ZZu1VY1DbqTF(c z4A4VNI6@Wa#2hk7*p?=b*N_7yA>`%)mAc>CpNnaQtkXve? zT;7i>9-eJpyYR8Ru&K_p2F$69Mr_S6)H%DcgQZ4oEe;Ee3MzlR5gf%mB7H0Vj!Nco zo7CwhA{cUlRaI(jDuZ@Kxv%Z3OdRE|kVs2X~BtU=6nK>H_w&Sj7A{hmQh0y`pTZH#w0z82b{jddJEN5(O~co1+_ zc~D?O9aVI#h!zSCJ5uicT_IgVa&v{W%P3HKaPugJ3M1XTbHgihZNuKyV3dP>xMawq zZ_XsX6$pj#h=-HZT|GHD#?^SU!NOfa2oO!S%tFZfhuft7PGBY9++^iuqj~~RFc7HV zSfT-lt{QZ;BzB6BuWhiFe5a>T@h;>g5n|)zW%A39-pVon`K?HePaW*JZ*3L_^1+Sv zJ8D}$Tt5=#Ahr4F@45sIfjl`Dc@8(?tE%~u^JsnG&E;ELZEpN;fF+C+pypa+kGDCB z=!BbI`n|EH*kE~jrC%LAj?jGGspuH2%+SQXOxes?=D6f%Tn-Py9#gK=-vgiPAPsX_ z9DA}6A#m(2kO$O^rRSNuuy3S2vRErZ%t@};Xy3`2yZR`ZjWDo5 z1*bmVJ1tMG3d(=IS+T?L3pn%eKh%?49Q$XAJRf@k4^u1|oFj8i*;T1MJc1RJQWz^d z_YCy-Ho(9wORZi~@;gPYdbTo~-R8i(0`AhvO>}9>;9%&aaFNfC$P49zDA76dO{%0x ze+4#^r9BwTPnDbPhR<>mBHEUVH^ZRd>thKvn6BbE2suA=H_`2eGL#N4^kxPK8o(MO zb_Og$?+py!Zp2NCp=14JM zCdJ#EIK6}*B{Q`b-WOwIs+Rn<1iQ6-ZhJUr%((_(BOm8JKDUuc#PwQZdH*UbU%jR< z?#HaNA9}snhYu(+@6Vq!|EItB9bL)EL`v4%sCpJ{qs_}tUNBGIFJ1Cqe@j^I-Its( zS*j#9x9+0p1po5h1FLTQ5+uO6U?7*-{-av^=`tg zjBnNwdQZ@7nh>`;O7r5CIWKEs5MzQHP4E2%Qb#658P}#7txj^{D z>6nCWOuP<&BDUo|X}UO0c!~I{wsyTdzFYHghqNZMcNh z?gW#;vC*2}8%rPFrVT!x5T70KhbL>t0&H?Gv|dx^mOn9&KYtCeTlqf2SIB4f8F@{u zHuIkPc~}lv_jq`)%I6@gM4I5pi>y(rx&xQPz4`3!{72e5rbm!GznvS&9paS|b2OE@ zG^GwHlUZm`Jl_2&;=8dDQ9I9@-PdeFAJ;DGC&}6q9IqcbH)Kcw%`a(&(y82@E4Z{> z89isxiGt7#`fKJN1TjUFc2~|m?7<0DD!!Yb^x%`gsO2xI$ca>m{$Hyij`O+r+}(-8 zfQ6ze9x9#O+Fzd?Dd0TT_B1wT+2gQVPHXN!lM#9Vi~IbTkTBXo-j`o}C?0~~pF^Dv ztmFeK^%-xhZHzMrQLh;`YLg&l02<$x{; zg^{R{`A2i@tdDMO?by@B*WaMTcc3|iH>@~rbDMw8{6KT`Cz3J7BlB(qs5w$#s?ICT zPx%VPFvEuyDQ)|Do!SCaMAASM!K(t_7EVASStKTNqNJ`b>mtLep&k$1?0mkO1Arbf zUqjc_Y}fRDG#VsN#@x?;$;6je$%6w|DN6AcR83bqa(W|Ojq~MV@D{~|h;D1j zv3L$a$x+7K$d)+?jZ8QEY4$?fIDLasLk-?2(T80G${}~i`XZTs+ozHDsgUerI)h-J zbF7@sqZ)ZF+E6Zsk)iEaZ8zdARQ>Ir_1m?hHG^h9W~arn@=Px>{L&%K4#FlFL=`f` z<2`KwAp6Sbgjrt`{V(KT)cT3v^yS*&h_YQXZHCJ&4F?OnsCy%G^q^pfQ!II}k{HC? zwme?29`JofLiJO;tEVg8yfFdww!PA;!Oq2lmy8XDh1@@J*U>|i_1Hhja>2yU8^eVs zn4eF!OC%+|+UcnEqp_=@PZn(s%Cvd>%RZHwwh2J8(tC@$G-YtYPNh@n!H_fBp2(J+ z?s@AyWW@5D*N^BGCJ%L0xO|oolPeMQT@X1iZm+JcF4^{~*u22MB3T}^#?Ct3O=a6S zFM95SWEFrO;v*SX+LnX{T7>wnAf^^S;;m4u*p-c5kdj!cNBDBEfPG=%Db93)W;`wl zCCov-@7@4Oqynx#UB1wr_Y8h;3s9n_mn(6-u6*-uP28OK%+w3r#cWRi5Rt zoJHRwd3kfaNs%bE7%Ug)`h1REJxR1=k79vN43u`NkR-~H`E|;Jv|=}**-|aFSfi(f z<7D^x-f3y1{rq5h!B&bp%|)G_v*=r;MaeLYEu3aHFrI?2Wdb=W(- zz@!#9)IV--l9AiTr{|!(ZC7x!`Skv7?!+dU$IMyhqS3}1x~|o6Qd^I;f&EJEDuw|j zFsMYLk!$`aA~A-2gmzEx@N~uHKU>ZE)I&gQ6O*CXxy<@)l4;ZFFH|Z0O1_It+sG_p zWsE=IYmkq-5k}X?P5&xlME6Wqz1=Ioqz1f9-PdwiUEcqmbXoQNu-xWvw^&QCsO5Y2 zq0K5cq|4+oaBt8q^ONsNXcezzBPTYViV4vxYc{N-KP?p9>pPEoo`^|O=LG7p``$|)MZ4*DI8TN2$lX~WID1>kY5QrIj=_)J~?JFxi zF;&(^1#6Ow=vIMa1cai=KH|fBXAZ3bHmLL`acU$+9du|)IMnzV1qp-yi*xICtK|yH)4pA(i$>7MF zNI&zJL;W?TK2cal^>-v!IL3L?wHIbTkh0xve*S>Wiy)0}$%>-5xL!8ZLsv5cx2gBH z5PX=M29IiJprStLJjPxL7ESX*BqsOt7|Uxb-44a1xce+WlL#vdF`Z8Y&mM06S&Mo~ zJU_CC!3&plQMN2YK3l@G(-LcpT6^5@|1TBCI|F^r&2R0NDKE_m&8NuN+fmkWXr`?c zrFWU0v}W4~c=X5r>z{>6#IjV(DNwrz5vt2vDM^W*ioff>k#0MT4GyUYJaUOkJj4=8 zykZNJye5J9!Q4St=%p+ApMp1(=uZowN5xwiZq!CkOCLhjZC{HMU%)NQxAk^YRJ0wL z9tvVdTa&Pa$Ns}g+b-C9pPMa{_CWT$DPy9_4O5aw*i6A^mBY<|Dm?DK{5rsb)LRe) zLAS-}b#ypeiElFR&My?*kEGMrG0Xo}7+_UaEn zi<^_zMN#ohtuv_$U5qmEz{L1CDR2Pw-JpDw!cW{M%6#-67M#L%LM(<|;(!+=2fmWR zyNL@quh8>y96S%vrFKE`$)C5nBP-vgU*$d&{!WZfJb%({6pcUmTS&hD*TmT<7QC}` zkAZz`aUv3}D|ogI$N8}KCeyL@ZaL$$!{hKJbhl9m$;vVdLWz3~mo|*s^2)#BTv+(F zVVQenE3^JFGFkBE!O=>%V=YmnJ5|3INGZlZI3;5kjV$UC{PBrfWI2m5G$0dkV zwn8_)smNMv=A|&^5nBmOo|Fa6GcdkviIs8*{)qfoI#2yGQd?GWR~OdE)n4t;ph8g= z_`zLVXqbF%l61PaT4Dl71Ygw|cjKEzhIUXk2FpmiB3hMYqB$V%DOtHXQahJ-|5(Kg zW9cAPm598eGah)R?rqrj%Jr^JMM|7*VoLyf^K+7j%^_kEfS2~@M_+pJ_4CTNYK@ut zS?|q*U$+PZyW%wE*OSy+hKpM&+p9HIi8!2-_JH0p=b2xuTvCXS zzB3__V-gC-<Y~*QQi^JFWxR@W4Rv9ffq@T_)D+BvkAiq_{5hw+VU<0*qp^Ihj zE^Bx!r*ik0&b#LTjbA)U=)>pUyGxGJPBmu6yYgTE2QEKLZUDl8*aBCSS`$MxC!Zro zNxth%=M&@Y*keB0{WxfX&!A8egwc%rQ7>J!Yk>^qVFh@_GHXKD4GqNojJIu5-E4VN z9i~I^_uc8LqD*4R+@;G0AWf@N$XzY*OG@vA6T64WMvhuVr%f4M?zP0_jd*Z2tw0-w zjf)^5N={&HDdT$Tn6F4Fu5=__%HANTg2XW~#8)^fnQ3{4J;w5CmAZ@I7SrXEZu!s@ z`UKgzaU4kZhkCj#1pA{pXP zh4Svv>f%aVKT+v?0qTk%fDa@?N_6PCZsd_q#n-Hu+Iz zGm;_y#+{Lsp|}5;3%$OUyY*{-z7j8E)qZUezRxMHAg|e-SKOUBx$!R%MoNY^LOKG9 z?NsadU;P)OGT;UQ(rEZjoI997+d-NV$GBd5T6>fJxaGaI)=M%SUYGCRd6t?pWgZ+2 z3bWS+-pd!Ij59O^g%taRtGXyn>;3(FBvz8&w(Kf&F7w4_Tp0G<@~zOLI1JuS_ucjC zeQG>~_*_}gpd5N{;`+iot5X(uYu1;7=nM!B7K`8tL5dh!+>-$#x%MW}=-CI7dxij%A=J8mRWuV4IR%2^^AS%1_JQN>Mli5XTJ{7&PZR6y0U=iTR0P#S% zts4CFG86~bjHR{}GMj-l6t`#78`~RWhU>5v7}`RxPAL5e7@&?-f4TtDMH?C{jm-Nd zU-JI^#t_Wq)-Wsqa7*|d7NFX)?=y4uvBahnkzyWVZ%0R`rn^;E31s(Zxb6@fN114& z!P#LsW2>iZ$#;*BVPe!C21FeIOE5QK^8Mp}bc}B#5T`~PDjXi+H@2DJ;pag?FyX`Z ze-ZW`KuvvH+qhDsm(Y74ARxULY0?o;RHQd4p-2an7C?IFMXEF@g7n@bfI#R1O7A6r zNGBi=2<40VyYId4{l5SFXUfKJ5iub$Cl3>Bq}o)ot%jBS z`u<13L1;)0{3#@#KYzmpBX^%VGAG9{&1wrL0^z{`MZZ6cRW$G!|M71=_aA1~|86++ z-Eyw;dy{$b4Krk82ccCf{7?*0wO0CaXVq8sJsY%z|rmjeZ_`@%>O zAtEM1ue?~wGUsP7*&X0FSksSFLA^Bg5g+qE`_btRWzH-~SXzS#>fSX8e|u`rjQ5B! z=4JO9)x>EmVt-d;sww%z$<@l|2hC^3TBhE{)SW~NnJ6{Q?2V8wMIOi*;Yc#9cXWHM zYEfo$O^Kt!g@l0q{5}<_y3&ac@uEf7dwX=8 z2hu2~Z+(VD?2~mqrh37f&u=>VF>aUd5Pn3$k{b;zo|@2o*|*W%b*Un{+0gM>m(5cr zYxiyWR0-|Dh(z3lcEfm;h!s7|uH)R{VHr1~c--*t>^PK)$do`9mFm`98+6e^V13lfvEt1<*Ui5Hy3 ztl?bi^mOchOVP04UXx;}&;bLsp*n1^>NKHW4#`iJg5TGIY`|dl)@By7HOHBDKP=>J z!kHYKc&%lkC=zvVb-sKT4%U}KfYsuZWBz-GdCmAMzawq>w;f-Rw1F0*Kas5A;efS{ z=!lxAC#K{#G~;mDmdgSV`ITNM5)pTmpCmFY%T9`8vLE9mt{uyY0i^TGVX!Z(*O9c5 z+idQe*LU=3XserDm^FK0p+va*0aupfuJ5ukq7OrImbEwB78yOv{zyH39pZxUXNUmd z`!eLHR_M1v+TX1A-q|17#NKN)BrPxO+1}(tSb#e5?;IJ2-_D5(S%L$87yrdK)1uwEUOE$&49+zm8G!T*r zVCK^7QP$b0jr&I?^L?EEiQ?NT5l;`6Eg8D40ggY~m{pWB)SUnee7g$ zvwm9VSN+w<@jGQPyn7te9>jijZwpRlO}ab94AGMB3nQ2W36t$&59fd9V=Lb>O= zb_zs+=&`i+f3!h=KB7i1yA`e}FCRVUME!0{|pGa#Es56lkgXEu-4Q{Y|a- zZXfzbL7`6ThbjGP`@Fv|_UrtO@aEiyOOh78L|zZn z%jbI5br&~FjMfYuM!vEG{yq5SX&CiPe~LN}G)>_X-QDTq7lj2U`xLgV(SY)d%?f;` zK|V!u&H52p&`}d6kkB3#cwy;4Z%{=x)9r?I#bCRX;Pnj{8{S?{|B&C#$|`%ar`I#x zs&vjD0~>O)TI3cc(=RMKP))KoPgi!(R4tUz2+^ybC66XD>?orOKaYRXp*`?Y8yxSZ zjB6g$l}e>75k2_M+9fc*`QW^6wHpoLj>e?x1r+|M%T*X$Kb*t|bj@RVNr&2z1MQ_X zDqEp3Nlw{5*LTmTby(tel1P>#fXuQqDk?hBTvAXn<<&5sa+kd?P7>CA^^ue34~Z?d zA3RhFeeC0UH~+IMm85ZPwT0-YiIG&ReeV_JJ&c&BKUd{TdowKRno$`Vsr95-I(+qy z>Sk9wCh*(3(9$%y`5`BC*^3p&J#t{XqrlqAePeDvp79@X4z%{@h%7|%Kv$99e2V%`$sYV3LeAo^(Kc^}vxiph+8y6$9qW;Os+LSW)PI6f!uiI` zrBjAQuQuO>Y70FKmQ;l8k)nb+Gt)CnzAdB(VzhAaW6VcOFdf_z?NUV;L%W4h{pCE* z&r6(OhR<0=CF4UTVkffavi^~>&_Tvb!-FcN^t>Z7P1({5HTMd(6C^4B-et2~9!B`H z&^c2hwI|CQfuID?d~CCg!YH|97Y*Vp)Al?+T+V(w+3d!9gvl;&Z}y?0S1q{@ZcB+s z=Cxf<9qjAN-SPJBDlK~Q#s?p1=05VJGgf)#60X!IA}`c3 zj{HA5y`a3C#+f)h7F77$B!jpxw{9fO9_!}>&H72^Q{PHTOfo`45F+|4VcPf3JtKz^ z@X(zUk)eKpq#~2MVhaX8mX&=!lR5w5T~mbky4+406BQr-?Z4#+yhBIv#!tT<=$${a z+>o=Jj6rgqzd!zfTgn_%I*q^9XC*j&A7jZ-#5l;?m(&iv6X46sD{!*?-~m50 zt=I?QOYcao;7bgM8z*D#PM?h_xyfxibBnVcIFP*|S6$e$5|2Ol?staY<#*8L=-p+~ zh0XDRJe>7b7oeUM^W*%3DmC5;ea2Sf#cTlq7j;gKVS61H@m7Xm`$Fm1Uu}cim1?i` z^$f&i&jVsmIIUxsB6XiXmu-(nmeaiPq<~I@v%xaW;oIT#GA3o-7J}KAmjJW5FiB5G zq0FZ0O9wiVBB5FBEc}VbhSgKxqz3v>)S#WWXkGB|u-@0WvjR?Wl{f6W5ucJC!*a}e z8hscUqjSHRkrO_I*JYq7xIAw^Ah(cky7FrL6(dGz9!!fQ)(=kszeY(_BDU5dmfCOo z$?S@2tqn2LXnz5!bcx19dFuczw1-D{*U>XOO(4j9-l6S7_c+Gx&3mgwy#mvCsqP)0 ziOz=RnS1=E)tjj$RszuTZM8kagN7Ee_aze(Ptxo8^5iDx4Sf*L$!AdnspoItCF@8O z&K>rFB*{lcQmS7MGGEC5XyR4gKGpX;qlpKT1TyIl$9M)m|MW- zaZbkt;O_|m@ATqOW9I31`1<{(LelAsGU5c_=}}D3$5CETT0QZQ%OJh=I03f1>{Aa- zl&*FO;Y4Tb4rV+3z#R3L=I`5_Y-U6C3cb8oqYgYF{33TUY%EkX@6*md0Fu$Jf#f;w z(a)-aNkwwnevH?z#8Vrrbi8{!dX^Mgwz>L_YSJUF6sS%;jo-T*9RisK;3$%F#WzAb zUNK8{v_;4)OCZoFGW!#ostU*seQ06jGJN^!gd+*%8^MY=@QPtnGFOibho15F44Dzp z%*}XU>1VW`6j9I8Z)pfqv$9WJ6x&-)9#ps0L05lRrE8ym`kD15987Pn=a10k{2jKMvdn3kpNpao6_6Dy}8tdd4R?&HHZ9--g~h z=#rR{CTwRa8ulxM9(I zfPiCP#}Nr1qksP|M4$DZt0`Epmv%Z!JZfc>MVFI;kt$=Bzpgjm?+t9ANN=BLj^CbCpv}vZ``fzmtCyc?9@H zO!BHKggE~`zfg))yF&YHn?_(`knB`ij=PGQ$ue-$zdp+QoNt{286nUh5xl|$*&p{a zV$AG+85nn{(RYyYAxg6Ur@7Kdv~%9q@Fzd$mAbR8?8_GqLYnNDLm*CD=Wz=8Wi`ud z@o7~fD`BlP5IJ=xEVO&Jvak0N{*jtw;`<6SHAU@vV|TuG38!);FDsfml5~;OQ`2PM zD|BTGJQPPb*BurB1u%QtP}O*(SWrei~VBVed&W{@9Y9uGMTN2ch@@>XsJkE zWrdMd3uWZ)S!*0Cez25Jz3OEH&+fIT*v-{Z;Pvg)mmzLJRbF!{m-7rLKi}?dLApEw zX>Y0ew`N12Io;V>DwuZDYIbxx$tq`~oT>!2JY7$L$;{P+O>4sHw(q9(3rAYlQX8vb zUNV0Z8dxPUzr9IXN5N6S0rJ%3gS zs;T*MibO{xa!gG+<|al~zMgL&R*)d!P)y)s;?gERJjYX|Fg7=rFQub2@3NDSWmH6N z9Y$-ayLD>W#8-mkEf2h{KvJzWq(R6GeC-mI-2&=$vNq*r{|?4`1%imSV+B@Md(ueU z9{d<;L?LbW^0Ef70wiBBfs|MSp45#wmQxwm)#^$S=2RRJr9K>kKV4SNFqCk9Mx_$3|0AVV3Br=OyxJk{;$Z z9bB~u4gVq`r@sGFV$35uc@@eHiG>BxLSrpKfpjhGl(iqNUm*%#6ZB08h-f{7@6Gf+ zT5l3(Z+tW0t`C98iG)ienm>u1pzRVi;NUk~<7niW)u53lYl&xJlPIIB!Da5ZQQ#zb z%+^p&<1U?|VV|y)(_N01+KIo_?Ws{kKDe&Wo_8Vc5gr@u9nP`l88xIrKf3AI3FI|C z>@vfwPwYNSvecjT`OcXWybQxu1=Ym_AQd_y{sTjc)9zjgH0CM-z-`h`dDt0N&tx zrt+`;SQ558@jMy@wPi2f&>9frrtEVQ6iL z67EINFnY%#d=yhvBSugUu0{6p^?}9P%;J+fiE`DG0*Imc)4|$^hT302cNgBtg}fm9 z$lA7<67y!Y*Rf4<`eMa37YjB+;ey4uR^3!?#PmMYH}h(@$Vl$7cXy#%$aM6pk~lk3 zUrBzts@F&Ich>U)!L+%)tw>u(>Z@~)@1JuevKCds^L!VACr7l67z~!0@RhrKJev(A zcm;m3_3}IZ)L2^+w))2U%f0Bej@yy{l%|)KQ+F_!AUMuDodeZF4z&T!^S=U?8VL;~j%-X}nRe$v3XgL0?Eo&yCe z)s`guc&rL}MOnmWORNtEDK*=_qhwg;{h>o7-1=MFi+g|(WGbuY$Ohyb4g z@@E>o3tECtsro+O(=m9F;iw#=#&p(YIeQ+-wlFcmJ&!>835AkG6ho15(eh7xmAdVrR5|UP{|XQ0nS^*plQ)N;DVuf6klJbl%qa2T+Pq7{}M3)|lTNxVLRtjPdL5mN7%uk|Fhh5WM zGs_@zQ`+3bkafFSku9gGYrwOqfICYyxq_y0 z7;W^+9v_`eSY7T8Q{Qo;ufi|1D}S;7UID-WdrxZc8o0}#IS|mXgOBm~-}SwPj((X~ zz9t9yu>TNQUO(CE$x$V1= z7dR9gR!l!-6f0})BoQoLyyddCM}Nc6>ekQUf#1m- zNfP8fHRLbmjF``-eIT$SyFLvyG5lpdTlyHx2iSh2;rP*hH)L>|{spaV5o?$It z0l(2yxHcTE_L1!_f45rJ9s`^#FGJ5BnAz6|CaH2nR7_*}TI=sblN{iM%-D+bEHh?| zPF2epzog9~eQjkDDv{im40SuEcGD;Yu9`^{IZWF$Q_o zs=zO&w`Qq0-r!$3aZkoqL_LE?P{XJ$(4+)4&ocNpyi5{wP3#v(AmLfx#S3i**63)F zFUf3QtM(rE5yqRASM}toHlPhooloC?L}Hoi!lRis&%1h#s}Hk}U+LW|rT$|wuP`Pv z>#xbwADL~4S2NYi>Up|#6I7YUB;Egy&7>no_ROGq$PQ)Gwa9>7z$+`tu&5)8w@_OA zKorZ5Y+PaqDmHdeTCkGePM4~(PU=Y>Wbm^&)Flf(vM*xZpvN|CqvZP-!1K}r@Ny`= z^ah%p4TeFU2QlduxdoZVbcso@PpQMWYs+Cu{y7huzwxQL7bOzi+Dz7e9(lNhv8X); z%^KMmCh%|lTFpbLq^qj3L)KtJBmIc@I^Yc7;g6|4kh(N`76FP3sDKe}^fFQ-W5$j& z-ETy2lJz{DudQPIR!?H}5sU+wdBeBva{-W^LD zTR(3R_Kc?)|(U9u&uV*MEJ0cuRnl+E;u)`>@_|I8>4Yp?fDm^5Tx=>6$dos_AIK9b_Nn zQXtHv9vE*`kK|#q1z{W|`10cydpv;P;Q7UE)96uQ0R$LJW?>?F7a*!lJyt6+C_<`| z#?lb10k zPdnvRAiK)$4h(2A98j++#Om`yy=GDR(IeC=7K}wrEW9x6pYCtIl7kUn&*k)**NLU} zX?Z&X1l^f7iKa)o!@)J<$0PDpo=hY23|HUwI+lJ8ApRjy9(-_)kd1q%^FuVMNH2XrQ;i;CM#JWPDk&xa?~d7e3;H zjqRqD1Nafu-q=^~7O0&AUwn!s`23=n{$ZEanlv=VOBG%Rqygnublnx=Aifd|-}8S_ zg>iMQ!Es0G_y$5AIiD?_*rn~9<-e=gQo32p9W@#U9%KjZr`7L9qGrTY`FN)o>-J{Z z#FBrovzL*R&0-I}vHQj(xJNsy{PiVaLQcsQxoDcpA*A+uTYY3poc4}|vrloJbpRfa zajhp)uL&CA?Yi{kMIMLA+4pcie4Zu3HAYuuwZdNoCoG5~7v5}*+G6BJ8({i=tl2SE zxXEB*wu6$I`MsB#&sGNp)hDezyolDA?`mjis%S$nGEfV*(~_@krPMp z=7)8pNx89@3PgpJXXq$_@0YZII9Y7flnq(HG<}`1#jZD7i8Y|r#AL>NUoC1+v3Bsj z5+lE^65}zQ6A$&=3$k{~xhT89jQr{LV&|!=N@~Ow1XIui#5|Pdl{nyJkPn?>TpQvB`}1bncQYBkrw} zjIx(hQz;^MJ3nsVM;(fPQ&+&5tij77Ws@D?m?(RoA!fpu;o41vu>@o~!y7C)4BJvP zkA(MAVo{r!LMe80IBD+!fNFj(&(-k-EhZB}(QUo7h+VH!1wn+r+LZf%f`qkIyTENzq>>UNp_flr4blB_>64l^=Y`~(%oHk}BRvBpDi8gn<(is{;r%TqP=fvWF)hch09C$4O*l@s~_ zK#)8S&C`^w8j0HAukY318~8Pz^NHtO=}-mk%bG6shbQS}H+Mu62WJm3kOgw96m&f^ zN-K~P5q6N3bxmSO6aFNHeSO2=1FKOZODpp~esxwv!$sbB;Now$x_E^l3WLa?JcL*g zf~&z6xIY39DKC$JH+a{}z-#Giw8ABY0(zWe0>}JmvK%qsfDm2cDMfcP`JNRzj!U`g z)F@-(Yx}}YhoNp7(*@?|UB`7@Co4Oxgo3MgPkGnO07%&}VxL}zjN7Ai%f}O))l2CW zflS?H7`e;)rWz5s;P99yE(GJk)PS$Y1JA{_)#}1fdd(PFC@_#wbjQXiezUz9H@mhuD1cLlQ%*V496poKBplk z5)!F)!b>N&Zd>J$fuiptU)thk%CvSpy1b+Rqgd#fvl)v9CZ%-?j{@l=!{D=Loo+n$ zW$FSX-+u8{bx6Xuc74y-UCZSQW8SEVjEsIJu_#BNig%m%N!d2>BA~l&}D?q!LQ1N?{`tckDYT_|A7j~VB<20a}B`@t7hvY z&aCKJswB^|I4;g$&-=ZhB11|U&Ng1LtUQmeoyge1va2lfp~?GtHmTKXrjLa1It81^ zwnnECC2K<#Hpih&S3QNt@`QZ`Pgtyg!yrF5lk&9W9(dk6317)XNmyk0seGvV{eUX& zZ7FIb#?8UsE7<_D!616d0t<_|aWYN^!_dH*19~J#I84LeMR94(S49I%H8g_%>lf=5 zQsPL)7*GP`Pxyt;*|@HK@7;DJ)YYo5xT5kfU|lv-Ma`cP@vQLVC;g-b&@#|6FH25< ztI-|%JlnbPP4-C*<>vJ{v{avsCi5N^XnXfV5$2M z>|prG@~pD%CQYi!jALr=5$dS2i^yFLb-^{Pr~4vNQJ-G+vzElsyM!is3X^S_~Dk#1RCT2M9(bVfJ0YMR?QoD)bRycN2uAZva1T z4TQ}+G(TgS(W?dVKt_j{*nj%x@KSq5u!1(%=>*P+>AEQND+hyH0opxM}DHhDbn^U^zl(`G^O zsu20ZK0>m;-!WhhIXo?%4G_qGO7P|yRg9VrJg2=J5*+3Rj0wQsAkey#&Bf2IZE)~@ zN8N@APz6E_xM&Nb$!vHjM}s(6ffg%a_LOuy+Hpq2)!N_T&cOvkBFm48XQ!UMw23{= z-swm)v$83ujpX0K;Z@YYw2rwKrd>sh*$F{j-dy8d0;@b%UbivLcT`8AjytbVRX3h2 z8Bs?Zzeh~oO9M`cIe(ej`&^Tp;<85R_tiM=mQ4$T;4TpIS6Y-9Eqm6BM0?NpRHXq;eRsj zwzxWTC`^KKVNRE){3)yYXZZjAh3@(H5I&W-au3iyaek!H9$MC8pLaHu`*=kOpXaJ2 zDuHq^9YSe;IHvk@{_inF-t|}>XRL|5r!nwVp30Dq=AG-N*kMcTWCFC~on)*6x;S@* zEWnXk_uaVR`LMzlo$IX#RPtFxKHr})Lewj7-uu^MV%7+U#63N>bTY!|fu?R~Ow98_ zX0ATzsqQatF=ZAp_X%HiI(=7x9Z0`4+!2<0{Q_Sx>Zp$kkxSa{@!?|`*+cr-C=V_z zRtjF00^x&Cq+iJkr|MVp?y2032ZU!evb{=ED_i%U{^0K}hQrFoPm4<}-XF7aOnN&P zpTm|q|Ci6V34X$LXX&DVO4PwUKFc=T(QtK02| z>h_ufV^)_bKJxC|+rA+g8;{bPrfX1qKo&@bHK*4LTAJvdkxZTnpdD!@pX|JqvrFDQ zd~^0tQ|YRe+NlE8-HihTw~Hpt)o{c)(4FV-oQDg4)1h8~g^*KZLZ2vrm*hs>Y$V#h zM9flSs`^@i_p#rPnKCU0L6wicF8R6C@U6O2Sp1MWRI&+RHy+Ymt>_mWw1m5bTmN4p zqV;D)6hTYk#87WCv76J;LoT9}e?0u7Fon+1fL3aks3Xh<%`L_h5TqFO>W-G(Ipi|~ z?5!jraMNBGgkXO|qTQ@I~a{HoGO=vqiAk#wAs~>-2 z5}S0Xt%6&6NrxcMRgwMIq`^UgyR^UsvRm7L)$zNiH*_2bv+m!fi0E3+@S-~xk*07X zd_7Ng!x(URY=I~aY>pwk8H%b^Q-QeN5WxZ@D-)1TnIG#Q?QsmS7ylYDvaU@KLI=BY zpNJm-3?B5N;cEmgzJH2Vq(HtS5srct^gvfILDLp{eG^7rn>ISW%ApGL5>|sZPO3m; z-`g22k*W4AhQhaSG;d)UxVjr9$ zaz_=$v9KXx&i{zayqYmA-g9&jFHzOZTdp;@BAeN%^6Q5x!}kqkAnTslHVou%J)hUV z=N%GXr)9V}(;Mf5O|&-pglf%>*OeN%Luu`eDK(F&=uxg6`+<%9Ip@B&5ilm^2OBKg z_{(4s{f}YrFWOmKQ@>EQIxy9E+5F6sVQ;ZPl~BuSKJ*Nq){58);zDAg6tgvL=KbQJ zimPidLbw$c$A3(tzo}sLferim)Z5r!v=MmP`^g{&@HYL31x9(%;=I&*aU{#ROj-cS z0kX5>Nb8udc7Fxw8+9}q2p?~k2nn2HB6t5)NO{u2!degZ>AKC1;rf;zS^=M6(jQgb ze7nwvk_ebn01|#Og=dW(h1G-(tkO`WG+MoYusF`!I`Ic~9S`C}&||V6RA7O|%oj_e zk!Y}=HSTe4_*0s_5}piYmVoLw2N;c_)seRWes+pLzHJb~n*LtgCPXHkMEn(|qd?m7!=Ccr3y*pte zyG+_m4H#|8!?Um;X^;0^(+jDp64z&uk7Fs1yA1$BqrTs^@k4IS(E*q|kSqY^o!U^l z!{9}3NR}Vqb^451_fQcQ2Nx&1bSqVn;X|seNbj;_Gx|zZH8oC=qtb8EHMfTlx*Sns z-`;n~$%KZFL2PVj3tV2%{8*$H>+_qnI!c=R+;)HNmagypXWFde`vK*u$=*WD~?6huE#Ljk)xrIcq z#>q&vX~3Y^wf7WTL2d6S2l@$p>v#kd{$BHi6*%g%FnG4-*fb!f)|>sc5SS2r%=`V+ zS9QkV;0fEQjy|t}jlPRy$ovnO5>!|`r z!5jqb26kS4%w=VnGe&{oVqOY1zxd`0Pe~>84QlQRkaW9oA{s7QqBBmPXTmnnFYe~~ zvzRzWr)Oq&j}m`^Ck0+il?&+-`tU{Z^9ls6W$4BqknaD zMeL*bBXJtOR~b$qL&wXwHN)ANn%Kogg(->MnjdpXHD}gK1X11xI8@?IAGpmg7VPmo z!tIEU7)`#X1KD?4kLhkdZTaNlqGH5g5~s$ZQ^0(nLe*5CFQL_lLjhb?nEYS``NjTaXvoITy8Eax8mhi z#y;(28$f+b5l_1-8$!FQwz{^U6%Y&_Vq)RaqR@!Lpy(F9z5F#KM5%XWDZEaG$zh(pz#?;6QOg14E#S=q#sDSAV|g?Uf$fz;u#H*{n= z#0;VBU+XoGj0;?OwnomdJBCxv20M=*eg*NKg<>*U%h`m(h^=1J4H`}a+1Qt8X8<(_+1eLXgHWDJ-c zbsidkD48W2GX$Lg1D>M_jsv_INX;m!|0kGVqNNV9!06?(kSk>l)*QwUGmGTqK zwDabWp*!ViL}r8_B!r!uLNDf8Pv4f_c_610t(vWZ8yT44s)0?%-HYVJ60y7^tWt70unQd=ivaryrO}ym0lHbth{VL4$<<{drb0^-Vs~$|P z^9=0VuLASP;z8n!wN6`R_}fsX!l2WPWrKcIlV0H{Rv1Cm0NkB2s9}|D*BqDY^G?DIfwj#V1CiX zV2xOjvxP@LmtHu?bfJ4fxqm-P;!>nIjh}(H&PT9mvp9lz*)J$<XHymSj&4%5|DNjuM_q9btBvKMT9MOh zQR8o@YO12<7qjt%JV?1<)BhE-Y+?zfY}6X0l z!=OWKN<=(67gq;)n?E+G!|zS*hW~vaI_=4gonlx%%n@|WdWgu5NI}kAi0YvXwWmUD z_4E9lSmsm3VU>|hkxXR|GM!=;dCRyE&PSCB7VOK}v~UQ0zAx43(A5UMhxqeFma1S`BUs z=eneikPN6nobKJ6);+xV0kwHDmWB3j%#QbSDnD}~X3^tsG(gwp*SAimnCvjr=vZ)A zBUNQ3wH{#fFj`%4=$1U+eCXvq#phZqx6!rRcT@*p({}8jr81idW83{2XYE0eIEpMx zc3awNle5jq+*Al~5q;aL1B7|`%B@v28(LbzFs4H1XQhDRQiv~cWYsX9Jvrq<$YFH>C}=F%ah6WS zcX5@pKb4HARmTfnf^-uNX5Z3qwleZ2a#kSsGh=E~ZK>ch*K!^_w?U-VcA>i|S6g_(y69M%v3KRkl*n;q=kvRkQnUq%YX{p*6ZQi^D1o*;jK6p!i z)`LF_YCsX6YBcM?i`iMkh=kYwi->#PCWFCqEW&HdTKwgEA zB_k6o4B)Rd4j(+NaCUfqdpfO1nhJ10AM@eGe3F*=>L&;|1<5F9ln}?l(UV3+95o8~Q%?>sBJ^L3AqgOi@F>0byCuYlUK! zYGZm-LBg$>VB32Po_HFs^9N!1i~68MRzKF7N;tNqM!#%t$NA%b{!Yw!B|&Qb#W7`^ zfj9nnM|Gk0LS<=554_mHo!+@b`|&llpLCIDW zl!4aFVn96M(>wfE^yR%OwYt^8ljW>r9dp;mL zA#QifSgc9ngBZVdBTnpNV!VO<=x^mO#hPSQ^l{Mc4=ae{)gU+N)-wMag3pO0wS z+gZK;>m43rnf}k}j<3>t>{u>a?t+01&|k){ByQb$d+VW!;`7GL2rf|m8r8;UKlJXJ zI}bw8n;J2Ga6{LDGFZvIMoc56j*9YQ7>Xad9b%b3g16q4Tp9jK*!bqnizUO(Upp*R zJ%cO@+w^heeN{rOjj_zfG*hXY$SpI4s2-z69;``4?g-Z6H4@Iv)S5;x#)sgsHJ4Mh zBsPgR+r|clET>YwojDSKWtjDx#?hNN4y9kA7~(z4MIn=i4;{PGp9EagaJc=YfX>C) zdjFdO(wqTeC?KUk$!ZaWu*7Ze?DmpEpa#5d&wjlj-twgbw#iO33bl<*!$1FXAgO6* z=K7*f81~-0x5$T)cbwh6A3qH8qXT1F7|Yz8LN`_ic8toM2+$4=3?bCo9%a84AOFK- zQQ97J(kT4O13f)`VLJSoXX|$|0@w8FMj!6#%zS6%x7^7V0+{`JL5?R5;nn@E&6A$=ul;;HG|c4S#)IU0BL&)6a74pep|oN_o-_ zelBwNjj1!Rmbb;5;a&gpP)X$m=sUq|#9Jy?JgsqE!zD4I+znM&pnj13?B~t>L|l76 z>5$2a*XZ4=Pl#e~>4uosQ)gzY{^myH^2_X>>MIBiXwt*yXmdl;Wx9lG<9c-3`bi{& z!lPVZ+19zjq7pP1prSMM0h)}!_;t!wu|6EHFt4@&(2{s$bWAL$@?$D5Z}zi`pOz=( zwqx0oBv&P;Pr2QjPi4ZxZO+KsTQwQK8(Tm%fUzQX5UpWFxuyRB3@b9xz@DcL*;Y(@ zJRP1+TbZ|P1|iNr+WlJDDeFA@J}c#Y`|rHm4JyZEOK506_SDpO$uUfwbO#}-*aNrv zQ1il7MCt~3)CCQS)84nMBd^69SA2}%=$h59-tzrn6tLS`cow8fK)T+ zY7vC3W8hWO+6qgE1E=jV1^kUT}t z=<@2`ypEyByb27FSF5flG*)W-B#n7Xs(bM+DSNnRR-eM7{{YB?rTG3qC2k%X#DlUw zBHjNwZ|x));_Kz1{{&+d!@ohaEGX)k)AeznT3KejS@7`9!HrUypv7YX0#d{Z4r|>W za}n)yH5da(jS(-Mh=a^d6V%l;;Fw4X@hIigL)C)C=;y2rXzl~F;{X{(9$n-h;X=TP zI2TcW@Z?UZ$#w#LX_8&?Z%jNJh49h5oBc1wqD$zqjZ-lT%e%x0rzOVV-68RxNaWu> z2R-c8^nc(dlmLrJ=F8jishc2~G~u^5hdK&fRw4Dn&`y6Z9_l-eJ z=<+Ye1|G0_p!{%xk_#7uCU)V#QZb0jo{%X3seXI84+F_>R=a@^?~tcDfl;~F6ZDSU z`V;H`9dfuZd+WDV!Ek&fl7C~8On4kWBGrMsd{Kx}8QnVC4e?!N`kH8qch}*yUkjDy*(UjNuJ<=Qt1kM5$?d2Nm z|GI$`cV(1M5)y9d`Im1Y?1WkTn{G+@-F`9SyedIpO@7=FNFx`mxeu_opAR*7_?zkZ zb5wUP^F|$>C65U6{CL<;2~Q*Y)21K~Eth68?R`xrTJ5f|3FhEtJ8C54e?~d9Op4I& z+B})|=;B$9jjST11;f~S-$xAwrB=#wfSDG%E+f?GJm$i`nK4u(sgvhQ^U!ki;=-P8 z?Kk!{#Vv5Q+Y6X|F8ueCB&-|&nD>FyXQ{>>IUNQmg|dNxS|T~D?L?5%KYYL_8gGS` zuCQN}P!MrfbC%Dud627Axf*1+9(Rd%D*}o9CPX9y-(1Y(-aoHQ`YPuNe+k_hp+EXpU(@m7b%D(V+JMB5_3K5o- zNb+1It8?~~Ivqs8_2XS?+P!nfPxv=;=9}}EO>~YidXXEUU(|>@N+Kk41J;oTtb6A) z+mwRyk*V<^GBjkN#6)|ZDv`9^=&HzM1N?CaP<)@0waSCUZ4o;~Z3 zFc_3|>>(r~Bo#H5Y+1%KjBKN1-!k^ySVuF)`{=v;e(!s||F~Sk+|T`-bMDVM=X2lZ zK1SbgkjBuZe2}73^QgA}p5g%=i(#`>CPs0MP(sQ6^@Pp0p{ z#V1kjzQ%Pkm#A^eN`ae`6%G&3vNZAWEDJxCZZl$)-X|_mHkc5UFj)I|ScDR#D7v9v z0tSTkh(Lo2#7`NjA(KMQe%5t9T1s{{0p0W{m@bWC*uj@I4eA8iHlvX2QXg6)t#YD# zE^0pcno0M>&gWo$AsITL(E~5r2pqX`k(2Xd-I(9J#HMiI>`|$=?u>G28p=*BRsKtS zrO)hseQ90TvGC?n^20Lqc46G{^TWrCdaWV;-r4#x!)XX8b44!rP6`{+eYT?n5=5G~ z9#oUOI_h^Imr{6vZcLBXBb?=25%l{}Tj#O}om%1$^GYzDhj>Eot#`Mm<}gV;44ZS$ zf~asX70%O~tb0TZh$7rC7=Hp`?Bk*KlWrSiV3Oj9N20G|fI2)FJSZ7`r#>#5yp1Z)Qh6uP`PVwh_%dwLLPw5{s6foUhu zGPt>)D%%83mI@NADPxk9ekUU*6Wf2F5;dKQ{y-*w$Ix>e_q;c5Or98XWcN0z2tCY| zxuRiFZ)}6`fo&zesSV%5t%Wvc5JwoBj!Vk6#_{h)G#3aN`6rQIkmh+!himWS$C>cS z8*BSmJ|a16C?tIEPN#VonGQXoMt-rCq644W3Ri!D-`}zM9rmNgTsdrEB&D0o4N`u3x!V?34n@D+m-@F5Gs8%>H>YP=n&x|4fdEduEp_ZF0$(rh z=_TR#+nXr10&1-xX+d0ebKi>ywe@Is$(P)Asl@s)PA>|( z(e#BY41u@+*cQjEHCtNZwOa8)1nA~c{1uS9rw0Mzfw?9&6OgX zfBcfz6jIm2`cM^a=HB~Oi|(j-a3*_o$$fsu_r@#f}uMeTj_`bC6wi&&JFBF@s*@!Me8tMa(K^U?KB1|C_t%{1 zK3*e`S2r}9=Ip4Z$Q(x`$eUy2o(4plXe0hUerzq@{2N7Z&)eMs+Y>t6JaI@}0mO#I zub@Bt>J6eODQTOp2!wYbAuJ0iaa@W}Mv-2lI4ip1+*-q6+wKI1>A2^C_5o_rAmF?d zH_RmLCtL=o+G6BM0Ja03?C*)~aL6a{v$k38|^4eOeuaC@a)#;a0Wg z23!ws8XLraICEn*SO%z{PI|d$JZce?^sbFg=;3ZMLY2F+9r)^ugm; ztn0a+mXBVUY-(Gz>Ff|6@wtI}#TGFa;cIn??CaaH7B8ixrA3%NY%XaVsbe{zu!-D2 z)P?ZQ!2O21M_jpkY!KQj%YXe=_4Bo2Z0DG1LBjvSa?1J~{;_14{mrukt8`m3KEKMC!8M%=MY+&Xgz5t|3t>?+A*igeQy(CaxRKTWpP5 zc6u+ph}?gbis0X>{bi0~tiev}Kd_MQzPKqiaR)HVQhuI5>0E$0{pS5>JP8TTPeJ}NAuu%cS+tQ) z&bvbO3m!e>bWYVr<x2wD&7Fnhgf&I)iz2=BaXEDKCfFOn zVT+BK&lOcv^Ri+ow73}>MK#@<*%`ydT+ty5h0wu>Z=FgR{v$#7J}6gQYaDDwfG)d_ zU5a5+oar}sE>r%=+m((VkMjAF&#|0EzSss%3Zg}RQvz6>Xihr(iY`2~boXJ=b|P@(rO#k3R?sS)&ONsVLn`t=}-td^E* z9esHjTnK2s-U(Oo=3fQ{{u*>g$LY)NeJ%8QbUacp!RA%v@mS5-u>{7uth%^Y)($p1 zI;KXNd5LgsY=XUWGh_4D=;u3OPm=o($Kk?kvId+pUr%ldmMQMg)i%mt4y1nztjwNK z(bp&pS-#}nW9oC5d-F6@%gRnO+1umEOKR3J^N%SbkJRy)CH!sf59a4g)hd1v} z+r2(62_dQKA;W(I4!9cnYu9)BoD54-8#!AS+wx}HmSVQ@m@v4N&o76vbH>Q{y4U+0 z#UKA`SRNx#aLLoPi;C=Gg|_^Q8L|r*vh&rhm|cO>B!-**k7-dehJ!C)fBlKWXdGV2 zI9h0h#NW)*hVM@3JNO7_RM01|(gyUoyxdrSXnvjRGJYhk_`8M{x4hV2)AdnVeG%qN z+qv+i;)Z?g- zzG$hJ)>IL%-q+H`ylLGBcgqA`D{=7d)6Gti7+h+@sSc;qnU6{`+EsWDX{EbUUSVy~ zkh^WNuvTwojpT??)?Ul|11{J9702g$q7BJQ9Wn^SW1%jrp-84ak;3=Iduo_?a?kho zl6sE$lu)MaTa|W=k6D(atkGkBY}@-|egdw#{kUF5Zo4gfMwjAhE`PIb*uHr<%gGv& z47HO(1ssh$srQ{bAGTl~7EM@JItMaiDr`QsySqb;dwZmojFlxvi(cRDK$(0~wbV#C z)&WPc9PJC{2U^4Z7kdlL?tJH)mc_4p_5n&`=9x!Ce;l@F%i4+mxGf$6`f)tYI6G~E z^QaNZIFmyHv_fg!_{4&F&$4RHCKdmVx=*kAu|`^HbzBHm;mC z2F|bj@tgmpr3aF;9DmMR89c)RH%=&gWu`-U3F)g@x=6lstB!-^Yty}wd>QuQz3F}F zzl9@8(fC-Qar_nxW3e6R3v8cQqk?Fw{QL+#D3T$Gw4{lV4#U&_cLFj(iII$loG^wH z=T4r56OU)mJhQw9KZ<+As^)l{Nj4%^!<-yAw%88`AVKCg6pJr@r2lqwo2{y+u@PEfQ5Y8>rff=48ayN5(UyDp1nX zSlUpda*Wd)3r42t*(@mG`t@&LmZA+h?0@ac>1%vO1i6iY)`RkF7l~~R35VbEiA9-DHmC^p6{f!4bHMeP<*{)0qy!a+} z!PyYl-Jmg^dSJQ1gk~%hoLvC945e)^MJHr;e-tAf&zz`-k>((C(qx=8aGS?5wI*p6 zh{*n>%5b_54~y5yFQ!V!dnKAWZWbKo&ME8 zS%4hEtr1?_GWmM*u~O-qTQ6|*UQIrS+m$^b>ZVuy^k(}yTO6v@xvHi6<~+G?{nqf@ zV7~Nweiho%KFB3K-On#3bz}WxZxNc{_|Vzt=n@XZV472=exnd0TpUN#`Rx%^at``D z?;c?_# zrr_A+2!g{`wmjIw^8vgrm0Jn&6U4@ zB*I+h9s;Y;PGK;&@3yO(r?qE|CKEn?IBbyYc!mp-Q-HOliq?OC;U^Y69m;a)s12Zx zui9xBa9|E~t{N_yOJASx)NcmP`$g$@{_0!L3~dNmRtI+4I&4v|r&yzviCS;95SSmr z<|nU}+O5UHnFVZrYekyO3=5L?&q3N*a(Plj&H^rg^AF!jM{@xw!<83urBrBkOvikz z9ZFt=lJ$L4b^a3&xP(67L`;2qNUL*#_g8l&W0-P%OvML(lf@WaUF9wUDa}1gPc6sf zwUxEojEn5YOZaat$vs&{_Gw28Z|~igJ=;T}2r-U`g9*ou?TGKluZ>-m8{|5%?ag{< zxO%;+f2=p4re7UmTzI%(V8S4~bF-{!l>=_KupPAHdxbGS+g#{opn_k1afS1fw85&5 zFSULX2f=AwE>*;$IWu%NX(!<2q-({fM~x?4dY9)rl?keyVz-yQE<}A!O{>TH8iP|D zp7l9|c{&1t60tve>q%xmPDac2&cOhXzy-`}KH`o>xc2J7Uizg};20z5O5kh;}{O5FUC$ zZc-A@GmXsEnm1l@6p$wZR?v+*Qriq)NU3g zn#2(fX;lx7yH+b8CfqLQo&)ia{2pG;97gOx^qxldNm|<&9(M5*K>1HebbQhC?N!jD zdp2xj1txr;&q13WDeGh>B%YS*KX8js`b65?B1`q>sY03XAy;iJ3m5xs)-KBLRO2o& zV&}_Bfmv$s!c@gSWBP@ZnU>QCPE~bdD$Aa6Y&CmyOj~*x`wn?_56eZf+66v=oSZfP zDyKr;^evXhjd{WpLh^3}T~4XSWY*XtEpj|Bu-#x^i-Spy2CtVPJ%v@)#1oFnJ5*FB z#+3GS)vN^bBrDo~j8#el%jBnULiW8o0?o-r*I8WO8_qqHd3z~nH03V|W1xQQK$WR~ zU0xg;_4UheGsQ$!$31!mH#(p{!4p@Hp(_9N;wORvdB=|)zW$CnWTOc#-zTNZKJFM< zr=qh(Ct#8X&U0~vVCT|*u*g`q{@)^-OG9Ky-f&Fc9)V7^D!O9lQcegdENt@sSqg=( zwF|XVt0t%B;@hkVOCDH!^H;fGt3h7sAM z&NoqX|8Yu5$x zj5Xap=BRLILX|JSL}ZaGXAPs-77XZ5w>y$wNA|^_BsJde_bR%wu*v-|*}o@^<>L%c zhpL3#@LK+zqLX7y8n^Iw5=(!%_QNGL%7-#6@Qnx&j&Prs2v^y`qrLPfb)}5`ZzGy_ zQUfQiFe3M#PD)e$hYPmMRkI5Mw!zm+{S`=FNn_h1-ji+F$QyskpZ_Rls>au#seA~896?(44Z33(glLK^19mWga^uJ?{#yy5I}Nx)xlDc3KV z;)YNJHO1L0XZRSY^`BB`J?~NuI49YYCYd5ZHC8FnlPM9mvknV9$XAC3oQx_jHSLD_ zmo_|IwHjL1c8?+-~ zYkD*ft3|Z~K9SevJmPJ9hBFq5F*ld~PtvcR*t z7LT|G?}hpJ`f6R<;MOqroIza99jd4Hr<&UdJn{~p3kuiAxeac1jQA+*Ua@AL5Hd);M`U?X(p*@X z^t=h<4VLDDOb?gK#U|f5)qekM=w5~hpTGcBo=RZd*R@|f>~ZrEoKJ2y#~QOND1LV} zQ(9|p+c{%>g+y@}uRU85$P^i5uE^8)S6k{!v*bl8L&Cz67!m1CMS({h@yA$+r!uk8 zL1rg|VMl^$mmxzuvapl5n@9dfz=`tY=u)44Yis{)db%j%Reus8!6et$`8ZH(Z)mG@ zed%F7-J9#4#(#N}UQQegT3vV8U<)%=mM@ppkmHL2SIUzI0xTK7oJ>g1w4$az-ig}w z|M$R!amqGz%(rhB#{naLV7n4S?(w+jEY^hq|hhqXC-1V1d&t>QVc$VYB> z`>DU;v}7v$fk|_=2RSkEDE{GoaKV##M>WP_^`N$Cu@egD^e0_=U%}3>uji((o@@v* zBaY24;7}y_>18)<$2IWuF%K#Uu*N+6JT2*PuPDCd_@w?G11bDOnp|BPJmi1zJ|+v- zbbEpQszdM8y4~Y8k6UDD@ieVBGnyn%z*twCarZOGJ48EZU5YbHh zhwSQ~`wYR|e$Sa%;?F?TC#UwQ5#9P5k+t|ZTwT7ya$-qc_-L`>wr(*`ekh681+I`rW|~(e z+kPt>!A7ADnBUaU$x^NRFEsZyHwTCm0#`3{xqh9tc1y`fkui1WeT>nAByy-BcVSJHuff`gITHCekfq?O<#w|r zwe-<3HvH}Td`tu(GL`|UqShyl{0K7E(Ng2ji3N4nj^|P1vXi3l@RrY~M%r`z6N#qZ znb*ov`$fRdGm(6;IDYDltc%uU)*I1?X zTa!NRZtFdIaOR1_14z_r_Kc4gV_gXUl>w?v2PlDnNe zVfNn<$*fKRzr6Io$O6c#eoS|e{~^t_hfhT&)1kjy+Z1#{uc!e;6C(#hc+-)z_~b>^ zZNbv7{?v%_FI9yQROLrA8l2+FBwQ`Zp=n`KSD0T$fv)-4lKmaHG~fZ1+BBQwOQ?S? zjNXxxc*>>&%P4T4e_#VYZY;?Q8xI3R9dluT%NtXJ-)SB?xCO=nY>d5w`*|bD5$1{e z?NsZQ_d-)Y>gU=f!upx9=#K!{qu0`&RKVXC%_DK7Y-EE=g7ZJ7}Xwxnn)7P zjxen8kqzN0KHV45tOfZ-dq+LIPE~IvBq?8Sg$chn`fKZOUPt7GuOtjHV_AI(ysyvN z8oRKbEq`Ce#sXiBa79h$Mx zOQj8&?AyTuBpj=M^O}FWM`+$$m;Ic)wrgp`)Po;IIE2LYBe7Uln z@H^Wa?Rd9)=BtQPNhKE=dF!XxdAverY{?(lMtdg0#O>d6A6*C&$uPTMx)J-u4)>%l)de`uNUw1`Wi?TNKd16!Op!Vf>m&C(t>ElFNepx(BN4 zczmPoh$$)bRheA)!(aEJEeu1L&niGaFg|l=GI+#iq#7q{pPhQ zS&!U-i}k$p)oRQPd!!6eNI7I=9ghdET*`NR8rQxL`7qKHQR-LK-v`tQ`6V8y_%C!0DxTbfxrKN%GL$wwuVQe}keT>AtK&Q1j$bsDz;jXKW~((~1? zea;F}{^Byc6Hx!FN{cx`>k-{}*dyDLOKtg$~z&3*4}4oigD7fITYNm8bgvp$#xPNJNZqef;R%c6vr=|=jb^cKHP z*$Dkg?I+E<58iPq7~1>4bgtudhP=Ss&y<<_$K*V@w+7>?GJl(yghS8hoHKuZnSs_( zc_ja!=uVlc)`5D~qYJnZKLEzl?NCVu=KXxr#x(QkzH>ag6M!?+=3f7n-j`h_uQTWL z$yhxp>n&fN(P;3VXfm&Aj3Xvxj?gpARMY(Xm2=yDLohZL4==w?{XO{<&p(53ikxk- zf8+T@Hgp0%F<(IZu5Ao0uA`i6dLt94BW^^xc6d7BjNd*xoz5D#<3+) zgh&JWd?Z1~ASHpRI$n!3cp5HuO=GBj8E?;hzU2pA<%|=DzKrEL+A$RDCssV>?P@yl z!zVdP@_({a`b}F9hNlm7BL!6ls2$NkP$#VCkxl~F=pIfzAI0nJ=JccLV(ENu!8Dou zLm?1NnGt*gXu6$<%2=J7*pbPUy7V$-%`TGCx9vWCXz+=YX@?_a;r#cM?FnN|^j)Ku zg~r3vm?=8L?KPH^S5&)%R^Vf#=I=9_9SdK?9u3o9ZUVDSowou={fCyUXqbntqxdsUGJ# zzG>4Kdig3-muSPinZj9IEqLkjrx2bvXJ&M|jl*j#QtAR9`K2;KQy1{uQA$)A2)5x+ zGV80tKAptntDEsmzLhe=^laLFS|z0n&8@B5L)krLtUtuszDv(wKHaU!7ysdH)tSFE zKD+uka#}|N9mf@s5VF1LYC2|Y4|L$AGoSk_&{&g#usXYli(BPbp;!JO`FVgyDflSp zsbQFyt9)A79tNtGNZ#2pPAV%ejS^y=YO^x z|AiDCS=eu+q6oFsLX`48OZB~eGd?PRlvnz0Vz`*9@u><{4pF4bz2%;nmo7Ti@S^vaWcgJ9fy)!e*FQX+sh0BwFfD_QcGa# z4u6u1S?kLcy(NUFKLu=9+wv0&C?i8NeF zOOn@NI1Niucw%Ml68IvVj;tlkUz!#Ht2FfFLulUmqaXeZKHKfJR8sfEhOsPXrY{VC^JQ z=nGW8KhL8h4sDVS*%5F2elN23{TbNThBlk6gz_l0FI>ekEi-4+NZZ9M4oLEYGoL+Y-iQhd^UIdjc!{Y_m!TeLXm1|`F@6{+7a}`H+zvukx^Y`!#sM$|fS_s;Zsm2s+ zjBq^F;2;=nz+dtX%*Z?W8M; zztkT_o1H4+XC6oMLE(1_fpg| z-27bk0l0E-^a9R;S{j5)Y1-jT-q9l2pHdG|o=FgYA%y)0s{?hCsWb8R-dg@KJ0$K| z#jn7v+54Hd*LAcp4e^z#W40?)Ko&Q(1=?_B=5J89jD4H>GLjG(&o>P?NJFr+(df<7 z=sN{K>sK*+8lVxQ+Ei?(NEQ;LoX~vaaoyxaAPxu+DpmeGcKyK?&Qc;IVT?Wqq;Yqw znJF}is4V3(6eP$e#Qe5)gTBkd8 z$oDVc)C+d;ktNt0lUJtnDm01Xy*-+AGS0O!vc4B8;*SCBj~ab*8KJ3lYQbO(u~lBT zVtb_NOp1o-ShL_U#X0#g1ZY1tD^^vlF#?=pmh`z8Bda z7&7wILaKPs4J)&=OlP2)d75fX>i5q5d42G`)4~7doO~aj&Lq?}<9iiWqqK!8H0t^59(`~=qVob1uen~Ennmg3 zN1Ro+ox~4{0Tnx(JI#Fmo?9;Wskac}(rTodgrA#Tu_upr=zz@csSNSEpQ`44;c3wL z=QR-NMZoV%PO^v@>Qfu}c>4Fh|Aw0fen%_2(c(hq?%WxSs^JRsDJkW$>2U{Rv;0rL z_WyY%y%zk+ZT~*U&nTw-p$t(;$@Xk#4r<|KAnHS&1eboyc0RT1x!?#d-DP zV(Vo0-}hfI8xy{uf4`kxj?Lh`r=SzpALi%JEYaV9P#WR)1B}MZwk<13p?BB3{G7mB z5ax*C0|prJNW8|ag9@Mab5dJxY=bGXyZHv4_4SHd&$;4O&ob#tp-W$6pbNdxQ3bmd zViVM3=X1Ex;fD<~m;M!H01F@$-`kuKFi_Oco@1EjnaDY_m)Wn|Cv)GjaELANaGe _r;BpFZW09tqPsj(YAj+eOs)N9{a+v;#Bg55g`Eu+lyzV^@1>(&!KXOOMe^;cLLxGXiw9Hy!qw-2H^O200qk=kO?Om zkew=Tm*U0F-Lnn%;p(@xflBeb+>Qv}vx7w`111?X`3oCbUCmEGDKR2f)LmN&eHk2^ z%uU!=yv8=;%G-pWfQ;?!eHnMnbRLP*RCvxs2JY^7&e%MmzwI#nu;piq27t=u`UYBj7~+77Uq~LI{-s*G9FLEkQ*o{kr!pX&mOvp4<3SrN48ND+#fv$!XvHkmH*A z>Q&6oTvO|noJcp)Rh(MXMnT51)vMOmuhf7Z-+cU4>?)Onxu-J*M2tC2Ug`uIWas=kqym;;laiGLu?yYGgG!OKMLu6KYHKUa5a*d+d~ z?{$4(o#JxkDDYaMArB+Iy?w_t7QZvGd#%}Q8d1v{_wg)K%<5adT+pEUuZts3Wd@^y zu1-cJyp&6NqoGB!`Dx9R7WXOkUEIYH$uA2n_pBEqU`XxBE1bSnT~O!zH#B>%TzoEP zYx442Jq1#YgSQ)XTYtkzrPaT79MaNZLuC$XqIvDf{(>brjBn;THFo=tUPUIKY#Sr> z*N%bSDZTaq4$SGh!nbU0S9_XYz=qx~>iL53uxK0MO;z#_xtI{+`$REcC*TWeowP&T z>2bMX{?2|Csa0OrONsZTXIIQFcam$X^JFw^`)VZ;TDHpXWOxUR@=hPD3cwS=IML+h zf2Wr7_tcot$P7V9*MA8CHg14sPl|IOu$cWu1#m{EuXRC7RjaX?y7rdvm!+HgFhS8z z%t?U;_E-AeG^lm%1gN57_hH6g4%_;3P|IPb%c^d$j-G9Rhyu}q8 z@k;p1R9&R1&D65x{YY4IS&k#53#cd_7@w}#{5N~FeymdN^flJO&drAJX2>un+OQEE z?nd$MTMxo)D$9-)CiKQ09aTT z;O@kc&xzKIVBN~wdNGB3OE4C8L}&v}FcjF^Ba$+Vl#Bv|45kkOUk`xk*v8*TN{x&3 z{pi8#cl`Q2^0Tg+8%MuH&$Z`;q|Fe%znA;%sQmW1V#+;RmAH9=l5VHdEUtw!f-oTQ zBDY&Bq8$3fl;LB8OZiW`Nbpc-Uyqrhcxq-^{QUah`?XgsX|rehqi%Z9ptFg>?7>{m zITvXaEIgh}@nt4D)&-H}t`7c4ePx!olYZl4`sT&T6;XtpV$j4@D?Z3v}18=3>i4JLNrcFq=(r;v6-ar{`@&o;e)cmaL zD*#6Z!ZKA4nUm#{3O@IUBEImhP07}u4~#95d656eVkxC=M`16yzoq<9%jEo(;GCV= z(q-Dyr9<#fuYC>b9@-{)bc{HHP$!ljDDlss{wwTon4$b?34%Xg&&jkAVSqG%(5GQ1ActVsey36ql5PEg?#Z7g-RhicqfV>chWFA$Easi|r1 zQ*2zU=>s58@pTD%7p9|wUsOrgglf#$2u7_`^X>$oYsuQ|+Wg3;Lzuh0nw5xG;7E{wipu#` z{(|_>Z~gzHp@|P#BwFdlHxFtv7g{FVr6$q+knBml)V`dD*n|YgWhfV1xi-6`9_|+Ov zy83$j0#|8x+oVN+t;R6B%Bq2X)*E=k)GxurH1xtWx%EEK3_#GpM63+D#jEWxa_08y zPwj?OAW16xy4S)%hz?8%Z}Y`}zg<(wn(mOmr@<$rEit#iRo>XU3RgR3_11i)?b6Dj zc1YUA)eL5^`P6*oV0#6{wT`92Jb7( z7;T?}=4j18*8@CECT_*yd2smX)8K;jf_8&#T!M7mhQ0k((aILTjJmle6K%bPdW!4x z#ls8bd#S0h6&)-~9gY>@(7ogY|9lCgjZ{y>{EyWHhlXH`QQhe9rGrQ>M~o{b$*`Xp zYxQ;e_!qJF_t0ptxl#%gv6QxPvbrXPQyA6{Xx=f-XIXQJkMg%nZkpz^{nYpU!iQzv z+*&bAn>TB46>{UXU_EItgFTn{jL_OOMIKw=(@@d4S2uhfy=7o^$T#V~7$A$huvApB zpG-J*oT=K`HAe^R_{)|VEg`ABR714X^2??yqA&GKn5()blGK`>Z1dgx$oXAfNhGsr z(ih~-8|-T}(!$+V#FTm1@N+te3)GpJ^^j=%bhFn;PhQ^mtFO^ znaa8PNmQ_a>ajNBqZb42kVb>TynU-`Okqt);%Qdwc493uALkVmrun}$PTmC$@a2Wi z>M>^wSyJMca=hRCx|LCv6g}oyUpf1cy$up_#3J)HG1-{0y??$JE+eE7OmZ&en;xT;MTpFX5rMX6|HNSubS_q3cHp4qx_`X6xTbvTY=- zdN_3TOdq&FXv+OsR$Va8{F1aGL(^TzrpG)>QGVDOo{M>bV*Pd&VMRGS4X$WJFKG~W z@Eq}gPLgzoE@*=%q)DsMGG~Jlf6&jE5;{Xr#El)MM^CN?H^Psb31_43Ht&0*U=qWu z0X&u4xvyoLZDTSV&#DpgD!-1#%1IDhM-ZdK_xd>|q%@5s%3UE0<)FVtD=c!xV(Heo zUwhOEhi$iXSD-lSq~ZAw)Yr$I*G#KuJ3twP1LgR4z%CR@ATWrQpL)~DthZ(Z0Vslp`)PWB(w=z66jENn4tmaNIU z&kCZN?qUBjJId*oP;)N$oMrch{hA@aNc;4-tGzTW(n6VavAeeF&6l#pG=e+?NJ=s( zznWUqKqjP`fXxm3NQi#fc=z;k7pPAa5@OU;PhvYqN9#OBMeX^T`KlpA|GoV)MOyd* z(a^5SaR#pBeJ_s^_wQn}j35m4Ga5@GTrtmUsz5hKbN1Ig0&M7d!)Dy!;Q{J&y`gsg z2*F`tvWjD?D)DJ-Xw$f|W6#bmnw%yeAnaQ&(Gd2)>Omd0A3HP=x_Cf1kHEaI|Jh7x zWhSbWyubBb!{9p@9XmHUr#vafP{SG1i6SU0@|p`i_|I1W36SpLaniTmUZE%@ezNOX zAh25Er^FkiG*xn=fzoMeJIybgf_X-_x~IJZ{YU2Z>VtDjQSN?;u=~kPD?KrwRH|^# zIxy?|ozZPUPgPovu>oHALs5I^kE+_ANBch%!aRcyCb6rB10e}oq@oX77GB8r9+qJq zuRu79>wVh6tOu2~2OKpv!I3#2+*rBj&eyRVtOO$TC}V{AIHP>Sw5wNR`T3Ug5))YB zASoJCX0*fqxEIsh@YxY6hsdh6Fa$%_xbge4K#U>R$k?tyNd3%p(6mGpvF8BUDS9vw zeRP_WLq*L-;O(2rj?TrqLKzvL)b?_Iw^6D)o&-MD?y3XV`PUa?=^WOIYv;#~93br5 z8-C0{BB^4z2q_YHVFd|hNLldX}QjX>^gheZx=}X>TKv-8oHVy=3Je_-f)GgOU zz?!E3$;5P}Zeh~3T|$^rD^a=+=aQ-vs=~@0z4u1B=y3~P_i1rhKdr`30Xu)hLxSz_ z<=HrTJ`!qXuzHX~08xsqA>*O%CsbYO0Mq8roSzsAdfqR%CP-|F=(A~=PIC>d3s|av zc~vSvJ9KMLV~%rop1ATx_61Q7SPie}qzuE^TQxeTRuNju zu@QIsPq}iWUYXf?YfCncoT!$8>v%7>ut%HGRH?e*rxp8Yt?=+W&1D^pa^#F9TZRc|b_F3J+#8uwHV`Jg= zeEw&&9~Skr2xH4Svn+{hlDIwG9WhJgC~>Qot-S;Ng&V(NtH4nTue=|#Awqwyt)}W% z8fChKwKH~RV@jt>Qi`p%G`d<%fnPkt>UEL|gOKJ-Qr8gWXwA3OLA= zop6yxxH9$Jw3p^G%S*DtChtz}ViTZhRf_K|TDsdp?}q^*qlH;)_awJ=$`}KoW&48w z?&{|j$&|H+F>GkRfbC!W_&%5d`Ua+{fF2T3&6gKiXY#xWB)vUC~F+8xN5;Q1O$0F4h4HLF3XqL5bL9n_qhO9@k-WbF9LJ6TX{WvU~R745p1fB)7av5KL^2-zhxh<(_Rq0S>+TC*G}n!vgXH2W`z5Pfb?bZukwp%OBjCV+-xQ z9Gx3t7dueH&ufqY{p?3U4kIIDUtMJ0yFP%7RSRS|Lo_NtqVwq+l^~@D;cvHQ11#hb zfuQ)OsFKI(bE|AgFMTvSf?fVM1t)O$@a=0HVf&sjYLzw&O6}uEzD#LSW5C1G#JS%h3obcA7Tq>@$G_c~ofC4V}R&Ge`Y_+ds{W zfo|u55J7UwvvUHMfh#j(vpRvmhAPJ=%g*2+`CbGpoOwpRu;kY9;$rBk6l;TFZD=B_ zegGkQ>sH^J4-2mxCfo^6ZN!`Nn7o70^2rasT|s!b(xztSC8Sp?$~e5B!mC7?tG5-} zQ;&%4JWtDu-8tZAPYM+UxoS1ke=hHiy~3$K5cZk+!)3SY;u6}8sB+0K7eftY>&(() zqv|MxoLB|bZz6xNY6rXl+MKiOGild!1WX+s(oAT+*-RULkYpdQmp=h!gQ}wsw*1J@yGL78x|f% zulw*L`Jjl4RjJ%7MU_cq2rcIsq4QF2Tl6ZtKFQ_zVIMIoOtlZpt0l%*B--^wHx%;Z zgdY>BCc3_cqZ?Uu-(cFoo3>w7bcH) z8@UEQ864bj3}TN>8M9u-&p$e7fXpQK)sy}6rg13G_wn0^t_1)^`-Z>ucmk3F81_;M z{Q9#%=4u&IaKdPCBIm^Oeq`W8FV$l&g^T|jLBPf>tI*{L-+@TmVgHbrvPK0m--Bn+ zJ!V&u{Wx21QqK_sKM)iJ+f^g{Jr|5tJv^vS-UPC+s-L88`mo z!fuL{iS5fIpet~0*?UYzrEbq|Rn483p2Ct#YT!%i!ikE)A3W112>a4aXCaInaigQ7 z3t-!};R-LS{TQ`VTD;_51mWR}eW_VrFFI?Mi9PDlFuK7aF^bDIUW@1IL%HZ@IW!&u zgi&nt`kcfy<20oCd3@Qec6TlK&mg+e7Z$TgZ@!qyP4h&z%0+HIuHItgR|Ws^xjBI| zid1+*P{V+{$2B{n#v|zt-EKY&!k!VGalb=r-P4ut2FvenMq?iz@j2OjjWuXzdYLEN zqNMl?qlhgkuVdh8DtalyUU6kCWjO5ttZU*IZ^W0~*ULQFt!J=wWB}otn6HhZ;=!i zf6u-iU&E{dFWvuP>@CCM%9?iJ7#7^!g9O*$O>hVnLa^Y$gS$)6#zTT@kO0BmokoI& z;0}#DjWy1<$;>=6@0{oT&gq|hUEOQ%s#;x0^2#-ypgc{`sS2ajme^hI zGt+f+O`ajcrc!jX_#H*b`A;a>;;n=IXMNn6vPI@n2U9F);@YNp-5ftXk?@~;Q6^DA z;&i%hVl0lY`9^7>q_#7cMH6|9t5t&?Jetu&)Ar&+XKg&b7D0e zI8rwU-7Q(Nq-W+uUEx6w0KKL+nrrDf>I*h0Z~cD)g@C8Su?e52D^8og+;~y$G>3Jp z8QG*Mw%$TopAlZhWmPB|g9vv!eSe&KS@cehm+*B@T%492i&WNJo@P4R@Uu=HGVN8* zPRhQ;OyqE&_AyDD2hAiO*i<{(cniA|JH~g^N~~Sh&X(YpPzXZu0}oVGRA_uenETI` zckT(qKQX;RB9*oDKW|QNpLn2z&=0=Rkji%SA$g zW`E+k-o++QB8Mg+gQt8NWitID7ms3WQG|(kGDiPTo}!5M?w>sBfAi`<#SUr4NKAO= zajQ-BY2dCJV5qfEU>!F{uyh1IU}O3}Ti(c*F=aL-JXm?_*Mvry59>eMm^$h`2pk!U`~r#>%pO7&bqw4n zoY-C4_&ye|#qxZezjBV*M(wGn@2^kE+Wx`&&sm5)?C6cZrN&UL;5WBRH zWru0~L(0#s#ckaYR!2>~95X`WYg@B$dun=hJ#&?=rG0w&QxB zCgpg3T6m-HHlh}fB?0BgNCI-Ww^ZtTwFgCbf~Xa=x6s0zgmd+-B{FUezR388u>$T& zb!ur&+9{f}7!;$E>JzxG$~j^C>4(+A<9ee%>SMdo1TH0*;T-`Rdkv$#vhq@Ix&`La zt2wnS>+LG#RU4hCB;VZSxQ#SRT6Az7Z_vS1{`Ch#mclb>lRr(DEj@l^F%7FDMQiXLN z)xx`H1%XIanSxs%BYX{xU&@XfC9aoJOU?AqqE4YXt`(6h1m!i{bHwGi;M zB^+~h!vrB_zAzhVWJuDo@sq!Rro3A#Krm~PoN&J^!lydJSzcLmj&28N2Yu!}$X%Xy5Bl;5!nf|`R zk0iB4CQ+=d+?#suURu_;E0`_*q98IeE9daR=ByCB8j`ZxQ(a(I+Qj+E7@O7{Z@nU+ zaRn!JzV>msw1%fc4s5+yPG11M18Z0HV2MJdCI9im!W7%%Qc-gM=EU8?%(4XSi(nKtB%;hr@l`@c=*l{VgF*(+jG5%22Bp7Hg@O6Q^CDf7bjH}CFR z^HHP0SOI6fy^LKiYNCMn{oFd`Baoq|eo3QR=$I$HRSA_;TtJxwQ_siuR~wbq2SQ5IQ5jx!}p1F7qu*mzHb4_axWAqe(K7e8;w5^8T%lfliujjqQ)HjxutTD8qu7Oei4$1g%W1mq zJa(#+}^4^Do4 zz#siKq72Pw>gD#@=khLST38vgl8$%wBokGu7_`fQD-HXM$AaWou4gA zuQmJ2S)>P9oImR5TB}~P9oXG7Yh5f{{BnM?2bbqXuEvFA9129TEpJ>^v)mG;Sjv}( zN>lpR`7`k>W?S1o*-ZnCiANVOrXLjt+l$=pX#ge+I(B}q?YbmUXRj=2nb}$S z03eXyhPqXiTDed0o&NbsA`nI++w)k9U6WWZJQkofR<<|58V)Y5;5Ehv*p&OO62h6I zf{XJIjBsm{9d>o;CEq4r0?Jl*5+m0y9~n;1^;=82zr8xupU}C+vETk>EliTs(R3J| zY3M>0eNY3nDk$djs{K^*fG=JX`9*MpX53zX(`&a`k5uWP@62G0-C7CreTHf9VB-Nm zgtgJ-6SnB!`5ke{~)4Yhmt^4T}_$w8CJ+oI#Ox5DK(hW_#M4vl6s2XN22L zf-~3;&Dp6mXAP4yujBPS7$hfrs{~lb92k3)Oz;&Bc8US3FwXZ939LBn zP-b|y0agnVyzt9%_P5~=*5%0j&Zvk4Eg^_BdENxUjV8!%EnD5tN}6#Rdo-p)u+UR! z!IRXt(7Gcy;s{#uuR+^V=106fTK#0&{!5N5OSY0#Y)IC}(XT0R{6^vNcC9Q#KHuQw z&zKv<*Yv1m8@SL%S3W$cTsxh!<=HdKTXZ>)yS_cUa_(S7A!Hcr%wa37B!ujQ+sU7& zS8p8MyC&ZoSQEC^Y>3oZ`)eX?rS>(8xYwGar@0R4M2(BwT3U^hA@zf28^Nl!;p4nk z*#ixiA4;POhg{88QnkxR)VnULZ+(}eB_u|HfjuN3w8rWIjKR0~reXWp(61-MRzj`O ztRYck_%l1f8bN0wwAWdk9d0!shAz^(OBLRQl;>?B@(hNX{cWN*`D96QzXHBR$gmVp z5XV!#Oo)$%@bM_*x-YkPpJ|h5cOShr5&e25**-|;=$SA(&^w{1GgE*3_Agst4sh$P zJ)}1!T<55*!htldIw4Q4h~QnlT6?52W5hsIP6+WN_&RFdv4n7;!>(;xP1PcX24$7# zf0EJ7n&~ZZM6GY!q`ZJNB!~H`Gsjsa->VLwi#3TGf)3R5t*`rBw6KP2XWw!BN|rH9 z{vgJrT{CJnUL))O^e3nhykFqpG%&bwDM$Kc7%{CwF(5M2VwWQ0)2Thse@IKtI!Z5` zuPA2hRHVQ(Bx-zYw5A;_7}IUI76{ahb1oZ}AJh)OXrNrG}?m6gxYG&7cy&#k5FAn=WZ> z{GG|myDk#NiC4(eY(NeuPS*2LtO>8KztgSS`K;_kGfP!hflYM6>+}mUwlsmTyW=13 zO?+S25f2ZP)%1Y58p5+q#5q5)-*Q_=SS66!WmwpYLT$h@X0+MGUu<)dhA1S)jqIGFeo% z^@+H4X6*88kyl37m&qUQ2tO3kU-a{ovad*{Emj42f9T?Cj_RD6RN3vrL#@wJ6zoeH2fORqcZ>kqNwVuPE}C~j3@#<&3wv{il}|rg?B{G)WPEJc z9F*cS3wUsEi@dJgPf#~y$8A0ZL%V9G`oga~XV;ZQi+C=NYuVjWYV~#eC@i4T0$%;Pj zM={SRjL>Sg`yocg8AUIBo|0BBEWl8-D9wbRB32=ylKKfJRR2gfr}_uWEgmMswP-rJ93fp+7~t>qQd)jZ;QAy1mUrV45CIAlm9n!OL#5!Mzj`sl{qlWc*qL z>azhg`}E{BM;aZtYvE_stD}cik|sC7{%$p7(OJy%M5(ESJ`@UH?`~-o64A_=KT%E& z2sLAf(2lNO*a8;$4KRxZ^6T(}U@-@lwn^$4-m-U=7>S z_?CBfQlZVDf?-#Nv;K8~+DY{_kF`_VQr%aA%EwPyYy_19WfCP^ga1kN{Q<)50LL5z z!&T+B#8{@=t%GGEaZXfVwvsPmUZ1yBxO!l&@GUZTw0ge@#{?jh#A-*Gz#B_{HeBGj zn_g=7N=kwqJ)Pu>iEO;L+PB8)gdwOB@f=t0KOgp2F}&Xn8Mwk6Zx_D!e27{cLczAL zclZ=p%D@eKCT|+_#a#CFcU8uN0ys{QZ7)Fz07{(uL|Wd*b=oAdD9DKzlW)~ zl;Ft`(G2l9JUAz`{X)_$tmimz$Z{WLIHpOY<1sH;p}nYl53stwT~i&gVz)1R;ZsL z-q)Y(_URGLSZ)2;wIS6kr-D*DX!+KNuq};2+hX>4$tb0Fw&_?AajlMF&05)3;q$lO zY7O+PHmp~-;`YjC@0`!R6b+ir!;4!(3RmFsgJ9rx8Jb!-%LD7-YQqHBr^$0j{~S(aITIUwH_BL!rMA^Wy4q<*Iw7|3FXM z8U2cik&XEIw=^K#{mQAbSLzNYz-%VGe)LFc*Rn>5qTALKc^D|mf*pbR$HuyyPx@~o z_OC>U$TRTnO!}=v^wlf0fG)gGBBm++=aJy2f`NSdWG~-LIr5)FL1aQzl%dN=Dls=S z4IeVv@4QB@gMrI~Zy(~(?S_OGHghb9Q-%%18y~_sOIjG+~JO;)}rtIrKy+p5As2a|)_^4gyE4h^M}~vigYw*x|+exMFoxr70}*4VUU=9>;^`+pq};(MFm6n#mF1B9nGj7J2`EJ>0Yl_F&)0j}9*blV z?8*zqw{o>a)S$GPiv-j7E$hS z=CrvEMs0mo7@GxN0QrJ&EFJ0=@nApxq=r^4g_24c#3Pf;MBMnSA#oK+xBkG zD`tkL#;aw@xOkKsRgSQ(r1@??(>+ZM2ZNb6Tgj-#TptPSk-8-n%FJVzPps=<^cmj%JBzZ7<;h5b*5Y z>3`I7KexSc-z*Zy&-a^#!-Jr*xw>cxERF@AkDXpS?1r3wmL6NRIgNk<4ZUuJd*4*< zrzqM}ses85;=P)yTcyrN0^;D~WnTiIhuEItx5>T@aQk6Hl-w=K_@sD`{P65p!)i1P z6H52JJ}9iBaJC@yja!r;26ajVfP`oNM;CQ534~k7yF&e6%?DAaz2weh;xGQwfAD8x z{p@eE`ri-(x`}iY2^_2O`^18kM-DRNA)geb@7rTOXCM4E6ad_Y^~XlrDJREVB}G}e zxG!%~Jg3qpqU`9s_pJgEr+QL`IkiO*qVM}>4(+@!q*sKyDQch!sgxSg&0N4)2XSE3 zX!OJuZ$z*qz07HGm9Amg%GJ{|NBh947ZptE(vx}=yzhCY8_Q8 zZFI^qpzWC!VU=6gxpz=j!eN3~$Acef^xOpEUbgqY)K(L2J6_5YYXMIGqpE9e`A>}y zB6fKpapsX)o&X_UcUiFfOL0@qU)d*2dvuWY7XskBOYV}uPvi#wo{a#G4`}@Y3`3A` z(brHI<)Y|-30uwvI!_URp(L436ZZ`BG32X(+Ku)!R?FwKZX=@`6v|6fh<%y_2 zOPB5P-y#F8XaSR&{!+8@pJx8w?$1K`35*ow&l;EvFBlH+J)<-wCMfznyaoe2LEVx& zH#qFd?L5EyWNV7PMesZsnj<4nl*kWV@7{rfA>U`eX!1m4H(rocWZ#%%f~aTjPvZg1 zXz{OR{K4PN_`tC$KG}Z_(B=cx2bwvqq@U?K)WN$exzBlu2r4X}8wm`Z57IkDvAu6O zcm&Ncw1{{AA43g@CF-*%(};k1R4@qqYbe2D5{Ns+sLibpG+qB1;BX0W8}{li<5uZ2 zG!x9pwBU)2A@8B9!r@|^3mdm3yL?3Y|rr&YHg>YRXgJ$?n3bI zaf16d;QoX82LAX$vS`g+R!%hqtR_#qvr^YH0gY(~hp3gW*Dgp@zII?jLV?+XmHNb- zC(+_BEwQr9*{Lz4#hMCm_e+8Fm41k@t<=` zv-gq7eUg#Vrw70aaOgccX3vHp&ydlzA?Xn0!v1uMxsy^R}9o_YEqBUrb-AI znE1QlXC6$S56!}?8%OkZhR^I4{(2hA-=iOY1WWwxSa;~{t&4YDE!79ew;qesUdMhH ztml27?XIKY!_XBzWusjiuEQLPyPe7Ld=~VCy;fEKbF4s}eQ61o*eS}jvw73fQ!~%M zTd#lH$Zt}R3~1uH`><%}b>oNH;=aUt-E$xu(CT&hsSZRvg!*3?pK?x*>02Q>FoR_1 zUvov~|9n_`tl$^QaRfRp=ns( zGq+QS1zYC&^OC9;DQ&KWJq60~?fL_cre>UmPm)a}=>aPY>J!n`@tv&Ch)e9vUQci% zjrP)!dD@sQ=XuYxnw5PioYA@E;n>bPf>9W^8j*?CeeL>;z{z^Pnx2HgDjGTuYL|@b z7zme4?DakWC6Y*a*?0E?bDvJX0{#}tuS5pnwz6`w%}C(*)%4#}qIjj{{6;*Gd>z%T z0f>z_qAnHM{ARtHYmRGhNulg$O5c}W0WFo14e0`?AWdA_o=+IH@kEL1SMObOY203* zjSZmeu|;bU2i(|kmLv^`UGwQ5bu5yd)%;vZxs4>^jQrZGf7$l3+a@E<8oN0$vf)BX zN3MDo?nVD$BO+h<@a_Kbqgm6A=27fE&XqliMfZ_EXBKwx@57#&r++XJM^p>_JMIIn z!mNk~m3!;HYYY1V5se9t>^hDE-|I_Z9cV|3eL)GArX@i8EX9JdQ<78RH;sNr*Yvd7 z6_JxiB;wTVYS3X*h4{u*--kQ2`(?`}~xygRJ zX&^aC&V5ZrwWz-XQ#-TA`P)|SHBu~oAD`N%@`GK+DIvf19wTGo`Dl6AP^Nntih6nJ zi`(>}ZWehU-W@9*lOEk&T_JtKZKU*t?e6H113q6X94P30Q0=g9o1YOSbLpH^j$O_j z@oU^rhVmZB3Lkt4`rNaF2GwV2!~cTfZ2w{Q!la~0|8cP57yd1FpXeRIJm*tEkW=fE@4D5 zRl(a{mECS2>2>LYys;LA50llVeeZU64o^L?;oF#B*YC^@hGu3a2%J>z0=*sClthL> ze2pK}#@|&Z3rwb3))@Y(%{=f#%ft+PDT_?~1X&JQtNrZ{ zSDrw{2o_Re<45a_ZW&V~5)z%Cv)3z$K3s~tQN7z`lb3E6LOHA70AI6GK-)7HK{^5b z^^qETtl<8Fm&S(NeN6I)zTnH4v_zqbmAz5=>*Tk7q6~9lJHIrLp}v#>`W`M+S_!bP z4NpbCEs9O7L{G(CKY`Wmf8A6<_xXHvZT$hgAP>{7YAVS0P2ceOz&mdN>`=;#eQ2*~ zh_@@2Xoj$knus!93wFPq)r!=thzW5k{=AG%6H(=D(b}Sp@IEd!HnvKg0hY34W(QR| zm~9vx9Xh@<)_ZxnHUu$ResBQDnDmJyo6aKPDW6m)7p zSUDQ5nLxHVaH_Xoei#2MG*oTGjBq!^lw|;ohIV47z4)PMrL=yN6sWz27UdXIc&BYx zc;BRHUN~@mt)qRPL{Q##NAq^DYXt;6KXwo*B1{sG2@PuUShX_m1T89i>g}HDfz|hX z_lt+Pd+sDA9?!D#_XN@>Oe|utgB~Knosf?+~*i*fX`F9-_JS;9I5i~dhXgE z+GN-y=r<`pKhza)5^)8x$%7{){F71)7SOecAi*~WK6gilnX3ZC2@|#GAS!d z>=j09F3fedj6X~m4ND8c=1CiHRd4L^PRLXhL z<3s8;LZ$XA)#_LrH(M;l4J||(K$=w6t>P*J2`|nToihuv9(_|oXB;}`n&&zuqF1F; zaNcukHV^2;(8*`t7ae`|d@U4Ic4`-J@1#O3|g=7WPq(${a^`hVUgc)M2r*sH-E z4Fl>u!;n=9wIqj!f`Ly&H&yzkDc0TOn3#KhkRq1vw|%g$UdRlLoc~3L;j1&YNkT{l zMKZl|k|||rr!Cam3pY1SX@CdlQ3yUe0cZK_%6e`xPmGpGFP@&{tbrr34m`A3&@LrbWiH_9Q-3_57 z7!f*3D8Ek0aPUYZZ_pxw;0DoUM_u{G-`S3yW-mnD>wS_Kc&Zc$^4O4>`hfG7h6O5w zs~$Q$frMiZ8}+tbgbm^Q15~*+x@D}VnVZI%B$;UJgE;Ex>)uJZwBS-A!N98N5Gxp{ zl`!QOb$7C3Op^DzSN_Pi)w4fSDP|*TKL>_-i0r8w80gc2MIAvZ><=yC_S|5Gq!<^K zLn2K4EsYsA6!#nFNB&BS;L^7pv1amnHI#4zU-E?rGGg+Qi+M2>G+wC`M{rX_=4UAl z>wema`Hdkz977KM|JY`(Q{*uVfqaeci_X+@m@q%!R~))&`8#J( zsHD3OJ%wh=|7@ij=2j1vg@N|ja#L|8uEMUbiLyq5TelX(u9Nl%vNZO4q?MPqw#FRGa)Z5=q`pdxku!={Je7#lOkYHvt zDSpW)%sFWmRg=9Yr`G0-*ee|wocgBd6mz8#a`xCq{o995XAfiw=X(#eT%Blt1AsI+ zeEcE1F6Q>0EMN{!X#mDG2;AG`7BaMu!&tXFJHlKtz6CP0fc45~bqmJoc97ggo^jc& z`$=DPyG#f%DJ|TtAs)uxBcdShgt#x9(Y5~ZF(npxmMct z1N+OPBc~tE4dNSBd9pJgNJV#-mw7)3>3>-0j*A))Df??TptVJ+(Eq%0CQ@3`gIsUO zy5oI9|JelRc)aJj7dD;ld7a_M`8EI15ABz89LY!LOB1BwV!|q#T-$Y}7;al0uWe-j z_|Mvj5EC95%*5!EPz0`~d@y=QQ8ZgPC0u2=FE#-?+0E0eY?3Zl_FiG@4P_koze>Vn z@6>1h%d?FpF)vWMwXEj$zyu>LpyNvq0Mh2kw!aFG zT_eF_%E3xak%7k5cn~^>1>zwE01mh+iH2Cpc<~W}`-PA7np@;O!R{%z+Nx>!EWP65 z;!l5tZ~s*P5@gG}jl_UEFN7n_e3z?o#+%5&vUufX|1(Wams6ZTm z;P80gJ(8~B>*5i! zzP}>p%XZ=(khc(ZnTiczAjq^frXVFCE%Mws=di=ng6XZHAea+hMvxb(>TQHf&FZx4 z)S|RLU$e;Hz47C9*URh2=)aVYm9b|aFp%P{%I!X-K|k3hXb=TYK=vG^g!B8Pd~I zt>MMDH}#tv=;Jf(ctI0O4n7aG>oa*g%mY&2pR%ZBsOuHl&|&o~ z&m`Mt;L(;YD$pZ=JW7Q@PC23y`|%vYV~n>-vtUMi??q?+V~9>qV6GhRGh{5o+|oj2 z!d+NS)TwLj(L7UzV^&lb>rfqNo^k`xZ^vhq9&hKiLiu-uFTPm|%S|%kLTAgL6=#WT z^;g+)d6v;O%j}gxo40L_Y+GeUEDM!i-+t5KWfiMxjgon;-)*M6v+z_9EIQxv%7uME z2AA@eD8fLsV?EiRO{NSQ~w^;1R-d_n*)1=}S5yB!Z2nV{jb-!Qa zsGp02*Ynlz5`UQDIOw#b#A)nA*(je*COcyD=|47|?)!ZsJ%=Ud?-@vO>#dMXuGEOh zDs5Mo*o?SYvRf%eZmY!hsG{uX>A9Z!fws?wckS|;QdfEsjj0$=W-tHa%7F95PxCJ# zrg^zBtko0Y7)!CauP^viT%@slmxoD(PhG|sUSjDBJS!a~9`nRx_;US#@*DXHLD-Su z)Z4W@|MT7_#xI0>PG%nNdZH3-G0OOR{8pKd4se23ylP;E?+t4{RZB$)inqk*1_qQ8 zj`rSAqhq_>cX=>R(d3=OhhQ#6UA|bz5yWA9G_(cL`8CA{LI3k7iNSD#dA+XnOms)g zil{s@(XQk*-mu@uULy1A9bz{(C9Z|$_GkQfR7=sYv(*40 zy+Km{MmHCI^~OgKJS=n9j@`}VEBsuP8GM&({vhNG(_Ah*c5)AXZmCo{r>~5qA0`%p z&i?fJWTPX4&uv=^^`hj#1HT7yl45W7@|FFl^y*hc+Q?z`!)0u~-91VAeagTy!()MhIa097aGV8Elt5)RH-lwl@V z#D0H!(bLQCWt5~!Na`{aUwPOc+uO^@_AyC#J7f5qzd&@hwQe!WjZRdGOxhPsJYyB* z-D|`X)^RYjZNS$mkEx6M6f)t8q$hS=nom^Q@nSJD3lAHyC(~nqfCGG2sgf} zwSsgZzI&BQVU_U&CTFf6Uv^vZ$X>->wznx#J8!1$PmFK``nOKvY3W#R!dE3P=IF>- zDXl<8kOSoxWYO4^anX6qY?=$MSB<@zLl5DVD~(w_iyD?k0s>Ktb@87!u4LMb53&N=(l_35UL$;hVwc9?klM^?8|x zUq!*?m#^inc2AFI*^0F92tn3@c;20(uaaD`D5>?cF$Y<`t{K9AG&v9Mz4D~wBXi^B z4Da0}%Ait}mv#ZsZ%n|{_g4m>Y<-2K%6*U{l6}{un=l06^0a(wk0ox=5fc?4RLErq zYd~m0B82r)0%oAYzI;@NW;T-U!pAPnV|&0LF%VLOz5kC3T`;)*q!YRSoo<&*aLHk& zytNv5+q{i?e3L}EDk+8+i`%Un+Zr1EzbTTEtvL6gBXg>;mp7ON9rq$!-4cM%`;Ws6 z@x3o!0&u>%!{vW|l8(47Z$>0icJ&j)aaZJ7DtfGZa;1Z=26uC9+ z{z|qKgSt7ujnheScq!#eMPSaX=JcKNvuBIy;+N%cW^;4*)zRSik3XXG>qfseDaCHU zZ8TU)ug2?+1RPN1%dIY4GcH2e!tawET`{k>w~rM{wCtUn0%<3o6|7lcUtn_H6sT~= z@@@MI6A_}UjQ`HeY@~9t*4J1F_*}|u9XqzBdb)QXkT~KtZ?8hPk^_rdpj7KIQYK$3 z%$}lZBb8|nYG#ne?mVmO9~W!#AhyW&KE>`&@tmp+^XvGNG4NHtvhV#s0-4K2a7jRY z(T~`j!mR|4mc{%#&S|E&4c3Rg!8+A`;&Z;nC%I74c8t%QyHmQefdY8cU_CS(?&BB_ zA%ybKT6>|^hozHNZoZ=N?QgrZL_NUw8*H+Lh>xP{7{oc!SP z=#0;q#N?b&3y$8aWu(a>{ZoqX?abWp8$(?9$<=qQ2aL`Qv1x7#N(fuVW;su|>|EcI zo35=tB4yDIPjvG@`T;rxTr=3;H4ZSbq6i|>+!Ds;IQ-Jz?Z8A5@6P%j*gT;E; z9fi_Knh=;~|I63?oxHfj1)R5U4a+v9l>~0n44gfIy(fbwHx~EpY);jE1@0QSxqIpL zo-0?>HoKw*xWl<2Q;K+T-qFlk=D>1Ztf{ss%Yh~NPH`*2ZI~*(1m{mL{kCf0UHNs^ zQ@N3SDi9NX?j&iMuens#?>9PakVR(C{|$8J2n*s}KdEyDLB^I7Dy@@=dHIx$p95+0 zaEEZ&$mF2QM(>eRmIBVTLyHsRbcl*aMtq#u_FK6-*QUDWe&UX;=N(d)If4ET?Bpi{ zJcMunv)45PK%Y>9mj>oS-x4?c&8G!FQwR(fE~;)>qn2J^UDj|-tgD0D_VzKq(c`Oi zHJm?d{gn9G1Lpcg$#B7a#$#pgysWl9)h?%~0I>xY8O|+pa>{jh*(2((JdbU) z?#U%)<(Lw!-|D>9vw9UmWH}^ddS>w)0ye7Z>ZBh0=@={vDp~FU^fLDBd9<$f)vAg%9*ySlz zg4@-4pmD~@*%(~VV7g}b9ctsEJeAJ?CWP=GzSo1j3tUk>*6#bs!`=g&^IL%!?s-=b z!BA==Q@pE0Zc1<#d#HCJaq>eINP7R!HgIyRloQvla3+=cpMHH6!p|6u!Y#SC{dJ;a ztI#4871D+NBDHuPAM^^FBzrdvk?R!hZOrpE=lIK@8(y}YRi9Os;YlpG&u*#2p>c%5i$2CGjr;YFHD@>1U>z-$8C(y zy?nB`|F6Ub>7n#zDhn;Q2;)B~ot}^IWs7D$=nfh5E)r!wZA7~h^IAHiebsPi-}$JqA0cnt|Fqta zXU*acSppC1@0{?zlV>1A6<8=(rQgT@+l^T4`8{M`UydL*4LhZ~gG-+OH{t)2C2XSH ziZ0&-;L5`z$DVL?a@bw`^mstRNfBVQp349K(OgE@X^9baxV15#b%WPOocTpXrNe$B zL}|ux;PysR$Y%0iAy8h^t?w4en}3ra0L4_wE8T><-o?qY(QWa6X2Ji<8)I<(sEt{Q ziH+DacTO9PX!t*<9q`^g`{1PeeOQk8FRlZKv`1vwip12Msib6&3QDVKX=(1CFCAbx zNXIb%(7}$Oc(;)Oppgy6pEyV+tmOwmC-R@wjyI66vOC|%Y%7sQisIU~eHLjx#e`JT2_*~7YDb1T#N z_Qzig)IB*2Sy~osaCke)onbJ|8TIP_?N`cTD-r08Us!v=j>_S35^IIL`~*VoGQ!wf zJ%4%)z5??5V46mxYDn~d$OJ5F+qTq49YuCEH9hA)z3%VriO4EY4su2)CZAS&@y~2E zA|}kgac^^TOU9_AA%bz>FB6IU5OVKpWigA7m4Vm$BNf&aYocKywvbZGw=sWdqD6s| z#k*Z+vDYh3N5}fu2=|MutP=m9Wtd5_Tb;ajwv!x$c@<7=F~-Zf7Xp7o{i&&>x-H-MuQoJF#j z|4Og^yP5%B{D{)PVK6K^o0hDeUWHXTyw?}Jr4gB&*)$Qw68oQ=VAByGe`<(r8{Lz3T zPEs!O;s|_mZ&Vep8)bpCatcT*9htc>Syw2_n=C$#1Lcu^jN?l(y=ns5{hTOuEXsh- zc^PFjo)1EHklC(4FE)B$Wn(u2O8#!_o~5rhZEUABCD0ZMOBp3#Tf9 z>dahwUi($=k#O5&shjbbXwSelg_Q&4E|QQYiH-P?+=crUW1mg-`7$!drf+A^9z1`l zO!R?Ws^dqK)7n49NQr%jhmTLo^s-k%Qc_>}okws*sk$=!FmQ3&9YMCgk2BD`9~WAX zu%XO&DMsj+zQ_2l?JM=O(GTf6)Or3pl~327`>>+_Q_`0Zk^YB>Au`^5k5qowoi1=H zs;MM3$c-qeAV!|AlHHC?E~p z%BmOSLM>CRuv5}#WkfV?&)Mr!@g&dl!liIf#49o|t({4-a9zZK?7gpki6g(i!}vy- zrmh}!E9%7K9XW078#-hsLZ9wV_o;d3=v^fKG66&K;x5})7gCo*AssE%E0-k_yH@)h z+c>AGiN3M8Wy&Y`|Jbg*l=ihIUxiph!J9j{S07G!qF)CsrekR|3;T+XV>Toj)MkJE zpSYwf)nA0)&tC8eZw~{S{l?Iv`QcTF`m@Wi$%UApnejSWJ@Sek+MQ($A&r&_~Ytn;(Gof!|y8Tx^3c#U zynD1)*oF0M`*55w%lh=_8pT9(^S%Fcl>fv9!#!aucCG*BKG6p-LR!)kS9+qzZraLD z)`g6s!C)~}(kSB&Qa536;l6f z5sZE4#_`aG@k)#kcfbYjPsm4u-X;H$EjQ5!5^qVx{TrFrvZ$NBVl0eHrZHaK zzss}-=*;)Ys7BS$5Y$CMm2so8um{sEVVNQD*OTW@Mqd$mKO3f<47KQpz~MN^DwUo0 zT|C3nqjtTK>(!0YARwo%?RFY1Q%f2Ky>o2vz9LeH@$o+oo8&*-3J~)?sbaM@ymV(z zKXtPdOP@H}-1!Hn$B2IHNGqf!?!wgj+6a=*ju3{v(uP%KlRPIek#xF2n9&H~dVopa zhWE)&oV0t z7$Yol5dAbB1TX&HR|5uzI!0ISGvq6^L}U7dcx%aM*!a2g=4RkS3MaqQtUUx#kk@ig zYJoLPJkZ|be2HpRCfqW5MYvPok(>AnTR(WI-RTurQY$$uC-UBB_{ayIc zoMa?dgTlQume0xT3QIM!cjm_xammbs8%c=D`VU@XGP`9@w1{Hq(O&NJ^dugJ*_s_AdP0VG^^^NTm+ zw>@{~lt3rdRjY>o#p2DrYnbnKk1s^@W>oa~?M22NFo(H#rw0oWy>1T?+@#GaWVXY~ zcpV{Qt8Q4{xKveSaITl`&_S9u58j|0w9}G#ote(?##6>6?t6eMuZj$L8xjW8gpG8- z6UmM-v*O9@tf%bj?VL>KA$OgP-r%Hoo;fCuDDrRVxL2g8=ZA(5XI}}WQ-QE~aYOdq zcguE@?afofi*Z0#CKf*wKTEbxcR6%? zN`8mjYS#^WPG3rVbD&;EtN&VP_i9S-D&qS-6a3;aG4<;@N1E?i?%k1&CFFA36`#17 zz{=>0cg%f`{zXLJ2_!&$2e{!Qpq2=cM%6VfPfguJ|x>dSUx~02Qx*LYh zp$8aXp36JVxzG8W|MQL)g4eaLz4l(~x4vsFYDKL>zWkiPW}h}2dC9$-C`nO#e?=fk z_ppo0&Ee=+Xt!{0M4%aEdh~IC2V=Q=S$INfL$6{jnQ5otLzjEh08nUb55YE1@{%-K z?ArP!u-@sDPnpSn0(a#$ku(s8I3j|gOvklM7)gY?VA?KEAGda# zq*13#tQR4@UlNgdQ4Qyvl&tz1c2oV#Yfku6$A+Ja8)=lj#w&5scfHGv&EvzwodcO; zDi`3V&g4Wh$tM`St5j2)sbBznYJ@zs&CNU;ePlj8=rk`c_{0sx#;g04bA-Lbzfb-> zE2bZLr!VY5QAW(0MB(g5n)vdxPb}Tk_5psicb<;uWSjta-dYZ zrL?Ryl~Oez%aqZ!VR%?m%{>mkSLIfqioC=Y(0}4;5POQ`7)1lVdm8B{!oU74dPUT>!Ra|cF) zJE1A=V|+-#BK^7qNmqM><#dE^G1|vi3iPU-V!j!E>UK`h@-mSGYtmOR?{b{+jwmp@ z4A; zAW6PBRVL3*f`RX~Kh_RN46wh3p0QQHd}_N?8}AMdj#n?C!to+e?&Z zs8~m<#<3Vp`~kToAU!pkrL8RWT=T00;==HHnQTxxkk-6+^W_VM!!4W+*Cp#j9BxT~ zdcJF{-AI^KZSkrPnE_K2h#4|V6LC?FS<%vg2T;3LDR?#&aPN0Yao|N<7?ic!egljE{?|z9{ zEd@Erf#pkC4OA*+n_gnq*Wl=bx`r#k{YoXb^v1^fgy*2+H=^{5)nU`PNh%r|zMiFV z#&Oke{GGJ{YDT!4`#764Cqz+Q;Ca@}t={*Un-3G}@6yX^?^o%=jAJi0oKZjyZWNp) zy~MqLa%_`Zb+`AZ^`8g??m6n#jcaoJZ;4v*c5S#4S%RrSNXQ%`;pQmEPg~ey6eYfT z*LSK0Rh8KAgH)iXZrP02*iXx+JIDt$h@dvOlbD@sMaE7qX1q3V>o=tFsBICk7g1#} z+)nzpHr_a;yaJO>hmAQR(0?}NwWtN3BnmX@R7%ZJzWnBdj?kW0(w(_6RDc9sWl3Yb ztLwOG-^BPv^$vDA7p3}qF?ol26pG2% zdOjbykBF~S6il|0&Z*y*;Z)+9T5+;7Er2~s)S=4UIuJ-M(Ku7ztly(RpF;mRJUZcp zt9&3vV)#>%H!@3L>-E)Al05ffzG^g)75XCKu;xbf;N?*P0bE8mlBt}9(zsqZ8gPOd z5XZ-8htHp9NFG}2cR4mCC%&s(vX)YY@XZ-`eUVMIsOTbypsr~U_X08Et#c(1#AW~2sgLOm~x=XN@keoim;0~{)z+h`m$ zm)dQ@)AIB9vWBCJR8R3M=djsi@OV7Vj_YB?r^Xw>n8(d|q?x{bs-wJ$!SfMu^W>80Jm>i7&V*?nRM`vUVg z*`(}LpU*AZtn~`}3a2@4Y#=*|VU9*g^14%IxK=VBpNk#t9%HPa@#yPM>!w4hmhUax zYkN@ZjZmEmBvfcO0evc80pOC9Mx?%v_S*BNhg>hZer{0Jqk6d*aTJnwr@n79YD#k| zzH*x5=HRlnF1s7(P{m9$F?h@w1lNz4b8I@8b~%KT9eg++{_5hW!#%G$B8+_ho9KQ* z6T~G63m|-FO=DWS#+P>S4^{2YFQ_Wvx{mpi_($$uERNQC{q13W^Qy`EBI&C!PyTL? z%|3Z}fM&{q#mtB$Tnp=ML8{6AxUeg3k>J97z${}MuaQ$6Iu0=?c<|-uA}1DP&Ud^j z&@D0~5>@re`JkWpLjDa?GLv4v^uY2Gf!o%#caAAWUYb)Y&v+45`HAl2Bb*f22=gJY zxLeY+CSdM1Yc5u3lnI78!N<@aDRIm}CraZ6OoY;bUj-0fiyI}@5O_xo*`wYjY~~Ea za@LtHHheCgJ?(2q#fMZJiZj$j!h}am2_D=HThWM*^reAx6o=IP$iz{@%v6bM`XJco zW2h(pm&;3{#<=N&3H{PlizkW4Qw*!p)*+gUg725o+EPtXQ8jejrU(|5T4Z}NYkXGJ zA8{>(?(Eht0p9=Z!O!GqQcLP-HuI|)gOZ%MDmYRh= z0Y#zX|B8=kAIIF2ytx8owVNRReU?^OF%83drv{>s4 z@fw{r^tcf(m2dufZmaoi_A^=>>g!Tnm3La7lG}x`iaY~D2}x$)#GKbrpB5}$I|LgE zzV=RAX5&KQnGV(ged#$ERtILFyPvdzY?Hr8`@w?C^C*Nqt?6SzWoFUl8cS{hMR-o& zR`3a}YC{U6qxV=&2QF_fJgCR|)v`n377*(DkhsdZNHHp5vrKWF9s#u)H+J^BQfBF) zebaQQHD@_s5SJKcK1=WkGRfst1j|J7jmiqcOomk^*-p93 z>(U7k5rWKMQ9@o23Cmr;O}pdj2>V9S}?vb63Zna z@QFAKomza$)#S~R;b0|#Wje9cdma3!>|v=ez}z&D%o4k0Yx=NE6Lf0!x~$(`?ShHP zK3J=0Pe+s&k4xUfAJW`q?m;X6;nMj7j=TQfEUuDe9HOz>52CFP=E}*wXY<4jir?LJv*)Sqz!sgjejy_->1n zpyIae3`}aOYLj0dI>0F+59(eeO-C;~Fx5ZqKs*tbD12$>ok2|CLDc1zJ9^9gw%Fx+ zuF}uQX%^gjVy6ham4@uuW;Z;aO@VPD9Hi!08oP7_R_6M8DS?;lrf93DyNHKG*m*QF zfk9|sRWrK;OJoV9Ce_qj)>YZmTEi`+nzA94oM!`NT10ZZjUd&ucttif`H(Hv(&(d; z;8#FT-l7Ezt~-r5tK){!2rRr$lOt-x6k3N*4sO_hEt!6OU+bC%g+d zWPs5$jI+!dui#u9d6ppS^|QOVTYD-BElS2_2=U4d$LzjOV>Ez{nDHp8}O2Wf#o)($i@Z5 zx|Dh%(RxiH8k)O`Hz9@x&|bU14pG0i!Elx&)^P3gnJXzQr9+Y1qbFF3rLQVgsL+p&RiBmz-9`&M=z0U$!|1*GR)}apkPW zFb#iDEp?oBrs%8MDj4=V+{iLyKIWC<;YsZWtb^II^MaH2H+Mx$uN~^Ac)XA}iiINF zRdj6vh17FM6EHhw=Y62$d# zPxtucr1r;}>h%03@&oKHvIEj`&NqLlD1dsytj-BcbA;AJVb+ZUQI(9$Z$&3cwddAo zeiVn;|LIe4iXLbQ_M2SnhR`)jwiBG@VFFI78r?HX3QUne2dX+FzEaa#RV;1%z@nV# z_{Fl;P2n#C#{OoW*!I*!$-l_I83e{TCzZ`mmK_$G7H|Nvl%>p8&C15veSu%^1ohs` zu#0GUzxWiZcI(RJgLqj&S7W-=*=|Jl{RYBlKday0u{M2+g!^+W9l%@mbj$9!_R}|%DmZFv2#*uwC3Gft!j*}l!-&4 zP$wdlWUt0q*kQpg@4GH?Q6}C49pXybziNm@e=GC1xXMK+$Xs8|Yhrq%I=GB@PkCbh z;f}1v8h8=wn^Kq8Gezuiy8AePI+%?Jz?wd^$W*Ip@35OeZB<l|T-SM5eo&uD?|9hDc6&97}01b&O9i!Xygmi|oqI%JCtS<`5z? zd!fM_GMBn$r5`It>QDGziu7op;1WjMuSU8(CIIbOOHw1Stb&DD$UpF#QKK{zOQ2|I%5R&%2G72I4;2HGGC|x-Xn4x85>7?w!uT{M zQ6)8PS8eOWEW_%CZb_6G2!Naat|F4Fl3pK95plz#B!}^(02Mnp@Xb{o0k< zeMW(#&*{IFz>qYWC0$V{Bp^p9HaQ!DpH)SNSrzJc{3n1xOHt}B6xtQ8zdxVsAbe5I zBl4+Rt)u@EXv6fUs&iOr>f;G0c#QO^6B0PkCM4OavMIahY8CwK$B+$Nl!e(#FrrJJ z4Jc`FCF&V@7rj3jIL|GX)SX*JsU;&}3rIS_4Ljp1h+Vqa7w|HBmsYFAj(#QFL>&*F zy0UQ8KmylO)^4h&v~ycOJuUrrEzY;VsU1IX>452{nf z+~*{=iNy|kVPVOhR`Rb3gx@(6y2Aisl*roow^$GWVblzqc@YZvnBJmy%>W$WczRfp zCJ#Vhwz+p)!)YpKzMPw){|;X=>4NNA$$^%VPcb9E?W-9A8aFAg^GTV7Q0vN39!Q(! zFIwg0>`66&#>wYab>zKUXCexr&C%G^{5A52p*H z?bhz!vUoP(T2+znKq3DH%7M`kA8r5hCH}=*{C?g`Et#~1WA+6(MOV^9`RP@lG>=96 zyvpr;hSTWZ6_OYv%%!4YIMI`mUgqdTdN}M&c9Po0C{B;}xXTVHJ2j1rBx-5AH7nAQ z6m2z|XV(g^#?bk@Ja^v%5u9Jj-P;X_m#dKCfU@1yE0r3@&v(gpDAKQ9>X?Zbc+CVz z3%4$0effloox}FG3iD57!T^|O$XD|S)n1$h>Amm~;IDq3zCLo_>b3A-Ui6#^zZ-QE zAGZ(b1106n3>rri_yG5Jb*_ubq~$dX8q=CUNUrFKQF99)Ily;-_V z=^@$U?%^}OFJQw0fuZ@d?I&jgY4gIHq+>n0q9KO1lDxR-y}4okaa{Zj?6?EeWo!i1 zM9s%!(oP>_MsuJVaR+j-AEZgW8O87HdB%7p@I>_ccYMm?66eIJ zh<#SIKkHQ=^fTd7HaQbhLff*Ja4Ex;WapXchNL8vv=wS&ZXdA8yRtXd2cAG_*NG-p zHWJiPp!1w3P<^-JmvrNW+x(Fm39o&DQoTHXu-mF%5Q8Ns0SbKtMu2$sNr*pR;jbM zd>CFF8Dk~&Q*=-bHtchSIhWR~e$CKt5HU98Rly_kkWvJ^6FQL@YH+; zxL$Q}>GMT_n*nMhIKw++nwB0}<`J}Ui&U8#?qxZ%;+t_D7U5S;cVWD%ko?7#!xtWj{iI@cREV5jEQ4d3cXk!a1k?hqhZL=34>^fxg$;XQFA0&uwpk2mi>QkYNUsOqUY1B>e&OikCR!kf8R)M#RM8`KD;JxF~K zgj+e(iHqW^cG_Qmx#zZ=C2!Z`jNTN#1vIP7zsMnWRn?v@Y9zWP8X!6dd1-ia^sE`Q zD?YOE7>Ewr_-#IKEE2@`vySpsV{G-f0wB%l4Aqf{H2`(!g@ z)ueNAmv;Ny?|Nf0ToKG^5US#C?g*c_ao!VHCr*l<9*FgkSM<10NU#Jg6m#SR8yTF9 zio7RwYr%a35uY@$9Y;;GlH$I7>J}U-GSaj~{H)4Dr|j=_qugI|E8ilDSsjm6bH6<; zt%)94Iz|0*nPIYc;j`A0<}K+pW?Mvg2ihoQ5JZLya7o`J3kj=FcOQ*&emsWG_j*psXP)L7pCcrxSvjFNO;CTPr>>nI! zgxdy%DF{B7yNNt`esnvu1%7^o-(ereee8&8ANPw%vXMlJfrsIMKFE+4iOA=Hb}i1E zJR&&Oq3q<+y<1Z!nHd$yLmY0y*%19BY)~90o_&RnHtl`-P>^8++iESCL(0strxxA_ zjc~&+33bqVaG)bo#}owjZY?5Fl-1Y7lkU$0a2N>-BDmMw)0<-4rr%2guNmsx%bNk8 zVGlEpcN(I4%{du4!VE#X5zU5eDFKN7z3W(c@a<;QiOvn-=j&5Q5Pgme_#sTFMZmfHrBOhAnP47c(s`d2@H=3s%+iYt+Ezu-w^JJ)9%9 z4Q=SOm@Za)$+2rC2A&{bMIMb0D?XwiNZTH7UcrEh*(Hi{#$aKGV-sG&1^lHwu|a8jV3wZOg2=}Y20Xt) zr=*R1t){nW46sD#r-I=#=g`%nB^1PP%fj1$q+sP&?*cNf4iPOZ<6Rz4Pd#w;>AeLn z=Wz|GcwZaJf1+uA&_w=DtN3%+YQ(VD|3wc2Z`lae+QU6LKMXwFz}k zzTb>t1~&5JANH5UPCe&o$P?_j`scM z5*`8$AF$l*C3>gVp>|$8;`O<2I&ORVeWjXEH(zE{yuI5_;AQ`1ugcRuvqAXWErtf6 z>rQTob__-Nk+}@z3wA0W>e63^9)5J8_T9+^(-e>t$QKbG6F*eQCsnLH5Mv*5pL;GG zMC{fU8rU0K4|5u3Vb%!f#&2tY*B;ZsVvjsNEag%jXDPu(DxVC6qBn&ch^$zZ3N##%N{*TDk6!$?o`<1Gp5WezGOJXoElj%d6(C7DN%c+fL_QP_CJNwj-s zhoac{egA%F)uY%hT=Yys+DF7F72r>K%;S%tXve;OkCruY?=aH@C>7(Ww~ezru33W6 zOG3&wWgDP9D6qiJ52NEHKnr;=M*#VgG~FAcXVueiiJQEedo(%jRGm87KA@e~#@dtl zuHNB6>QqW>)lHWf8S;m|a6^@m>9*h9k8uC01^7cr%KuFV(`It#ir=AUy4<-Z0L2p> zIzI4*WfoEC@kb_FWWyagXdpb8GUZuH;3F%sjzm1wZ6Jim9QeBp!f$uar?bo5%jxm& zcn+u)Xd~!I#!~V=OL!yG4McnYmbd@V5`bR@It1AR>d-c;DIOPje4^M73kQm>oE6ul z>mUEpgPtau#56udOGZ_)x;6!^U?UV1&PU$mH+&RRO8jsIrNKV$~dm8JAMS zgLQm*1>xgzeOM$au?sq#Yq^q0K*5BW9ZwgS*Z%V&Z)8Nf;PT1K5JMx-l@ecGr{Gp7+o4fj^ov0f8mCFPOHz|Ng_{&0A@Icx7c6dz#0G-<&A^ zf3{PK5N``;`>U7Ij#})g4W&Hbpr~3%speyBT*VRI84zl60T>FqsrMb9!Bh&YfkYH& z>7s*9v><`^OXAlN2QJ`;wvh4MR3L=;C)ZA3L$c2m1nTm2$7tZ{=ijVjDKP*P zjF>$&A{`(s?WJF@Ia`&r$8=hoG_wGdFd1PImjz(st@$0+I=bJ#G1DJ;JW}_*TgMQM z)~i3v6BcnvODSb*PwCQ`x^HalHieQ50yaenBY+B7uhSuD1f2pM|Mn89%N!~@pk z_maksj3&e1KpHb6@q@3((%!!7&=tj2(hc=uh1q-U3N#)iheP6N+-x5H#PJ+r)hpIYll!>h^i? z6ifC_A!?BNLl1E?uaZJrk0Iq%^iXhsu^nH%te1j215RYPH{bfC3WB!5PF%K|4Xwc- zdgCiaDfnktWPoY14rP1fgc?a~pvs;z&yJ63+JtB%B#Tra5IZ+eH~x^l+tl8>CMYgY zEFnMq=~Ql(^|afq|JMlVi%>TV;okxKAnqieqlInI`UR^|PG z4Z%2*+>T2Zg%X9m%hz8ebFa^fY>C`n ziX-R3XR=HC+>HT0e$P9<@=@4Ge6I7O8>vat@f-s_tn+vYC#+I}qv!Id1P#YZ_=hrT1gIDaPP$-HC1lH%&CWD2B7JBaMk%m*xhV2QEH8+7PN?q7AcXFhE7MSl z07;F-=;T<$i`OE8<&Pi|BAvSq1aE0KXvN9$t8U|$ML&(cZ*QCOY;=$HeN-2OJiw?( zYw4PRvc|@3a`TEtkP;nZIqyB1bT>2T znz*xW$w{0=Sh4Oh0j*8GyM)skfILfwsM(;-BceYet|AJ9>WrQo)n_l8T}6urgbYN; zGIalHhxHxWv{W{tpbmu(Y=0<{anvSceSAI?(9AeCSH4?mQo%VYp&Y|a6-v%DW1-S# z)?>gFC;m{X?>VH|z5q0BFw7J-Tveq!T(=XMTUk9C=~tQSeRbAOn`#_CAI6ElJhSkH zAPV90JbFGK@@??}3y9CpHP!gN%>ow(o?=@Gv6;|!iWeM@h)Wn4m`m*1T0=0lE?h)G zu_u>?6Y}Pmup8vHYKQM?^zHfB)+d>GI9sM^^%I>bvxm%aqX$T)d}xTb*B&% zm+h3NZL&arGyb@KFXUT_VfYZ7u@+~UmPzUrXs31aiZr&~`fFTNVB-m6wE2iKo>|&+ z8LyAn<}xVWIHy20yB)0KJtQ)qC-P$mSRuV(r7Ye^^yix1LU%+LBMa83EY)-!c5eID z#A+RnslJU=7>G)t)6mA*?#0^K#N*s6%E#25>EK`6oC6zv8)r|eok6oGvW@m4%0#&q zz7!X8 z*w_@AI@;l!zJ~D(17FRKGD8@)$Hqi79P1B@I~QNyBBm{^rO+4@1J}5-dgYRG>!s#Z z3~H#RzBr`)z2)qjlzcOCn>4o~36ImHxG})WW3V?)v*jb-hj+krtL=S9E|h$6OdA`* zLmYg0ibo(dCa)goCdLJ+I`FTidp06J&krk~@8s;(J)WMf-V=Y}|L}(&(}^P6&B>0x ztY`;!O+p#|agC{5qplQn<4}_p@wY(9RoW#{gJ5)My%F*G&i2!O?lXlQ9by2Y$5*wx zYFLb6Nh*#FwtH6UQqXfL_mJhcxbh~C+>usAE<6+H6?u2DL;*ACd|D|ljIJl^YDjq(c_EjT zoclq%9t4#6sd3rjAy2!vM;T-r=||Giy~fsuPVa9KCEqW=o9lp<5&P!&d$J(WE8GN7S=}`42 z#~?Cp`|=KRKI6lLvX(=!m+F1o)yj;?$?R*|?P52~6vU+ra=oq-U?H0m%%#AiU@UE7<9Zo2=N>_q;HZeY#YJz$5K$Bjvhr{9E zf<}9)dg4&O_RSJ4q5u?*wjXGp@FoyVo*bqEQ=ayyc=N9LkVhkRttzz)bIKfw!K>R{ z?kKZn*)iMpQ21nfvmy1ufYKJfl$M6Cp=^iOH2ZrjpNd3vL<%b1P|=~I_)>D`>CJnP zcg;SxeW}T$k>8|UVBZlHM1!;f4^8pe$W z?~n(Eia#6xWz)1kqy&k6m0{ZQBt?`pl>1%Kno%zTlEb4OL|gEdvuD6<6^|&}*TA>O z)S;~q7ntW2&kH0w%3H(rJ+5vEY}pjP8DU2WZE)lB@Qd&2+T>rtmNPs>xA}fH@CR)Y z1N|FCza%uHDtX2qGD{^JE{~7itx{~}3ya?E_eent%pO=fCr1n$mBFx(<+wKqN-}Q- zM2aYr>(e^L#PHI@PYy~?NFG%vw_Z$YgtKMt07krbh5%hSjdeLzJO;pzO1kYeicNPm2sanyt^)N99;n&(qnZasF}}WJGUE zi4r%fgDyeej{M%1v8irO%ejBv&9RJrlT-SQk&}&Umkv*J&Of+Hj3Q0jNL1k-4FhdBN3i`gLTg*BK_sBF9lV6dbJh zovdH9-wao_Lolmi*#DNp;&&|$f6Alhc| zQ;T+7$s`xD4cs3j$Fti@&+vV|lo!0ZiJuKVUj&NoE}rc%Mc@wuCzMJG{5&S>SvcCE z#aa7`a1UY=`~b4s=iFSspk~vSw&I~Kf*xp%Jq+cXo2RdwJ}Hs9yld%KGZjYgAfI$~ zNl6sw@Bo#Od*DYxP;B*LroHuTO0^B4rKWwPpMIHugw%%~`BkagdupX*DtbVZ%AdV3x0-mUC+ zthb6}pad8~MNQ*#MIvML`|;}sPjRW+87M5&76IGjno00f0zb2?sketi0r?|P!OOU{s71MPP z1eJ{}hwK)7P_I%7>RI^cTM>EUdY)dd(>p)$Im6|8!7g3jCT&jZ!Cs-~G7M*$CO7>h zJ;z37NmQRX>^hA9F*$x@a#(=~WfANi|HUfVEKN_C`pt)7E#=Cq_KKM#w>gf46R|oM8Vniz01h+2@08 zsAo0JU4*5=c{VZDqvAE4dwpmA%yR<&udKcbz*Anoh{^?U#|q72k#~abrjkXM?5^~+Ssm?S|=v* z?3MXN*grC94oYpi?@sjts2nDz8!Q^G?In2C8S1ng798*~JNxPhF`a*Bu%w9s+rwp` zq$2v~SnLK9%OeeZQ0M!M95*uRaH?lM-e0~|FxA4#h<;}NYTmA)!lxds-5zotfz8I< zxgUxZwfZe`^e_H=6yqK}K-J$ZcDzE&WtXU@+DRO#-T84I?B9?)#-pXsIUa@o7~gP} z@K|VjHYWHo*}&#|&@!3K64&orO@QE!TdnfPaZ{j1{Kr~{lN|0#HPDBxLL*T5j{BEA zM>-uYw?W0)`GR{fz(*R5@R+zAPdY@8=*t*xZQqlsQg}{<-OADcE`M zj|O|Ku%2LGcHg&GPx>`qwXB=33Vt-oXE?~jO1oq3lU}B>HQd*pvP_!0{G!x{&9$3n%W=zj2}QCeRGK8b!HSeLaA==`P@z#b_VX#&=3B_yDK7jxN}cn(Xu`9ZW9h3a)!2N!2sJOm50xAT z3{yh~?QZEnc6PTATkx0p%o{lsW;?Yo$yMiO0(~~GM;sCN8zd7Pr7XZ`@3(ntRdkvOiS&C7i`tfO+3(D8&Qr*Wo z++|DT0q2q$UMNlMuCpdo24&x1Sn9`xf;Vg$U%Ft(OnT5u2DH$}H+xworaxJXWQzve z{?V%Vs)n-ZIJ$wXrvUd-$B?|ATOfApmHm~l*%Y*DXYNlu>sh*C_5nY9p$6T7H1O+M zT+TBnbr&7y62TNBEh)!<9jQGjo9~3H&Ky0X1g>e}SxWWrM zQZ7r=nr~>V^uk#b$k07^6%M@4X?oRcJW^LYjvlyRO6i9xAL5zRP2gAsH2S1?q>?p! zl%RTHI=j&cM&sDSpBL_#DCOL!2sE;N1N$s#MzOeU1)Idk4l+qu87X5nzq zESFPT^b2@O6~Q3XIDC7vOgR>m!rYA)-NY)T(cTQw&?{YAYWAw{DxvZ^gVsv+`)i#x z{$1b)Sq_{p!ZjY+{cFb$!-ymN`NGtDcfSUMk=_N-Oe&HY6gW#XntJd0w~Qbfvv3_- zCYf)pA*)XAOM=niDeW%&em5`Y3yOSBycD+Qc~ez`FdX$!uHiBB<{4_2;hw?<1T+)h zk{qc1*Dk*lt>hSws%6qj+LnbFCxW)|CQWHZ1lo4hNJU=oqb}hf-eN-iH+r5&af|pJ zE9LNa2|$%>k>wMbNW$yvB12jgk;+|F7Pjqjg0s^scC|W++{@;P54w!PFMc>h9ozIp zOdfKw3&BUC*s?9)M)NM8n!gBAt`&_1Xl>N(RNOkPA@sADA_eGIonQCkCq>iVEU&yk z=!Y)6J$Js|VurphOe!n0>co|}3*&0O90T1z?C2#1yKm9)p~z+AB|&N@6Jnzdhc=Ts zbPSoB=5zkK!LIpgQ+GY?oo8GYRZs8>%jhd9NmW_O=9^u%mUGQj;dyu6y2_yGsFDj= z*Cf>tH->0=mKD@sFyf9vIH^EY?_Hp)$NSrbPpRxy0WSAbOEKI99;zf?KY))J7$X(} z!fV2re+^1{yhB^s@0az$V$ET1$L$cRd>5TrdoA?g#4;eEyQ}o3{HmJlY0RaBKxtEP z|FscdD}}#p1^*SK3M2RQK`3O9nTCtLTOBt0+U!tX{t9%d`&9?adWp=@c*?$EQT3;>fqS=TWT3_sH!>kn86F7I3f zBiJ%np|ZxNJv$jF^YaKnml4?xA%VI4Cj4G1(KEeQg8tN^fWTBYP@5R!)@CdK?LXGdx-Kdf!=+5jqx(!I9232e(D9; z%G|C@W_-?Di!$eRjq6nm9{~;J0Ugj(->6WIw+l<07~uWOm3}>xh_L&8xmbo?I)%ks z%9-7D)f>@@m!rLI7aSI_%I;xh@Z*hin)PLV7yY(R^FHH-MtV`jm^8}wm@&}PU?QcO zbvlvZxXNg-j0Gmdd9M8S5UoEH^PBF%r_tLLsH_X5)lr3$?IaGTgP8(T+N?tOJ?&CU z8tXyPgy>|ZXbo|sz1MGZ6c3t&b!H6(gP*d-D)qrMwWrr+q3jXbIors8bqr3@fxvk066+yysCM1rEjt}4=_DnIf!G&)W{cv&Ba`lmT^>B6 zr1bOru#~_{3(T@8FOb5^$!O9GIM-L29F}VQy(8+^i?>ZSY0>A1i65glgC6Srk6rg4 z+}wW5i#jy9k#lNhTV6pMhXc3OwEMcQcw%?b&;MlzX-n{{!@|Mgm5e8 zHGDvMg?#XXd*!oys;8~g@rN&G+9FaS-mm9jzoIQnWI&1WVb8dLG zQrAd$p{qTV-Tn@tpaK3_fg3f5Gtft6o))z~S$j$N&47N(9c8pfVK#J~b8e^Qw-J45 zjr>FW>?C;4A8DjPBGNF9GM|8t&}G{$J!&M007r*a+?nmCjbb|52=Y;xSU?_QLtnp+ zF@=@9$hCb!9%vzz(0)k3UgwSq*Q%HP$(CO+oPG@~LoH zrHdb562X>K+8bN}p!2c+0|iTYvkYS}sV5_OvXoCzt{la_SF0C@G_#ZU2Cb$gXjo>P zzKNvgL8yLSkCm^L4|Yd;WVt2FP=ldRznM^ZuZ=5pq`)dz)NSOr)_1ZluJO4Mn`>k_Y|EY75ja{m;>b!f;MTC7l5!HXi3S4mYsVlTpo2EzG6nk8Xou`Xag}HMs z4@Kk+eo%|k8tnB63;p|M%Wt_cFzep zKh>fpr{^n=05oGSEc*~MKKhs^Dhl`yWp=hO7*;TfjlTwZeZwCY40N6r*7-rwSx&lr zNut-7SAr{u({CnwH$ki=r0I9>TQJzHL1Q3PBS8CzGIRx^F+_%|u9;lxvw6_ZO`?h0 z^F;j)q~Eb+HPwmyS#_oqL0N^rbuOVGR1 z6|qNRZwP9Gx;t8Xz=?*Y(T_ZCDbVdj%^p%(#0W4CDaXO$JoQ&btN|nH+YlPyn zmbc7`4+^-%8t%Oov0!E|?ONzA*Lo+96!E$pe6e(}7L{i5q3hGZI1+FEodJBo(>J|t z?YY54c*u9%$27LHCM4q#?(t?1SIZ*txFu0z+eJ&)a`jt%`a(nO zMuJm2iiVLXzI9us^P0wLZ3+V5r5w{+!rPm31=N=FAjb+;CS(fWIi%+lk|CncH;6gy zD_5mYe%hJl0=iTS*}O;NL3Xv8VYS~{VD)RILA)N7-J}OqDRXjG>XH1Y5}Z7Q1TeQU ztDC?XyT`+HRY?)~ny71*1Q+-}IAA)RrXW@|(!_wVyhwHNsvS)b`Tg)){k15WMxqaf z@7*YWe8Q!N-g3T5D3e$2}>RoCzGa%dwHnJre^K|n+ zlm6xU0%PawyY@3fqQ(6wOM7)8IZ%-x9_LBdz~#S;tn>vRvS z2Mdg^ARkP(7^oPsnA+iSl&<3s`uW!in9^Sstv$lLC7)N-l{sGgL!V&Ti+ePI z6YeqbpVuz3a!&4jmh}Jwh3`-(xvBQz%#+xZc8eRAjA*Wz-vY(aLiW$U1PVrR294-r z#n}=x49__^?TAi&{9~4ct4nI1D+>UBIRoX{SW4>-mocxl+->TrcE1G^K7q9?dVbo` zK;?A~?J`TeeIpgs?yJH~<|udLyi9t`XN{iO$oTnYZMeng{ROH)WSyD)Tb)?l$8elb z+9;{8uPY37q)G9#oB8%TPfsUAJy|$`wpLX)#LdJdeKBu&v>^2D3Qr`gVFYn4h&%LO zKB7eS%p)g}61VJ+7m9A4IGSJj%ByNEJ*TV5lTwOXcUI8w{R$^`Ts%pYecHz_f~Y~@ zE{xSz&(2eQuNn&j{|LgJn98nHQgwRK;>E!k^(r^%(bMa|SkC|pnesP#Erb!|^(Z4R zc-eAP5tO)LhdRIT(^=VoWFmTEafOfyIanDu)~PO(UPLmcJwO|EIJzQcl^UvwfglMM__dZ zzm=S*f6S~90`&CJ0i7ynrw}0ki+}ER7HX=9kSs_tpBUF-o4?|3c)3ppsTqwQ34i(l z^9LFkqCxH@zK_O4bMK+BA_|bu`9>EEo*yNenwD5mo}66P#+xb&vWPFESKop_Hbmtn zFL{2M)N;i+@K}8wE&E)T5yz|$thzqwy{jN>VR**I_AAVE>>FzG+?p8pMmTr*l?pAWs(hUOA-3zpUUN=uu$7P|ja|hGo9axSP9kREaP=8wjeYh} z;B;e1l~%(qb(;#MS~%?)vM-?Ms8P}3MOC8YWrgV!Imx-@WHA|%JUC2x{6a$AzL9Kn z)3g`zxA+&cy}wGy-l>IDBW+fvt6z*(DmTG|rZ?N6$nG)ook^bK_4BiT`} zB?m*w@vTYHmT$j1gNC7I@6jhQV(#xUuHv&VLx(6|thnL8TBHr!C}2Qq)5C+;un+Ld zDmCRRYwCkAlZfM7PCR|~d5%jFY!Ny!4jqlM4y4cLP z2r9?q*CbRHj;Q~Z3GhQrw;ul5CQd@})=E3!ymLc;#p<9*7LQqdpmfxp@vhMlzjHx$ z2XLD2Mm|Kfwdj?N8p~Rc&Y;RKD?qg5SWD%8dSq`0vS&bLrOf%g+jX zp60ov>|D|6IN6?)Ez{V2a+q{#{-gkH8>K?W7b$%5e!3L?vUNf=Vm{-71%#*(K@&L415KU>hTHX-{Ns?|LCl?{4uLTb5L^Y>P8X?9&#v!ZVKv zggk1qlgjB$TDXxxGKs72e{Gk8uKcbSJ3QYB>aNBmxNWdQ+-=>ctK z$s&ZTMjC35;6b5Kpt-`CmiADPaviS@f1Wi_WFt0NBD|>Vo>3JPZAjv8dMZ=10MCR@ zT3W`*O*Oi?d+0+6k+r^gbnuRC6k5h3<6{X5CV!9N)|PU{tlWsJ7%wS)9Z&O?A~W-2 zOkzOfi)E~AeksdISr7k-Jk7&S@w=bkScUsDtY?RCsYME#{bGA#*4|nLl3?W)uMi)z z{Ou$ak4u(N_vTP4g#zdSVFu=7QbSxO+E+A*0shc8Mt-v&fe?IX_{sU`iUk63G1VyH zaxt(ZB^v{&LXFHgXFJ@!E6)VyYDMQAdL^jCXM_9vYh4kJiOD8yg1vI z`yIx>tU#kEvnI3S7h8PNobX{0wwFcx=opm}ObE|urA;EK*8oBtQ>&|r33vDJ>iUpU zn|UIgZCXg05rDbk9SicqLqAl`gb8)`;LZ5B{)XeU54QpAXpthiuOk5-?E3;tW}Wi8 zVaOY2yd4C&BM?%KiWxY6W1~S=;8@<=#d4JJ^nXhS+1*7o$-vP1uRCeBHeHX5k_O)r zUZI%2L&%ePy>5W+xA^|u`4}t`jp-Z_R@crSgf%{e{jwi^$Sg6>I~`)7K*n7BdGhEj zvKBSG%V7jzk7#+O3(|_W>6fAUDk95)Ump`Fgy(oaq@Fpd_?rfXh=P?nEMzx0hUkbQ^w1T^UE;GXXni}d z7Bj@}y7I?qe4Kr|Z>u#t1T5}0*34*ag@w4@WXC+Y(*aK;1fC~S6}$|9zoM%0A?3b& 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 literal 0 HcmV?d00001 diff --git a/img_1.png b/img_1.png new file mode 100644 index 0000000000000000000000000000000000000000..8e9a96589291110480ee6380b02c78f44304fa70 GIT binary patch literal 60946 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�D86VQL1`jXvV`cq@{U?XiBc7$B6&eTq$Q|O0pUvuvx z;3pPEhsx;aOaff`^U+*gX_5iU{W>lsz?dlUh|n%vnVv3G&^CYVMgHob8jJ{i-mGI< z3COOrK3Q9y*sZSnO2U~gffb=3rz_9G$h17M^62?|f-YRt9}Eb6?(Bsbe!v&2wP;H~ zC`*^@A&dI+(+jV!SY2a;BqTwD{!T7H*X8ZaarlMF#IbBvaS^TKR~q=pKHRfW?VZjC z!djJmXXn@K!#(-ArvdS{$(nkLO2Urs(h5!Hn literal 0 HcmV?d00001 diff --git a/img_3.png b/img_3.png new file mode 100644 index 0000000000000000000000000000000000000000..411332d99a87a442e7fc33ccec594bd1696c6b40 GIT binary patch literal 82280 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%0AQGzH-GAIVYY`65&VW-eksbK{1Hq5Xg~1A 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∓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 Date: Mon, 11 Apr 2022 14:55:18 +0530 Subject: [PATCH 008/222] Issue #SB-29476 feat: Redirecting Shallow copy publish to Flink --- kubernetes/helm_charts/datapipeline_jobs/values.j2 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kubernetes/helm_charts/datapipeline_jobs/values.j2 b/kubernetes/helm_charts/datapipeline_jobs/values.j2 index 7ba2174cf4..a1aa4094ae 100644 --- a/kubernetes/helm_charts/datapipeline_jobs/values.j2 +++ b/kubernetes/helm_charts/datapipeline_jobs/values.j2 @@ -214,7 +214,7 @@ post-publish-processor: kafka { input.topic = {{ env_name }}.content.postpublish.request groupId = {{ env_name }}-post-publish-processor-group - publish.topic = {{ env_name }}.learning.job.request + publish.topic = {{ env_name }}.publish.job.request qrimage.topic = {{ env_name }}.qrimage.request } task { From 4b14d9dcc5a9a9dcb57638a02126f3107e70c653 Mon Sep 17 00:00:00 2001 From: pritha-tarento Date: Tue, 12 Apr 2022 12:36:50 +0530 Subject: [PATCH 009/222] Issue #SB-29492 suppress exception variable for empty metadata pre-processor --- kubernetes/ansible/roles/flink-jobs-deploy/defaults/main.yml | 1 + kubernetes/helm_charts/datapipeline_jobs/values.j2 | 1 + 2 files changed, 2 insertions(+) diff --git a/kubernetes/ansible/roles/flink-jobs-deploy/defaults/main.yml b/kubernetes/ansible/roles/flink-jobs-deploy/defaults/main.yml index 777f8c8615..a2b30cf9f8 100644 --- a/kubernetes/ansible/roles/flink-jobs-deploy/defaults/main.yml +++ b/kubernetes/ansible/roles/flink-jobs-deploy/defaults/main.yml @@ -120,6 +120,7 @@ collection_certificate_generator_consumer_parallelism: 1 collection_certificate_generator_parallelism: 1 collection_certificate_generator_enable_suppress_exception: false collection_certificate_generator_enable_rc_certificate: false +collection_certificate_pre_processor_enable_suppress_exception: false registry_sunbird_keyspace: "sunbird" cert_registry_table: "cert_registry" diff --git a/kubernetes/helm_charts/datapipeline_jobs/values.j2 b/kubernetes/helm_charts/datapipeline_jobs/values.j2 index 713919d0fd..b458138596 100644 --- a/kubernetes/helm_charts/datapipeline_jobs/values.j2 +++ b/kubernetes/helm_charts/datapipeline_jobs/values.j2 @@ -685,6 +685,7 @@ collection-cert-pre-processor: content.basePath = "{{ content_service_base_url }}" learner.basePath = "{{ learner_service_base_url }}" } + enable.suppress.exception = {{ collection_certificate_pre_processor_enable_suppress_exception | lower }} redis-meta { {% if metadata2_redis_host is defined %} host = {{ metadata2_redis_host }} From e3b316f76ea739322e2d5a0b915f7abad05e5244 Mon Sep 17 00:00:00 2001 From: pritha-tarento Date: Tue, 12 Apr 2022 13:00:45 +0530 Subject: [PATCH 010/222] Issue #SB-29492 suppress exception variable for empty metadata pre-processor --- ansible/inventory/env/group_vars/all.yml | 3 +++ .../ansible/roles/flink-jobs-deploy/defaults/main.yml | 6 +++--- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/ansible/inventory/env/group_vars/all.yml b/ansible/inventory/env/group_vars/all.yml index 42fb4d1385..f88d9da6fd 100644 --- a/ansible/inventory/env/group_vars/all.yml +++ b/ansible/inventory/env/group_vars/all.yml @@ -116,3 +116,6 @@ cert_azure_storage_key: "{{sunbird_private_storage_account_name}}" default_channel: "org.sunbird" download_neo4j: true neo4j_upstream_download: false +collection_certificate_generator_enable_suppress_exception: false +collection_certificate_generator_enable_rc_certificate: true +collection_certificate_pre_processor_enable_suppress_exception: false diff --git a/kubernetes/ansible/roles/flink-jobs-deploy/defaults/main.yml b/kubernetes/ansible/roles/flink-jobs-deploy/defaults/main.yml index a2b30cf9f8..1d1a7acf6c 100644 --- a/kubernetes/ansible/roles/flink-jobs-deploy/defaults/main.yml +++ b/kubernetes/ansible/roles/flink-jobs-deploy/defaults/main.yml @@ -118,9 +118,9 @@ middleware_assessment_aggregator_table: "assessment_aggregator" ### Collection Generator Job related Vars collection_certificate_generator_consumer_parallelism: 1 collection_certificate_generator_parallelism: 1 -collection_certificate_generator_enable_suppress_exception: false -collection_certificate_generator_enable_rc_certificate: false -collection_certificate_pre_processor_enable_suppress_exception: false +collection_certificate_generator_enable_suppress_exception: {{ collection_certificate_generator_enable_suppress_exception | lower }} +collection_certificate_generator_enable_rc_certificate: {{ collection_certificate_generator_enable_rc_certificate | lower }} +collection_certificate_pre_processor_enable_suppress_exception: {{ collection_certificate_pre_processor_enable_suppress_exception | lower }} registry_sunbird_keyspace: "sunbird" cert_registry_table: "cert_registry" From 8b4d621969ab7649fd53eda0618eb1b0f56c3136 Mon Sep 17 00:00:00 2001 From: pritha-tarento Date: Tue, 12 Apr 2022 13:03:18 +0530 Subject: [PATCH 011/222] Issue #SB-29492 suppress exception variable for empty metadata pre-processor --- ansible/inventory/env/group_vars/all.yml | 5 ++--- .../ansible/roles/flink-jobs-deploy/defaults/main.yml | 6 +++--- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/ansible/inventory/env/group_vars/all.yml b/ansible/inventory/env/group_vars/all.yml index f88d9da6fd..9307cc79e4 100644 --- a/ansible/inventory/env/group_vars/all.yml +++ b/ansible/inventory/env/group_vars/all.yml @@ -116,6 +116,5 @@ cert_azure_storage_key: "{{sunbird_private_storage_account_name}}" default_channel: "org.sunbird" download_neo4j: true neo4j_upstream_download: false -collection_certificate_generator_enable_suppress_exception: false -collection_certificate_generator_enable_rc_certificate: true -collection_certificate_pre_processor_enable_suppress_exception: false +enable_suppress_exception: false +enable_rc_certificate: true diff --git a/kubernetes/ansible/roles/flink-jobs-deploy/defaults/main.yml b/kubernetes/ansible/roles/flink-jobs-deploy/defaults/main.yml index 1d1a7acf6c..21ed12cdb0 100644 --- a/kubernetes/ansible/roles/flink-jobs-deploy/defaults/main.yml +++ b/kubernetes/ansible/roles/flink-jobs-deploy/defaults/main.yml @@ -118,9 +118,9 @@ middleware_assessment_aggregator_table: "assessment_aggregator" ### Collection Generator Job related Vars collection_certificate_generator_consumer_parallelism: 1 collection_certificate_generator_parallelism: 1 -collection_certificate_generator_enable_suppress_exception: {{ collection_certificate_generator_enable_suppress_exception | lower }} -collection_certificate_generator_enable_rc_certificate: {{ collection_certificate_generator_enable_rc_certificate | lower }} -collection_certificate_pre_processor_enable_suppress_exception: {{ collection_certificate_pre_processor_enable_suppress_exception | lower }} +collection_certificate_generator_enable_suppress_exception: {{ enable_suppress_exception | lower }} +collection_certificate_generator_enable_rc_certificate: {{ enable_rc_certificate | lower }} +collection_certificate_pre_processor_enable_suppress_exception: {{ enable_suppress_exception | lower }} registry_sunbird_keyspace: "sunbird" cert_registry_table: "cert_registry" From 010a765a765aded038b142773dc9670863773917 Mon Sep 17 00:00:00 2001 From: Keshav Prasad Date: Tue, 12 Apr 2022 15:30:19 +0530 Subject: [PATCH 012/222] fix: perform pre_checks for to check for allowed tags (#1699) --- pipelines/build/learning/auto_build_deploy | 5 ----- 1 file changed, 5 deletions(-) diff --git a/pipelines/build/learning/auto_build_deploy b/pipelines/build/learning/auto_build_deploy index 8e5b5db73d..05735c6086 100644 --- a/pipelines/build/learning/auto_build_deploy +++ b/pipelines/build/learning/auto_build_deploy @@ -13,11 +13,6 @@ node() { tag_name = env.JOB_NAME.split("/")[-1] pre_checks() cleanWs() - if (!tag_name.contains(env.public_repo_branch)) { - println("Error.. Tag does not contain " + env.public_repo_branch) - error("Oh ho! Tag is not a release candidate.. Skipping build") - } - cleanWs() def scmVars = checkout scm checkout scm: [$class: 'GitSCM', branches: [[name: "refs/tags/$tag_name"]], userRemoteConfigs: [[url: scmVars.GIT_URL]]] commit_hash = sh(script: 'git rev-parse --short HEAD', returnStdout: true).trim() From 078113e325d10636dc72e914ca59b38ccb6206a1 Mon Sep 17 00:00:00 2001 From: Jayaprakash8887 Date: Tue, 12 Apr 2022 16:18:10 +0530 Subject: [PATCH 013/222] Issue #SB-28066 feat: Content Auto-creator Flink Job --- .../roles/flink-jobs-deploy/defaults/main.yml | 13 ++++ .../helm_charts/datapipeline_jobs/values.j2 | 65 +++++++++++++++++++ 2 files changed, 78 insertions(+) diff --git a/kubernetes/ansible/roles/flink-jobs-deploy/defaults/main.yml b/kubernetes/ansible/roles/flink-jobs-deploy/defaults/main.yml index bdec39a513..3c617dc28f 100644 --- a/kubernetes/ansible/roles/flink-jobs-deploy/defaults/main.yml +++ b/kubernetes/ansible/roles/flink-jobs-deploy/defaults/main.yml @@ -199,6 +199,13 @@ flink_job_names: taskmanager_memory: 1024m taskslots: 1 cpu_requests: 0.3 + content-auto-creator: + job_class_name: 'org.sunbird.job.contentautocreator.task.ContentAutoCreatorStreamTask' + replica: 1 + jobmanager_memory: 1024m + taskmanager_memory: 1024m + taskslots: 1 + cpu_requests: 0.3 audit-event-generator: job_class_name: 'org.sunbird.job.auditevent.task.AuditEventGeneratorStreamTask' replica: 1 @@ -280,6 +287,12 @@ audit_history_indexer_parallelism: 1 auto_creator_v2_consumer_parallelism: 1 auto_creator_v2_parallelism: 1 +### Content Auto Creator Related Vars +content_auto_creator_consumer_parallelism: 1 +content_auto_creator_parallelism: 1 +auto_creator_g_service_acct_cred: "{{ auto_creator_gservice_acct_cred | default('') }}" + + ### MVC Indexer Related Vars mvc_indexer_consumer_parallelism: 1 mvc_indexer_parallelism: 1 diff --git a/kubernetes/helm_charts/datapipeline_jobs/values.j2 b/kubernetes/helm_charts/datapipeline_jobs/values.j2 index 6b8a03a566..fa6f5bb829 100644 --- a/kubernetes/helm_charts/datapipeline_jobs/values.j2 +++ b/kubernetes/helm_charts/datapipeline_jobs/values.j2 @@ -527,6 +527,71 @@ auto-creator-v2: jobmanager.execution.failover-strategy: region taskmanager.memory.network.fraction: 0.1 +content-auto-creator: + content-auto-creator: |+ + include file("/data/flink/conf/base-config.conf") + kafka { + input.topic = "{{ env_name }}.auto.creation.job.request" + groupId = "{{ env_name }}-content-auto-creator-group" + failed.topic = "{{ env_name }}.auto.creation.job.request.failed" + } + + task { + consumer.parallelism = {{ content_auto_creator_consumer_parallelism }} + parallelism = {{ content_auto_creator_parallelism }} + window.time = 60 + } + + redis { + database { + relationCache.id = 10 + collectionCache.id = 5 + } + } + + service { + content_service.basePath = "{{ kp_content_service_base_url }}" + search.basePath = "{{ kp_search_service_base_url }}" + lms.basePath = "{{ lms_service_base_url }}" + learning_service.basePath = "{{ kp_learning_service_base_url }}" + } + + cloud_storage_type="{{ cloud_store }}" + azure_storage_key="{{ sunbird_public_storage_account_name }}" + azure_storage_secret="{{ sunbird_public_storage_account_key }}" + azure_storage_container="{{ azure_public_container }}" + + content_auto_creator { + actions=auto-create + allowed_object_types=["Content"] + allowed_content_stages=["create","upload","review","publish"] + content_mandatory_fields=["name","code","mimeType","primaryCategory","artifactUrl","lastPublishedBy"] + content_props_to_removed=["identifier","downloadUrl","variants","createdOn","collections","children","lastUpdatedOn","SYS_INTERNAL_LAST_UPDATED_ON","versionKey","s3Key","status","pkgVersion","toc_url","mimeTypesCount","contentTypesCount","leafNodesCount","childNodes","prevState","lastPublishedOn","flagReasons","compatibilityLevel","size","publishChecklist","publishComment","lastPublishedBy","rejectReasons","rejectComment","badgeAssertions","leafNodes","sYS_INTERNAL_LAST_UPDATED_ON","previewUrl","channel","objectType","visibility","version","pragma","prevStatus","streamingUrl","idealScreenSize","contentDisposition","lastStatusChangedOn","idealScreenDensity","lastSubmittedOn","publishError","flaggedBy","flags","lastFlaggedOn","publisher","lastUpdatedBy","lastSubmittedBy","uploadError","lockKey","publish_type","reviewError","totalCompressedSize","origin","originData","importError","questions"] + bulk_upload_mime_types=["video/mp4"] + artifact_upload_max_size=52428800 + content_create_props=["name","code","mimeType","contentType","framework","processId","primaryCategory"] + artifact_upload_allowed_source=[] + g_service_acct_cred="{{ auto_creator_g_service_acct_cred }}" + gdrive.application_name=drive-download + initial_backoff_delay=120000 + maximum_backoff_delay=1200000 + increment_backoff_delay=2 + api_call_delay=1 + maxIteration=1 + } + + search_exists_fields=["originData"] + search_fields=["identifier","mimeType","pkgVersion","channel","status","origin","originData","artifactUrl"] + + + flink-conf: |+ + jobmanager.memory.flink.size: {{ flink_job_names['content-auto-creator'].jobmanager_memory }} + taskmanager.memory.flink.size: {{ flink_job_names['content-auto-creator'].taskmanager_memory }} + taskmanager.numberOfTaskSlots: {{ flink_job_names['content-auto-creator'].taskslots }} + parallelism.default: 1 + jobmanager.execution.failover-strategy: region + taskmanager.memory.network.fraction: 0.1 + audit-event-generator: audit-event-generator: |+ From 5f02a188fc0def4241437ccbe7ef33487761e2b1 Mon Sep 17 00:00:00 2001 From: Jayaprakash8887 Date: Tue, 12 Apr 2022 16:58:01 +0530 Subject: [PATCH 014/222] Issue #SB-28066 feat: Content Auto-creator Flink Job --- .../templates/application.conf.j2 | 86 ++++++++++++------- 1 file changed, 55 insertions(+), 31 deletions(-) diff --git a/ansible/roles/learning-service/templates/application.conf.j2 b/ansible/roles/learning-service/templates/application.conf.j2 index 5ba385b4e1..6fa2f4e0c3 100644 --- a/ansible/roles/learning-service/templates/application.conf.j2 +++ b/ansible/roles/learning-service/templates/application.conf.j2 @@ -165,37 +165,61 @@ kafka.topics.instruction="{{ kafka_topics_instruction }}" kafka.publish.request.topic="{{ kafka_publish_request_topic }}" kafka.urls="{{ graphevent_kafka_url }}" kafka.topic.system.command="{{ kafka_topic_system_command }}" -job.request.event.mimetype=["application/pdf", "video/avi", "video/mpeg", "video/quicktime", "video/3gpp", "video/mpeg", "video/mp4", "video/ogg", "video/webm", "application/vnd.ekstep.html-archive","application/vnd.ekstep.ecml-archive","application/vnd.ekstep.content-collection" - "application/vnd.ekstep.ecml-archive", - "application/vnd.ekstep.html-archive", - "application/vnd.android.package-archive", - "application/vnd.ekstep.content-archive", - "application/octet-stream", - "application/json", - "application/javascript", - "application/xml", - "text/plain", - "text/html", - "text/javascript", - "text/xml", - "text/css", - "image/jpeg", "image/jpg", "image/png", "image/tiff", "image/bmp", "image/gif", "image/svg+xml", - "image/x-quicktime", - "video/avi", "video/mpeg", "video/quicktime", "video/3gpp", "video/mpeg", "video/mp4", "video/ogg", "video/webm", - "video/msvideo", - "video/x-msvideo", - "video/x-qtc", - "video/x-mpeg", - "audio/mp3", "audio/mp4", "audio/mpeg", "audio/ogg", "audio/webm", "audio/x-wav", "audio/wav", - "audio/mpeg3", - "audio/x-mpeg-3", - "audio/vorbis", - "application/x-font-ttf", - "application/pdf", "application/epub", "application/msword", - "application/vnd.ekstep.h5p-archive", - "application/vnd.ekstep.plugin-archive", - "video/x-youtube", "video/youtube", - "text/x-url"] +job.request.event.mimetype=["application/pdf", + "application/vnd.ekstep.ecml-archive", + "application/vnd.ekstep.html-archive", + "application/vnd.android.package-archive", + "application/vnd.ekstep.content-archive", + "application/epub", + "application/msword", + "application/vnd.ekstep.h5p-archive", + "video/webm", + "video/mp4", + "application/vnd.ekstep.content-collection", + "video/quicktime", + "application/octet-stream", + "application/json", + "application/javascript", + "application/xml", + "text/plain", + "text/html", + "text/javascript", + "text/xml", + "text/css", + "image/jpeg", + "image/jpg", + "image/png", + "image/tiff", + "image/bmp", + "image/gif", + "image/svg+xml", + "image/x-quicktime", + "video/avi", + "video/mpeg", + "video/quicktime", + "video/3gpp", + "video/mp4", + "video/ogg", + "video/webm", + "video/msvideo", + "video/x-msvideo", + "video/x-qtc", + "video/x-mpeg", + "audio/mp3", + "audio/mp4", + "audio/mpeg", + "audio/ogg", + "audio/webm", + "audio/x-wav", + "audio/wav", + "audio/mpeg3", + "audio/x-mpeg-3", + "audio/vorbis", + "application/x-font-ttf", + "application/vnd.ekstep.plugin-archive", + "video/x-youtube", + "video/youtube", + "text/x-url"] #Youtube Standard Licence Validation learning.content.youtube.validate.license=true From 6d894ddea62d26d3f6fb1ba67ca8ef6880c6c05c Mon Sep 17 00:00:00 2001 From: pritha-tarento Date: Tue, 12 Apr 2022 17:16:48 +0530 Subject: [PATCH 015/222] Issue #SB-29492 suppress exception variable for empty metadata pre-processor --- .../ansible/roles/flink-jobs-deploy/defaults/main.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/kubernetes/ansible/roles/flink-jobs-deploy/defaults/main.yml b/kubernetes/ansible/roles/flink-jobs-deploy/defaults/main.yml index 21ed12cdb0..6cdf89e5bc 100644 --- a/kubernetes/ansible/roles/flink-jobs-deploy/defaults/main.yml +++ b/kubernetes/ansible/roles/flink-jobs-deploy/defaults/main.yml @@ -118,9 +118,9 @@ middleware_assessment_aggregator_table: "assessment_aggregator" ### Collection Generator Job related Vars collection_certificate_generator_consumer_parallelism: 1 collection_certificate_generator_parallelism: 1 -collection_certificate_generator_enable_suppress_exception: {{ enable_suppress_exception | lower }} -collection_certificate_generator_enable_rc_certificate: {{ enable_rc_certificate | lower }} -collection_certificate_pre_processor_enable_suppress_exception: {{ enable_suppress_exception | lower }} +collection_certificate_generator_enable_suppress_exception: "{{ enable_suppress_exception | lower }}" +collection_certificate_generator_enable_rc_certificate: "{{ enable_rc_certificate | lower }}" +collection_certificate_pre_processor_enable_suppress_exception: "{{ enable_suppress_exception | lower }}" registry_sunbird_keyspace: "sunbird" cert_registry_table: "cert_registry" From 340e9f06930120743e69a739dc7b6c3a9eb220e0 Mon Sep 17 00:00:00 2001 From: Jayaprakash8887 Date: Tue, 19 Apr 2022 15:36:08 +0530 Subject: [PATCH 016/222] Issue #SB-28066 feat: Content Auto-creator Flink Job --- kubernetes/ansible/roles/flink-jobs-deploy/defaults/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kubernetes/ansible/roles/flink-jobs-deploy/defaults/main.yml b/kubernetes/ansible/roles/flink-jobs-deploy/defaults/main.yml index 3c617dc28f..810a6deaea 100644 --- a/kubernetes/ansible/roles/flink-jobs-deploy/defaults/main.yml +++ b/kubernetes/ansible/roles/flink-jobs-deploy/defaults/main.yml @@ -287,12 +287,12 @@ audit_history_indexer_parallelism: 1 auto_creator_v2_consumer_parallelism: 1 auto_creator_v2_parallelism: 1 + ### Content Auto Creator Related Vars content_auto_creator_consumer_parallelism: 1 content_auto_creator_parallelism: 1 auto_creator_g_service_acct_cred: "{{ auto_creator_gservice_acct_cred | default('') }}" - ### MVC Indexer Related Vars mvc_indexer_consumer_parallelism: 1 mvc_indexer_parallelism: 1 From f8efe884ab537f19722f51cee869fb51688c77f4 Mon Sep 17 00:00:00 2001 From: Jayaprakash8887 Date: Tue, 19 Apr 2022 15:36:57 +0530 Subject: [PATCH 017/222] Issue #SB-28066 fix: Content Auto-creator Flink Job --- kubernetes/helm_charts/datapipeline_jobs/values.j2 | 1 + 1 file changed, 1 insertion(+) diff --git a/kubernetes/helm_charts/datapipeline_jobs/values.j2 b/kubernetes/helm_charts/datapipeline_jobs/values.j2 index fa6f5bb829..eadbe332d4 100644 --- a/kubernetes/helm_charts/datapipeline_jobs/values.j2 +++ b/kubernetes/helm_charts/datapipeline_jobs/values.j2 @@ -527,6 +527,7 @@ auto-creator-v2: jobmanager.execution.failover-strategy: region taskmanager.memory.network.fraction: 0.1 + content-auto-creator: content-auto-creator: |+ include file("/data/flink/conf/base-config.conf") From 1866b9b9303d846fc2c6050a766d1662db9616c3 Mon Sep 17 00:00:00 2001 From: Jayaprakash8887 Date: Tue, 19 Apr 2022 15:58:46 +0530 Subject: [PATCH 018/222] Issue #SB-24965 feat: Updating Local Setup details --- .../service/src/main/resources/application.conf | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/platform-modules/service/src/main/resources/application.conf b/platform-modules/service/src/main/resources/application.conf index 9d00054b19..5c057273cd 100644 --- a/platform-modules/service/src/main/resources/application.conf +++ b/platform-modules/service/src/main/resources/application.conf @@ -21,8 +21,8 @@ content.metadata.visibility.parent=["textbookunit", "courseunit", "lessonplanuni # Cassandra Configuration content.keyspace.name=content_store -cassandra.lp.connection="localhost:9042" -cassandra.lpa.connection="localhost:9042" +cassandra.lp.connection="127.0.0.1:9042,127.0.0.2:9042,127.0.0.3:9042" +cassandra.lpa.connection="127.0.0.1:9042,127.0.0.2:9042,127.0.0.3:9042" # Redis Configuration redis.host=localhost @@ -171,14 +171,6 @@ channel.default="in.ekstep" learning.content.link_dialcode_validation=true dialcode.api.search.url="http://localhost:8080/learning-service/v3/dialcode/search" dialcode.api.authorization=auth_key -dialcode.api.update.url="" -dialcode.context = { - textbook = { - primaryCategory = ["Text Book"], - schemaPath = "" - } -} - # Language-Code Configuration language.graph_ids=["as","bn","en","gu","hi","hoc","jun","ka","mai","mr","unx","or","san","sat","ta","te","urd", "pj"] @@ -229,5 +221,3 @@ content.tagging.property="subject,medium" # This is added to handle large artifacts sizes differently content.artifact.size.for_online=209715200 - -kp.search_service.base_url = "https://dev.sunbirded.org/" \ No newline at end of file From cb6ddf490239bf7343d18d4591aba27345f81b67 Mon Sep 17 00:00:00 2001 From: Mahesh Kumar Gangula Date: Wed, 4 May 2022 11:44:11 +0530 Subject: [PATCH 019/222] Issue #SB-29785 fix: bad char remove for RC certificate issue. --- kubernetes/ansible/roles/flink-jobs-deploy/defaults/main.yml | 1 + kubernetes/helm_charts/datapipeline_jobs/values.j2 | 1 + 2 files changed, 2 insertions(+) diff --git a/kubernetes/ansible/roles/flink-jobs-deploy/defaults/main.yml b/kubernetes/ansible/roles/flink-jobs-deploy/defaults/main.yml index 02858d8e7d..0826d3fb30 100644 --- a/kubernetes/ansible/roles/flink-jobs-deploy/defaults/main.yml +++ b/kubernetes/ansible/roles/flink-jobs-deploy/defaults/main.yml @@ -121,6 +121,7 @@ collection_certificate_generator_parallelism: 1 collection_certificate_generator_enable_suppress_exception: "{{ enable_suppress_exception | lower }}" collection_certificate_generator_enable_rc_certificate: "{{ enable_rc_certificate | lower }}" collection_certificate_pre_processor_enable_suppress_exception: "{{ enable_suppress_exception | lower }}" +collection_certificate_generator_rc_badcharlist: ["\x00","\\aaa","Ø","Ý"] registry_sunbird_keyspace: "sunbird" cert_registry_table: "cert_registry" diff --git a/kubernetes/helm_charts/datapipeline_jobs/values.j2 b/kubernetes/helm_charts/datapipeline_jobs/values.j2 index a707be5f97..a2acc160bb 100644 --- a/kubernetes/helm_charts/datapipeline_jobs/values.j2 +++ b/kubernetes/helm_charts/datapipeline_jobs/values.j2 @@ -739,6 +739,7 @@ collection-certificate-generator: } enable.suppress.exception = {{ collection_certificate_generator_enable_suppress_exception | lower }} enable.rc.certificate = {{ collection_certificate_generator_enable_rc_certificate | lower }} + task.rc.badcharlist : {{ collection_certificate_generator_rc_badcharlist }} flink-conf: |+ From 14c6e718511ffe63d623fa39f45b7b811183455c Mon Sep 17 00:00:00 2001 From: Mahesh Kumar Gangula Date: Wed, 4 May 2022 11:51:40 +0530 Subject: [PATCH 020/222] Issue #SB-29785 fix: bad char remove for RC certificate issue. --- kubernetes/helm_charts/datapipeline_jobs/values.j2 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kubernetes/helm_charts/datapipeline_jobs/values.j2 b/kubernetes/helm_charts/datapipeline_jobs/values.j2 index a2acc160bb..a2a5854d46 100644 --- a/kubernetes/helm_charts/datapipeline_jobs/values.j2 +++ b/kubernetes/helm_charts/datapipeline_jobs/values.j2 @@ -739,7 +739,7 @@ collection-certificate-generator: } enable.suppress.exception = {{ collection_certificate_generator_enable_suppress_exception | lower }} enable.rc.certificate = {{ collection_certificate_generator_enable_rc_certificate | lower }} - task.rc.badcharlist : {{ collection_certificate_generator_rc_badcharlist }} + task.rc.badcharlist = {{ collection_certificate_generator_rc_badcharlist }} flink-conf: |+ From 22fb440e3e1d1e0442e1df8927981642d20ae441 Mon Sep 17 00:00:00 2001 From: Mahesh Kumar Gangula Date: Wed, 4 May 2022 12:13:02 +0530 Subject: [PATCH 021/222] Issue #SB-29785 fix: bad char remove for RC certificate issue. --- kubernetes/ansible/roles/flink-jobs-deploy/defaults/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kubernetes/ansible/roles/flink-jobs-deploy/defaults/main.yml b/kubernetes/ansible/roles/flink-jobs-deploy/defaults/main.yml index 0826d3fb30..d1e2367097 100644 --- a/kubernetes/ansible/roles/flink-jobs-deploy/defaults/main.yml +++ b/kubernetes/ansible/roles/flink-jobs-deploy/defaults/main.yml @@ -121,7 +121,7 @@ collection_certificate_generator_parallelism: 1 collection_certificate_generator_enable_suppress_exception: "{{ enable_suppress_exception | lower }}" collection_certificate_generator_enable_rc_certificate: "{{ enable_rc_certificate | lower }}" collection_certificate_pre_processor_enable_suppress_exception: "{{ enable_suppress_exception | lower }}" -collection_certificate_generator_rc_badcharlist: ["\x00","\\aaa","Ø","Ý"] +collection_certificate_generator_rc_badcharlist: ["\\x00","\\aaa","Ø","Ý"] registry_sunbird_keyspace: "sunbird" cert_registry_table: "cert_registry" From 6435a05741f99e1bf1c45699c36a15bf0b6c07f8 Mon Sep 17 00:00:00 2001 From: Mahesh Kumar Gangula Date: Wed, 4 May 2022 13:07:24 +0530 Subject: [PATCH 022/222] Issue #SB-29785 fix: bad char remove for RC certificate issue. --- kubernetes/ansible/roles/flink-jobs-deploy/defaults/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kubernetes/ansible/roles/flink-jobs-deploy/defaults/main.yml b/kubernetes/ansible/roles/flink-jobs-deploy/defaults/main.yml index d1e2367097..3b21d33fbb 100644 --- a/kubernetes/ansible/roles/flink-jobs-deploy/defaults/main.yml +++ b/kubernetes/ansible/roles/flink-jobs-deploy/defaults/main.yml @@ -121,7 +121,7 @@ collection_certificate_generator_parallelism: 1 collection_certificate_generator_enable_suppress_exception: "{{ enable_suppress_exception | lower }}" collection_certificate_generator_enable_rc_certificate: "{{ enable_rc_certificate | lower }}" collection_certificate_pre_processor_enable_suppress_exception: "{{ enable_suppress_exception | lower }}" -collection_certificate_generator_rc_badcharlist: ["\\x00","\\aaa","Ø","Ý"] +collection_certificate_generator_rc_badcharlist: "\\x00,\\\\aaa,\\aaa,Ø,Ý" registry_sunbird_keyspace: "sunbird" cert_registry_table: "cert_registry" From 81a6097cb47410f6354db174bc155d9a15c882d4 Mon Sep 17 00:00:00 2001 From: Harikumar Palemkota Date: Wed, 4 May 2022 14:10:24 +0530 Subject: [PATCH 023/222] SB-29785 corrected property value --- kubernetes/ansible/roles/flink-jobs-deploy/defaults/main.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/kubernetes/ansible/roles/flink-jobs-deploy/defaults/main.yml b/kubernetes/ansible/roles/flink-jobs-deploy/defaults/main.yml index 02858d8e7d..358c7fd031 100644 --- a/kubernetes/ansible/roles/flink-jobs-deploy/defaults/main.yml +++ b/kubernetes/ansible/roles/flink-jobs-deploy/defaults/main.yml @@ -121,6 +121,7 @@ collection_certificate_generator_parallelism: 1 collection_certificate_generator_enable_suppress_exception: "{{ enable_suppress_exception | lower }}" collection_certificate_generator_enable_rc_certificate: "{{ enable_rc_certificate | lower }}" collection_certificate_pre_processor_enable_suppress_exception: "{{ enable_suppress_exception | lower }}" +collection_certificate_generator_rc_badcharlist: "{{ rc_bad_char_list | default('\\x00,\\\\aaa,\\aaa,Ø,Ý') }}" registry_sunbird_keyspace: "sunbird" cert_registry_table: "cert_registry" From 187e08cc0a23954572882a2a2f8d8dc65789540e Mon Sep 17 00:00:00 2001 From: Harikumar Palemkota Date: Wed, 4 May 2022 14:14:13 +0530 Subject: [PATCH 024/222] SB-29785 corrected property value --- kubernetes/helm_charts/datapipeline_jobs/values.j2 | 1 + 1 file changed, 1 insertion(+) diff --git a/kubernetes/helm_charts/datapipeline_jobs/values.j2 b/kubernetes/helm_charts/datapipeline_jobs/values.j2 index c9b0136da5..ccc25388a3 100644 --- a/kubernetes/helm_charts/datapipeline_jobs/values.j2 +++ b/kubernetes/helm_charts/datapipeline_jobs/values.j2 @@ -739,6 +739,7 @@ collection-certificate-generator: } enable.suppress.exception = {{ collection_certificate_generator_enable_suppress_exception | lower }} enable.rc.certificate = {{ collection_certificate_generator_enable_rc_certificate | lower }} + task.rc.badcharlist = {{ collection_certificate_generator_rc_badcharlist }} flink-conf: |+ From 2e24741b140823cef8e05eba38e0f6a0b8b40712 Mon Sep 17 00:00:00 2001 From: Harikumar Palemkota Date: Wed, 4 May 2022 16:32:33 +0530 Subject: [PATCH 025/222] SB-29785-pre-prod corrected property value --- kubernetes/ansible/roles/flink-jobs-deploy/defaults/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kubernetes/ansible/roles/flink-jobs-deploy/defaults/main.yml b/kubernetes/ansible/roles/flink-jobs-deploy/defaults/main.yml index 3b21d33fbb..ddd5856d3a 100644 --- a/kubernetes/ansible/roles/flink-jobs-deploy/defaults/main.yml +++ b/kubernetes/ansible/roles/flink-jobs-deploy/defaults/main.yml @@ -121,7 +121,7 @@ collection_certificate_generator_parallelism: 1 collection_certificate_generator_enable_suppress_exception: "{{ enable_suppress_exception | lower }}" collection_certificate_generator_enable_rc_certificate: "{{ enable_rc_certificate | lower }}" collection_certificate_pre_processor_enable_suppress_exception: "{{ enable_suppress_exception | lower }}" -collection_certificate_generator_rc_badcharlist: "\\x00,\\\\aaa,\\aaa,Ø,Ý" +collection_certificate_generator_rc_badcharlist: "{{ rc_bad_char_list | default('\"\\\\x00,\\\\\\\\aaa,\\\\aaa,Ø,Ý\"') }}" registry_sunbird_keyspace: "sunbird" cert_registry_table: "cert_registry" From 51780b03dffdc362265062d36ba5ffeff06cef76 Mon Sep 17 00:00:00 2001 From: Harikumar Palemkota Date: Wed, 4 May 2022 17:33:02 +0530 Subject: [PATCH 026/222] SB-29785-pre-prod corrected property value --- kubernetes/ansible/roles/flink-jobs-deploy/defaults/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kubernetes/ansible/roles/flink-jobs-deploy/defaults/main.yml b/kubernetes/ansible/roles/flink-jobs-deploy/defaults/main.yml index ddd5856d3a..26fdca05b1 100644 --- a/kubernetes/ansible/roles/flink-jobs-deploy/defaults/main.yml +++ b/kubernetes/ansible/roles/flink-jobs-deploy/defaults/main.yml @@ -121,7 +121,7 @@ collection_certificate_generator_parallelism: 1 collection_certificate_generator_enable_suppress_exception: "{{ enable_suppress_exception | lower }}" collection_certificate_generator_enable_rc_certificate: "{{ enable_rc_certificate | lower }}" collection_certificate_pre_processor_enable_suppress_exception: "{{ enable_suppress_exception | lower }}" -collection_certificate_generator_rc_badcharlist: "{{ rc_bad_char_list | default('\"\\\\x00,\\\\\\\\aaa,\\\\aaa,Ø,Ý\"') }}" +collection_certificate_generator_rc_badcharlist: "{{ rc_bad_char_list | default('\"\\\\x00,\\\\\\\\aaa,\\\\aaa,Ø,Ý,\\\\\"') }}" registry_sunbird_keyspace: "sunbird" cert_registry_table: "cert_registry" From d06b54df44fbe7c6168e7e754b0fde1b90ce2c05 Mon Sep 17 00:00:00 2001 From: Jayaprakash8887 Date: Mon, 9 May 2022 12:59:27 +0530 Subject: [PATCH 027/222] Issue #SB-29869 fix: Increasing checkpoint timeout --- kubernetes/helm_charts/datapipeline_jobs/values.j2 | 1 + 1 file changed, 1 insertion(+) diff --git a/kubernetes/helm_charts/datapipeline_jobs/values.j2 b/kubernetes/helm_charts/datapipeline_jobs/values.j2 index ccc25388a3..cb27e9c9af 100644 --- a/kubernetes/helm_charts/datapipeline_jobs/values.j2 +++ b/kubernetes/helm_charts/datapipeline_jobs/values.j2 @@ -541,6 +541,7 @@ content-auto-creator: consumer.parallelism = {{ content_auto_creator_consumer_parallelism }} parallelism = {{ content_auto_creator_parallelism }} window.time = 60 + checkpointing.timeout = 4200000 } redis { From 895c8c4e5b8fa5404b493bfe53c909a71a52593e Mon Sep 17 00:00:00 2001 From: Jayaprakash8887 Date: Mon, 9 May 2022 13:42:02 +0530 Subject: [PATCH 028/222] Issue #SB-29869 fix: Increasing checkpoint timeout --- kubernetes/helm_charts/datapipeline_jobs/values.j2 | 1 + 1 file changed, 1 insertion(+) diff --git a/kubernetes/helm_charts/datapipeline_jobs/values.j2 b/kubernetes/helm_charts/datapipeline_jobs/values.j2 index a707be5f97..85f3b58a0b 100644 --- a/kubernetes/helm_charts/datapipeline_jobs/values.j2 +++ b/kubernetes/helm_charts/datapipeline_jobs/values.j2 @@ -541,6 +541,7 @@ content-auto-creator: consumer.parallelism = {{ content_auto_creator_consumer_parallelism }} parallelism = {{ content_auto_creator_parallelism }} window.time = 60 + checkpointing.timeout = 4200000 } redis { From 0525d4453f5ce0bfd6cd1de379a1b76b9544c1f2 Mon Sep 17 00:00:00 2001 From: Keshav Prasad Date: Tue, 10 May 2022 16:40:36 +0530 Subject: [PATCH 029/222] fix: install az cli in order to upload backups the azure (#1721) --- ansible/roles/neo4j-backup/meta/main.yml | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 ansible/roles/neo4j-backup/meta/main.yml diff --git a/ansible/roles/neo4j-backup/meta/main.yml b/ansible/roles/neo4j-backup/meta/main.yml new file mode 100644 index 0000000000..23b18a800a --- /dev/null +++ b/ansible/roles/neo4j-backup/meta/main.yml @@ -0,0 +1,2 @@ +dependencies: + - azure-cli \ No newline at end of file From 879710e763a49197124d0841646599e1bf091297 Mon Sep 17 00:00:00 2001 From: Keshav Prasad Date: Tue, 10 May 2022 16:52:18 +0530 Subject: [PATCH 030/222] fix: ensure tasks are run as root irrespective of playbook user (#1722) --- ansible/roles/azure-cli/tasks/main.yml | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/ansible/roles/azure-cli/tasks/main.yml b/ansible/roles/azure-cli/tasks/main.yml index 4bc0c13b3b..50088a1bf0 100644 --- a/ansible/roles/azure-cli/tasks/main.yml +++ b/ansible/roles/azure-cli/tasks/main.yml @@ -1,14 +1,21 @@ - name: Import Azure signing key - #become: yes + become: yes + become_user: root shell: curl -L https://packages.microsoft.com/keys/microsoft.asc | apt-key add - - name: Add Azure apt repository + become: yes + become_user: root apt_repository: repo='deb [arch=amd64] https://packages.microsoft.com/repos/azure-cli/ {{ ansible_distribution_release }} main' state=present - name: Add distribution release security apt repository + become: yes + become_user: root apt_repository: repo='deb http://security.ubuntu.com/ubuntu bionic-security main' state=present - name: install azure cli dependency + become: yes + become_user: root apt: name={{ item }} state=present update_cache=yes #allow_unauthenticated: yes with_items: @@ -16,9 +23,10 @@ when: ansible_distribution_release == "focal" - name: ensure azure-cli and apt-transport-https is installed + become: yes + become_user: root apt: name={{ item }} state=present update_cache=yes #allow_unauthenticated: yes with_items: - apt-transport-https - - azure-cli - + - azure-cli \ No newline at end of file From 125c0d0c0ab349fd2163e0172db9be272456951d Mon Sep 17 00:00:00 2001 From: Jayaprakash8887 Date: Wed, 1 Jun 2022 12:00:29 +0530 Subject: [PATCH 031/222] Issue #SB-30107 fix: originData data format --- kubernetes/helm_charts/datapipeline_jobs/values.j2 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kubernetes/helm_charts/datapipeline_jobs/values.j2 b/kubernetes/helm_charts/datapipeline_jobs/values.j2 index cb27e9c9af..43c897fcd5 100644 --- a/kubernetes/helm_charts/datapipeline_jobs/values.j2 +++ b/kubernetes/helm_charts/datapipeline_jobs/values.j2 @@ -885,7 +885,7 @@ content-publish: full = ["appIcon", "grayScaleAppIcon", "artifactUrl", "itemSetPreviewUrl", "media"] } - nested.fields=["badgeAssertions", "targets", "badgeAssociations", "plugins", "me_totalTimeSpent", "me_totalPlaySessionCount", "me_totalTimeSpentInSec", "batches", "trackable", "credentials", "discussionForum", "provider", "osMetadata", "actions", "transcripts", "accessibility"] + nested.fields=["badgeAssertions", "targets", "badgeAssociations", "plugins", "me_totalTimeSpent", "me_totalPlaySessionCount", "me_totalTimeSpentInSec", "batches", "trackable", "credentials", "discussionForum", "provider", "osMetadata", "actions", "transcripts", "accessibility", "originData"] } cloud_storage { From 80b5e6258f3227aed73d5918f8dd6a1e9ee9d0d0 Mon Sep 17 00:00:00 2001 From: Jayaprakash8887 Date: Mon, 6 Jun 2022 16:17:23 +0530 Subject: [PATCH 032/222] Issue #SB-30118 feat: Content service to update the DIAL context --- ansible/inventory/env/group_vars/all.yml | 1 + .../helm_charts/datapipeline_jobs/values.j2 | 30 +++++++++++++++++++ 2 files changed, 31 insertions(+) diff --git a/ansible/inventory/env/group_vars/all.yml b/ansible/inventory/env/group_vars/all.yml index 9307cc79e4..9b0ecd87f0 100644 --- a/ansible/inventory/env/group_vars/all.yml +++ b/ansible/inventory/env/group_vars/all.yml @@ -97,6 +97,7 @@ middleware_hierarchy_keyspace: "{{ instance }}_hierarchy_store" kp_print_service_base_url: "http://{{private_ingressgateway_ip}}/print" cert_reg_service_base_url: "http://{{private_ingressgateway_ip}}/certreg" kp_search_service_base_url: "http://{{private_ingressgateway_ip}}/search" +kp_dial_service_base_url: "http://{{private_ingressgateway_ip}}/dial" lms_service_base_url: "http://{{private_ingressgateway_ip}}/lms" learner_service_base_url: "http://{{private_ingressgateway_ip}}/learner" sourcing_content_service_base_url: "http://{{dock_private_ingressgateway_ip | default('localhost') }}/content" diff --git a/kubernetes/helm_charts/datapipeline_jobs/values.j2 b/kubernetes/helm_charts/datapipeline_jobs/values.j2 index 43c897fcd5..7a5e103321 100644 --- a/kubernetes/helm_charts/datapipeline_jobs/values.j2 +++ b/kubernetes/helm_charts/datapipeline_jobs/values.j2 @@ -216,6 +216,7 @@ post-publish-processor: groupId = {{ env_name }}-post-publish-processor-group publish.topic = {{ env_name }}.publish.job.request qrimage.topic = {{ env_name }}.qrimage.request + dialcode.context.topic = "sunbirddev.dialcode.context.job.request" } task { consumer.parallelism = {{ post_publish_processor_consumer_parallelism }} @@ -223,6 +224,7 @@ post-publish-processor: shallow_copy.parallelism = {{ post_publish_shallow_copy_parallelism }} link_dialcode.parallelism = {{ post_publish_link_dialcode_parallelism }} batch_create.parallelism = {{ post_publish_batch_create_parallelism }} + dialcode_context_updater.parallelism = {{ post_publish_link_dialcode_parallelism }} } lms-cassandra { keyspace = "{{ middleware_course_keyspace }}" @@ -940,6 +942,7 @@ content-publish: compositesearch.index.name = "compositesearch" search.document.type = "cs" + enableDIALContextUpdate = "Yes" cloud_storage_type="{{ cloud_store }}" azure_storage_key="{{ sunbird_public_storage_account_name }}" @@ -1002,3 +1005,30 @@ qrcode-image-generator: parallelism.default: 1 jobmanager.execution.failover-strategy: region taskmanager.memory.network.fraction: 0.1 + +dialcode-context-updater: + dialcode-context-updater: |+ + include file("/data/flink/conf/base-config.conf") + kafka { + input.topic = "sunbirddev.dialcode.context.job.request" + failed.topic = "sunbirddev.dialcode.context.job.request.failed" + groupId = "sunbirddev-dialcode-group" + } + task { + consumer.parallelism = 1 + parallelism = 1 + content-auto-creator.parallelism = 1 + } + dialcode_context_updater { + actions="dialcode-context-update" + search_mode="Collection" + context_map_path = "https://sunbirddev.blob.core.windows.net/sunbird-dial-dev/schemas/local/dialcode/contextMapping.json" + identifier_search_fields = ["identifier", "primaryCategory", "channel"] + dial_code_context_read_api_path = "/dialcode/v4/read/" + dial_code_context_update_api_path = "/dialcode/v4/update/" + } + service { + content_service.basePath = "{{ kp_content_service_base_url }}" + search.basePath = "{{ kp_search_service_base_url }}" + dial_service.basePath = "{{ kp_dial_service_base_url }}" + } \ No newline at end of file From 3796c0f89126f20c5e4f87bc091c1a1b3687d30e Mon Sep 17 00:00:00 2001 From: Jayaprakash8887 Date: Mon, 6 Jun 2022 16:20:29 +0530 Subject: [PATCH 033/222] Issue #SB-30118 feat: Content service to update the DIAL context --- kubernetes/ansible/roles/flink-jobs-deploy/defaults/main.yml | 1 + kubernetes/helm_charts/datapipeline_jobs/values.j2 | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/kubernetes/ansible/roles/flink-jobs-deploy/defaults/main.yml b/kubernetes/ansible/roles/flink-jobs-deploy/defaults/main.yml index 26fdca05b1..98ce28c7b2 100644 --- a/kubernetes/ansible/roles/flink-jobs-deploy/defaults/main.yml +++ b/kubernetes/ansible/roles/flink-jobs-deploy/defaults/main.yml @@ -70,6 +70,7 @@ post_publish_event_router_parallelism: 1 post_publish_shallow_copy_parallelism: 1 post_publish_link_dialcode_parallelism: 1 post_publish_batch_create_parallelism: 1 +post_publish_dialcode_context_parallelism: 1 ### Certificate Job related Vars certificate_generator_consumer_parallelism: 1 diff --git a/kubernetes/helm_charts/datapipeline_jobs/values.j2 b/kubernetes/helm_charts/datapipeline_jobs/values.j2 index 7a5e103321..4f7ef99066 100644 --- a/kubernetes/helm_charts/datapipeline_jobs/values.j2 +++ b/kubernetes/helm_charts/datapipeline_jobs/values.j2 @@ -224,7 +224,7 @@ post-publish-processor: shallow_copy.parallelism = {{ post_publish_shallow_copy_parallelism }} link_dialcode.parallelism = {{ post_publish_link_dialcode_parallelism }} batch_create.parallelism = {{ post_publish_batch_create_parallelism }} - dialcode_context_updater.parallelism = {{ post_publish_link_dialcode_parallelism }} + dialcode_context_updater.parallelism = {{ post_publish_dialcode_context_parallelism }} } lms-cassandra { keyspace = "{{ middleware_course_keyspace }}" From b4937239a6ebe08add5f2fb1dfeb5c6d68fcf75c Mon Sep 17 00:00:00 2001 From: Jayaprakash8887 Date: Mon, 6 Jun 2022 16:24:21 +0530 Subject: [PATCH 034/222] Issue #SB-30118 feat: Content service to update the DIAL context --- kubernetes/helm_charts/datapipeline_jobs/values.j2 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kubernetes/helm_charts/datapipeline_jobs/values.j2 b/kubernetes/helm_charts/datapipeline_jobs/values.j2 index 4f7ef99066..aa439fb43f 100644 --- a/kubernetes/helm_charts/datapipeline_jobs/values.j2 +++ b/kubernetes/helm_charts/datapipeline_jobs/values.j2 @@ -216,7 +216,7 @@ post-publish-processor: groupId = {{ env_name }}-post-publish-processor-group publish.topic = {{ env_name }}.publish.job.request qrimage.topic = {{ env_name }}.qrimage.request - dialcode.context.topic = "sunbirddev.dialcode.context.job.request" + dialcode.context.topic = {{ env_name }}.dialcode.context.job.request } task { consumer.parallelism = {{ post_publish_processor_consumer_parallelism }} From c378ed22bb280accba8d42fc9eb34cbb3012478e Mon Sep 17 00:00:00 2001 From: Jayaprakash8887 Date: Mon, 6 Jun 2022 16:27:28 +0530 Subject: [PATCH 035/222] Issue #SB-30118 feat: Content service to update the DIAL context --- kubernetes/helm_charts/datapipeline_jobs/values.j2 | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/kubernetes/helm_charts/datapipeline_jobs/values.j2 b/kubernetes/helm_charts/datapipeline_jobs/values.j2 index aa439fb43f..906b1057f6 100644 --- a/kubernetes/helm_charts/datapipeline_jobs/values.j2 +++ b/kubernetes/helm_charts/datapipeline_jobs/values.j2 @@ -1010,14 +1010,14 @@ dialcode-context-updater: dialcode-context-updater: |+ include file("/data/flink/conf/base-config.conf") kafka { - input.topic = "sunbirddev.dialcode.context.job.request" - failed.topic = "sunbirddev.dialcode.context.job.request.failed" - groupId = "sunbirddev-dialcode-group" + input.topic = "{{ env_name }}.dialcode.context.job.request" + failed.topic = "{{ env_name }}.dialcode.context.job.request.failed" + groupId = "{{ env_name }}-dialcode-group" } task { consumer.parallelism = 1 parallelism = 1 - content-auto-creator.parallelism = 1 + dialcode-context-updater.parallelism = 1 } dialcode_context_updater { actions="dialcode-context-update" From cf5247d6c1e0f260427d2d4de1e007cbbfcdb29f Mon Sep 17 00:00:00 2001 From: Jayaprakash8887 Date: Mon, 6 Jun 2022 16:44:06 +0530 Subject: [PATCH 036/222] Issue #SB-30118 feat: Content service to update the DIAL context --- .../ansible/roles/flink-jobs-deploy/defaults/main.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/kubernetes/ansible/roles/flink-jobs-deploy/defaults/main.yml b/kubernetes/ansible/roles/flink-jobs-deploy/defaults/main.yml index 98ce28c7b2..794c5263dd 100644 --- a/kubernetes/ansible/roles/flink-jobs-deploy/defaults/main.yml +++ b/kubernetes/ansible/roles/flink-jobs-deploy/defaults/main.yml @@ -258,6 +258,13 @@ flink_job_names: taskmanager_memory: 1024m taskslots: 1 cpu_requests: 0.3 + dialcode-context-updater: + job_class_name: 'org.sunbird.job.dialcodecontextupdater.task.DialcodeContextUpdaterStreamTask' + replica: 1 + jobmanager_memory: 1024m + taskmanager_memory: 1024m + taskslots: 1 + cpu_requests: 0.3 ### Global vars middleware_course_keyspace: "sunbird_courses" From 9a1834baf17e2b08e9d8ed036c8c8922d1f225e8 Mon Sep 17 00:00:00 2001 From: Jayaprakash8887 Date: Tue, 7 Jun 2022 16:11:10 +0530 Subject: [PATCH 037/222] Issue #SB-30118 feat: Content service to update the DIAL context --- kubernetes/helm_charts/datapipeline_jobs/values.j2 | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/kubernetes/helm_charts/datapipeline_jobs/values.j2 b/kubernetes/helm_charts/datapipeline_jobs/values.j2 index 906b1057f6..4d982d362e 100644 --- a/kubernetes/helm_charts/datapipeline_jobs/values.j2 +++ b/kubernetes/helm_charts/datapipeline_jobs/values.j2 @@ -1031,4 +1031,12 @@ dialcode-context-updater: content_service.basePath = "{{ kp_content_service_base_url }}" search.basePath = "{{ kp_search_service_base_url }}" dial_service.basePath = "{{ kp_dial_service_base_url }}" - } \ No newline at end of file + } + + flink-conf: |+ + jobmanager.memory.flink.size: {{ flink_job_names['qrcode-image-generator'].jobmanager_memory }} + taskmanager.memory.flink.size: {{ flink_job_names['qrcode-image-generator'].taskmanager_memory }} + taskmanager.numberOfTaskSlots: {{ flink_job_names['qrcode-image-generator'].taskslots }} + parallelism.default: 1 + jobmanager.execution.failover-strategy: region + taskmanager.memory.network.fraction: 0.1 \ No newline at end of file From 558e3c22f203ace9544fb6d4ea4e6a14b22b90cd Mon Sep 17 00:00:00 2001 From: Jayaprakash8887 Date: Tue, 7 Jun 2022 18:29:34 +0530 Subject: [PATCH 038/222] Issue #SB-30118 feat: Content service to update the DIAL context --- ansible/roles/setup-kafka/defaults/main.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/ansible/roles/setup-kafka/defaults/main.yml b/ansible/roles/setup-kafka/defaults/main.yml index 5a2934a8b5..fada28a137 100644 --- a/ansible/roles/setup-kafka/defaults/main.yml +++ b/ansible/roles/setup-kafka/defaults/main.yml @@ -129,6 +129,9 @@ processing_kafka_topics: - name: object.import.request num_of_partitions: 1 replication_factor: 1 + - name: dialcode.context.job.request + num_of_partitions: 1 + replication_factor: 1 processing_kafka_overriden_topics: - name: telemetry.raw From 09c6c534cfe01f537dc7372b6f9300efcf68cacb Mon Sep 17 00:00:00 2001 From: Jayaprakash8887 Date: Tue, 7 Jun 2022 18:33:50 +0530 Subject: [PATCH 039/222] Issue #SB-30118 feat: Content service to update the DIAL context --- ansible/roles/setup-kafka/defaults/main.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ansible/roles/setup-kafka/defaults/main.yml b/ansible/roles/setup-kafka/defaults/main.yml index fada28a137..6799561a95 100644 --- a/ansible/roles/setup-kafka/defaults/main.yml +++ b/ansible/roles/setup-kafka/defaults/main.yml @@ -130,8 +130,8 @@ processing_kafka_topics: num_of_partitions: 1 replication_factor: 1 - name: dialcode.context.job.request - num_of_partitions: 1 - replication_factor: 1 + num_of_partitions: 1 + replication_factor: 1 processing_kafka_overriden_topics: - name: telemetry.raw From aee77448774660a7e3954f4486ef98d73a236e57 Mon Sep 17 00:00:00 2001 From: Jayaprakash8887 Date: Tue, 7 Jun 2022 18:36:38 +0530 Subject: [PATCH 040/222] Issue #SB-30118 feat: Content service to update the DIAL context --- ansible/roles/setup-kafka/defaults/main.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/ansible/roles/setup-kafka/defaults/main.yml b/ansible/roles/setup-kafka/defaults/main.yml index 6799561a95..3f3b6ac7f8 100644 --- a/ansible/roles/setup-kafka/defaults/main.yml +++ b/ansible/roles/setup-kafka/defaults/main.yml @@ -255,3 +255,6 @@ processing_kafka_overriden_topics: - name: object.import.request retention_time: 1209600000 replication_factor: 1 + - name: dialcode.context.job.request + retention_time: 1209600000 + replication_factor: 1 \ No newline at end of file From 158ab1631623343d603aedaf6ac47e1a1f768809 Mon Sep 17 00:00:00 2001 From: Jayaprakash8887 Date: Thu, 9 Jun 2022 11:24:17 +0530 Subject: [PATCH 041/222] Issue #SB-301078 fix: Patch reversal --- kubernetes/helm_charts/datapipeline_jobs/values.j2 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kubernetes/helm_charts/datapipeline_jobs/values.j2 b/kubernetes/helm_charts/datapipeline_jobs/values.j2 index 43c897fcd5..cb27e9c9af 100644 --- a/kubernetes/helm_charts/datapipeline_jobs/values.j2 +++ b/kubernetes/helm_charts/datapipeline_jobs/values.j2 @@ -885,7 +885,7 @@ content-publish: full = ["appIcon", "grayScaleAppIcon", "artifactUrl", "itemSetPreviewUrl", "media"] } - nested.fields=["badgeAssertions", "targets", "badgeAssociations", "plugins", "me_totalTimeSpent", "me_totalPlaySessionCount", "me_totalTimeSpentInSec", "batches", "trackable", "credentials", "discussionForum", "provider", "osMetadata", "actions", "transcripts", "accessibility", "originData"] + nested.fields=["badgeAssertions", "targets", "badgeAssociations", "plugins", "me_totalTimeSpent", "me_totalPlaySessionCount", "me_totalTimeSpentInSec", "batches", "trackable", "credentials", "discussionForum", "provider", "osMetadata", "actions", "transcripts", "accessibility"] } cloud_storage { From f36b221701008170034f128ceb64caaffe3529c0 Mon Sep 17 00:00:00 2001 From: Jayaprakash8887 Date: Fri, 10 Jun 2022 15:19:09 +0530 Subject: [PATCH 042/222] Issue #SB-30118 feat: DIAL context update by content service --- kubernetes/helm_charts/datapipeline_jobs/values.j2 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kubernetes/helm_charts/datapipeline_jobs/values.j2 b/kubernetes/helm_charts/datapipeline_jobs/values.j2 index 4d982d362e..9fe6c66363 100644 --- a/kubernetes/helm_charts/datapipeline_jobs/values.j2 +++ b/kubernetes/helm_charts/datapipeline_jobs/values.j2 @@ -1022,7 +1022,7 @@ dialcode-context-updater: dialcode_context_updater { actions="dialcode-context-update" search_mode="Collection" - context_map_path = "https://sunbirddev.blob.core.windows.net/sunbird-dial-dev/schemas/local/dialcode/contextMapping.json" + context_map_path = "https://raw.githubusercontent.com/project-sunbird/knowledge-platform-jobs/dialcode-context-updater/dialcode-context-updater/src/main/resources/contextMapping.json" identifier_search_fields = ["identifier", "primaryCategory", "channel"] dial_code_context_read_api_path = "/dialcode/v4/read/" dial_code_context_update_api_path = "/dialcode/v4/update/" From e7a956f5547347ef4d74f4c1b78eb860262e4b47 Mon Sep 17 00:00:00 2001 From: G33tha Date: Thu, 16 Jun 2022 12:00:23 +0530 Subject: [PATCH 043/222] Update main.yml --- ansible/roles/neo4j-deploy/tasks/main.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ansible/roles/neo4j-deploy/tasks/main.yml b/ansible/roles/neo4j-deploy/tasks/main.yml index 02d94a9858..3281c49a2f 100644 --- a/ansible/roles/neo4j-deploy/tasks/main.yml +++ b/ansible/roles/neo4j-deploy/tasks/main.yml @@ -1,5 +1,5 @@ -- name: checking the list of installed services - service_facts: +#- name: checking the list of installed services +# service_facts: - name: Stop the monit service: name=monit state=stopped From 78036927344a2925bff416772e30de181c7eb403 Mon Sep 17 00:00:00 2001 From: Jayaprakash8887 Date: Tue, 21 Jun 2022 10:27:24 +0530 Subject: [PATCH 044/222] Issue #SB-30332 fix: Config update --- kubernetes/helm_charts/datapipeline_jobs/values.j2 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kubernetes/helm_charts/datapipeline_jobs/values.j2 b/kubernetes/helm_charts/datapipeline_jobs/values.j2 index cb27e9c9af..d0e1217620 100644 --- a/kubernetes/helm_charts/datapipeline_jobs/values.j2 +++ b/kubernetes/helm_charts/datapipeline_jobs/values.j2 @@ -570,7 +570,7 @@ content-auto-creator: content_mandatory_fields=["name","code","mimeType","primaryCategory","artifactUrl","lastPublishedBy"] content_props_to_removed=["identifier","downloadUrl","variants","createdOn","collections","children","lastUpdatedOn","SYS_INTERNAL_LAST_UPDATED_ON","versionKey","s3Key","status","pkgVersion","toc_url","mimeTypesCount","contentTypesCount","leafNodesCount","childNodes","prevState","lastPublishedOn","flagReasons","compatibilityLevel","size","publishChecklist","publishComment","lastPublishedBy","rejectReasons","rejectComment","badgeAssertions","leafNodes","sYS_INTERNAL_LAST_UPDATED_ON","previewUrl","channel","objectType","visibility","version","pragma","prevStatus","streamingUrl","idealScreenSize","contentDisposition","lastStatusChangedOn","idealScreenDensity","lastSubmittedOn","publishError","flaggedBy","flags","lastFlaggedOn","publisher","lastUpdatedBy","lastSubmittedBy","uploadError","lockKey","publish_type","reviewError","totalCompressedSize","origin","originData","importError","questions"] bulk_upload_mime_types=["video/mp4"] - artifact_upload_max_size=52428800 + artifact_upload_max_size=157286400 content_create_props=["name","code","mimeType","contentType","framework","processId","primaryCategory"] artifact_upload_allowed_source=[] g_service_acct_cred="{{ auto_creator_g_service_acct_cred }}" From 7aa99f9e6072f0d1cd96790c9485d1bd4a727b8c Mon Sep 17 00:00:00 2001 From: Jayaprakash8887 Date: Tue, 21 Jun 2022 14:47:10 +0530 Subject: [PATCH 045/222] Issue #SB-30118 feat: DIAL context updater --- kubernetes/helm_charts/datapipeline_jobs/values.j2 | 1 - 1 file changed, 1 deletion(-) diff --git a/kubernetes/helm_charts/datapipeline_jobs/values.j2 b/kubernetes/helm_charts/datapipeline_jobs/values.j2 index 9fe6c66363..8297c87f32 100644 --- a/kubernetes/helm_charts/datapipeline_jobs/values.j2 +++ b/kubernetes/helm_charts/datapipeline_jobs/values.j2 @@ -1028,7 +1028,6 @@ dialcode-context-updater: dial_code_context_update_api_path = "/dialcode/v4/update/" } service { - content_service.basePath = "{{ kp_content_service_base_url }}" search.basePath = "{{ kp_search_service_base_url }}" dial_service.basePath = "{{ kp_dial_service_base_url }}" } From cc8a6a23de611c9a10275bb3bdadedcb1f84bfee Mon Sep 17 00:00:00 2001 From: AmiableAnil Date: Tue, 28 Jun 2022 19:00:59 +0530 Subject: [PATCH 046/222] Issue #SB-25534 chore: Removed publish-pipeline from samza job distribution. --- platform-jobs/samza/distribution/pom.xml | 18 ++---------------- 1 file changed, 2 insertions(+), 16 deletions(-) diff --git a/platform-jobs/samza/distribution/pom.xml b/platform-jobs/samza/distribution/pom.xml index ab285c244d..55b51b2806 100644 --- a/platform-jobs/samza/distribution/pom.xml +++ b/platform-jobs/samza/distribution/pom.xml @@ -11,18 +11,11 @@ - - + + - - org.sunbird - publish-pipeline - 0.0.386 - tar.gz - distribution - org.sunbird merge-user-courses @@ -30,13 +23,6 @@ tar.gz distribution - - - - - - - org.sunbird mvc-processor-indexer From 5236209138ed0ee3898ac20b4896452f3df0a18e Mon Sep 17 00:00:00 2001 From: Jayaprakash8887 Date: Mon, 25 Jul 2022 15:04:53 +0530 Subject: [PATCH 047/222] Issue #SB-00000 debug: Framework Actor issue debug in knowlg BB server --- .../org/sunbird/learning/actor/FrameworkHierarchyActor.java | 1 + .../org/sunbird/learning/framework/FrameworkHierarchy.java | 6 +++++- .../org/sunbird/learning/router/LearningRequestRouter.java | 3 +++ 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/platform-modules/actors/src/main/java/org/sunbird/learning/actor/FrameworkHierarchyActor.java b/platform-modules/actors/src/main/java/org/sunbird/learning/actor/FrameworkHierarchyActor.java index 2d79096539..d2fd0fed4c 100644 --- a/platform-modules/actors/src/main/java/org/sunbird/learning/actor/FrameworkHierarchyActor.java +++ b/platform-modules/actors/src/main/java/org/sunbird/learning/actor/FrameworkHierarchyActor.java @@ -34,6 +34,7 @@ protected void invokeMethod(Request request, ActorRef parent) { } else { if (StringUtils.equalsIgnoreCase(FrameworkHierarchyOperations.generateFrameworkHierarchy.name(), methodName)) { String id = (String) request.get("identifier"); + TelemetryManager.log("FrameworkHierarchyActor: invoking generateFrameworkHierarchy method" ); fwHierarchy.generateFrameworkHierarchy(id); OK(parent); } else if(StringUtils.equalsIgnoreCase(FrameworkHierarchyOperations.getFrameworkHierarchy.name(), methodName)){ diff --git a/platform-modules/actors/src/main/java/org/sunbird/learning/framework/FrameworkHierarchy.java b/platform-modules/actors/src/main/java/org/sunbird/learning/framework/FrameworkHierarchy.java index 235bfecc88..278d3028db 100644 --- a/platform-modules/actors/src/main/java/org/sunbird/learning/framework/FrameworkHierarchy.java +++ b/platform-modules/actors/src/main/java/org/sunbird/learning/framework/FrameworkHierarchy.java @@ -21,6 +21,7 @@ import org.sunbird.graph.model.cache.CategoryCache; import org.sunbird.graph.model.node.DefinitionDTO; import org.sunbird.learning.hierarchy.store.HierarchyStore; +import org.sunbird.telemetry.logger.TelemetryManager; import java.util.ArrayList; import java.util.Collections; @@ -53,15 +54,17 @@ public class FrameworkHierarchy extends BaseManager { */ public void generateFrameworkHierarchy(String id) throws Exception { Response responseNode = getDataNode(GRAPH_ID, id); + TelemetryManager.log("FrameworkHierarchy: generateFrameworkHierarchy method"); if (checkError(responseNode)) throw new ResourceNotFoundException("ERR_DATA_NOT_FOUND", "Data not found with id : " + id); Node node = (Node) responseNode.get(GraphDACParams.node.name()); if (StringUtils.equalsIgnoreCase(node.getObjectType(), "Framework")) { FrameworkCache.delete(id); + TelemetryManager.log("FrameworkHierarchy: generateFrameworkHierarchy method:: After Deleting Framework Cache"); Map frameworkDocument = new HashMap<>(); Map frameworkHierarchy = getHierarchy(node.getIdentifier(), 0, false, true); CategoryCache.setFramework(node.getIdentifier(), frameworkHierarchy); - + TelemetryManager.log("FrameworkHierarchy: generateFrameworkHierarchy method:: After generating Hierarchy: frameworkHierarchy: " + frameworkHierarchy); frameworkDocument.putAll(frameworkHierarchy); frameworkDocument.put("identifier", node.getIdentifier()); frameworkDocument.put("objectType", node.getObjectType()); @@ -71,6 +74,7 @@ public void generateFrameworkHierarchy(String id) throws Exception { if(null!=node.getMetadata().get(field)) frameworkDocument.put(field, node.getMetadata().get(field)); } + TelemetryManager.log("FrameworkHierarchy: generateFrameworkHierarchy method::Before saving Hierarchy to cassandra " ); hierarchyStore.saveOrUpdateHierarchy(node.getIdentifier(),frameworkDocument); } else { throw new ClientException(ResponseCode.CLIENT_ERROR.name(), "The object with given identifier is not a framework: " + id); diff --git a/platform-modules/actors/src/main/java/org/sunbird/learning/router/LearningRequestRouter.java b/platform-modules/actors/src/main/java/org/sunbird/learning/router/LearningRequestRouter.java index b9277b9ec5..b254f8e8aa 100644 --- a/platform-modules/actors/src/main/java/org/sunbird/learning/router/LearningRequestRouter.java +++ b/platform-modules/actors/src/main/java/org/sunbird/learning/router/LearningRequestRouter.java @@ -92,9 +92,11 @@ private void initActorPool() { ActorRef contentStoreActor = system.actorOf(new SmallestMailboxPool(poolSize).props(contentStoreProps)); ActorRef fwHierarchyActor = system.actorOf(new SmallestMailboxPool(poolSize).props(fwhierarchyProps)); ActorRef localCacheUpdaterActor = system.actorOf(new SmallestMailboxPool(poolSize).props(localCacheUpdaterProps)); + TelemetryManager.log("LearningRequestRouter: initActorPool:: Before adding actors to pool" ); LearningActorPool.addActorRefToPool(LearningActorNames.CONTENT_STORE_ACTOR.name(), contentStoreActor); LearningActorPool.addActorRefToPool(LearningActorNames.FRAMEWORK_HIERARCHY_ACTOR.name(), fwHierarchyActor); LearningActorPool.addActorRefToPool(LearningActorNames.CACHE_UPDATE_ACTOR.name(), localCacheUpdaterActor); + TelemetryManager.log("LearningRequestRouter: initActorPool:: After adding actors to pool" ); } /** @@ -110,6 +112,7 @@ private ActorRef getActorFromPool(Request request) { if (null == ref) throw new ClientException(LearningErrorCodes.ERR_ROUTER_ACTOR_NOT_FOUND.name(), "Actor not found in the pool for manager: " + manager); + TelemetryManager.log("LearningRequestRouter: getActorFromPool:: Actor ref: " + ref ); return ref; } From 16408ed990922b539e9b160ae9f33803907f7fe1 Mon Sep 17 00:00:00 2001 From: Jayaprakash8887 Date: Tue, 26 Jul 2022 11:46:11 +0530 Subject: [PATCH 048/222] Issue #SB-00000 debug: Framework Actor issue debug in knowlg BB server --- .../sunbird/learning/actor/FrameworkHierarchyActor.java | 4 ++-- .../sunbird/learning/framework/FrameworkHierarchy.java | 8 ++++---- .../sunbird/learning/router/LearningRequestRouter.java | 6 +++--- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/platform-modules/actors/src/main/java/org/sunbird/learning/actor/FrameworkHierarchyActor.java b/platform-modules/actors/src/main/java/org/sunbird/learning/actor/FrameworkHierarchyActor.java index d2fd0fed4c..8c58a6ecaf 100644 --- a/platform-modules/actors/src/main/java/org/sunbird/learning/actor/FrameworkHierarchyActor.java +++ b/platform-modules/actors/src/main/java/org/sunbird/learning/actor/FrameworkHierarchyActor.java @@ -34,7 +34,7 @@ protected void invokeMethod(Request request, ActorRef parent) { } else { if (StringUtils.equalsIgnoreCase(FrameworkHierarchyOperations.generateFrameworkHierarchy.name(), methodName)) { String id = (String) request.get("identifier"); - TelemetryManager.log("FrameworkHierarchyActor: invoking generateFrameworkHierarchy method" ); + TelemetryManager.info("FrameworkHierarchyActor: invoking generateFrameworkHierarchy method" ); fwHierarchy.generateFrameworkHierarchy(id); OK(parent); } else if(StringUtils.equalsIgnoreCase(FrameworkHierarchyOperations.getFrameworkHierarchy.name(), methodName)){ @@ -42,7 +42,7 @@ protected void invokeMethod(Request request, ActorRef parent) { Map frameworkData = fwHierarchy.getFrameworkHierarchy(frameworkId); OK("framework", frameworkData, sender()); } else { - TelemetryManager.log("Unsupported operation: " + methodName); + TelemetryManager.info("Unsupported operation: " + methodName); throw new ClientException(LearningErrorCodes.ERR_INVALID_OPERATION.name(), "Unsupported operation: " + methodName); } diff --git a/platform-modules/actors/src/main/java/org/sunbird/learning/framework/FrameworkHierarchy.java b/platform-modules/actors/src/main/java/org/sunbird/learning/framework/FrameworkHierarchy.java index 278d3028db..a4c65187da 100644 --- a/platform-modules/actors/src/main/java/org/sunbird/learning/framework/FrameworkHierarchy.java +++ b/platform-modules/actors/src/main/java/org/sunbird/learning/framework/FrameworkHierarchy.java @@ -54,17 +54,17 @@ public class FrameworkHierarchy extends BaseManager { */ public void generateFrameworkHierarchy(String id) throws Exception { Response responseNode = getDataNode(GRAPH_ID, id); - TelemetryManager.log("FrameworkHierarchy: generateFrameworkHierarchy method"); + TelemetryManager.info("FrameworkHierarchy: generateFrameworkHierarchy method"); if (checkError(responseNode)) throw new ResourceNotFoundException("ERR_DATA_NOT_FOUND", "Data not found with id : " + id); Node node = (Node) responseNode.get(GraphDACParams.node.name()); if (StringUtils.equalsIgnoreCase(node.getObjectType(), "Framework")) { FrameworkCache.delete(id); - TelemetryManager.log("FrameworkHierarchy: generateFrameworkHierarchy method:: After Deleting Framework Cache"); + TelemetryManager.info("FrameworkHierarchy: generateFrameworkHierarchy method:: After Deleting Framework Cache"); Map frameworkDocument = new HashMap<>(); Map frameworkHierarchy = getHierarchy(node.getIdentifier(), 0, false, true); CategoryCache.setFramework(node.getIdentifier(), frameworkHierarchy); - TelemetryManager.log("FrameworkHierarchy: generateFrameworkHierarchy method:: After generating Hierarchy: frameworkHierarchy: " + frameworkHierarchy); + TelemetryManager.info("FrameworkHierarchy: generateFrameworkHierarchy method:: After generating Hierarchy: frameworkHierarchy: " + frameworkHierarchy); frameworkDocument.putAll(frameworkHierarchy); frameworkDocument.put("identifier", node.getIdentifier()); frameworkDocument.put("objectType", node.getObjectType()); @@ -74,7 +74,7 @@ public void generateFrameworkHierarchy(String id) throws Exception { if(null!=node.getMetadata().get(field)) frameworkDocument.put(field, node.getMetadata().get(field)); } - TelemetryManager.log("FrameworkHierarchy: generateFrameworkHierarchy method::Before saving Hierarchy to cassandra " ); + TelemetryManager.info("FrameworkHierarchy: generateFrameworkHierarchy method::Before saving Hierarchy to cassandra " ); hierarchyStore.saveOrUpdateHierarchy(node.getIdentifier(),frameworkDocument); } else { throw new ClientException(ResponseCode.CLIENT_ERROR.name(), "The object with given identifier is not a framework: " + id); diff --git a/platform-modules/actors/src/main/java/org/sunbird/learning/router/LearningRequestRouter.java b/platform-modules/actors/src/main/java/org/sunbird/learning/router/LearningRequestRouter.java index b254f8e8aa..525ddeba05 100644 --- a/platform-modules/actors/src/main/java/org/sunbird/learning/router/LearningRequestRouter.java +++ b/platform-modules/actors/src/main/java/org/sunbird/learning/router/LearningRequestRouter.java @@ -92,11 +92,11 @@ private void initActorPool() { ActorRef contentStoreActor = system.actorOf(new SmallestMailboxPool(poolSize).props(contentStoreProps)); ActorRef fwHierarchyActor = system.actorOf(new SmallestMailboxPool(poolSize).props(fwhierarchyProps)); ActorRef localCacheUpdaterActor = system.actorOf(new SmallestMailboxPool(poolSize).props(localCacheUpdaterProps)); - TelemetryManager.log("LearningRequestRouter: initActorPool:: Before adding actors to pool" ); + TelemetryManager.info("LearningRequestRouter: initActorPool:: Before adding actors to pool" ); LearningActorPool.addActorRefToPool(LearningActorNames.CONTENT_STORE_ACTOR.name(), contentStoreActor); LearningActorPool.addActorRefToPool(LearningActorNames.FRAMEWORK_HIERARCHY_ACTOR.name(), fwHierarchyActor); LearningActorPool.addActorRefToPool(LearningActorNames.CACHE_UPDATE_ACTOR.name(), localCacheUpdaterActor); - TelemetryManager.log("LearningRequestRouter: initActorPool:: After adding actors to pool" ); + TelemetryManager.info("LearningRequestRouter: initActorPool:: After adding actors to pool" ); } /** @@ -112,7 +112,7 @@ private ActorRef getActorFromPool(Request request) { if (null == ref) throw new ClientException(LearningErrorCodes.ERR_ROUTER_ACTOR_NOT_FOUND.name(), "Actor not found in the pool for manager: " + manager); - TelemetryManager.log("LearningRequestRouter: getActorFromPool:: Actor ref: " + ref ); + TelemetryManager.info("LearningRequestRouter: getActorFromPool:: Actor ref: " + ref ); return ref; } From 20591f594aa155e63fe236d514bdc9eecac2402c Mon Sep 17 00:00:00 2001 From: Jayaprakash8887 Date: Fri, 29 Jul 2022 15:53:30 +0530 Subject: [PATCH 049/222] Issue #SB-30118 feat: DIAL context updater topics --- ansible/roles/setup-kafka/defaults/main.yml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/ansible/roles/setup-kafka/defaults/main.yml b/ansible/roles/setup-kafka/defaults/main.yml index 3f3b6ac7f8..3936489237 100644 --- a/ansible/roles/setup-kafka/defaults/main.yml +++ b/ansible/roles/setup-kafka/defaults/main.yml @@ -132,6 +132,9 @@ processing_kafka_topics: - name: dialcode.context.job.request num_of_partitions: 1 replication_factor: 1 + - name: dialcode.context.job.request.failed + num_of_partitions: 1 + replication_factor: 1 processing_kafka_overriden_topics: - name: telemetry.raw @@ -257,4 +260,7 @@ processing_kafka_overriden_topics: replication_factor: 1 - name: dialcode.context.job.request retention_time: 1209600000 - replication_factor: 1 \ No newline at end of file + replication_factor: 1 + - name: dialcode.context.job.request.failed + retention_time: 1209600000 + replication_factor: 1 \ No newline at end of file From 25c8ae79596681f23f102d6708b4f48bbf5d1dc6 Mon Sep 17 00:00:00 2001 From: Jayaprakash8887 Date: Fri, 29 Jul 2022 15:55:08 +0530 Subject: [PATCH 050/222] Issue #SB-30118 feat: DIAL context updater topics --- ansible/roles/setup-kafka/defaults/main.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/ansible/roles/setup-kafka/defaults/main.yml b/ansible/roles/setup-kafka/defaults/main.yml index 3936489237..6e70a29d56 100644 --- a/ansible/roles/setup-kafka/defaults/main.yml +++ b/ansible/roles/setup-kafka/defaults/main.yml @@ -133,8 +133,8 @@ processing_kafka_topics: num_of_partitions: 1 replication_factor: 1 - name: dialcode.context.job.request.failed - num_of_partitions: 1 - replication_factor: 1 + num_of_partitions: 1 + replication_factor: 1 processing_kafka_overriden_topics: - name: telemetry.raw @@ -262,5 +262,5 @@ processing_kafka_overriden_topics: retention_time: 1209600000 replication_factor: 1 - name: dialcode.context.job.request.failed - retention_time: 1209600000 - replication_factor: 1 \ No newline at end of file + retention_time: 1209600000 + replication_factor: 1 \ No newline at end of file From c91e864ba49d9675522f9441b788c13f0b0daf05 Mon Sep 17 00:00:00 2001 From: Jayaprakash8887 Date: Tue, 2 Aug 2022 10:43:07 +0530 Subject: [PATCH 051/222] Issue #SB-30670 fix: Context Info update failure due to node ES sync delay --- kubernetes/helm_charts/datapipeline_jobs/values.j2 | 2 ++ 1 file changed, 2 insertions(+) diff --git a/kubernetes/helm_charts/datapipeline_jobs/values.j2 b/kubernetes/helm_charts/datapipeline_jobs/values.j2 index 8297c87f32..0c0148eae4 100644 --- a/kubernetes/helm_charts/datapipeline_jobs/values.j2 +++ b/kubernetes/helm_charts/datapipeline_jobs/values.j2 @@ -1032,6 +1032,8 @@ dialcode-context-updater: dial_service.basePath = "{{ kp_dial_service_base_url }}" } + es_sync_wait_time = 20000 + flink-conf: |+ jobmanager.memory.flink.size: {{ flink_job_names['qrcode-image-generator'].jobmanager_memory }} taskmanager.memory.flink.size: {{ flink_job_names['qrcode-image-generator'].taskmanager_memory }} From bc858c33f9015c88a5c6d828f945005a551a203b Mon Sep 17 00:00:00 2001 From: Jayaprakash8887 Date: Tue, 2 Aug 2022 11:17:20 +0530 Subject: [PATCH 052/222] Issue #SB-0000 fix: Reverting debug statements --- .../org/sunbird/learning/actor/FrameworkHierarchyActor.java | 2 -- .../org/sunbird/learning/framework/FrameworkHierarchy.java | 4 ---- .../org/sunbird/learning/router/LearningRequestRouter.java | 3 --- 3 files changed, 9 deletions(-) diff --git a/platform-modules/actors/src/main/java/org/sunbird/learning/actor/FrameworkHierarchyActor.java b/platform-modules/actors/src/main/java/org/sunbird/learning/actor/FrameworkHierarchyActor.java index 8c58a6ecaf..7466db10cf 100644 --- a/platform-modules/actors/src/main/java/org/sunbird/learning/actor/FrameworkHierarchyActor.java +++ b/platform-modules/actors/src/main/java/org/sunbird/learning/actor/FrameworkHierarchyActor.java @@ -34,7 +34,6 @@ protected void invokeMethod(Request request, ActorRef parent) { } else { if (StringUtils.equalsIgnoreCase(FrameworkHierarchyOperations.generateFrameworkHierarchy.name(), methodName)) { String id = (String) request.get("identifier"); - TelemetryManager.info("FrameworkHierarchyActor: invoking generateFrameworkHierarchy method" ); fwHierarchy.generateFrameworkHierarchy(id); OK(parent); } else if(StringUtils.equalsIgnoreCase(FrameworkHierarchyOperations.getFrameworkHierarchy.name(), methodName)){ @@ -42,7 +41,6 @@ protected void invokeMethod(Request request, ActorRef parent) { Map frameworkData = fwHierarchy.getFrameworkHierarchy(frameworkId); OK("framework", frameworkData, sender()); } else { - TelemetryManager.info("Unsupported operation: " + methodName); throw new ClientException(LearningErrorCodes.ERR_INVALID_OPERATION.name(), "Unsupported operation: " + methodName); } diff --git a/platform-modules/actors/src/main/java/org/sunbird/learning/framework/FrameworkHierarchy.java b/platform-modules/actors/src/main/java/org/sunbird/learning/framework/FrameworkHierarchy.java index a4c65187da..6dd68d6dd8 100644 --- a/platform-modules/actors/src/main/java/org/sunbird/learning/framework/FrameworkHierarchy.java +++ b/platform-modules/actors/src/main/java/org/sunbird/learning/framework/FrameworkHierarchy.java @@ -54,17 +54,14 @@ public class FrameworkHierarchy extends BaseManager { */ public void generateFrameworkHierarchy(String id) throws Exception { Response responseNode = getDataNode(GRAPH_ID, id); - TelemetryManager.info("FrameworkHierarchy: generateFrameworkHierarchy method"); if (checkError(responseNode)) throw new ResourceNotFoundException("ERR_DATA_NOT_FOUND", "Data not found with id : " + id); Node node = (Node) responseNode.get(GraphDACParams.node.name()); if (StringUtils.equalsIgnoreCase(node.getObjectType(), "Framework")) { FrameworkCache.delete(id); - TelemetryManager.info("FrameworkHierarchy: generateFrameworkHierarchy method:: After Deleting Framework Cache"); Map frameworkDocument = new HashMap<>(); Map frameworkHierarchy = getHierarchy(node.getIdentifier(), 0, false, true); CategoryCache.setFramework(node.getIdentifier(), frameworkHierarchy); - TelemetryManager.info("FrameworkHierarchy: generateFrameworkHierarchy method:: After generating Hierarchy: frameworkHierarchy: " + frameworkHierarchy); frameworkDocument.putAll(frameworkHierarchy); frameworkDocument.put("identifier", node.getIdentifier()); frameworkDocument.put("objectType", node.getObjectType()); @@ -74,7 +71,6 @@ public void generateFrameworkHierarchy(String id) throws Exception { if(null!=node.getMetadata().get(field)) frameworkDocument.put(field, node.getMetadata().get(field)); } - TelemetryManager.info("FrameworkHierarchy: generateFrameworkHierarchy method::Before saving Hierarchy to cassandra " ); hierarchyStore.saveOrUpdateHierarchy(node.getIdentifier(),frameworkDocument); } else { throw new ClientException(ResponseCode.CLIENT_ERROR.name(), "The object with given identifier is not a framework: " + id); diff --git a/platform-modules/actors/src/main/java/org/sunbird/learning/router/LearningRequestRouter.java b/platform-modules/actors/src/main/java/org/sunbird/learning/router/LearningRequestRouter.java index 525ddeba05..b9277b9ec5 100644 --- a/platform-modules/actors/src/main/java/org/sunbird/learning/router/LearningRequestRouter.java +++ b/platform-modules/actors/src/main/java/org/sunbird/learning/router/LearningRequestRouter.java @@ -92,11 +92,9 @@ private void initActorPool() { ActorRef contentStoreActor = system.actorOf(new SmallestMailboxPool(poolSize).props(contentStoreProps)); ActorRef fwHierarchyActor = system.actorOf(new SmallestMailboxPool(poolSize).props(fwhierarchyProps)); ActorRef localCacheUpdaterActor = system.actorOf(new SmallestMailboxPool(poolSize).props(localCacheUpdaterProps)); - TelemetryManager.info("LearningRequestRouter: initActorPool:: Before adding actors to pool" ); LearningActorPool.addActorRefToPool(LearningActorNames.CONTENT_STORE_ACTOR.name(), contentStoreActor); LearningActorPool.addActorRefToPool(LearningActorNames.FRAMEWORK_HIERARCHY_ACTOR.name(), fwHierarchyActor); LearningActorPool.addActorRefToPool(LearningActorNames.CACHE_UPDATE_ACTOR.name(), localCacheUpdaterActor); - TelemetryManager.info("LearningRequestRouter: initActorPool:: After adding actors to pool" ); } /** @@ -112,7 +110,6 @@ private ActorRef getActorFromPool(Request request) { if (null == ref) throw new ClientException(LearningErrorCodes.ERR_ROUTER_ACTOR_NOT_FOUND.name(), "Actor not found in the pool for manager: " + manager); - TelemetryManager.info("LearningRequestRouter: getActorFromPool:: Actor ref: " + ref ); return ref; } From d5d67e8df25117aeae731da0f23de5f1c842ea89 Mon Sep 17 00:00:00 2001 From: Jayaprakash8887 Date: Thu, 4 Aug 2022 15:19:15 +0530 Subject: [PATCH 053/222] Issue #SB-29985 feat: DIAL Context Info testing --- kubernetes/helm_charts/datapipeline_jobs/values.j2 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kubernetes/helm_charts/datapipeline_jobs/values.j2 b/kubernetes/helm_charts/datapipeline_jobs/values.j2 index cdb531196d..4f93038211 100644 --- a/kubernetes/helm_charts/datapipeline_jobs/values.j2 +++ b/kubernetes/helm_charts/datapipeline_jobs/values.j2 @@ -1022,7 +1022,7 @@ dialcode-context-updater: dialcode_context_updater { actions="dialcode-context-update" search_mode="Collection" - context_map_path = "https://raw.githubusercontent.com/project-sunbird/knowledge-platform-jobs/dialcode-context-updater/dialcode-context-updater/src/main/resources/contextMapping.json" + context_map_path = "https://raw.githubusercontent.com/project-sunbird/knowledge-platform-jobs/release-5.0.0/dialcode-context-updater/src/main/resources/contextMapping.json" identifier_search_fields = ["identifier", "primaryCategory", "channel"] dial_code_context_read_api_path = "/dialcode/v4/read/" dial_code_context_update_api_path = "/dialcode/v4/update/" From 1aa88ae6eb93ff7ac3c266f888d5782d8f0da0b6 Mon Sep 17 00:00:00 2001 From: Harikumar Palemkota Date: Wed, 24 Aug 2022 18:19:13 +0530 Subject: [PATCH 054/222] removing merge-user-courses samza job --- platform-jobs/samza/distribution/pom.xml | 7 - .../samza/merge-user-courses/pom.xml | 139 ------ .../src/main/assembly/src.xml | 69 --- .../local.merge-user-courses.properties | 81 --- .../main/config/merge-user-courses.properties | 78 --- .../samza/model/BatchEnrollmentSyncModel.java | 32 -- .../service/MergeUserCoursesService.java | 460 ------------------ .../jobs/samza/task/MergeUserCoursesTask.java | 40 -- .../samza/util/MergeUserCoursesParams.java | 9 - .../src/main/resources/log4j.xml | 20 - platform-jobs/samza/pom.xml | 1 - 11 files changed, 936 deletions(-) delete mode 100644 platform-jobs/samza/merge-user-courses/pom.xml delete mode 100644 platform-jobs/samza/merge-user-courses/src/main/assembly/src.xml delete mode 100644 platform-jobs/samza/merge-user-courses/src/main/config/local.merge-user-courses.properties delete mode 100644 platform-jobs/samza/merge-user-courses/src/main/config/merge-user-courses.properties delete mode 100644 platform-jobs/samza/merge-user-courses/src/main/java/org/sunbird/jobs/samza/model/BatchEnrollmentSyncModel.java delete mode 100644 platform-jobs/samza/merge-user-courses/src/main/java/org/sunbird/jobs/samza/service/MergeUserCoursesService.java delete mode 100644 platform-jobs/samza/merge-user-courses/src/main/java/org/sunbird/jobs/samza/task/MergeUserCoursesTask.java delete mode 100644 platform-jobs/samza/merge-user-courses/src/main/java/org/sunbird/jobs/samza/util/MergeUserCoursesParams.java delete mode 100644 platform-jobs/samza/merge-user-courses/src/main/resources/log4j.xml diff --git a/platform-jobs/samza/distribution/pom.xml b/platform-jobs/samza/distribution/pom.xml index ab285c244d..ff5db82fb0 100644 --- a/platform-jobs/samza/distribution/pom.xml +++ b/platform-jobs/samza/distribution/pom.xml @@ -23,13 +23,6 @@ tar.gz distribution - - org.sunbird - merge-user-courses - 0.0.19 - tar.gz - distribution - diff --git a/platform-jobs/samza/merge-user-courses/pom.xml b/platform-jobs/samza/merge-user-courses/pom.xml deleted file mode 100644 index e7cb6dc116..0000000000 --- a/platform-jobs/samza/merge-user-courses/pom.xml +++ /dev/null @@ -1,139 +0,0 @@ - - - - samza - org.sunbird - 1.1-SNAPSHOT - - 4.0.0 - - merge-user-courses - - - UTF-8 - 0.12.0 - 2.11 - 2.6.2 - - 0.0.19 - - - org.sunbird - course-common - 1.1-SNAPSHOT - - - org.sunbird - unit-tests - 1.1-SNAPSHOT - test - - - org.mockito - mockito-all - 1.10.19 - test - - - com.fasterxml.jackson.core - jackson-databind - 2.7.8 - - - com.fasterxml.jackson.core - jackson-core - 2.6.0 - - - com.fasterxml.jackson.core - jackson-annotations - 2.7.8 - - - org.powermock - powermock-api-mockito - 1.7.4 - test - - - org.powermock - powermock-module-junit4 - 1.7.4 - test - - - - - - org.apache.maven.plugins - maven-compiler-plugin - - 1.8 - 1.8 - - - - - maven-assembly-plugin - - - src/main/assembly/src.xml - - - - - make-assembly - package - - single - - - - - - org.apache.maven.plugins - maven-surefire-plugin - 2.20 - - - - org.jacoco - jacoco-maven-plugin - 0.7.9 - - - **/common/** - **/dto/** - **/enums/** - **/pipeline/** - **/servlet/** - **/interceptor/** - - - - - default-prepare-agent - - prepare-agent - - - - default-report - prepare-package - - report - - - - report-aggregate - verify - - report-aggregate - - - - - - - \ No newline at end of file diff --git a/platform-jobs/samza/merge-user-courses/src/main/assembly/src.xml b/platform-jobs/samza/merge-user-courses/src/main/assembly/src.xml deleted file mode 100644 index b8c4bf8a85..0000000000 --- a/platform-jobs/samza/merge-user-courses/src/main/assembly/src.xml +++ /dev/null @@ -1,69 +0,0 @@ - - - - - distribution - - tar.gz - - false - - - ${basedir} - - README* - LICENSE* - NOTICE* - - - - - - ${basedir}/src/main/resources/log4j.xml - lib - - - - ${basedir}/src/main/config/merge-user-courses.properties - config - true - - - - - bin - - org.apache.samza:samza-shell:tgz:dist:* - - 0744 - true - - - lib - - org.apache.samza:samza-api - org.sunbird:merge-user-courses - org.apache.samza:samza-core_2.11 - org.apache.samza:samza-kafka_2.11 - org.apache.samza:samza-yarn_2.11 - org.apache.samza:samza-log4j - org.apache.kafka:kafka_2.11 - org.apache.hadoop:hadoop-hdfs - - true - - - \ No newline at end of file diff --git a/platform-jobs/samza/merge-user-courses/src/main/config/local.merge-user-courses.properties b/platform-jobs/samza/merge-user-courses/src/main/config/local.merge-user-courses.properties deleted file mode 100644 index d162cc8e60..0000000000 --- a/platform-jobs/samza/merge-user-courses/src/main/config/local.merge-user-courses.properties +++ /dev/null @@ -1,81 +0,0 @@ -# Job -job.factory.class=org.apache.samza.job.yarn.YarnJobFactory -job.name=local.merge-user-courses - -# YARN -yarn.package.path=file://${basedir}/target/${project.artifactId}-${pom.version}-distribution.tar.gz - -# Metrics -metrics.reporters=snapshot,jmx -metrics.reporter.snapshot.class=org.apache.samza.metrics.reporter.MetricsSnapshotReporterFactory -metrics.reporter.snapshot.stream=kafka.local.metrics -metrics.reporter.jmx.class=org.apache.samza.metrics.reporter.JmxReporterFactory - -# Task -task.class=org.sunbird.jobs.samza.task.MergeUserCoursesTask -task.inputs=kafka.local.lms.user.account.merge -task.checkpoint.factory=org.apache.samza.checkpoint.kafka.KafkaCheckpointManagerFactory -task.checkpoint.system=kafka -task.checkpoint.replication.factor=1 -task.commit.ms=60000 -task.window.ms=300000 - -# Serializers -serializers.registry.json.class=org.sunbird.jobs.samza.serializers.EkstepJsonSerdeFactory -serializers.registry.metrics.class=org.apache.samza.serializers.MetricsSnapshotSerdeFactory - -# Systems -systems.kafka.samza.factory=org.apache.samza.system.kafka.KafkaSystemFactory -systems.kafka.samza.msg.serde=json -systems.kafka.streams.metrics.samza.msg.serde=metrics -systems.kafka.consumer.zookeeper.connect=localhost:2181 -systems.kafka.consumer.auto.offset.reset=smallest -systems.kafka.samza.offset.default=oldest -systems.kafka.producer.bootstrap.servers=localhost:9092 - -# Job Coordinator -job.coordinator.system=kafka - -# Normally, this would be 3, but we have only one broker. -job.coordinator.replication.factor=1 - -# Job specific configuration - -# Metrics -output.metrics.job.name=merge-user-courses -output.metrics.topic.name=local.pipeline_metrics -kafka.topics.backend.telemetry=local.telemetry.raw - -#Failed Topic Config -output.failed.events.topic.name=local.learning.events.failed - -# Retry Topic -kafka.topics.failed=local.lms.user.account.merge - -#Remote Debug Configuration -# task.opts=-agentlib:jdwp=transport=dt_socket,address=localhost:9009,server=y,suspend=y - -# Configuration for default channel ID -channel.default=in.ekstep - -#elastic-search -sunbird_es_cluster=local.lms.es.cluster -sunbird_es_host=127.0.0.1 -sunbird_es_port=9200 - -cassandra.lp.connection=localhost:9042 -cassandra.lpa.connection=localhost:9042 - -cassandra.connection.platform_courses=localhost:9042 -kp.learning_service.base_url=https://dev.sunbirded.org/action -courses.keyspace.name=sunbird_courses -search.es_conn_info=localhost:9200 -job.time_zone=IST -sunbird.installation=local -user.courses.table=user_enrolments -content.consumption.table=user_content_consumption -user.courses.es.index=user-courses -user.courses.es.type=_doc -course.batch.updater.kafka.topic=local.coursebatch.job.request -max.iteration.count.samza.job=2 -course.date.format=yyyy-MM-dd HH:mm:ss:SSSZ \ No newline at end of file diff --git a/platform-jobs/samza/merge-user-courses/src/main/config/merge-user-courses.properties b/platform-jobs/samza/merge-user-courses/src/main/config/merge-user-courses.properties deleted file mode 100644 index f386bc21c3..0000000000 --- a/platform-jobs/samza/merge-user-courses/src/main/config/merge-user-courses.properties +++ /dev/null @@ -1,78 +0,0 @@ -# Job -job.factory.class=org.apache.samza.job.yarn.YarnJobFactory -job.name=__env__.merge-user-courses - -# YARN -yarn.package.path=http://__yarn_host__:__yarn_port__/__env__/${project.artifactId}-${pom.version}-distribution.tar.gz - -# Metrics -metrics.reporters=snapshot,jmx -metrics.reporter.snapshot.class=org.apache.samza.metrics.reporter.MetricsSnapshotReporterFactory -metrics.reporter.snapshot.stream=kafka.__env__.metrics -metrics.reporter.jmx.class=org.apache.samza.metrics.reporter.JmxReporterFactory - -# Task -task.class=org.sunbird.jobs.samza.task.MergeUserCoursesTask -task.inputs=kafka.__env__.lms.user.account.merge -task.checkpoint.factory=org.apache.samza.checkpoint.kafka.KafkaCheckpointManagerFactory -task.checkpoint.system=kafka -task.checkpoint.replication.factor=__samza_checkpoint_replication_factor__ -task.commit.ms=60000 -task.window.ms=300000 - -# Serializers -serializers.registry.json.class=org.sunbird.jobs.samza.serializers.EkstepJsonSerdeFactory -serializers.registry.metrics.class=org.apache.samza.serializers.MetricsSnapshotSerdeFactory - -# Systems -systems.kafka.samza.factory=org.apache.samza.system.kafka.KafkaSystemFactory -systems.kafka.samza.msg.serde=json -systems.kafka.streams.metrics.samza.msg.serde=metrics -systems.kafka.consumer.zookeeper.connect=__zookeepers__ -systems.kafka.consumer.auto.offset.reset=smallest -systems.kafka.samza.offset.default=oldest -systems.kafka.producer.bootstrap.servers=__kafka_brokers__ - -# Job Coordinator -job.coordinator.system=kafka - -# Normally, this would be 3, but we have only one broker. -job.coordinator.replication.factor=__samza_coordinator_replication_factor__ - -# Job specific configuration - -# Metrics -output.metrics.job.name=merge-user-courses -output.metrics.topic.name=__env__.pipeline_metrics -kafka.topics.backend.telemetry=__env__.telemetry.raw - -#Failed Topic Config -output.failed.events.topic.name=__env__.learning.events.failed - -# Retry Topic -kafka.topics.failed=__env__.lms.user.account.merge - -# Configuration for default channel ID -channel.default=in.ekstep - -#elastic-search -sunbird_es_cluster=__lms_es_cluster__ -sunbird_es_host=__lms_es_host__ -sunbird_es_port=__lms_es_port__ - -cassandra.lp.connection=__cassandra_lp_connection__ -cassandra.lpa.connection=__cassandra_lpa_connection__ - -cassandra.connection.platform_courses=__cassandra_sunbird_connection__ -kp.learning_service.base_url=__kp_learning_service_base_url__ -courses.keyspace.name=sunbird_courses -search.es_conn_info=__search_lms_es_host__ -job.time_zone=IST -sunbird.installation=__sunbird_installation__ -user.courses.table=user_enrolments -content.consumption.table=user_content_consumption -user.courses.es.index=user-courses -user.courses.es.type=_doc -course.batch.updater.kafka.topic=__env__.coursebatch.job.request -max.iteration.count.samza.job=__max_iteration_count_for_samza_job__ -course.date.format=yyyy-MM-dd HH:mm:ss:SSSZ \ No newline at end of file diff --git a/platform-jobs/samza/merge-user-courses/src/main/java/org/sunbird/jobs/samza/model/BatchEnrollmentSyncModel.java b/platform-jobs/samza/merge-user-courses/src/main/java/org/sunbird/jobs/samza/model/BatchEnrollmentSyncModel.java deleted file mode 100644 index 7e29db4a19..0000000000 --- a/platform-jobs/samza/merge-user-courses/src/main/java/org/sunbird/jobs/samza/model/BatchEnrollmentSyncModel.java +++ /dev/null @@ -1,32 +0,0 @@ -package org.sunbird.jobs.samza.model; - -public class BatchEnrollmentSyncModel { - - private String batchId; - private String userId; - private String courseId; - - public String getBatchId() { - return batchId; - } - - public void setBatchId(String batchId) { - this.batchId = batchId; - } - - public String getUserId() { - return userId; - } - - public void setUserId(String userId) { - this.userId = userId; - } - - public String getCourseId() { - return courseId; - } - - public void setCourseId(String courseId) { - this.courseId = courseId; - } -} diff --git a/platform-jobs/samza/merge-user-courses/src/main/java/org/sunbird/jobs/samza/service/MergeUserCoursesService.java b/platform-jobs/samza/merge-user-courses/src/main/java/org/sunbird/jobs/samza/service/MergeUserCoursesService.java deleted file mode 100644 index 91bf959f85..0000000000 --- a/platform-jobs/samza/merge-user-courses/src/main/java/org/sunbird/jobs/samza/service/MergeUserCoursesService.java +++ /dev/null @@ -1,460 +0,0 @@ -package org.sunbird.jobs.samza.service; - -import com.datastax.driver.core.RegularStatement; -import com.datastax.driver.core.Session; -import com.datastax.driver.core.querybuilder.Batch; -import com.datastax.driver.core.querybuilder.QueryBuilder; -import com.datastax.driver.core.querybuilder.Update; -import com.fasterxml.jackson.databind.ObjectMapper; -import org.apache.commons.collections4.CollectionUtils; -import org.apache.commons.collections4.MapUtils; -import org.apache.commons.lang3.StringUtils; -import org.apache.samza.config.Config; -import org.apache.samza.system.OutgoingMessageEnvelope; -import org.apache.samza.system.SystemStream; -import org.apache.samza.task.MessageCollector; -import org.sunbird.common.Platform; -import org.sunbird.common.exception.ClientException; -import org.sunbird.jobs.samza.exception.PlatformErrorCodes; -import org.sunbird.jobs.samza.service.ISamzaService; -import org.sunbird.jobs.samza.service.task.JobMetrics; -import org.sunbird.jobs.samza.util.FailedEventsUtil; -import org.sunbird.jobs.samza.util.JSONUtils; -import org.sunbird.jobs.samza.util.JobLogger; -import org.sunbird.searchindex.elasticsearch.ElasticSearchUtil; -import org.sunbird.jobs.samza.model.BatchEnrollmentSyncModel; -import org.sunbird.jobs.samza.util.CassandraConnector; -import org.sunbird.jobs.samza.util.MergeUserCoursesParams; -import org.sunbird.jobs.samza.util.SunbirdCassandraUtil; - -import java.text.ParseException; -import java.text.SimpleDateFormat; -import java.util.*; -import java.util.stream.Collectors; - -public class MergeUserCoursesService implements ISamzaService { - private static JobLogger LOGGER = new JobLogger(MergeUserCoursesService.class); - private SystemStream systemStream; - private Config config = null; - private static final String UNDERSCORE = "_"; - private ObjectMapper mapper = new ObjectMapper(); - private static final String ACTION = "merge-user-courses-and-cert"; - private static int MAXITERTIONCOUNT = 2; - - private static String KEYSPACE; - private static String CONTENT_CONSUMPTION_TABLE; - private static String USER_COURSES_TABLE; - private static String USER_COURSE_ES_INDEX; - private static String USER_COURSE_ES_TYPE; - private static String COURSE_BATCH_UPDATER_KAFKA_TOPIC; - private static String COURSE_DATE_FORMAT; - private static SimpleDateFormat DateFormatter; - private static String USER_ACTIVITY_AGG; - private Session cassandraSession = null; - - protected int getMaxIterations() { - if (Platform.config.hasPath("max.iteration.count.samza.job")) - return Platform.config.getInt("max.iteration.count.samza.job"); - else - return MAXITERTIONCOUNT; - } - - private boolean validateObject(Map edata) { - String action = (String) edata.get(MergeUserCoursesParams.action.name()); - Integer iteration = (Integer) edata.get(MergeUserCoursesParams.iteration.name()); - if (StringUtils.equalsIgnoreCase(ACTION, action) && (iteration <= getMaxIterations())) { - return true; - } - return false; - } - - private static void initializeConfigurations() { - KEYSPACE = Platform.config.hasPath("courses.keyspace.name") ? - Platform.config.getString("courses.keyspace.name") : "sunbird_courses"; - - CONTENT_CONSUMPTION_TABLE = Platform.config.hasPath("content.consumption.table") ? - Platform.config.getString("content.consumption.table") : "user_content_consumption"; - - USER_COURSES_TABLE = Platform.config.hasPath("user.courses.table") ? - Platform.config.getString("user.courses.table") : "user_enrolments"; - - USER_COURSE_ES_INDEX = Platform.config.hasPath("user.courses.es.index") ? - Platform.config.getString("user.courses.es.index") : "user-courses"; - - USER_COURSE_ES_TYPE = Platform.config.hasPath("user.courses.es.type") ? - Platform.config.getString("user.courses.es.type") : "_doc"; - - COURSE_BATCH_UPDATER_KAFKA_TOPIC = Platform.config.getString("course.batch.updater.kafka.topic"); - - COURSE_DATE_FORMAT = Platform.config.hasPath("course.date.format") ? - Platform.config.getString("course.date.format") : "yyyy-MM-dd HH:mm:ss:SSSZ"; - - USER_ACTIVITY_AGG = "user_activity_agg"; - - DateFormatter = new SimpleDateFormat(COURSE_DATE_FORMAT); - } - - @Override - public void initialize(Config config) throws Exception { - this.config = config; - JSONUtils.loadProperties(config); - initializeConfigurations(); - this.cassandraSession = new CassandraConnector(config).getSession(); - LOGGER.info("MergeUserCoursesService:initialize: Service config initialized"); - ElasticSearchUtil.initialiseESClient(USER_COURSE_ES_INDEX, Platform.config.getString("search.es_conn_info")); - LOGGER.info("MergeUserCoursesService:initialize: ESClient initialized for index:" + USER_COURSE_ES_INDEX); - systemStream = new SystemStream("kafka", config.get("output.failed.events.topic.name")); - LOGGER.info("MergeUserCoursesService:initialize: Stream initialized for Failed Events"); - } - - @Override - public void processMessage(Map message, JobMetrics metrics, MessageCollector collector) throws Exception { - if (MapUtils.isEmpty(message)) { - LOGGER.info("MergeUserCoursesService:processMessage: Ignoring the event since message is empty."); - FailedEventsUtil.pushEventForRetry(systemStream, message, metrics, collector, - PlatformErrorCodes.DATA_ERROR.name(), new ClientException("ERR_MERGE_USER_COURSES_SAMZA", "message is empty")); - metrics.incSkippedCounter(); - return; - } - - Map edata = (Map) message.get(MergeUserCoursesParams.edata.name()); - if (MapUtils.isEmpty(edata)) { - LOGGER.info("MergeUserCoursesService:processMessage: Ignoring the event since edata is empty."); - FailedEventsUtil.pushEventForRetry(systemStream, message, metrics, collector, - PlatformErrorCodes.DATA_ERROR.name(), new ClientException("ERR_MERGE_USER_COURSES_SAMZA", "message.edata is empty")); - metrics.incSkippedCounter(); - return; - } - - String fromUserId = (String) edata.get(MergeUserCoursesParams.fromAccountId.name()); - String toUserId = (String) edata.get(MergeUserCoursesParams.toAccountId.name()); - - if (StringUtils.isBlank(fromUserId) || StringUtils.isBlank(toUserId) || !validateObject(edata)) { - LOGGER.info("MergeUserCoursesService:processMessage: Ignoring the event due to invalid edata:" + edata); - FailedEventsUtil.pushEventForRetry(systemStream, message, metrics, collector, - PlatformErrorCodes.DATA_ERROR.name(), new ClientException("ERR_MERGE_USER_COURSES_SAMZA", "message.edata values are not valid")); - metrics.incSkippedCounter(); - return; - } - - try { - mergeContentConsumption(fromUserId, toUserId); - mergeUserBatches(fromUserId, toUserId); - generateBatchEnrollmentSyncEvents(toUserId, collector); - mergeUserActivityAggregates(fromUserId, toUserId); - metrics.incSuccessCounter(); - LOGGER.info("MergeUserCoursesService:processMessage: Event processed successfully", message); - } catch (Exception e) { - edata.put(MergeUserCoursesParams.status.name(), MergeUserCoursesParams.FAILED.name()); - FailedEventsUtil.pushEventForRetry(systemStream, message, metrics, collector, - PlatformErrorCodes.PROCESSING_ERROR.name(), e); - throw e; - } - - } - - private void generateBatchEnrollmentSyncEvents(String userId, MessageCollector collector) throws Exception { - List objects = getBatchDetailsOfUser(userId); - if (CollectionUtils.isNotEmpty(objects)) { - for (BatchEnrollmentSyncModel model : objects) { - Map event = getBatchEnrollmentSyncEvent(model); - collector.send(new OutgoingMessageEnvelope(new SystemStream("kafka", COURSE_BATCH_UPDATER_KAFKA_TOPIC), event)); - } - } - } - - private void mergeUserBatches(String fromUserId, String toUserId) throws Exception { - List fromBatches = getBatchDetailsOfUser(fromUserId); - List toBatches = getBatchDetailsOfUser(toUserId); - - Map fromBatchIds = new HashMap<>(); - Map toBatchIds = new HashMap<>(); - if (CollectionUtils.isNotEmpty(fromBatches)) { - for (BatchEnrollmentSyncModel fromBatch : fromBatches) { - if (StringUtils.isNotBlank(fromBatch.getBatchId())) - fromBatchIds.put(fromBatch.getBatchId(), fromBatch); - } - } - if (CollectionUtils.isNotEmpty(toBatches)) { - for (BatchEnrollmentSyncModel toBatch : toBatches) { - if (StringUtils.isNotBlank(toBatch.getBatchId())) - toBatchIds.put(toBatch.getBatchId(), toBatch); - } - } - - List batchIdsToBeMigrated = (List) CollectionUtils.subtract(fromBatchIds.keySet(), toBatchIds.keySet()); - - //Migrate batch records in Cassandra and ES - if (CollectionUtils.isNotEmpty(batchIdsToBeMigrated)) { - for (String batchId : batchIdsToBeMigrated) { - String courseId = fromBatchIds.get(batchId).getCourseId(); - Map userCourse = getUserCourse(batchId, fromUserId, courseId); - if (MapUtils.isNotEmpty(userCourse)) { - userCourse.put(MergeUserCoursesParams.userId.name(), toUserId); - LOGGER.info("MergeUserCoursesService:mergeUserBatches: Merging batch:" + batchId + " updated record:" + userCourse); - SunbirdCassandraUtil.upsert(KEYSPACE, USER_COURSES_TABLE, userCourse); - - /*String documentJson = ElasticSearchUtil.getDocumentAsStringById(USER_COURSE_ES_INDEX, USER_COURSE_ES_TYPE, - batchId + UNDERSCORE + fromUserId); - Map userCourseDoc = mapper.readValue(documentJson, Map.class); - userCourseDoc.put(MergeUserCoursesParams.userId.name(), toUserId); - userCourseDoc.put(MergeUserCoursesParams.id.name(), batchId + UNDERSCORE + toUserId); - userCourseDoc.put(MergeUserCoursesParams.identifier.name(), batchId + UNDERSCORE + toUserId); - ElasticSearchUtil.addDocumentWithId(USER_COURSE_ES_INDEX, USER_COURSE_ES_TYPE, - batchId + UNDERSCORE + toUserId, mapper.writeValueAsString(userCourseDoc));*/ - } else { - LOGGER.info("MergeUserCoursesService:mergeUserBatches: user_courses record with batchId:" + batchId + " userId:" + fromUserId + " found in ES but not in Cassandra"); - } - } - } - } - - private void mergeContentConsumption(String fromUserId, String toUserId) { - //Get content consumption data - List> fromContentConsumptionList = getContentConsumption(fromUserId); - List> toContentConsumptionList = getContentConsumption(toUserId); - - if (CollectionUtils.isNotEmpty(fromContentConsumptionList)) { - for (Map contentConsumption : fromContentConsumptionList) { - Map matchingRecord = getMatchingRecord(contentConsumption, toContentConsumptionList); - if (MapUtils.isEmpty(matchingRecord)) { - matchingRecord = contentConsumption; - matchingRecord.put(MergeUserCoursesParams.userId.name(), toUserId); - } else { - mergeContentConsumptionRecord(contentConsumption, matchingRecord); - } - SunbirdCassandraUtil.upsert(KEYSPACE, CONTENT_CONSUMPTION_TABLE, matchingRecord); - } - } - } - - private void mergeContentConsumptionRecord(Map oldRecord, Map newRecord) { - /* - * for status, progress, datetime, lastaccesstime, lastcompletedtime, lastupdatedtime fields, - * max value should be considered - * for completedcount, viewcount fields, sum of both records should be considered - * */ - newRecord.put(MergeUserCoursesParams.status.name(), getUpdatedValue("Integer", "Max", - MergeUserCoursesParams.status.name(), oldRecord, newRecord)); - newRecord.put(MergeUserCoursesParams.progress.name(), getUpdatedValue("Integer", "Max", - MergeUserCoursesParams.progress.name(), oldRecord, newRecord)); - newRecord.put(MergeUserCoursesParams.viewCount.name(), getUpdatedValue("Integer", "Sum", - MergeUserCoursesParams.viewCount.name(), oldRecord, newRecord)); - newRecord.put(MergeUserCoursesParams.completedCount.name(), getUpdatedValue("Integer", "Sum", - MergeUserCoursesParams.completedCount.name(), oldRecord, newRecord)); - - newRecord.put(MergeUserCoursesParams.dateTime.name(), getUpdatedValue("Date", "Max", - MergeUserCoursesParams.dateTime.name(), oldRecord, newRecord)); - newRecord.put(MergeUserCoursesParams.lastAccessTime.name(), getUpdatedValue("DateString", "Max", - MergeUserCoursesParams.lastAccessTime.name(), oldRecord, newRecord)); - newRecord.put(MergeUserCoursesParams.lastCompletedTime.name(), getUpdatedValue("DateString", "Max", - MergeUserCoursesParams.lastCompletedTime.name(), oldRecord, newRecord)); - newRecord.put(MergeUserCoursesParams.lastUpdatedTime.name(), getUpdatedValue("DateString", "Max", - MergeUserCoursesParams.lastUpdatedTime.name(), oldRecord, newRecord)); - } - - private Object getUpdatedValue(String dataType, String operation, String fieldName, Map oldRecord, Map newRecord) { - if (null == oldRecord.get(fieldName)) { - return newRecord.get(fieldName); - } - if (null == newRecord.get(fieldName)) { - return oldRecord.get(fieldName); - } - switch (dataType) { - case "Integer": - if (oldRecord.get(fieldName) instanceof Integer && - newRecord.get(fieldName) instanceof Integer) { - int val1 = (int) oldRecord.get(fieldName); - int val2 = (int) newRecord.get(fieldName); - if (StringUtils.equalsIgnoreCase("Sum", operation)) { - return val1 + val2; - } else if (StringUtils.equalsIgnoreCase("Max", operation)) { - return val1 > val2 ? val1 : val2; - } - } - break; - case "DateString": - if (oldRecord.get(fieldName) instanceof String && - newRecord.get(fieldName) instanceof String) { - String dateStr1 = (String) oldRecord.get(fieldName); - String dateStr2 = (String) newRecord.get(fieldName); - Date date1; - Date date2; - try { - date1 = DateFormatter.parse(dateStr1); - } catch (ParseException pe) { - LOGGER.info("MergeUserCoursesService:getUpdatedValue: Date Parsing failed for field:" + fieldName + " value:" + dateStr1); - return dateStr2; - } - try { - date2 = DateFormatter.parse(dateStr2); - } catch (ParseException pe) { - LOGGER.info("MergeUserCoursesService:getUpdatedValue: Date Parsing failed for field:" + fieldName + " value:" + dateStr2); - return dateStr1; - } - if (StringUtils.equalsIgnoreCase("Max", operation)) { - if (date1.after(date2)) { - return dateStr1; - } else { - return dateStr2; - } - } - } - break; - case "Date": - if (oldRecord.get(fieldName) instanceof Date && - newRecord.get(fieldName) instanceof Date) { - Date date1 = (Date) oldRecord.get(fieldName); - Date date2 = (Date) newRecord.get(fieldName); - if (StringUtils.equalsIgnoreCase("Max", operation)) { - if (date1.after(date2)) { - return date1; - } else { - return date2; - } - } - } - break; - } - return newRecord.get(fieldName); - } - - private Map getMatchingRecord(Map contentConsumption, List> toContentConsumptionList) { - Map matchingRecord = new HashMap(); - if (CollectionUtils.isNotEmpty(toContentConsumptionList)) { - for (Map toContentConsumption : toContentConsumptionList) { - if (StringUtils.equalsIgnoreCase((String) contentConsumption.get(MergeUserCoursesParams.contentId.name()), (String) toContentConsumption.get(MergeUserCoursesParams.contentId.name())) && - StringUtils.equalsIgnoreCase((String) contentConsumption.get(MergeUserCoursesParams.batchId.name()), (String) toContentConsumption.get(MergeUserCoursesParams.batchId.name())) && - StringUtils.equalsIgnoreCase((String) contentConsumption.get(MergeUserCoursesParams.courseId.name()), (String) toContentConsumption.get(MergeUserCoursesParams.courseId.name()))) { - matchingRecord = toContentConsumption; - break; - } - } - } - return matchingRecord; - } - - private List> getContentConsumption(String userId) { - Map key = new HashMap<>(); - key.put(MergeUserCoursesParams.userId.name(), userId); - return SunbirdCassandraUtil.readAsListOfMap(KEYSPACE, CONTENT_CONSUMPTION_TABLE, key); - } - - private Map getUserCourse(String batchId, String userId, String courseId) { - Map key = new HashMap<>(); - key.put(MergeUserCoursesParams.batchId.name(), batchId); - key.put(MergeUserCoursesParams.userId.name(), userId); - key.put(MergeUserCoursesParams.courseId.name(), courseId); - List> data = SunbirdCassandraUtil.readAsListOfMap(KEYSPACE, USER_COURSES_TABLE, key); - return CollectionUtils.isEmpty(data) ? new HashMap() : data.get(0); - } - - private List getBatchDetailsOfUser(String userId) throws Exception { - List objects = new ArrayList<>(); - Map searchQuery = new HashMap<>(); - List userIdList = new ArrayList<>(); - userIdList.add(userId); - searchQuery.put(MergeUserCoursesParams.userId.name(), userIdList); - Map key = new HashMap<>(); - key.put(MergeUserCoursesParams.userId.name(), userIdList); - List> documents = SunbirdCassandraUtil.readAsListOfMap(KEYSPACE, USER_COURSES_TABLE, key); - //List documents = ElasticSearchUtil.textSearchReturningId(searchQuery, USER_COURSE_ES_INDEX, USER_COURSE_ES_TYPE); - if (CollectionUtils.isNotEmpty(documents)) { - documents.forEach(doc -> { - BatchEnrollmentSyncModel model = new BatchEnrollmentSyncModel(); - model.setBatchId((String) doc.get(MergeUserCoursesParams.batchId.name())); - model.setUserId((String) doc.get(MergeUserCoursesParams.userId.name())); - model.setCourseId((String) doc.get(MergeUserCoursesParams.courseId.name())); - objects.add(model); - }); - } - return objects; - } - - private Map getBatchEnrollmentSyncEvent(BatchEnrollmentSyncModel model) { - return new HashMap() {{ - put("actor", new HashMap() {{ - put("id", "Course Batch Updater"); - put("type", "System"); - }}); - put("eid", "BE_JOB_REQUEST"); - put("edata", new HashMap() {{ - put("action", "batch-enrolment-sync"); - put("iteration", 1); - put("batchId", model.getBatchId()); - put("userId", model.getUserId()); - put("courseId", model.getCourseId()); - put("reset", Arrays.asList("completionPercentage", "status", "progress")); - }}); - put("ets", System.currentTimeMillis()); - put("context", new HashMap() {{ - put("pdata", new HashMap() {{ - put("ver", "1.0"); - put("id", "org.sunbird.platform"); - }}); - }}); - put("mid", "LP." + System.currentTimeMillis() + "." + UUID.randomUUID()); - put("object", new HashMap() {{ - put("id", model.getBatchId() + UNDERSCORE + model.getUserId()); - put("type", "CourseBatchEnrolment"); - }}); - }}; - } - - - private void mergeUserActivityAggregates(String fromUserId, String toUserId) throws Exception { - List fromBatches = getBatchDetailsOfUser(fromUserId); - if(CollectionUtils.isNotEmpty(fromBatches)) { - List fromCourseIds = fromBatches.stream().map(enrol -> enrol.getCourseId()).collect(Collectors.toList()); - List toCourseIds = fromBatches.stream().map(enrol -> enrol.getCourseId()).collect(Collectors.toList()); - Map key = new HashMap<>(); - key.put(MergeUserCoursesParams.activity_type.name(), "Course"); - key.put(MergeUserCoursesParams.user_id.name(), fromUserId); - key.put(MergeUserCoursesParams.activity_id.name(), fromCourseIds); - List> fromData = SunbirdCassandraUtil.readAsListOfMap(KEYSPACE, USER_ACTIVITY_AGG, key); - key.put(MergeUserCoursesParams.activity_id.name(), toCourseIds); - List> toData = SunbirdCassandraUtil.readAsListOfMap(KEYSPACE, USER_ACTIVITY_AGG, key); - Map toDataMap = toData.stream().collect(Collectors.toMap(m -> (String)m.get("context_id"), m -> m)); - List updateQueryList = new ArrayList<>(); - if(CollectionUtils.isNotEmpty(fromData)) { - fromData.stream().filter(data -> MapUtils.isNotEmpty(data)).collect(Collectors.toList()).forEach(data -> { - data.put(MergeUserCoursesParams.user_id.name(), toUserId); - Map fromAgg = (Map) data.get("agg"); - Map toAgg = (Map) ((Map)toDataMap.getOrDefault(data.get("context_id"), new HashMap())).getOrDefault("agg", new HashMap()); - data.put("agg", new HashMap(){{ - put("completedCount", Math.max(fromAgg.getOrDefault("completedCount", 0), toAgg.getOrDefault("completedCount", 0))); - }}); - data.put("agg_last_updated", new HashMap(){{ - put("completedCount", new Date()); - }}); - Map dataToSelect = new HashMap() {{ - put(MergeUserCoursesParams.activity_type.name(), "Course"); - put(MergeUserCoursesParams.activity_id.name(), data.get("activity_id")); - put(MergeUserCoursesParams.user_id.name(), toUserId); - put("context_id", data.get("context_id")); - }}; - updateQueryList.add(updateQuery(KEYSPACE, USER_ACTIVITY_AGG, data, dataToSelect)); - }); - } - if(CollectionUtils.isNotEmpty(updateQueryList)){ - Batch batch = QueryBuilder.batch(updateQueryList.toArray(new RegularStatement[updateQueryList.size()])); - cassandraSession.execute(batch); - } - } - - } - - - public Update.Where updateQuery(String keyspace, String table, Map propertiesToUpdate, Map propertiesToSelect) { - Update.Where updateQuery = QueryBuilder.update(keyspace, table).where(); - propertiesToUpdate.entrySet().forEach(entry -> updateQuery.with(QueryBuilder.set(entry.getKey(), entry.getValue()))); - propertiesToSelect.entrySet().forEach(entry -> { - if (entry.getValue() instanceof List) - updateQuery.and(QueryBuilder.in(entry.getKey(), (List) entry.getValue())); - else - updateQuery.and(QueryBuilder.eq(entry.getKey(), entry.getValue())); - }); - return updateQuery; - } - -} diff --git a/platform-jobs/samza/merge-user-courses/src/main/java/org/sunbird/jobs/samza/task/MergeUserCoursesTask.java b/platform-jobs/samza/merge-user-courses/src/main/java/org/sunbird/jobs/samza/task/MergeUserCoursesTask.java deleted file mode 100644 index 3c360c690b..0000000000 --- a/platform-jobs/samza/merge-user-courses/src/main/java/org/sunbird/jobs/samza/task/MergeUserCoursesTask.java +++ /dev/null @@ -1,40 +0,0 @@ -package org.sunbird.jobs.samza.task; - - -import org.apache.samza.task.MessageCollector; -import org.apache.samza.task.TaskCoordinator; -import org.sunbird.jobs.samza.service.ISamzaService; -import org.sunbird.jobs.samza.util.JobLogger; -import org.sunbird.jobs.samza.service.MergeUserCoursesService; - -import java.util.Arrays; -import java.util.Map; - -public class MergeUserCoursesTask extends BaseTask { - - private ISamzaService service = new MergeUserCoursesService(); - private static JobLogger LOGGER = new JobLogger(MergeUserCoursesTask.class); - - @Override - public ISamzaService initialize() throws Exception { - LOGGER.info("MergeUserCoursesTask:initialize: Task initialized"); - this.action = Arrays.asList("merge-user-courses-and-cert"); - this.jobStartMessage = "Started processing of merge-user-courses samza job"; - this.jobEndMessage = "merge-user-courses job processing complete"; - this.jobClass = "org.sunbird.jobs.samza.task.MergeUserCoursesTask"; - return service; - } - - @Override - public void process(Map message, MessageCollector collector, TaskCoordinator coordinator) throws Exception { - try { - LOGGER.info("MergeUserCoursesTask:process: Starting to process for mid : " + message.get("mid") + " at :: " + System.currentTimeMillis()); - service.processMessage(message, metrics, collector); - LOGGER.info("MergeUserCoursesTask:process: Successfully completed processing for mid : " + message.get("mid") + " at :: " + System.currentTimeMillis()); - } catch (Exception e) { - metrics.incErrorCounter(); - LOGGER.error("MergeUserCoursesTask:process: Message processing failed", message, e); - } - } - -} diff --git a/platform-jobs/samza/merge-user-courses/src/main/java/org/sunbird/jobs/samza/util/MergeUserCoursesParams.java b/platform-jobs/samza/merge-user-courses/src/main/java/org/sunbird/jobs/samza/util/MergeUserCoursesParams.java deleted file mode 100644 index 8645c89bcd..0000000000 --- a/platform-jobs/samza/merge-user-courses/src/main/java/org/sunbird/jobs/samza/util/MergeUserCoursesParams.java +++ /dev/null @@ -1,9 +0,0 @@ -package org.sunbird.jobs.samza.util; - -import org.sunbird.graph.dac.util.RelationType; - -public enum MergeUserCoursesParams { - userId, batchId, contentId, courseId, status, edata, id, identifier, action, fromAccountId, - toAccountId, FAILED, iteration, progress, dateTime, lastAccessTime, lastCompletedTime, - lastUpdatedTime, completedCount, viewCount, activity_type, activity_id, user_id; -} diff --git a/platform-jobs/samza/merge-user-courses/src/main/resources/log4j.xml b/platform-jobs/samza/merge-user-courses/src/main/resources/log4j.xml deleted file mode 100644 index 0f37824c0c..0000000000 --- a/platform-jobs/samza/merge-user-courses/src/main/resources/log4j.xml +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/platform-jobs/samza/pom.xml b/platform-jobs/samza/pom.xml index 687411d755..051a354bbc 100644 --- a/platform-jobs/samza/pom.xml +++ b/platform-jobs/samza/pom.xml @@ -26,7 +26,6 @@ qrcode-image-generator distribution qr-image-generator - merge-user-courses auto-creator mvc-processor-indexer From 327bbd535b74499846137c3a55afc19e2c0253ec Mon Sep 17 00:00:00 2001 From: Kumar Gauraw Date: Thu, 25 Aug 2022 21:11:33 +0530 Subject: [PATCH 055/222] Issue #KN-225 feat: added config for aws media service --- .../roles/flink-jobs-deploy/defaults/main.yml | 12 ++++++++++ .../helm_charts/datapipeline_jobs/values.j2 | 23 +++++++++++++++++++ 2 files changed, 35 insertions(+) diff --git a/kubernetes/ansible/roles/flink-jobs-deploy/defaults/main.yml b/kubernetes/ansible/roles/flink-jobs-deploy/defaults/main.yml index 26fdca05b1..2f05da5596 100644 --- a/kubernetes/ansible/roles/flink-jobs-deploy/defaults/main.yml +++ b/kubernetes/ansible/roles/flink-jobs-deploy/defaults/main.yml @@ -321,3 +321,15 @@ qrcode_image_generator_consumer_parallelism: 1 qrcode_image_generator_parallelism: 1 source_base_url: "{{proto}}://{{domain_name}}/api" + +### video-stream-generator related vars +media_service_provider_name: "azure" +## AWS media convert service vars +aws_mediaconvert_api_version: "2017-08-29" +aws_mediaconvert_region: "" +aws_content_bucket_name: "" +aws_mediaconvert_access_key: "" +aws_mediaconvert_access_secret: "" +aws_mediaconvert_api_endpoint: "" +aws_mediaconvert_queue_id: "" +aws_mediaconvert_role_name: "" diff --git a/kubernetes/helm_charts/datapipeline_jobs/values.j2 b/kubernetes/helm_charts/datapipeline_jobs/values.j2 index d0e1217620..88852a547b 100644 --- a/kubernetes/helm_charts/datapipeline_jobs/values.j2 +++ b/kubernetes/helm_charts/datapipeline_jobs/values.j2 @@ -328,6 +328,29 @@ video-stream-generator: azure_resource_group_name="{{ video_stream_generator_azure_resource_group_name }}" azure_token_client_key="{{ video_stream_generator_azure_token_client_key }}" azure_token_client_secret="{{ video_stream_generator_azure_token_client_secret }}" + ## CSP Name. e.g: aws or azure + media_service_type="{{ media_service_provider_name }}" + ## AWS Elemental Media Convert Config + aws { + region="{{ aws_mediaconvert_region }}" + content_bucket_name="{{ aws_content_bucket_name }}" + token { + access_key="{{ aws_mediaconvert_access_key }}" + access_secret="{{ aws_mediaconvert_access_secret }}" + } + api { + endpoint="{{ aws_mediaconvert_api_endpoint }}" + version="{{ aws_mediaconvert_api_version }}" + } + service { + name="mediaconvert" + queue="{{ aws_mediaconvert_queue_id }}" + role="{{ aws_mediaconvert_role_name }}" + } + stream { + protocol="Hls" + } + } flink-conf: |+ From 3268d120c34d66782fed4098e2f6c9d26ef96953 Mon Sep 17 00:00:00 2001 From: Harikumar Palemkota Date: Fri, 2 Sep 2022 12:34:45 +0530 Subject: [PATCH 056/222] Reverting back the merge-course module, will remove in next release --- .../samza/merge-user-courses/pom.xml | 139 ++++++ .../src/main/assembly/src.xml | 69 +++ .../local.merge-user-courses.properties | 81 +++ .../main/config/merge-user-courses.properties | 78 +++ .../samza/model/BatchEnrollmentSyncModel.java | 32 ++ .../service/MergeUserCoursesService.java | 460 ++++++++++++++++++ .../jobs/samza/task/MergeUserCoursesTask.java | 40 ++ .../samza/util/MergeUserCoursesParams.java | 9 + .../src/main/resources/log4j.xml | 0 9 files changed, 908 insertions(+) create mode 100644 platform-jobs/samza/merge-user-courses/pom.xml create mode 100644 platform-jobs/samza/merge-user-courses/src/main/assembly/src.xml create mode 100644 platform-jobs/samza/merge-user-courses/src/main/config/local.merge-user-courses.properties create mode 100644 platform-jobs/samza/merge-user-courses/src/main/config/merge-user-courses.properties create mode 100644 platform-jobs/samza/merge-user-courses/src/main/java/org/sunbird/jobs/samza/model/BatchEnrollmentSyncModel.java create mode 100644 platform-jobs/samza/merge-user-courses/src/main/java/org/sunbird/jobs/samza/service/MergeUserCoursesService.java create mode 100644 platform-jobs/samza/merge-user-courses/src/main/java/org/sunbird/jobs/samza/task/MergeUserCoursesTask.java create mode 100644 platform-jobs/samza/merge-user-courses/src/main/java/org/sunbird/jobs/samza/util/MergeUserCoursesParams.java create mode 100644 platform-jobs/samza/merge-user-courses/src/main/resources/log4j.xml diff --git a/platform-jobs/samza/merge-user-courses/pom.xml b/platform-jobs/samza/merge-user-courses/pom.xml new file mode 100644 index 0000000000..e7cb6dc116 --- /dev/null +++ b/platform-jobs/samza/merge-user-courses/pom.xml @@ -0,0 +1,139 @@ + + + + samza + org.sunbird + 1.1-SNAPSHOT + + 4.0.0 + + merge-user-courses + + + UTF-8 + 0.12.0 + 2.11 + 2.6.2 + + 0.0.19 + + + org.sunbird + course-common + 1.1-SNAPSHOT + + + org.sunbird + unit-tests + 1.1-SNAPSHOT + test + + + org.mockito + mockito-all + 1.10.19 + test + + + com.fasterxml.jackson.core + jackson-databind + 2.7.8 + + + com.fasterxml.jackson.core + jackson-core + 2.6.0 + + + com.fasterxml.jackson.core + jackson-annotations + 2.7.8 + + + org.powermock + powermock-api-mockito + 1.7.4 + test + + + org.powermock + powermock-module-junit4 + 1.7.4 + test + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + 1.8 + 1.8 + + + + + maven-assembly-plugin + + + src/main/assembly/src.xml + + + + + make-assembly + package + + single + + + + + + org.apache.maven.plugins + maven-surefire-plugin + 2.20 + + + + org.jacoco + jacoco-maven-plugin + 0.7.9 + + + **/common/** + **/dto/** + **/enums/** + **/pipeline/** + **/servlet/** + **/interceptor/** + + + + + default-prepare-agent + + prepare-agent + + + + default-report + prepare-package + + report + + + + report-aggregate + verify + + report-aggregate + + + + + + + \ No newline at end of file diff --git a/platform-jobs/samza/merge-user-courses/src/main/assembly/src.xml b/platform-jobs/samza/merge-user-courses/src/main/assembly/src.xml new file mode 100644 index 0000000000..29bd7d10f1 --- /dev/null +++ b/platform-jobs/samza/merge-user-courses/src/main/assembly/src.xml @@ -0,0 +1,69 @@ + + + + + distribution + + tar.gz + + false + + + ${basedir} + + README* + LICENSE* + NOTICE* + + + + + + ${basedir}/src/main/resources/log4j.xml + lib + + + + ${basedir}/src/main/config/local.merge-user-courses.properties + config + true + + + + + bin + + org.apache.samza:samza-shell:tgz:dist:* + + 0744 + true + + + lib + + org.apache.samza:samza-api + org.sunbird:merge-user-courses + org.apache.samza:samza-core_2.11 + org.apache.samza:samza-kafka_2.11 + org.apache.samza:samza-yarn_2.11 + org.apache.samza:samza-log4j + org.apache.kafka:kafka_2.11 + org.apache.hadoop:hadoop-hdfs + + true + + + \ No newline at end of file diff --git a/platform-jobs/samza/merge-user-courses/src/main/config/local.merge-user-courses.properties b/platform-jobs/samza/merge-user-courses/src/main/config/local.merge-user-courses.properties new file mode 100644 index 0000000000..d162cc8e60 --- /dev/null +++ b/platform-jobs/samza/merge-user-courses/src/main/config/local.merge-user-courses.properties @@ -0,0 +1,81 @@ +# Job +job.factory.class=org.apache.samza.job.yarn.YarnJobFactory +job.name=local.merge-user-courses + +# YARN +yarn.package.path=file://${basedir}/target/${project.artifactId}-${pom.version}-distribution.tar.gz + +# Metrics +metrics.reporters=snapshot,jmx +metrics.reporter.snapshot.class=org.apache.samza.metrics.reporter.MetricsSnapshotReporterFactory +metrics.reporter.snapshot.stream=kafka.local.metrics +metrics.reporter.jmx.class=org.apache.samza.metrics.reporter.JmxReporterFactory + +# Task +task.class=org.sunbird.jobs.samza.task.MergeUserCoursesTask +task.inputs=kafka.local.lms.user.account.merge +task.checkpoint.factory=org.apache.samza.checkpoint.kafka.KafkaCheckpointManagerFactory +task.checkpoint.system=kafka +task.checkpoint.replication.factor=1 +task.commit.ms=60000 +task.window.ms=300000 + +# Serializers +serializers.registry.json.class=org.sunbird.jobs.samza.serializers.EkstepJsonSerdeFactory +serializers.registry.metrics.class=org.apache.samza.serializers.MetricsSnapshotSerdeFactory + +# Systems +systems.kafka.samza.factory=org.apache.samza.system.kafka.KafkaSystemFactory +systems.kafka.samza.msg.serde=json +systems.kafka.streams.metrics.samza.msg.serde=metrics +systems.kafka.consumer.zookeeper.connect=localhost:2181 +systems.kafka.consumer.auto.offset.reset=smallest +systems.kafka.samza.offset.default=oldest +systems.kafka.producer.bootstrap.servers=localhost:9092 + +# Job Coordinator +job.coordinator.system=kafka + +# Normally, this would be 3, but we have only one broker. +job.coordinator.replication.factor=1 + +# Job specific configuration + +# Metrics +output.metrics.job.name=merge-user-courses +output.metrics.topic.name=local.pipeline_metrics +kafka.topics.backend.telemetry=local.telemetry.raw + +#Failed Topic Config +output.failed.events.topic.name=local.learning.events.failed + +# Retry Topic +kafka.topics.failed=local.lms.user.account.merge + +#Remote Debug Configuration +# task.opts=-agentlib:jdwp=transport=dt_socket,address=localhost:9009,server=y,suspend=y + +# Configuration for default channel ID +channel.default=in.ekstep + +#elastic-search +sunbird_es_cluster=local.lms.es.cluster +sunbird_es_host=127.0.0.1 +sunbird_es_port=9200 + +cassandra.lp.connection=localhost:9042 +cassandra.lpa.connection=localhost:9042 + +cassandra.connection.platform_courses=localhost:9042 +kp.learning_service.base_url=https://dev.sunbirded.org/action +courses.keyspace.name=sunbird_courses +search.es_conn_info=localhost:9200 +job.time_zone=IST +sunbird.installation=local +user.courses.table=user_enrolments +content.consumption.table=user_content_consumption +user.courses.es.index=user-courses +user.courses.es.type=_doc +course.batch.updater.kafka.topic=local.coursebatch.job.request +max.iteration.count.samza.job=2 +course.date.format=yyyy-MM-dd HH:mm:ss:SSSZ \ No newline at end of file diff --git a/platform-jobs/samza/merge-user-courses/src/main/config/merge-user-courses.properties b/platform-jobs/samza/merge-user-courses/src/main/config/merge-user-courses.properties new file mode 100644 index 0000000000..f386bc21c3 --- /dev/null +++ b/platform-jobs/samza/merge-user-courses/src/main/config/merge-user-courses.properties @@ -0,0 +1,78 @@ +# Job +job.factory.class=org.apache.samza.job.yarn.YarnJobFactory +job.name=__env__.merge-user-courses + +# YARN +yarn.package.path=http://__yarn_host__:__yarn_port__/__env__/${project.artifactId}-${pom.version}-distribution.tar.gz + +# Metrics +metrics.reporters=snapshot,jmx +metrics.reporter.snapshot.class=org.apache.samza.metrics.reporter.MetricsSnapshotReporterFactory +metrics.reporter.snapshot.stream=kafka.__env__.metrics +metrics.reporter.jmx.class=org.apache.samza.metrics.reporter.JmxReporterFactory + +# Task +task.class=org.sunbird.jobs.samza.task.MergeUserCoursesTask +task.inputs=kafka.__env__.lms.user.account.merge +task.checkpoint.factory=org.apache.samza.checkpoint.kafka.KafkaCheckpointManagerFactory +task.checkpoint.system=kafka +task.checkpoint.replication.factor=__samza_checkpoint_replication_factor__ +task.commit.ms=60000 +task.window.ms=300000 + +# Serializers +serializers.registry.json.class=org.sunbird.jobs.samza.serializers.EkstepJsonSerdeFactory +serializers.registry.metrics.class=org.apache.samza.serializers.MetricsSnapshotSerdeFactory + +# Systems +systems.kafka.samza.factory=org.apache.samza.system.kafka.KafkaSystemFactory +systems.kafka.samza.msg.serde=json +systems.kafka.streams.metrics.samza.msg.serde=metrics +systems.kafka.consumer.zookeeper.connect=__zookeepers__ +systems.kafka.consumer.auto.offset.reset=smallest +systems.kafka.samza.offset.default=oldest +systems.kafka.producer.bootstrap.servers=__kafka_brokers__ + +# Job Coordinator +job.coordinator.system=kafka + +# Normally, this would be 3, but we have only one broker. +job.coordinator.replication.factor=__samza_coordinator_replication_factor__ + +# Job specific configuration + +# Metrics +output.metrics.job.name=merge-user-courses +output.metrics.topic.name=__env__.pipeline_metrics +kafka.topics.backend.telemetry=__env__.telemetry.raw + +#Failed Topic Config +output.failed.events.topic.name=__env__.learning.events.failed + +# Retry Topic +kafka.topics.failed=__env__.lms.user.account.merge + +# Configuration for default channel ID +channel.default=in.ekstep + +#elastic-search +sunbird_es_cluster=__lms_es_cluster__ +sunbird_es_host=__lms_es_host__ +sunbird_es_port=__lms_es_port__ + +cassandra.lp.connection=__cassandra_lp_connection__ +cassandra.lpa.connection=__cassandra_lpa_connection__ + +cassandra.connection.platform_courses=__cassandra_sunbird_connection__ +kp.learning_service.base_url=__kp_learning_service_base_url__ +courses.keyspace.name=sunbird_courses +search.es_conn_info=__search_lms_es_host__ +job.time_zone=IST +sunbird.installation=__sunbird_installation__ +user.courses.table=user_enrolments +content.consumption.table=user_content_consumption +user.courses.es.index=user-courses +user.courses.es.type=_doc +course.batch.updater.kafka.topic=__env__.coursebatch.job.request +max.iteration.count.samza.job=__max_iteration_count_for_samza_job__ +course.date.format=yyyy-MM-dd HH:mm:ss:SSSZ \ No newline at end of file diff --git a/platform-jobs/samza/merge-user-courses/src/main/java/org/sunbird/jobs/samza/model/BatchEnrollmentSyncModel.java b/platform-jobs/samza/merge-user-courses/src/main/java/org/sunbird/jobs/samza/model/BatchEnrollmentSyncModel.java new file mode 100644 index 0000000000..7e29db4a19 --- /dev/null +++ b/platform-jobs/samza/merge-user-courses/src/main/java/org/sunbird/jobs/samza/model/BatchEnrollmentSyncModel.java @@ -0,0 +1,32 @@ +package org.sunbird.jobs.samza.model; + +public class BatchEnrollmentSyncModel { + + private String batchId; + private String userId; + private String courseId; + + public String getBatchId() { + return batchId; + } + + public void setBatchId(String batchId) { + this.batchId = batchId; + } + + public String getUserId() { + return userId; + } + + public void setUserId(String userId) { + this.userId = userId; + } + + public String getCourseId() { + return courseId; + } + + public void setCourseId(String courseId) { + this.courseId = courseId; + } +} diff --git a/platform-jobs/samza/merge-user-courses/src/main/java/org/sunbird/jobs/samza/service/MergeUserCoursesService.java b/platform-jobs/samza/merge-user-courses/src/main/java/org/sunbird/jobs/samza/service/MergeUserCoursesService.java new file mode 100644 index 0000000000..91bf959f85 --- /dev/null +++ b/platform-jobs/samza/merge-user-courses/src/main/java/org/sunbird/jobs/samza/service/MergeUserCoursesService.java @@ -0,0 +1,460 @@ +package org.sunbird.jobs.samza.service; + +import com.datastax.driver.core.RegularStatement; +import com.datastax.driver.core.Session; +import com.datastax.driver.core.querybuilder.Batch; +import com.datastax.driver.core.querybuilder.QueryBuilder; +import com.datastax.driver.core.querybuilder.Update; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.collections4.MapUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.samza.config.Config; +import org.apache.samza.system.OutgoingMessageEnvelope; +import org.apache.samza.system.SystemStream; +import org.apache.samza.task.MessageCollector; +import org.sunbird.common.Platform; +import org.sunbird.common.exception.ClientException; +import org.sunbird.jobs.samza.exception.PlatformErrorCodes; +import org.sunbird.jobs.samza.service.ISamzaService; +import org.sunbird.jobs.samza.service.task.JobMetrics; +import org.sunbird.jobs.samza.util.FailedEventsUtil; +import org.sunbird.jobs.samza.util.JSONUtils; +import org.sunbird.jobs.samza.util.JobLogger; +import org.sunbird.searchindex.elasticsearch.ElasticSearchUtil; +import org.sunbird.jobs.samza.model.BatchEnrollmentSyncModel; +import org.sunbird.jobs.samza.util.CassandraConnector; +import org.sunbird.jobs.samza.util.MergeUserCoursesParams; +import org.sunbird.jobs.samza.util.SunbirdCassandraUtil; + +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.*; +import java.util.stream.Collectors; + +public class MergeUserCoursesService implements ISamzaService { + private static JobLogger LOGGER = new JobLogger(MergeUserCoursesService.class); + private SystemStream systemStream; + private Config config = null; + private static final String UNDERSCORE = "_"; + private ObjectMapper mapper = new ObjectMapper(); + private static final String ACTION = "merge-user-courses-and-cert"; + private static int MAXITERTIONCOUNT = 2; + + private static String KEYSPACE; + private static String CONTENT_CONSUMPTION_TABLE; + private static String USER_COURSES_TABLE; + private static String USER_COURSE_ES_INDEX; + private static String USER_COURSE_ES_TYPE; + private static String COURSE_BATCH_UPDATER_KAFKA_TOPIC; + private static String COURSE_DATE_FORMAT; + private static SimpleDateFormat DateFormatter; + private static String USER_ACTIVITY_AGG; + private Session cassandraSession = null; + + protected int getMaxIterations() { + if (Platform.config.hasPath("max.iteration.count.samza.job")) + return Platform.config.getInt("max.iteration.count.samza.job"); + else + return MAXITERTIONCOUNT; + } + + private boolean validateObject(Map edata) { + String action = (String) edata.get(MergeUserCoursesParams.action.name()); + Integer iteration = (Integer) edata.get(MergeUserCoursesParams.iteration.name()); + if (StringUtils.equalsIgnoreCase(ACTION, action) && (iteration <= getMaxIterations())) { + return true; + } + return false; + } + + private static void initializeConfigurations() { + KEYSPACE = Platform.config.hasPath("courses.keyspace.name") ? + Platform.config.getString("courses.keyspace.name") : "sunbird_courses"; + + CONTENT_CONSUMPTION_TABLE = Platform.config.hasPath("content.consumption.table") ? + Platform.config.getString("content.consumption.table") : "user_content_consumption"; + + USER_COURSES_TABLE = Platform.config.hasPath("user.courses.table") ? + Platform.config.getString("user.courses.table") : "user_enrolments"; + + USER_COURSE_ES_INDEX = Platform.config.hasPath("user.courses.es.index") ? + Platform.config.getString("user.courses.es.index") : "user-courses"; + + USER_COURSE_ES_TYPE = Platform.config.hasPath("user.courses.es.type") ? + Platform.config.getString("user.courses.es.type") : "_doc"; + + COURSE_BATCH_UPDATER_KAFKA_TOPIC = Platform.config.getString("course.batch.updater.kafka.topic"); + + COURSE_DATE_FORMAT = Platform.config.hasPath("course.date.format") ? + Platform.config.getString("course.date.format") : "yyyy-MM-dd HH:mm:ss:SSSZ"; + + USER_ACTIVITY_AGG = "user_activity_agg"; + + DateFormatter = new SimpleDateFormat(COURSE_DATE_FORMAT); + } + + @Override + public void initialize(Config config) throws Exception { + this.config = config; + JSONUtils.loadProperties(config); + initializeConfigurations(); + this.cassandraSession = new CassandraConnector(config).getSession(); + LOGGER.info("MergeUserCoursesService:initialize: Service config initialized"); + ElasticSearchUtil.initialiseESClient(USER_COURSE_ES_INDEX, Platform.config.getString("search.es_conn_info")); + LOGGER.info("MergeUserCoursesService:initialize: ESClient initialized for index:" + USER_COURSE_ES_INDEX); + systemStream = new SystemStream("kafka", config.get("output.failed.events.topic.name")); + LOGGER.info("MergeUserCoursesService:initialize: Stream initialized for Failed Events"); + } + + @Override + public void processMessage(Map message, JobMetrics metrics, MessageCollector collector) throws Exception { + if (MapUtils.isEmpty(message)) { + LOGGER.info("MergeUserCoursesService:processMessage: Ignoring the event since message is empty."); + FailedEventsUtil.pushEventForRetry(systemStream, message, metrics, collector, + PlatformErrorCodes.DATA_ERROR.name(), new ClientException("ERR_MERGE_USER_COURSES_SAMZA", "message is empty")); + metrics.incSkippedCounter(); + return; + } + + Map edata = (Map) message.get(MergeUserCoursesParams.edata.name()); + if (MapUtils.isEmpty(edata)) { + LOGGER.info("MergeUserCoursesService:processMessage: Ignoring the event since edata is empty."); + FailedEventsUtil.pushEventForRetry(systemStream, message, metrics, collector, + PlatformErrorCodes.DATA_ERROR.name(), new ClientException("ERR_MERGE_USER_COURSES_SAMZA", "message.edata is empty")); + metrics.incSkippedCounter(); + return; + } + + String fromUserId = (String) edata.get(MergeUserCoursesParams.fromAccountId.name()); + String toUserId = (String) edata.get(MergeUserCoursesParams.toAccountId.name()); + + if (StringUtils.isBlank(fromUserId) || StringUtils.isBlank(toUserId) || !validateObject(edata)) { + LOGGER.info("MergeUserCoursesService:processMessage: Ignoring the event due to invalid edata:" + edata); + FailedEventsUtil.pushEventForRetry(systemStream, message, metrics, collector, + PlatformErrorCodes.DATA_ERROR.name(), new ClientException("ERR_MERGE_USER_COURSES_SAMZA", "message.edata values are not valid")); + metrics.incSkippedCounter(); + return; + } + + try { + mergeContentConsumption(fromUserId, toUserId); + mergeUserBatches(fromUserId, toUserId); + generateBatchEnrollmentSyncEvents(toUserId, collector); + mergeUserActivityAggregates(fromUserId, toUserId); + metrics.incSuccessCounter(); + LOGGER.info("MergeUserCoursesService:processMessage: Event processed successfully", message); + } catch (Exception e) { + edata.put(MergeUserCoursesParams.status.name(), MergeUserCoursesParams.FAILED.name()); + FailedEventsUtil.pushEventForRetry(systemStream, message, metrics, collector, + PlatformErrorCodes.PROCESSING_ERROR.name(), e); + throw e; + } + + } + + private void generateBatchEnrollmentSyncEvents(String userId, MessageCollector collector) throws Exception { + List objects = getBatchDetailsOfUser(userId); + if (CollectionUtils.isNotEmpty(objects)) { + for (BatchEnrollmentSyncModel model : objects) { + Map event = getBatchEnrollmentSyncEvent(model); + collector.send(new OutgoingMessageEnvelope(new SystemStream("kafka", COURSE_BATCH_UPDATER_KAFKA_TOPIC), event)); + } + } + } + + private void mergeUserBatches(String fromUserId, String toUserId) throws Exception { + List fromBatches = getBatchDetailsOfUser(fromUserId); + List toBatches = getBatchDetailsOfUser(toUserId); + + Map fromBatchIds = new HashMap<>(); + Map toBatchIds = new HashMap<>(); + if (CollectionUtils.isNotEmpty(fromBatches)) { + for (BatchEnrollmentSyncModel fromBatch : fromBatches) { + if (StringUtils.isNotBlank(fromBatch.getBatchId())) + fromBatchIds.put(fromBatch.getBatchId(), fromBatch); + } + } + if (CollectionUtils.isNotEmpty(toBatches)) { + for (BatchEnrollmentSyncModel toBatch : toBatches) { + if (StringUtils.isNotBlank(toBatch.getBatchId())) + toBatchIds.put(toBatch.getBatchId(), toBatch); + } + } + + List batchIdsToBeMigrated = (List) CollectionUtils.subtract(fromBatchIds.keySet(), toBatchIds.keySet()); + + //Migrate batch records in Cassandra and ES + if (CollectionUtils.isNotEmpty(batchIdsToBeMigrated)) { + for (String batchId : batchIdsToBeMigrated) { + String courseId = fromBatchIds.get(batchId).getCourseId(); + Map userCourse = getUserCourse(batchId, fromUserId, courseId); + if (MapUtils.isNotEmpty(userCourse)) { + userCourse.put(MergeUserCoursesParams.userId.name(), toUserId); + LOGGER.info("MergeUserCoursesService:mergeUserBatches: Merging batch:" + batchId + " updated record:" + userCourse); + SunbirdCassandraUtil.upsert(KEYSPACE, USER_COURSES_TABLE, userCourse); + + /*String documentJson = ElasticSearchUtil.getDocumentAsStringById(USER_COURSE_ES_INDEX, USER_COURSE_ES_TYPE, + batchId + UNDERSCORE + fromUserId); + Map userCourseDoc = mapper.readValue(documentJson, Map.class); + userCourseDoc.put(MergeUserCoursesParams.userId.name(), toUserId); + userCourseDoc.put(MergeUserCoursesParams.id.name(), batchId + UNDERSCORE + toUserId); + userCourseDoc.put(MergeUserCoursesParams.identifier.name(), batchId + UNDERSCORE + toUserId); + ElasticSearchUtil.addDocumentWithId(USER_COURSE_ES_INDEX, USER_COURSE_ES_TYPE, + batchId + UNDERSCORE + toUserId, mapper.writeValueAsString(userCourseDoc));*/ + } else { + LOGGER.info("MergeUserCoursesService:mergeUserBatches: user_courses record with batchId:" + batchId + " userId:" + fromUserId + " found in ES but not in Cassandra"); + } + } + } + } + + private void mergeContentConsumption(String fromUserId, String toUserId) { + //Get content consumption data + List> fromContentConsumptionList = getContentConsumption(fromUserId); + List> toContentConsumptionList = getContentConsumption(toUserId); + + if (CollectionUtils.isNotEmpty(fromContentConsumptionList)) { + for (Map contentConsumption : fromContentConsumptionList) { + Map matchingRecord = getMatchingRecord(contentConsumption, toContentConsumptionList); + if (MapUtils.isEmpty(matchingRecord)) { + matchingRecord = contentConsumption; + matchingRecord.put(MergeUserCoursesParams.userId.name(), toUserId); + } else { + mergeContentConsumptionRecord(contentConsumption, matchingRecord); + } + SunbirdCassandraUtil.upsert(KEYSPACE, CONTENT_CONSUMPTION_TABLE, matchingRecord); + } + } + } + + private void mergeContentConsumptionRecord(Map oldRecord, Map newRecord) { + /* + * for status, progress, datetime, lastaccesstime, lastcompletedtime, lastupdatedtime fields, + * max value should be considered + * for completedcount, viewcount fields, sum of both records should be considered + * */ + newRecord.put(MergeUserCoursesParams.status.name(), getUpdatedValue("Integer", "Max", + MergeUserCoursesParams.status.name(), oldRecord, newRecord)); + newRecord.put(MergeUserCoursesParams.progress.name(), getUpdatedValue("Integer", "Max", + MergeUserCoursesParams.progress.name(), oldRecord, newRecord)); + newRecord.put(MergeUserCoursesParams.viewCount.name(), getUpdatedValue("Integer", "Sum", + MergeUserCoursesParams.viewCount.name(), oldRecord, newRecord)); + newRecord.put(MergeUserCoursesParams.completedCount.name(), getUpdatedValue("Integer", "Sum", + MergeUserCoursesParams.completedCount.name(), oldRecord, newRecord)); + + newRecord.put(MergeUserCoursesParams.dateTime.name(), getUpdatedValue("Date", "Max", + MergeUserCoursesParams.dateTime.name(), oldRecord, newRecord)); + newRecord.put(MergeUserCoursesParams.lastAccessTime.name(), getUpdatedValue("DateString", "Max", + MergeUserCoursesParams.lastAccessTime.name(), oldRecord, newRecord)); + newRecord.put(MergeUserCoursesParams.lastCompletedTime.name(), getUpdatedValue("DateString", "Max", + MergeUserCoursesParams.lastCompletedTime.name(), oldRecord, newRecord)); + newRecord.put(MergeUserCoursesParams.lastUpdatedTime.name(), getUpdatedValue("DateString", "Max", + MergeUserCoursesParams.lastUpdatedTime.name(), oldRecord, newRecord)); + } + + private Object getUpdatedValue(String dataType, String operation, String fieldName, Map oldRecord, Map newRecord) { + if (null == oldRecord.get(fieldName)) { + return newRecord.get(fieldName); + } + if (null == newRecord.get(fieldName)) { + return oldRecord.get(fieldName); + } + switch (dataType) { + case "Integer": + if (oldRecord.get(fieldName) instanceof Integer && + newRecord.get(fieldName) instanceof Integer) { + int val1 = (int) oldRecord.get(fieldName); + int val2 = (int) newRecord.get(fieldName); + if (StringUtils.equalsIgnoreCase("Sum", operation)) { + return val1 + val2; + } else if (StringUtils.equalsIgnoreCase("Max", operation)) { + return val1 > val2 ? val1 : val2; + } + } + break; + case "DateString": + if (oldRecord.get(fieldName) instanceof String && + newRecord.get(fieldName) instanceof String) { + String dateStr1 = (String) oldRecord.get(fieldName); + String dateStr2 = (String) newRecord.get(fieldName); + Date date1; + Date date2; + try { + date1 = DateFormatter.parse(dateStr1); + } catch (ParseException pe) { + LOGGER.info("MergeUserCoursesService:getUpdatedValue: Date Parsing failed for field:" + fieldName + " value:" + dateStr1); + return dateStr2; + } + try { + date2 = DateFormatter.parse(dateStr2); + } catch (ParseException pe) { + LOGGER.info("MergeUserCoursesService:getUpdatedValue: Date Parsing failed for field:" + fieldName + " value:" + dateStr2); + return dateStr1; + } + if (StringUtils.equalsIgnoreCase("Max", operation)) { + if (date1.after(date2)) { + return dateStr1; + } else { + return dateStr2; + } + } + } + break; + case "Date": + if (oldRecord.get(fieldName) instanceof Date && + newRecord.get(fieldName) instanceof Date) { + Date date1 = (Date) oldRecord.get(fieldName); + Date date2 = (Date) newRecord.get(fieldName); + if (StringUtils.equalsIgnoreCase("Max", operation)) { + if (date1.after(date2)) { + return date1; + } else { + return date2; + } + } + } + break; + } + return newRecord.get(fieldName); + } + + private Map getMatchingRecord(Map contentConsumption, List> toContentConsumptionList) { + Map matchingRecord = new HashMap(); + if (CollectionUtils.isNotEmpty(toContentConsumptionList)) { + for (Map toContentConsumption : toContentConsumptionList) { + if (StringUtils.equalsIgnoreCase((String) contentConsumption.get(MergeUserCoursesParams.contentId.name()), (String) toContentConsumption.get(MergeUserCoursesParams.contentId.name())) && + StringUtils.equalsIgnoreCase((String) contentConsumption.get(MergeUserCoursesParams.batchId.name()), (String) toContentConsumption.get(MergeUserCoursesParams.batchId.name())) && + StringUtils.equalsIgnoreCase((String) contentConsumption.get(MergeUserCoursesParams.courseId.name()), (String) toContentConsumption.get(MergeUserCoursesParams.courseId.name()))) { + matchingRecord = toContentConsumption; + break; + } + } + } + return matchingRecord; + } + + private List> getContentConsumption(String userId) { + Map key = new HashMap<>(); + key.put(MergeUserCoursesParams.userId.name(), userId); + return SunbirdCassandraUtil.readAsListOfMap(KEYSPACE, CONTENT_CONSUMPTION_TABLE, key); + } + + private Map getUserCourse(String batchId, String userId, String courseId) { + Map key = new HashMap<>(); + key.put(MergeUserCoursesParams.batchId.name(), batchId); + key.put(MergeUserCoursesParams.userId.name(), userId); + key.put(MergeUserCoursesParams.courseId.name(), courseId); + List> data = SunbirdCassandraUtil.readAsListOfMap(KEYSPACE, USER_COURSES_TABLE, key); + return CollectionUtils.isEmpty(data) ? new HashMap() : data.get(0); + } + + private List getBatchDetailsOfUser(String userId) throws Exception { + List objects = new ArrayList<>(); + Map searchQuery = new HashMap<>(); + List userIdList = new ArrayList<>(); + userIdList.add(userId); + searchQuery.put(MergeUserCoursesParams.userId.name(), userIdList); + Map key = new HashMap<>(); + key.put(MergeUserCoursesParams.userId.name(), userIdList); + List> documents = SunbirdCassandraUtil.readAsListOfMap(KEYSPACE, USER_COURSES_TABLE, key); + //List documents = ElasticSearchUtil.textSearchReturningId(searchQuery, USER_COURSE_ES_INDEX, USER_COURSE_ES_TYPE); + if (CollectionUtils.isNotEmpty(documents)) { + documents.forEach(doc -> { + BatchEnrollmentSyncModel model = new BatchEnrollmentSyncModel(); + model.setBatchId((String) doc.get(MergeUserCoursesParams.batchId.name())); + model.setUserId((String) doc.get(MergeUserCoursesParams.userId.name())); + model.setCourseId((String) doc.get(MergeUserCoursesParams.courseId.name())); + objects.add(model); + }); + } + return objects; + } + + private Map getBatchEnrollmentSyncEvent(BatchEnrollmentSyncModel model) { + return new HashMap() {{ + put("actor", new HashMap() {{ + put("id", "Course Batch Updater"); + put("type", "System"); + }}); + put("eid", "BE_JOB_REQUEST"); + put("edata", new HashMap() {{ + put("action", "batch-enrolment-sync"); + put("iteration", 1); + put("batchId", model.getBatchId()); + put("userId", model.getUserId()); + put("courseId", model.getCourseId()); + put("reset", Arrays.asList("completionPercentage", "status", "progress")); + }}); + put("ets", System.currentTimeMillis()); + put("context", new HashMap() {{ + put("pdata", new HashMap() {{ + put("ver", "1.0"); + put("id", "org.sunbird.platform"); + }}); + }}); + put("mid", "LP." + System.currentTimeMillis() + "." + UUID.randomUUID()); + put("object", new HashMap() {{ + put("id", model.getBatchId() + UNDERSCORE + model.getUserId()); + put("type", "CourseBatchEnrolment"); + }}); + }}; + } + + + private void mergeUserActivityAggregates(String fromUserId, String toUserId) throws Exception { + List fromBatches = getBatchDetailsOfUser(fromUserId); + if(CollectionUtils.isNotEmpty(fromBatches)) { + List fromCourseIds = fromBatches.stream().map(enrol -> enrol.getCourseId()).collect(Collectors.toList()); + List toCourseIds = fromBatches.stream().map(enrol -> enrol.getCourseId()).collect(Collectors.toList()); + Map key = new HashMap<>(); + key.put(MergeUserCoursesParams.activity_type.name(), "Course"); + key.put(MergeUserCoursesParams.user_id.name(), fromUserId); + key.put(MergeUserCoursesParams.activity_id.name(), fromCourseIds); + List> fromData = SunbirdCassandraUtil.readAsListOfMap(KEYSPACE, USER_ACTIVITY_AGG, key); + key.put(MergeUserCoursesParams.activity_id.name(), toCourseIds); + List> toData = SunbirdCassandraUtil.readAsListOfMap(KEYSPACE, USER_ACTIVITY_AGG, key); + Map toDataMap = toData.stream().collect(Collectors.toMap(m -> (String)m.get("context_id"), m -> m)); + List updateQueryList = new ArrayList<>(); + if(CollectionUtils.isNotEmpty(fromData)) { + fromData.stream().filter(data -> MapUtils.isNotEmpty(data)).collect(Collectors.toList()).forEach(data -> { + data.put(MergeUserCoursesParams.user_id.name(), toUserId); + Map fromAgg = (Map) data.get("agg"); + Map toAgg = (Map) ((Map)toDataMap.getOrDefault(data.get("context_id"), new HashMap())).getOrDefault("agg", new HashMap()); + data.put("agg", new HashMap(){{ + put("completedCount", Math.max(fromAgg.getOrDefault("completedCount", 0), toAgg.getOrDefault("completedCount", 0))); + }}); + data.put("agg_last_updated", new HashMap(){{ + put("completedCount", new Date()); + }}); + Map dataToSelect = new HashMap() {{ + put(MergeUserCoursesParams.activity_type.name(), "Course"); + put(MergeUserCoursesParams.activity_id.name(), data.get("activity_id")); + put(MergeUserCoursesParams.user_id.name(), toUserId); + put("context_id", data.get("context_id")); + }}; + updateQueryList.add(updateQuery(KEYSPACE, USER_ACTIVITY_AGG, data, dataToSelect)); + }); + } + if(CollectionUtils.isNotEmpty(updateQueryList)){ + Batch batch = QueryBuilder.batch(updateQueryList.toArray(new RegularStatement[updateQueryList.size()])); + cassandraSession.execute(batch); + } + } + + } + + + public Update.Where updateQuery(String keyspace, String table, Map propertiesToUpdate, Map propertiesToSelect) { + Update.Where updateQuery = QueryBuilder.update(keyspace, table).where(); + propertiesToUpdate.entrySet().forEach(entry -> updateQuery.with(QueryBuilder.set(entry.getKey(), entry.getValue()))); + propertiesToSelect.entrySet().forEach(entry -> { + if (entry.getValue() instanceof List) + updateQuery.and(QueryBuilder.in(entry.getKey(), (List) entry.getValue())); + else + updateQuery.and(QueryBuilder.eq(entry.getKey(), entry.getValue())); + }); + return updateQuery; + } + +} diff --git a/platform-jobs/samza/merge-user-courses/src/main/java/org/sunbird/jobs/samza/task/MergeUserCoursesTask.java b/platform-jobs/samza/merge-user-courses/src/main/java/org/sunbird/jobs/samza/task/MergeUserCoursesTask.java new file mode 100644 index 0000000000..3c360c690b --- /dev/null +++ b/platform-jobs/samza/merge-user-courses/src/main/java/org/sunbird/jobs/samza/task/MergeUserCoursesTask.java @@ -0,0 +1,40 @@ +package org.sunbird.jobs.samza.task; + + +import org.apache.samza.task.MessageCollector; +import org.apache.samza.task.TaskCoordinator; +import org.sunbird.jobs.samza.service.ISamzaService; +import org.sunbird.jobs.samza.util.JobLogger; +import org.sunbird.jobs.samza.service.MergeUserCoursesService; + +import java.util.Arrays; +import java.util.Map; + +public class MergeUserCoursesTask extends BaseTask { + + private ISamzaService service = new MergeUserCoursesService(); + private static JobLogger LOGGER = new JobLogger(MergeUserCoursesTask.class); + + @Override + public ISamzaService initialize() throws Exception { + LOGGER.info("MergeUserCoursesTask:initialize: Task initialized"); + this.action = Arrays.asList("merge-user-courses-and-cert"); + this.jobStartMessage = "Started processing of merge-user-courses samza job"; + this.jobEndMessage = "merge-user-courses job processing complete"; + this.jobClass = "org.sunbird.jobs.samza.task.MergeUserCoursesTask"; + return service; + } + + @Override + public void process(Map message, MessageCollector collector, TaskCoordinator coordinator) throws Exception { + try { + LOGGER.info("MergeUserCoursesTask:process: Starting to process for mid : " + message.get("mid") + " at :: " + System.currentTimeMillis()); + service.processMessage(message, metrics, collector); + LOGGER.info("MergeUserCoursesTask:process: Successfully completed processing for mid : " + message.get("mid") + " at :: " + System.currentTimeMillis()); + } catch (Exception e) { + metrics.incErrorCounter(); + LOGGER.error("MergeUserCoursesTask:process: Message processing failed", message, e); + } + } + +} diff --git a/platform-jobs/samza/merge-user-courses/src/main/java/org/sunbird/jobs/samza/util/MergeUserCoursesParams.java b/platform-jobs/samza/merge-user-courses/src/main/java/org/sunbird/jobs/samza/util/MergeUserCoursesParams.java new file mode 100644 index 0000000000..8645c89bcd --- /dev/null +++ b/platform-jobs/samza/merge-user-courses/src/main/java/org/sunbird/jobs/samza/util/MergeUserCoursesParams.java @@ -0,0 +1,9 @@ +package org.sunbird.jobs.samza.util; + +import org.sunbird.graph.dac.util.RelationType; + +public enum MergeUserCoursesParams { + userId, batchId, contentId, courseId, status, edata, id, identifier, action, fromAccountId, + toAccountId, FAILED, iteration, progress, dateTime, lastAccessTime, lastCompletedTime, + lastUpdatedTime, completedCount, viewCount, activity_type, activity_id, user_id; +} diff --git a/platform-jobs/samza/merge-user-courses/src/main/resources/log4j.xml b/platform-jobs/samza/merge-user-courses/src/main/resources/log4j.xml new file mode 100644 index 0000000000..e69de29bb2 From 05e37092a1ea7f7d0c89d7f36017fa21e40ff62a Mon Sep 17 00:00:00 2001 From: Harikumar Palemkota Date: Fri, 2 Sep 2022 12:52:13 +0530 Subject: [PATCH 057/222] Reverting back the merge-course module, will remove in next release --- .../samza/merge-user-courses/src/main/assembly/src.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/platform-jobs/samza/merge-user-courses/src/main/assembly/src.xml b/platform-jobs/samza/merge-user-courses/src/main/assembly/src.xml index 29bd7d10f1..b8c4bf8a85 100644 --- a/platform-jobs/samza/merge-user-courses/src/main/assembly/src.xml +++ b/platform-jobs/samza/merge-user-courses/src/main/assembly/src.xml @@ -34,10 +34,10 @@ ${basedir}/src/main/resources/log4j.xml lib - - ${basedir}/src/main/config/local.merge-user-courses.properties + ${basedir}/src/main/config/merge-user-courses.properties config true From a7eb4aff1c2d186dd27bec99ccfeb1d6854382c1 Mon Sep 17 00:00:00 2001 From: Harikumar Palemkota Date: Fri, 2 Sep 2022 12:55:16 +0530 Subject: [PATCH 058/222] Reverting back the merge-course module, will remove in next release --- .../src/main/resources/log4j.xml | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/platform-jobs/samza/merge-user-courses/src/main/resources/log4j.xml b/platform-jobs/samza/merge-user-courses/src/main/resources/log4j.xml index e69de29bb2..0f37824c0c 100644 --- a/platform-jobs/samza/merge-user-courses/src/main/resources/log4j.xml +++ b/platform-jobs/samza/merge-user-courses/src/main/resources/log4j.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file From 05062ac30a63fc02c1da443aee0853766afb8e38 Mon Sep 17 00:00:00 2001 From: Jayaprakash8887 Date: Wed, 28 Sep 2022 11:14:24 +0530 Subject: [PATCH 059/222] Issue #KN-445 feat: Republishing of Live Nodes --- .../helm_charts/datapipeline_jobs/values.j2 | 252 +++++++++++++++++- 1 file changed, 251 insertions(+), 1 deletion(-) diff --git a/kubernetes/helm_charts/datapipeline_jobs/values.j2 b/kubernetes/helm_charts/datapipeline_jobs/values.j2 index 8145d2df5b..87cd57de66 100644 --- a/kubernetes/helm_charts/datapipeline_jobs/values.j2 +++ b/kubernetes/helm_charts/datapipeline_jobs/values.j2 @@ -1063,4 +1063,254 @@ dialcode-context-updater: taskmanager.numberOfTaskSlots: {{ flink_job_names['qrcode-image-generator'].taskslots }} parallelism.default: 1 jobmanager.execution.failover-strategy: region - taskmanager.memory.network.fraction: 0.1 \ No newline at end of file + taskmanager.memory.network.fraction: 0.1 + +live-node-publisher: + live-node-publisher: |+ + include file("/data/flink/conf/base-config.conf") + kafka { + input.topic = {{ env_name }}.republish.job.request + live_video_stream.topic = "{{ env_name }}.live.video.stream.request" + error.topic = "{{ env_name }}.learning.events.failed" + skipped.topic = "{{ env_name }}.learning.events.skipped" + groupId = {{ env_name }}-content-republish-group + } + task { + consumer.parallelism = 1 + parallelism = 1 + router.parallelism = 1 + } + redis { + host={{redis_host}} + port=6379 + database { + contentCache.id = 0 + } + } + content { + bundleLocation = "/tmp/contentBundle" + isECARExtractionEnabled = true + retry_asset_download_count = 1 + keyspace = "{{ content_keyspace_name }}" + table = "content_data" + tmp_file_location = "/tmp" + objectType = ["Content", "ContentImage","Collection","CollectionImage"] + mimeType = ["application/pdf", + "application/vnd.ekstep.ecml-archive", + "application/vnd.ekstep.html-archive", + "application/vnd.android.package-archive", + "application/vnd.ekstep.content-archive", + "application/epub", + "application/msword", + "application/vnd.ekstep.h5p-archive", + "video/webm", + "video/mp4", + "application/vnd.ekstep.content-collection", + "video/quicktime", + "application/octet-stream", + "application/json", + "application/javascript", + "application/xml", + "text/plain", + "text/html", + "text/javascript", + "text/xml", + "text/css", + "image/jpeg", + "image/jpg", + "image/png", + "image/tiff", + "image/bmp", + "image/gif", + "image/svg+xml", + "image/x-quicktime", + "video/avi", + "video/mpeg", + "video/quicktime", + "video/3gpp", + "video/mp4", + "video/ogg", + "video/webm", + "video/msvideo", + "video/x-msvideo", + "video/x-qtc", + "video/x-mpeg", + "audio/mp3", + "audio/mp4", + "audio/mpeg", + "audio/ogg", + "audio/webm", + "audio/x-wav", + "audio/wav", + "audio/mpeg3", + "audio/x-mpeg-3", + "audio/vorbis", + "application/x-font-ttf", + "application/vnd.ekstep.plugin-archive", + "video/x-youtube", + "video/youtube", + "text/x-url"] + asset_download_duration = "60 seconds" + stream { + enabled = {{ content_stream_enabled | lower }} + mimeType = ["video/mp4", "video/webm"] + } + artifact.size.for_online= {{ content_artifact_size_for_online }} + + downloadFiles { + spine = ["appIcon"] + full = ["appIcon", "grayScaleAppIcon", "artifactUrl", "itemSetPreviewUrl", "media"] + } + + nested.fields=["badgeAssertions", "targets", "badgeAssociations", "plugins", "me_totalTimeSpent", "me_totalPlaySessionCount", "me_totalTimeSpentInSec", "batches", "trackable", "credentials", "discussionForum", "provider", "osMetadata", "actions", "transcripts", "accessibility"] + + } + cloud_storage { + folder { + content = "content" + artifact = "artifact" + } + } + + hierarchy { + keyspace = "{{ hierarchy_keyspace_name }}" + table = "content_hierarchy" + } + + contentTypeToPrimaryCategory { + ClassroomTeachingVideo: "Explanation Content" + ConceptMap: "Learning Resource" + Course: "Course" + CuriosityQuestionSet: "Practice Question Set" + eTextBook: "eTextbook" + Event: "Event" + EventSet: "Event Set" + ExperientialResource: "Learning Resource" + ExplanationResource: "Explanation Content" + ExplanationVideo: "Explanation Content" + FocusSpot: "Teacher Resource" + LearningOutcomeDefinition: "Teacher Resource" + MarkingSchemeRubric: "Teacher Resource" + PedagogyFlow: "Teacher Resource" + PracticeQuestionSet: "Practice Question Set" + PracticeResource: "Practice Question Set" + SelfAssess: "Course Assessment" + TeachingMethod: "Teacher Resource" + TextBook: "Digital Textbook" + Collection: "Content Playlist" + ExplanationReadingMaterial: "Learning Resource" + LearningActivity: "Learning Resource" + LessonPlan: "Content Playlist" + LessonPlanResource: "Teacher Resource" + PreviousBoardExamPapers: "Learning Resource" + TVLesson: "Explanation Content" + OnboardingResource: "Learning Resource" + ReadingMaterial: "Learning Resource" + Template: "Template" + Asset: "Asset" + Plugin: "Plugin" + LessonPlanUnit: "Lesson Plan Unit" + CourseUnit: "Course Unit" + TextBookUnit: "Textbook Unit" + Asset: "Certificate Template" + } + + compositesearch.index.name = "compositesearch" + search.document.type = "cs" + enableDIALContextUpdate = "Yes" + + cloud_storage_type="{{ cloud_store }}" + azure_storage_key="{{ sunbird_public_storage_account_name }}" + azure_storage_secret="{{ sunbird_public_storage_account_key }}" + azure_storage_container="{{ azure_public_container }}" + + master.category.validation.enabled ="{{ master_category_validation_enabled }}" + service { + print.basePath = "{{ kp_print_service_base_url }}" + search.basePath = "{{ kp_search_service_base_url }}" + } + + flink-conf: |+ + jobmanager.memory.flink.size: {{ flink_job_names['content-publish'].jobmanager_memory }} + taskmanager.memory.flink.size: {{ flink_job_names['content-publish'].taskmanager_memory }} + taskmanager.numberOfTaskSlots: {{ flink_job_names['content-publish'].taskslots }} + parallelism.default: 1 + jobmanager.execution.failover-strategy: region + taskmanager.memory.network.fraction: 0.1 + +live-video-stream-generator: + live-video-stream-generator: |+ + include file("/data/flink/conf/base-config.conf") + kafka { + input.topic = "{{ env_name }}.live.video.stream.request" + groupId = "{{ env_name }}-live-video-stream-generator-group" + } + task { + timer.duration = {{ video_stream_generator_timer_duration }} + consumer.parallelism = {{ video_stream_generator_consumer_parallelism }} + parallelism = {{ video_stream_generator_parallelism }} + max.retries = {{ video_stream_generator_max_retries }} + } + lms-cassandra { + keyspace = {{ platform_keyspace_name }} + table = "job_request" + } + service.content.basePath="{{ kp_content_service_base_url }}" + azure { + location = "centralindia" + login { + endpoint="https://login.microsoftonline.com" + } + api { + endpoint="https://management.azure.com" + version = "2018-07-01" + } + transform { + default = "media_transform_default" + hls = "media_transform_hls" + } + stream { + base_url="{{ video_stream_generator_azure_stream_base_url }}" + endpoint_name = "default" + protocol = "Hls" + policy_name = "Predefined_ClearStreamingOnly" + } + } + azure_tenant="{{ video_stream_generator_azure_tenant }}" + azure_subscription_id="{{ video_stream_generator_azure_subscription_id }}" + azure_account_name="{{ video_stream_generator_azure_account_name }}" + azure_resource_group_name="{{ video_stream_generator_azure_resource_group_name }}" + azure_token_client_key="{{ video_stream_generator_azure_token_client_key }}" + azure_token_client_secret="{{ video_stream_generator_azure_token_client_secret }}" + ## CSP Name. e.g: aws or azure + media_service_type="{{ media_service_provider_name }}" + ## AWS Elemental Media Convert Config + aws { + region="{{ aws_mediaconvert_region }}" + content_bucket_name="{{ aws_content_bucket_name }}" + token { + access_key="{{ aws_mediaconvert_access_key }}" + access_secret="{{ aws_mediaconvert_access_secret }}" + } + api { + endpoint="{{ aws_mediaconvert_api_endpoint }}" + version="{{ aws_mediaconvert_api_version }}" + } + service { + name="mediaconvert" + queue="{{ aws_mediaconvert_queue_id }}" + role="{{ aws_mediaconvert_role_name }}" + } + stream { + protocol="Hls" + } + } + + + flink-conf: |+ + jobmanager.memory.flink.size: {{ flink_job_names['video-stream-generator'].jobmanager_memory }} + taskmanager.memory.flink.size: {{ flink_job_names['video-stream-generator'].taskmanager_memory }} + taskmanager.numberOfTaskSlots: {{ flink_job_names['video-stream-generator'].taskslots }} + parallelism.default: 1 + jobmanager.execution.failover-strategy: region + taskmanager.memory.network.fraction: 0.1 From 2439dbd35a04bbf500e977c5cdbb0e1583c9327a Mon Sep 17 00:00:00 2001 From: santhosh-tg Date: Wed, 9 Nov 2022 13:09:09 +0530 Subject: [PATCH 060/222] Add gcp cloud role --- .../roles/gcp-cloud-storage/defaults/main.yml | 49 +++++++++++++++++++ .../gcp-cloud-storage/tasks/delete-batch.yml | 11 +++++ .../gcp-cloud-storage/tasks/download.yml | 11 +++++ .../gcp-cloud-storage/tasks/gcloud-auth.yml | 14 ++++++ .../gcp-cloud-storage/tasks/gcloud-revoke.yml | 8 +++ .../roles/gcp-cloud-storage/tasks/main.yml | 20 ++++++++ .../gcp-cloud-storage/tasks/upload-batch.yml | 11 +++++ .../roles/gcp-cloud-storage/tasks/upload.yml | 11 +++++ 8 files changed, 135 insertions(+) create mode 100644 ansible/roles/gcp-cloud-storage/defaults/main.yml create mode 100644 ansible/roles/gcp-cloud-storage/tasks/delete-batch.yml create mode 100644 ansible/roles/gcp-cloud-storage/tasks/download.yml create mode 100644 ansible/roles/gcp-cloud-storage/tasks/gcloud-auth.yml create mode 100644 ansible/roles/gcp-cloud-storage/tasks/gcloud-revoke.yml create mode 100644 ansible/roles/gcp-cloud-storage/tasks/main.yml create mode 100644 ansible/roles/gcp-cloud-storage/tasks/upload-batch.yml create mode 100644 ansible/roles/gcp-cloud-storage/tasks/upload.yml diff --git a/ansible/roles/gcp-cloud-storage/defaults/main.yml b/ansible/roles/gcp-cloud-storage/defaults/main.yml new file mode 100644 index 0000000000..086cf9c50d --- /dev/null +++ b/ansible/roles/gcp-cloud-storage/defaults/main.yml @@ -0,0 +1,49 @@ +# GCP bucket name +# Example - +# bucket_name: "sunbird-dev-public" +gcp_bucket_name: "" + +# The service account key file +# Example - +# gcp_storage_key_file: "/tmp/gcp.json" +gcp_storage_key_file: "" + +# Folder name in GCP bucket +# Example - +# dest_folder_name: "my-destination-folder" +dest_folder_name: "" + +# The delete pattern to delete files and folder +# Example - +# file_delete_pattern: "my-drectory/*" +# file_delete_pattern: "my-drectory/another-directory/*" +# file_delete_pattern: "*" +file_delete_pattern: "" + +# The path to local file which has to be uploaded to gcloud storage +# The local path to store the file after downloading from gcloud storage +# Example - +# local_file_or_folder_path: "/workspace/my-folder/myfile.json" +# local_file_or_folder_path: "/workspace/my-folder" +local_file_or_folder_path: "" + +# The name of the file in gcloud storage after uploading from local path +# The name of the file in gcloud storage that has to be downloaded +# Example - +# dest_file_name: "/myfile-blob.json" +dest_file_name: "" + + +# The folder path in gcloud storage to upload the files starting from the root of the bucket +# This path should start with / if we provide a value for this variable since we are going to append this path as below +# {{ bucket_name }}{{ dest_folder_name }} +# The above translates to "my-bucket/my-folder-path" +# Example - +# dest_folder_path: "/my-folder/json-files-folder" +# This variable can also be empty as shown below, which means we will upload directly at the root path of the bucket +dest_folder_path: "" + +# The local folder path which has to be uploaded to gcloud storage +# Example - +# local_source_folder: "/workspace/my-folder/json-files-folder" +local_source_folder: "" diff --git a/ansible/roles/gcp-cloud-storage/tasks/delete-batch.yml b/ansible/roles/gcp-cloud-storage/tasks/delete-batch.yml new file mode 100644 index 0000000000..17fe952b16 --- /dev/null +++ b/ansible/roles/gcp-cloud-storage/tasks/delete-batch.yml @@ -0,0 +1,11 @@ +--- +- name: Authenticate to gcloud + include_tasks: gcloud-auth.yml + +- name: Delete folder recursively in gcp storage + shell: gsutil rm -r "gs://{{ gcp_bucket_name }}/{{ file_delete_pattern }}" + async: 3600 + poll: 10 + +- name: Revoke gcloud access + include_tasks: gcloud-revoke.yml diff --git a/ansible/roles/gcp-cloud-storage/tasks/download.yml b/ansible/roles/gcp-cloud-storage/tasks/download.yml new file mode 100644 index 0000000000..c8c6e956ad --- /dev/null +++ b/ansible/roles/gcp-cloud-storage/tasks/download.yml @@ -0,0 +1,11 @@ +--- +- name: Authenticate to gcloud + include_tasks: gcloud-auth.yml + +- name: Download from gcloud storage + shell: gsutil cp "gs://{{ gcp_bucket_name }}/{{ dest_folder_name }}/{{ dest_file_name }}" "{{ local_file_or_folder_path }}" + async: 3600 + poll: 10 + +- name: Revoke gcloud access + include_tasks: gcloud-revoke.yml \ No newline at end of file diff --git a/ansible/roles/gcp-cloud-storage/tasks/gcloud-auth.yml b/ansible/roles/gcp-cloud-storage/tasks/gcloud-auth.yml new file mode 100644 index 0000000000..a480bdc275 --- /dev/null +++ b/ansible/roles/gcp-cloud-storage/tasks/gcloud-auth.yml @@ -0,0 +1,14 @@ +--- +- name: create tmp gcp service key file + tempfile: + state: file + suffix: gcp + register: config_key + +- name: Copy service account key file + copy: + content: "{{ gcp_storage_key_file }}" + dest: "{{ config_key.path }}" + +- name: Configure gcloud service account + shell: gcloud auth activate-service-account "{{ gcp_storage_service_account_name }}" --key-file="{{ config_key.path }}" diff --git a/ansible/roles/gcp-cloud-storage/tasks/gcloud-revoke.yml b/ansible/roles/gcp-cloud-storage/tasks/gcloud-revoke.yml new file mode 100644 index 0000000000..8c26cd0ef0 --- /dev/null +++ b/ansible/roles/gcp-cloud-storage/tasks/gcloud-revoke.yml @@ -0,0 +1,8 @@ +- name: Revoke gcloud service account access + shell: gcloud auth revoke "{{ gcp_storage_service_account_name }}" + +- name: Remove key file + file: + path: "{{ config_key.path }}" + state: absent + when: config_key.path is defined diff --git a/ansible/roles/gcp-cloud-storage/tasks/main.yml b/ansible/roles/gcp-cloud-storage/tasks/main.yml new file mode 100644 index 0000000000..aa41c090ed --- /dev/null +++ b/ansible/roles/gcp-cloud-storage/tasks/main.yml @@ -0,0 +1,20 @@ +--- +- name: upload file to gcloud storage + include: upload.yml + tags: + - file-upload + +- name: upload batch of files to gcloud storage + include: upload-batch.yml + tags: + - upload-batch + +- name: delete batch of files from gcloud storage + include: delete-batch.yml + tags: + - delete-batch + +- name: download a file from gcloud storage + include: download.yml + tags: + - file-download \ No newline at end of file diff --git a/ansible/roles/gcp-cloud-storage/tasks/upload-batch.yml b/ansible/roles/gcp-cloud-storage/tasks/upload-batch.yml new file mode 100644 index 0000000000..49abd5b822 --- /dev/null +++ b/ansible/roles/gcp-cloud-storage/tasks/upload-batch.yml @@ -0,0 +1,11 @@ +--- +- name: Authenticate to gcloud + include_tasks: gcloud-auth.yml + +- name: Upload files from a local directory gcp storage + shell: gsutil -m cp -r "{{ local_file_or_folder_path }}" "gs://{{ gcp_bucket_name }}/{{ dest_folder_name }}/{{ dest_folder_path }}" + async: 3600 + poll: 10 + +- name: Revoke gcloud access + include_tasks: gcloud-revoke.yml diff --git a/ansible/roles/gcp-cloud-storage/tasks/upload.yml b/ansible/roles/gcp-cloud-storage/tasks/upload.yml new file mode 100644 index 0000000000..2f88d9407f --- /dev/null +++ b/ansible/roles/gcp-cloud-storage/tasks/upload.yml @@ -0,0 +1,11 @@ +--- +- name: Authenticate to gcloud + include_tasks: gcloud-auth.yml + +- name: Upload to gcloud storage + shell: gsutil cp "{{ local_file_or_folder_path }}" "gs://{{ gcp_bucket_name }}/{{ dest_folder_name }}/{{ dest_file_name }}" + async: 3600 + poll: 10 + +- name: Revoke gcloud access + include_tasks: gcloud-revoke.yml From 6d2998c41dcbe35c83f54e71e7e272132bcf4c7f Mon Sep 17 00:00:00 2001 From: santhosh-tg Date: Wed, 9 Nov 2022 13:14:36 +0530 Subject: [PATCH 061/222] Add gcp supoort for backup and restore roles --- ansible/roles/cassandra-backup/defaults/main.yml | 6 ++++++ ansible/roles/cassandra-backup/meta/main.yml | 2 -- ansible/roles/cassandra-backup/tasks/main.yml | 11 +++++++++++ .../roles/cassandra-restore/defaults/main.yml | 7 +++++++ ansible/roles/cassandra-restore/meta/main.yml | 2 -- ansible/roles/cassandra-restore/tasks/main.yml | 11 +++++++++++ ansible/roles/neo4j-backup/defaults/main.yml | 8 ++++++++ ansible/roles/neo4j-backup/meta/main.yml | 2 -- ansible/roles/neo4j-backup/tasks/main.yml | 16 ++++++++++++---- ansible/roles/neo4j-restore/defaults/main.yml | 9 ++++++++- ansible/roles/neo4j-restore/meta/main.yml | 2 -- ansible/roles/neo4j-restore/tasks/main.yml | 11 +++++++++++ ansible/roles/redis-backup/defaults/main.yml | 8 ++++++++ ansible/roles/redis-backup/meta/main.yml | 2 -- ansible/roles/redis-backup/tasks/main.yml | 11 +++++++++++ ansible/roles/redis-restore/defaults/main.yml | 7 +++++++ ansible/roles/redis-restore/meta/main.yml | 2 -- ansible/roles/redis-restore/tasks/main.yml | 11 +++++++++++ 18 files changed, 111 insertions(+), 17 deletions(-) delete mode 100644 ansible/roles/cassandra-backup/meta/main.yml delete mode 100644 ansible/roles/cassandra-restore/meta/main.yml delete mode 100644 ansible/roles/neo4j-backup/meta/main.yml delete mode 100644 ansible/roles/neo4j-restore/meta/main.yml delete mode 100644 ansible/roles/redis-backup/meta/main.yml delete mode 100644 ansible/roles/redis-restore/meta/main.yml diff --git a/ansible/roles/cassandra-backup/defaults/main.yml b/ansible/roles/cassandra-backup/defaults/main.yml index f523c62620..00346662d8 100644 --- a/ansible/roles/cassandra-backup/defaults/main.yml +++ b/ansible/roles/cassandra-backup/defaults/main.yml @@ -2,3 +2,9 @@ cassandra_root_dir: /etc/cassandra cassandra_backup_dir: /data/cassandra/backup cassandra_backup_azure_container_name: lp-cassandra-backup +# This variable is added for the below reason - +# 1. Introduce a common variable for various clouds. In case of azure, it refers to container name, in case of aws / gcp, it refers to folder name +# 2. We want to avoid too many new variable introduction / replacement in first phase. Hence we will reuse the existing variable defined in private repo +# or other default files and just assign the value to the newly introduced common variable +# 3. After few releases, we will remove the older variables and use only the new variables across the repos +cassandra_backup_storage: "{{ cassandra_backup_azure_container_name }}" diff --git a/ansible/roles/cassandra-backup/meta/main.yml b/ansible/roles/cassandra-backup/meta/main.yml deleted file mode 100644 index 23b18a800a..0000000000 --- a/ansible/roles/cassandra-backup/meta/main.yml +++ /dev/null @@ -1,2 +0,0 @@ -dependencies: - - azure-cli \ No newline at end of file diff --git a/ansible/roles/cassandra-backup/tasks/main.yml b/ansible/roles/cassandra-backup/tasks/main.yml index 83a8bb263d..90e32a38f4 100755 --- a/ansible/roles/cassandra-backup/tasks/main.yml +++ b/ansible/roles/cassandra-backup/tasks/main.yml @@ -32,6 +32,17 @@ artifact: "{{ cassandra_backup_gzip_file_name }}" artifact_path: "{{ cassandra_backup_gzip_file_path }}" artifacts_container: "{{ cassandra_backup_azure_container_name }}" + +- name: upload file to gcloud storage + include_role: + name: gcp-cloud-storage + tasks_from: upload-batch.yml + vars: + gcp_bucket_name: "{{ gcloud_management_bucket_name }}" + dest_folder_name: "{{ cassandra_backup_storage }}" + dest_folder_path: "" + local_file_or_folder_path: "{{ cassandra_backup_gzip_file_path }}" + when: cloud_service_provider == "gcloud" - name: clean up backup dir after upload file: path={{ cassandra_backup_dir }} state=absent diff --git a/ansible/roles/cassandra-restore/defaults/main.yml b/ansible/roles/cassandra-restore/defaults/main.yml index db4bea795c..ddacb70b3e 100644 --- a/ansible/roles/cassandra-restore/defaults/main.yml +++ b/ansible/roles/cassandra-restore/defaults/main.yml @@ -4,3 +4,10 @@ user: "{{ ansible_ssh_user }}" restore_path: /home/{{user}} backup_folder_name: cassandra_backup backup_dir: "{{restore_path}}/cassandra_backup" + +# This variable is added for the below reason - +# 1. Introduce a common variable for various clouds. In case of azure, it refers to container name, in case of aws / gcp, it refers to folder name +# 2. We want to avoid too many new variable introduction / replacement in first phase. Hence we will reuse the existing variable defined in private repo +# or other default files and just assign the value to the newly introduced common variable +# 3. After few releases, we will remove the older variables and use only the new variables across the repos +cassandra_backup_storage: "{{ cassandra_backup_azure_container_name }}" diff --git a/ansible/roles/cassandra-restore/meta/main.yml b/ansible/roles/cassandra-restore/meta/main.yml deleted file mode 100644 index 23b18a800a..0000000000 --- a/ansible/roles/cassandra-restore/meta/main.yml +++ /dev/null @@ -1,2 +0,0 @@ -dependencies: - - azure-cli \ No newline at end of file diff --git a/ansible/roles/cassandra-restore/tasks/main.yml b/ansible/roles/cassandra-restore/tasks/main.yml index 22e5f41614..80d489d2b0 100755 --- a/ansible/roles/cassandra-restore/tasks/main.yml +++ b/ansible/roles/cassandra-restore/tasks/main.yml @@ -11,6 +11,17 @@ chdir: "{{ restore_path }}" become_user: "{{user}}" +- name: download file from gcloud storage + include_role: + name: gcp-cloud-storage + tasks_from: download.yml + vars: + gcp_bucket_name: "{{ gcloud_management_bucket_name }}" + dest_folder_name: "{{ cassandra_backup_storage }}" + dest_file_name: "{{ cassandra_restore_file_name }}" + local_file_or_folder_path: "{{ cassandra_restore_gzip_file_path }}" + when: cloud_service_provider == "gcloud" + - name: unarchieve backup file unarchive: src={{restore_path}}/{{ cassandra_restore_file_name }} dest={{restore_path}}/ copy=no become_user: "{{user}}" diff --git a/ansible/roles/neo4j-backup/defaults/main.yml b/ansible/roles/neo4j-backup/defaults/main.yml index b2bb981a3a..97f1272d73 100644 --- a/ansible/roles/neo4j-backup/defaults/main.yml +++ b/ansible/roles/neo4j-backup/defaults/main.yml @@ -7,3 +7,11 @@ var1: "_graph" service: learning graph_machine: "{{service}}{{var1}}" neo4j_backup_azure_container_name: neo4j-backup +neo4j_backup_dir: "{{ learner_user_home }}/backup" + +# This variable is added for the below reason - +# 1. Introduce a common variable for various clouds. In case of azure, it refers to container name, in case of aws / gcp, it refers to folder name +# 2. We want to avoid too many new variable introduction / replacement in first phase. Hence we will reuse the existing variable defined in private repo +# or other default files and just assign the value to the newly introduced common variable +# 3. After few releases, we will remove the older variables and use only the new variables across the repos +neo4j_backup_storage: "{{ neo4j_backup_azure_container_name }}" diff --git a/ansible/roles/neo4j-backup/meta/main.yml b/ansible/roles/neo4j-backup/meta/main.yml deleted file mode 100644 index 23b18a800a..0000000000 --- a/ansible/roles/neo4j-backup/meta/main.yml +++ /dev/null @@ -1,2 +0,0 @@ -dependencies: - - azure-cli \ No newline at end of file diff --git a/ansible/roles/neo4j-backup/tasks/main.yml b/ansible/roles/neo4j-backup/tasks/main.yml index 59f4a29954..f507a78f6f 100755 --- a/ansible/roles/neo4j-backup/tasks/main.yml +++ b/ansible/roles/neo4j-backup/tasks/main.yml @@ -1,6 +1,3 @@ -- name: clean up backup dir after upload - file: path={{learner_user_home}}/backup state=absent - - name: delete learning_graph or language_graph #become: yes file: path={{learner_user_home}}/{{graph_machine}} state=absent @@ -21,7 +18,7 @@ - name: ls backup directory #become: yes #become_user: "{{ learner_user }}" - command: ls {{learner_user_home}}/backup/ + command: ls {{ neo4j_backup_dir }} register: var1 - name: debugging variable @@ -44,6 +41,17 @@ async: 3600 poll: 10 +- name: upload file to gcloud storage + include_role: + name: gcp-cloud-storage + tasks_from: upload.yml + vars: + gcp_bucket_name: "{{ gcloud_management_bucket_name }}" + dest_folder_name: "{{ neo4j_backup_storage }}" + dest_file_name: "{{ var1.stdout }}" + local_file_or_folder_path: "{{ neo4j_backup_dir }}/{{ var1.stdout }}" + when: cloud_service_provider == "gcloud" + - name: clean up backup dir after upload file: path={{learner_user_home}}/backup state=absent diff --git a/ansible/roles/neo4j-restore/defaults/main.yml b/ansible/roles/neo4j-restore/defaults/main.yml index 4676400443..52e004992f 100644 --- a/ansible/roles/neo4j-restore/defaults/main.yml +++ b/ansible/roles/neo4j-restore/defaults/main.yml @@ -8,4 +8,11 @@ path_to_neo4j_db: "{{neo4j_home}}/data/databases" ##### #neo4j_backup_file_name: input from jenkins job #backup_azure_storage_account_name: defined in private repo -#backup_azure_storage_access_key: defined in secrets.yml \ No newline at end of file +#backup_azure_storage_access_key: defined in secrets.yml + +# This variable is added for the below reason - +# 1. Introduce a common variable for various clouds. In case of azure, it refers to container name, in case of aws / gcp, it refers to folder name +# 2. We want to avoid too many new variable introduction / replacement in first phase. Hence we will reuse the existing variable defined in private repo +# or other default files and just assign the value to the newly introduced common variable +# 3. After few releases, we will remove the older variables and use only the new variables across the repos +neo4j_backup_storage: "{{ neo4j_backup_azure_container_name }}" diff --git a/ansible/roles/neo4j-restore/meta/main.yml b/ansible/roles/neo4j-restore/meta/main.yml deleted file mode 100644 index 23b18a800a..0000000000 --- a/ansible/roles/neo4j-restore/meta/main.yml +++ /dev/null @@ -1,2 +0,0 @@ -dependencies: - - azure-cli \ No newline at end of file diff --git a/ansible/roles/neo4j-restore/tasks/main.yml b/ansible/roles/neo4j-restore/tasks/main.yml index 5d3edcfd47..7d28f4bb26 100644 --- a/ansible/roles/neo4j-restore/tasks/main.yml +++ b/ansible/roles/neo4j-restore/tasks/main.yml @@ -12,6 +12,17 @@ async: 3600 poll: 10 +- name: download file from gcloud storage + include_role: + name: gcp-cloud-storage + tasks_from: download.yml + vars: + gcp_bucket_name: "{{ gcloud_management_bucket_name }}" + dest_folder_name: "{{ neo4j_backup_storage }}" + dest_file_name: "{{ neo4j_backup_file_name }}" + local_file_or_folder_path: "{{ neo4j_backup_file_path }}" + when: cloud_service_provider == "gcloud" + - name: Check if neo4j is running become_user: "{{ learner_user }}" shell: ps -ef | grep "{{ neo4j_home }}" | grep -v grep | wc -l diff --git a/ansible/roles/redis-backup/defaults/main.yml b/ansible/roles/redis-backup/defaults/main.yml index 2bb4e0e31c..12b6153beb 100644 --- a/ansible/roles/redis-backup/defaults/main.yml +++ b/ansible/roles/redis-backup/defaults/main.yml @@ -4,3 +4,11 @@ learner_user: learning redis_data_dir: /data redis_version: 6.2.5 redis_dir: "/home/{{ learner_user }}/redis-{{ redis_version }}" + +# This variable is added for the below reason - +# 1. Introduce a common variable for various clouds. In case of azure, it refers to container name, in case of aws / gcp, it refers to folder name +# 2. We want to avoid too many new variable introduction / replacement in first phase. Hence we will reuse the existing variable defined in private repo +# or other default files and just assign the value to the newly introduced common variable +# 3. After few releases, we will remove the older variables and use only the new variables across the repos +redis_backup_storage: "{{ redis_backup_azure_container_name }}" + diff --git a/ansible/roles/redis-backup/meta/main.yml b/ansible/roles/redis-backup/meta/main.yml deleted file mode 100644 index a124d4f7cb..0000000000 --- a/ansible/roles/redis-backup/meta/main.yml +++ /dev/null @@ -1,2 +0,0 @@ -dependencies: - - azure-cli diff --git a/ansible/roles/redis-backup/tasks/main.yml b/ansible/roles/redis-backup/tasks/main.yml index b18cf1412f..e7eb7cebf7 100644 --- a/ansible/roles/redis-backup/tasks/main.yml +++ b/ansible/roles/redis-backup/tasks/main.yml @@ -23,6 +23,17 @@ artifact: "{{ redis_backup_file_name }}" artifact_path: "{{ redis_backup_file_path }}" artifacts_container: "{{ redis_backup_azure_container_name }}" + +- name: upload file to gcloud storage + include_role: + name: gcp-cloud-storage + tasks_from: upload.yml + vars: + gcp_bucket_name: "{{ gcloud_management_bucket_name }}" + dest_folder_name: "{{ redis_backup_storage }}" + dest_file_name: "{{ redis_backup_file_name }}" + local_file_or_folder_path: "{{ redis_backup_file_path }}" + when: cloud_service_provider == "gcloud" - name: clean up backup dir after upload file: path={{ redis_backup_dir }} state=absent diff --git a/ansible/roles/redis-restore/defaults/main.yml b/ansible/roles/redis-restore/defaults/main.yml index fec539e0d6..452d62b67f 100644 --- a/ansible/roles/redis-restore/defaults/main.yml +++ b/ansible/roles/redis-restore/defaults/main.yml @@ -1,2 +1,9 @@ redis_backup_azure_container_name: redis-backup learning_user_home: /home/learning + +# This variable is added for the below reason - +# 1. Introduce a common variable for various clouds. In case of azure, it refers to container name, in case of aws / gcp, it refers to folder name +# 2. We want to avoid too many new variable introduction / replacement in first phase. Hence we will reuse the existing variable defined in private repo +# or other default files and just assign the value to the newly introduced common variable +# 3. After few releases, we will remove the older variables and use only the new variables across the repos +redis_backup_storage: "{{ redis_backup_azure_container_name }}" diff --git a/ansible/roles/redis-restore/meta/main.yml b/ansible/roles/redis-restore/meta/main.yml deleted file mode 100644 index a124d4f7cb..0000000000 --- a/ansible/roles/redis-restore/meta/main.yml +++ /dev/null @@ -1,2 +0,0 @@ -dependencies: - - azure-cli diff --git a/ansible/roles/redis-restore/tasks/main.yml b/ansible/roles/redis-restore/tasks/main.yml index e435883030..4b6d0f70f2 100644 --- a/ansible/roles/redis-restore/tasks/main.yml +++ b/ansible/roles/redis-restore/tasks/main.yml @@ -5,6 +5,17 @@ args: chdir: /tmp/ +- name: download file from gcloud storage + include_role: + name: gcp-cloud-storage + tasks_from: download.yml + vars: + gcp_bucket_name: "{{ gcloud_management_bucket_name }}" + dest_folder_name: "{{ redis_backup_storage }}" + dest_file_name: "{{ redis_restore_file_name }}" + local_file_or_folder_path: "/tmp" + when: cloud_service_provider == "gcloud" + - name: stop redis to take backup become: yes systemd: From c17a5f8e07dc2a6ed4c350815648c71bbaa345be Mon Sep 17 00:00:00 2001 From: Kartheek Palla Date: Wed, 9 Nov 2022 14:52:27 +0530 Subject: [PATCH 062/222] Issue #KN-603 fix: CSP changes --- .../helm_charts/datapipeline_jobs/values.j2 | 36 +++++++++---------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/kubernetes/helm_charts/datapipeline_jobs/values.j2 b/kubernetes/helm_charts/datapipeline_jobs/values.j2 index 8145d2df5b..8369a6f61a 100644 --- a/kubernetes/helm_charts/datapipeline_jobs/values.j2 +++ b/kubernetes/helm_charts/datapipeline_jobs/values.j2 @@ -272,9 +272,9 @@ questionset-publish: } print_service.base_url = "{{ kp_print_service_base_url }}" cloud_storage_type="{{ cloud_store }}" - azure_storage_key="{{ sunbird_public_storage_account_name }}" - azure_storage_secret="{{ sunbird_public_storage_account_key }}" - azure_storage_container="{{ azure_public_container }}" + cloud_storage_key="{{ cloud_storage_key }}" + cloud_storage_secret="{{ cloud_storage_secret }}" + cloud_storage_container="{{ cloud_storage_container }}" master.category.validation.enabled ="{{ master_category_validation_enabled }}" @@ -476,9 +476,9 @@ asset-enrichment: } content_youtube_apikey="{{ youtube_api_key }}" cloud_storage_type="{{ cloud_store }}" - azure_storage_key="{{ sunbird_public_storage_account_name }}" - azure_storage_secret="{{ sunbird_public_storage_account_key }}" - azure_storage_container="{{ azure_public_container }}" + cloud_storage_key="{{ cloud_storage_key }}" + cloud_storage_secret="{{ cloud_storage_secret }}" + cloud_storage_container="{{ cloud_storage_container }}" flink-conf: |+ jobmanager.memory.flink.size: {{ flink_job_names['asset-enrichment'].jobmanager_memory }} @@ -536,9 +536,9 @@ auto-creator-v2: content.basePath = "{{ kp_content_service_base_url }}" } cloud_storage_type="{{ cloud_store }}" - azure_storage_key="{{ sunbird_public_storage_account_name }}" - azure_storage_secret="{{ sunbird_public_storage_account_key }}" - azure_storage_container="{{ azure_public_container }}" + cloud_storage_key="{{ cloud_storage_key }}" + cloud_storage_secret="{{ cloud_storage_secret }}" + cloud_storage_container="{{ cloud_storage_container }}" source { baseUrl="{{ source_base_url }}" @@ -584,9 +584,9 @@ content-auto-creator: } cloud_storage_type="{{ cloud_store }}" - azure_storage_key="{{ sunbird_public_storage_account_name }}" - azure_storage_secret="{{ sunbird_public_storage_account_key }}" - azure_storage_container="{{ azure_public_container }}" + cloud_storage_key="{{ cloud_storage_key }}" + cloud_storage_secret="{{ cloud_storage_secret }}" + cloud_storage_container="{{ cloud_storage_container }}" content_auto_creator { actions=auto-create @@ -968,9 +968,9 @@ content-publish: enableDIALContextUpdate = "Yes" cloud_storage_type="{{ cloud_store }}" - azure_storage_key="{{ sunbird_public_storage_account_name }}" - azure_storage_secret="{{ sunbird_public_storage_account_key }}" - azure_storage_container="{{ azure_public_container }}" + cloud_storage_key="{{ cloud_storage_key }}" + cloud_storage_secret="{{ cloud_storage_secret }}" + cloud_storage_container="{{ cloud_storage_container }}" master.category.validation.enabled ="{{ master_category_validation_enabled }}" service { @@ -1009,9 +1009,9 @@ qrcode-image-generator: } cloud_storage_type="{{ cloud_store }}" - azure_storage_key="{{ sunbird_public_storage_account_name }}" - azure_storage_secret="{{ sunbird_public_storage_account_key }}" - azure_storage_container="{{ azure_public_container }}" + cloud_storage_key="{{ cloud_storage_key }}" + cloud_storage_secret="{{ cloud_storage_secret }}" + cloud_storage_container="{{ cloud_storage_container }}" lms-cassandra { keyspace = "dialcodes" From 3a6953c78622244da691622cf3dcfe7a7ebf8f63 Mon Sep 17 00:00:00 2001 From: Kartheek Palla Date: Wed, 9 Nov 2022 14:54:55 +0530 Subject: [PATCH 063/222] Issue #KN-603 fix: CSP changes --- .../ansible/roles/flink-jobs-deploy/defaults/main.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/kubernetes/ansible/roles/flink-jobs-deploy/defaults/main.yml b/kubernetes/ansible/roles/flink-jobs-deploy/defaults/main.yml index c88444ecf1..dfa96ab3f6 100644 --- a/kubernetes/ansible/roles/flink-jobs-deploy/defaults/main.yml +++ b/kubernetes/ansible/roles/flink-jobs-deploy/defaults/main.yml @@ -341,3 +341,9 @@ aws_mediaconvert_access_secret: "" aws_mediaconvert_api_endpoint: "" aws_mediaconvert_queue_id: "" aws_mediaconvert_role_name: "" + +#Cloud storage config +cloud_storage_key: "" +cloud_storage_secret: "" +cloud_storage_container: "" +cloud_storage_endpoint: "" From 1517c4c260188fc7bc874895c5551e40d1b9a471 Mon Sep 17 00:00:00 2001 From: santhosh-tg Date: Wed, 9 Nov 2022 14:59:17 +0530 Subject: [PATCH 064/222] Add when condition for azure --- ansible/roles/cassandra-backup/tasks/main.yml | 1 + ansible/roles/cassandra-restore/tasks/main.yml | 2 ++ ansible/roles/neo4j-backup/tasks/main.yml | 1 + ansible/roles/neo4j-restore/tasks/main.yml | 2 ++ ansible/roles/redis-backup/tasks/main.yml | 1 + ansible/roles/redis-restore/tasks/main.yml | 1 + 6 files changed, 8 insertions(+) diff --git a/ansible/roles/cassandra-backup/tasks/main.yml b/ansible/roles/cassandra-backup/tasks/main.yml index 90e32a38f4..6161ca0501 100755 --- a/ansible/roles/cassandra-backup/tasks/main.yml +++ b/ansible/roles/cassandra-backup/tasks/main.yml @@ -32,6 +32,7 @@ artifact: "{{ cassandra_backup_gzip_file_name }}" artifact_path: "{{ cassandra_backup_gzip_file_path }}" artifacts_container: "{{ cassandra_backup_azure_container_name }}" + when: cloud_service_provider == "azure" - name: upload file to gcloud storage include_role: diff --git a/ansible/roles/cassandra-restore/tasks/main.yml b/ansible/roles/cassandra-restore/tasks/main.yml index 80d489d2b0..ac7aacfdb2 100755 --- a/ansible/roles/cassandra-restore/tasks/main.yml +++ b/ansible/roles/cassandra-restore/tasks/main.yml @@ -10,6 +10,8 @@ args: chdir: "{{ restore_path }}" become_user: "{{user}}" + when: cloud_service_provider == "azure" + - name: download file from gcloud storage include_role: diff --git a/ansible/roles/neo4j-backup/tasks/main.yml b/ansible/roles/neo4j-backup/tasks/main.yml index f507a78f6f..442d056fd3 100755 --- a/ansible/roles/neo4j-backup/tasks/main.yml +++ b/ansible/roles/neo4j-backup/tasks/main.yml @@ -40,6 +40,7 @@ # AZURE_STORAGE_KEY: "{{ backup_azure_storage_access_key }}" async: 3600 poll: 10 + when: cloud_service_provider == "azure" - name: upload file to gcloud storage include_role: diff --git a/ansible/roles/neo4j-restore/tasks/main.yml b/ansible/roles/neo4j-restore/tasks/main.yml index 7d28f4bb26..eac2e2c226 100644 --- a/ansible/roles/neo4j-restore/tasks/main.yml +++ b/ansible/roles/neo4j-restore/tasks/main.yml @@ -11,6 +11,8 @@ AZURE_STORAGE_KEY: "{{sunbird_management_storage_account_key}}" async: 3600 poll: 10 + when: cloud_service_provider == "azure" + - name: download file from gcloud storage include_role: diff --git a/ansible/roles/redis-backup/tasks/main.yml b/ansible/roles/redis-backup/tasks/main.yml index e7eb7cebf7..d4138404d9 100644 --- a/ansible/roles/redis-backup/tasks/main.yml +++ b/ansible/roles/redis-backup/tasks/main.yml @@ -23,6 +23,7 @@ artifact: "{{ redis_backup_file_name }}" artifact_path: "{{ redis_backup_file_path }}" artifacts_container: "{{ redis_backup_azure_container_name }}" + when: cloud_service_provider == "azure" - name: upload file to gcloud storage include_role: diff --git a/ansible/roles/redis-restore/tasks/main.yml b/ansible/roles/redis-restore/tasks/main.yml index 4b6d0f70f2..7a3a5d80d2 100644 --- a/ansible/roles/redis-restore/tasks/main.yml +++ b/ansible/roles/redis-restore/tasks/main.yml @@ -4,6 +4,7 @@ shell: "az storage blob download --container-name {{ redis_backup_azure_container_name }} --file {{ redis_restore_file_name }} --name {{ redis_restore_file_name }} --account-name {{sunbird_management_storage_account_name}} --account-key {{sunbird_management_storage_account_key}}" args: chdir: /tmp/ + when: cloud_service_provider == "azure" - name: download file from gcloud storage include_role: From 0a53a31ede0f6fa24b26eb3238ed15cda33106b1 Mon Sep 17 00:00:00 2001 From: santhosh-tg Date: Wed, 9 Nov 2022 18:01:39 +0530 Subject: [PATCH 065/222] Add cloud-cli Add gcp support for artifact upload/download --- ansible/artifacts-download.yml | 23 ++++++++++++++++++----- ansible/artifacts-upload.yml | 23 ++++++++++++++++++----- ansible/roles/gcloud-cli/tasks/main.yml | 19 +++++++++++++++++++ 3 files changed, 55 insertions(+), 10 deletions(-) create mode 100644 ansible/roles/gcloud-cli/tasks/main.yml diff --git a/ansible/artifacts-download.yml b/ansible/artifacts-download.yml index bf675c8bd3..69212fbfd9 100644 --- a/ansible/artifacts-download.yml +++ b/ansible/artifacts-download.yml @@ -3,8 +3,21 @@ become: yes vars_files: - "{{inventory_dir}}/secrets.yml" - environment: - AZURE_STORAGE_ACCOUNT: "{{sunbird_artifact_storage_account_name}}" - AZURE_STORAGE_SAS_TOKEN: "{{sunbird_artifact_storage_account_sas}}" - roles: - - artifacts-download-azure \ No newline at end of file + tasks: + - name: download artifact from azure storage + include_role: artifacts-download-azure + environment: + AZURE_STORAGE_ACCOUNT: "{{sunbird_artifact_storage_account_name}}" + AZURE_STORAGE_SAS_TOKEN: "{{sunbird_artifact_storage_account_sas}}" + when: cloud_service_provider == "azure" + + - name: download artifact from gcloud storage + include_role: + name: gcp-cloud-storage + tasks_from: download.yml + vars: + gcp_bucket_name: "{{ gcloud_artifact_bucket_name }}" + dest_folder_name: "{{ artifacts_container }}" + dest_file_name: "{{ artifact }}" + local_file_or_folder_path: "{{ artifact_path }}" + when: cloud_service_provider == "gcloud" diff --git a/ansible/artifacts-upload.yml b/ansible/artifacts-upload.yml index 4b651f6dd0..66c1a34948 100644 --- a/ansible/artifacts-upload.yml +++ b/ansible/artifacts-upload.yml @@ -3,8 +3,21 @@ become: yes vars_files: - "{{inventory_dir}}/secrets.yml" - environment: - AZURE_STORAGE_ACCOUNT: "{{sunbird_artifact_storage_account_name}}" - AZURE_STORAGE_SAS_TOKEN: "{{sunbird_artifact_storage_account_sas}}" - roles: - - artifacts-upload-azure \ No newline at end of file + tasks: + - name: upload artifact to azure storage + include_role: artifacts-upload-azure + environment: + AZURE_STORAGE_ACCOUNT: "{{sunbird_artifact_storage_account_name}}" + AZURE_STORAGE_SAS_TOKEN: "{{sunbird_artifact_storage_account_sas}}" + when: cloud_service_provider == "azure" + + - name: upload artifact to gcloud storage + include_role: + name: gcp-cloud-storage + tasks_from: upload.yml + vars: + gcp_bucket_name: "{{ gcloud_artifact_bucket_name }}" + dest_folder_name: "{{ artifacts_container }}" + dest_file_name: "{{ artifact }}" + local_file_or_folder_path: "{{ artifact_path }}" + when: cloud_service_provider == "gcloud" diff --git a/ansible/roles/gcloud-cli/tasks/main.yml b/ansible/roles/gcloud-cli/tasks/main.yml new file mode 100644 index 0000000000..4e39b7ceaf --- /dev/null +++ b/ansible/roles/gcloud-cli/tasks/main.yml @@ -0,0 +1,19 @@ +--- +- name: Add gcloud signing key + apt_key: + url: https://packages.cloud.google.com/apt/doc/apt-key.gpg + state: present + +- name: Add gcloud repository into sources list + apt_repository: + repo: "deb https://packages.cloud.google.com/apt cloud-sdk main" + state: present + +- name: Install google cloud cli with specific version and dependent packages + apt: + pkg: + - ca-certificates + - curl + - apt-transport-https + - gnupg + - google-cloud-cli=406.0.0-0 From b2c69b552fe1dbecbebed8809a461dcbd89f8a84 Mon Sep 17 00:00:00 2001 From: santhosh-tg Date: Wed, 9 Nov 2022 18:24:41 +0530 Subject: [PATCH 066/222] Update neo4j backup role --- ansible/roles/neo4j-backup/tasks/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ansible/roles/neo4j-backup/tasks/main.yml b/ansible/roles/neo4j-backup/tasks/main.yml index 442d056fd3..cb198a493f 100755 --- a/ansible/roles/neo4j-backup/tasks/main.yml +++ b/ansible/roles/neo4j-backup/tasks/main.yml @@ -54,6 +54,6 @@ when: cloud_service_provider == "gcloud" - name: clean up backup dir after upload - file: path={{learner_user_home}}/backup state=absent + file: path={{ neo4j_backup_dir }} state=absent From 92a40fc8021b9425e36e6ec3aa9d090bd5e39f13 Mon Sep 17 00:00:00 2001 From: Santhosh Kumar Date: Wed, 9 Nov 2022 20:05:11 +0530 Subject: [PATCH 067/222] Fix syntax --- ansible/artifacts-download.yml | 10 ++++++---- ansible/artifacts-upload.yml | 10 ++++++---- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/ansible/artifacts-download.yml b/ansible/artifacts-download.yml index 69212fbfd9..5966c81220 100644 --- a/ansible/artifacts-download.yml +++ b/ansible/artifacts-download.yml @@ -5,10 +5,12 @@ - "{{inventory_dir}}/secrets.yml" tasks: - name: download artifact from azure storage - include_role: artifacts-download-azure - environment: - AZURE_STORAGE_ACCOUNT: "{{sunbird_artifact_storage_account_name}}" - AZURE_STORAGE_SAS_TOKEN: "{{sunbird_artifact_storage_account_sas}}" + include_role: + name: artifacts-download-azure + apply: + environment: + AZURE_STORAGE_ACCOUNT: "{{sunbird_artifact_storage_account_name}}" + AZURE_STORAGE_SAS_TOKEN: "{{sunbird_artifact_storage_account_sas}}" when: cloud_service_provider == "azure" - name: download artifact from gcloud storage diff --git a/ansible/artifacts-upload.yml b/ansible/artifacts-upload.yml index 66c1a34948..2d3bf76414 100644 --- a/ansible/artifacts-upload.yml +++ b/ansible/artifacts-upload.yml @@ -5,10 +5,12 @@ - "{{inventory_dir}}/secrets.yml" tasks: - name: upload artifact to azure storage - include_role: artifacts-upload-azure - environment: - AZURE_STORAGE_ACCOUNT: "{{sunbird_artifact_storage_account_name}}" - AZURE_STORAGE_SAS_TOKEN: "{{sunbird_artifact_storage_account_sas}}" + include_role: + name: artifacts-upload-azure + apply: + environment: + AZURE_STORAGE_ACCOUNT: "{{sunbird_artifact_storage_account_name}}" + AZURE_STORAGE_SAS_TOKEN: "{{sunbird_artifact_storage_account_sas}}" when: cloud_service_provider == "azure" - name: upload artifact to gcloud storage From 4a51e491b6595ebbd49c2638f5142a051d0cb2eb Mon Sep 17 00:00:00 2001 From: saiakhil46 Date: Thu, 10 Nov 2022 16:32:54 +0530 Subject: [PATCH 068/222] azure CSP related changes in ansible roles and playbooks --- ansible/cassandra-backup.yml | 3 - ansible/cassandra-restore.yml | 3 - ansible/es_backup.yml | 24 +++++-- ansible/lp_neo4j-backup.yml | 3 - ansible/redis-backup.yml | 3 - .../azure-cloud-storage/defaults/main.yml | 67 +++++++++++++++++++ .../tasks/blob-delete-batch.yml | 5 ++ .../tasks/blob-download.yml | 5 ++ .../tasks/blob-upload-batch.yml | 10 +++ .../azure-cloud-storage/tasks/blob-upload.yml | 10 +++ .../tasks/container-create.yml | 8 +++ .../tasks/delete-using-azcopy.yml | 7 ++ .../roles/azure-cloud-storage/tasks/main.yml | 21 ++++++ .../tasks/upload-using-azcopy.yml | 12 ++++ .../roles/cassandra-backup/defaults/main.yml | 6 ++ ansible/roles/cassandra-backup/tasks/main.yml | 19 ++++-- .../roles/cassandra-restore/defaults/main.yml | 8 +++ .../roles/cassandra-restore/tasks/main.yml | 18 +++-- ansible/roles/es-azure-restore/tasks/main.yml | 6 +- .../roles/es-azure-snapshot/defaults/main.yml | 15 ++++- .../roles/es-azure-snapshot/tasks/main.yml | 12 ++-- .../roles/es-gcs-snapshot/defaults/main.yml | 12 ++++ ansible/roles/es-gcs-snapshot/tasks/main.yml | 42 ++++++++++++ .../roles/es-s3-snapshot/defaults/main.yml | 12 ++++ ansible/roles/es-s3-snapshot/tasks/main.yml | 42 ++++++++++++ ansible/roles/neo4j-backup/defaults/main.yml | 7 ++ ansible/roles/neo4j-backup/tasks/main.yml | 36 ++++++---- ansible/roles/neo4j-restore/defaults/main.yml | 9 ++- ansible/roles/neo4j-restore/tasks/main.yml | 20 ++++-- ansible/roles/redis-backup/defaults/main.yml | 7 ++ ansible/roles/redis-backup/tasks/main.yml | 16 +++-- ansible/roles/redis-restore/defaults/main.yml | 7 ++ ansible/roles/redis-restore/tasks/main.yml | 15 +++-- 33 files changed, 417 insertions(+), 73 deletions(-) create mode 100644 ansible/roles/azure-cloud-storage/defaults/main.yml create mode 100644 ansible/roles/azure-cloud-storage/tasks/blob-delete-batch.yml create mode 100644 ansible/roles/azure-cloud-storage/tasks/blob-download.yml create mode 100644 ansible/roles/azure-cloud-storage/tasks/blob-upload-batch.yml create mode 100644 ansible/roles/azure-cloud-storage/tasks/blob-upload.yml create mode 100644 ansible/roles/azure-cloud-storage/tasks/container-create.yml create mode 100644 ansible/roles/azure-cloud-storage/tasks/delete-using-azcopy.yml create mode 100644 ansible/roles/azure-cloud-storage/tasks/main.yml create mode 100644 ansible/roles/azure-cloud-storage/tasks/upload-using-azcopy.yml create mode 100644 ansible/roles/es-gcs-snapshot/defaults/main.yml create mode 100644 ansible/roles/es-gcs-snapshot/tasks/main.yml create mode 100644 ansible/roles/es-s3-snapshot/defaults/main.yml create mode 100644 ansible/roles/es-s3-snapshot/tasks/main.yml diff --git a/ansible/cassandra-backup.yml b/ansible/cassandra-backup.yml index 204653b815..c9e4d37d22 100644 --- a/ansible/cassandra-backup.yml +++ b/ansible/cassandra-backup.yml @@ -2,8 +2,5 @@ become: yes vars_files: - ['{{inventory_dir}}/secrets.yml'] - environment: - AZURE_STORAGE_ACCOUNT: "{{sunbird_management_storage_account_name}}" - AZURE_STORAGE_KEY: "{{sunbird_management_storage_account_key}}" roles: - cassandra-backup diff --git a/ansible/cassandra-restore.yml b/ansible/cassandra-restore.yml index 2504153192..ad2ccfd44e 100644 --- a/ansible/cassandra-restore.yml +++ b/ansible/cassandra-restore.yml @@ -3,8 +3,5 @@ gather_facts: no vars_files: - ['{{inventory_dir}}/secrets.yml'] - environment: - AZURE_STORAGE_ACCOUNT: "{{sunbird_management_storage_account_name}}" - AZURE_STORAGE_KEY: "{{sunbird_management_storage_account_key}}" roles: - cassandra-restore diff --git a/ansible/es_backup.yml b/ansible/es_backup.yml index 44e6c3d0b6..fd47fa6ed3 100644 --- a/ansible/es_backup.yml +++ b/ansible/es_backup.yml @@ -4,19 +4,29 @@ vars_files: - ['{{inventory_dir}}/secrets.yml'] tasks: - - name: Create a container for storing backups - command: az storage container create --name elasticsearch-snapshots --public-access blob - environment: - AZURE_STORAGE_ACCOUNT: "{{sunbird_management_storage_account_name}}" - AZURE_STORAGE_KEY: "{{sunbird_management_storage_account_key}}" - + - name: Ensure azure blob storage container exists + include_role: + name: azure-cloud-storage + tasks_from: container-create.yml + vars: + blob_container_name: "elasticsearch-snapshots" + container_public_access: "off" + storage_account_name: "{{ azure_management_storage_account_name }}" + storage_account_sas_token: "{{ azure_management_storage_account_sas }}" + when: cloud_service_provider == "azure" - hosts: composite-search-cluster become: yes vars_files: - ['{{inventory_dir}}/secrets.yml'] roles: - - es-azure-snapshot + - role: es-azure-snapshot + when: cloud_service_provider == "azure" + - role: es-s3-snapshot + when: cloud_service_provider == "aws" + - role: es-gcs-snapshot + when: cloud_service_provider == "gcloud" + - role: es5-snapshot-purge tags: - es_backup run_once: true diff --git a/ansible/lp_neo4j-backup.yml b/ansible/lp_neo4j-backup.yml index 33a6d7bded..35e3c63c1b 100644 --- a/ansible/lp_neo4j-backup.yml +++ b/ansible/lp_neo4j-backup.yml @@ -1,9 +1,6 @@ - hosts: learning-neo4j-node1 #if it is a cluster learning-neo4j-node1 should be always master node vars_files: - "{{inventory_dir}}/secrets.yml" - environment: - AZURE_STORAGE_ACCOUNT: "{{sunbird_management_storage_account_name}}" - AZURE_STORAGE_KEY: "{{sunbird_management_storage_account_key}}" become: yes become_user: "{{ learner_user }}" roles: diff --git a/ansible/redis-backup.yml b/ansible/redis-backup.yml index 9d3a8df1aa..114cec8627 100644 --- a/ansible/redis-backup.yml +++ b/ansible/redis-backup.yml @@ -3,9 +3,6 @@ gather_facts: true vars_files: - ['{{inventory_dir}}/secrets.yml'] - environment: - AZURE_STORAGE_ACCOUNT: "{{sunbird_management_storage_account_name}}" - AZURE_STORAGE_KEY: "{{sunbird_management_storage_account_key}}" roles: - redis-backup run_once: true diff --git a/ansible/roles/azure-cloud-storage/defaults/main.yml b/ansible/roles/azure-cloud-storage/defaults/main.yml new file mode 100644 index 0000000000..0e4e45bf95 --- /dev/null +++ b/ansible/roles/azure-cloud-storage/defaults/main.yml @@ -0,0 +1,67 @@ +# The name of the blob container in the azure storage account +# Example - +# blob_container_name: "my-container" +blob_container_name: "" + +# The delete pattern to delete files and folder +# Example - +# blob_delete_pattern: "my-drectory/*" +# blob_delete_pattern: "my-drectory/another-directory/*" +# blob_delete_pattern: "*" +blob_delete_pattern: "" + +# The storage account name +# Example - +# storage_account_name: "sunbird-dev-public" +storage_account_name: "" + +# The storage account key +# Example - +# storage_account_name: "cmFuZG9tcmFuZG9tcmFuZG9tcmFuZG9tCg==" +storage_account_key: "" + +# The path to local file which has to be uploaded to azure storage +# The local path to store the file after downloading from azure storage +# Example - +# local_file_or_folder_path: "/workspace/my-folder/myfile.json" +# local_file_or_folder_path: "/workspace/my-folder" +local_file_or_folder_path: "" + +# The name of the file in azure storage after uploading from local +# The name of the file in azure storage that has to be downloaded +# Example - +# blob_file_name: "myfile-blob.json" +# You can also pass folder path in order to upload / download the file from a speciic folder +# blob_file_name "my-folder/my-file.json" +blob_file_name: "" + +# The storage account sas token +# Example - +# storage_account_sas_token: "?sv=2022-01-01&ss=abc&srt=rws%3D" +storage_account_sas_token: "" + +# The folder path in azure storage to upload the files starting from the root of the container +# This path should alwasy start with a slash / as we are going to append this value as shown in below example +# Example - +# blob_container_name: "my-container" +# blob_container_folder_path: "/my-folder-path" +# {{ blob_container_name }}{{ blob_container_folder_path }} +# The above translates to "my-container/my-folder-path" + +# The variable can also be empty as shown below, which means we will upload directly at the root path of the container +# Example - +# blob_container_name: "my-container" +# blob_container_folder_path: "" +# The above translates to "my-container" +blob_container_folder_path: "" + +# At what access level the container should be created +# Example - +# container_public_access: "off" +# container_public_access: "blob" +# container_public_access: "container" +# Allowed values are - off, blob, container +# This variable affects only new containers and has no affect on a container if it already exists +# If the container already exists, the access level will not be changed +# You will need to change the access level from Azure portal or using az storage container set-permission command +container_public_access: "" \ No newline at end of file diff --git a/ansible/roles/azure-cloud-storage/tasks/blob-delete-batch.yml b/ansible/roles/azure-cloud-storage/tasks/blob-delete-batch.yml new file mode 100644 index 0000000000..4e8ad68a2d --- /dev/null +++ b/ansible/roles/azure-cloud-storage/tasks/blob-delete-batch.yml @@ -0,0 +1,5 @@ +--- +- name: delete files and folders from a blob container recursively + shell: "az storage blob delete-batch --source {{ blob_container_name }} --pattern '{{ blob_delete_pattern }}' --account-name {{ storage_account_name }} --account-key {{ storage_account_key }}" + async: 3600 + poll: 10 \ No newline at end of file diff --git a/ansible/roles/azure-cloud-storage/tasks/blob-download.yml b/ansible/roles/azure-cloud-storage/tasks/blob-download.yml new file mode 100644 index 0000000000..3bbf4b607a --- /dev/null +++ b/ansible/roles/azure-cloud-storage/tasks/blob-download.yml @@ -0,0 +1,5 @@ +--- +- name: download a file from azure storage + shell: "az storage blob download --container-name {{ blob_container_name }} --file {{ local_file_or_folder_path }} --name {{ blob_file_name }} --account-name {{ storage_account_name }} --account-key {{ storage_account_key }}" + async: 3600 + poll: 10 \ No newline at end of file diff --git a/ansible/roles/azure-cloud-storage/tasks/blob-upload-batch.yml b/ansible/roles/azure-cloud-storage/tasks/blob-upload-batch.yml new file mode 100644 index 0000000000..3043da46cc --- /dev/null +++ b/ansible/roles/azure-cloud-storage/tasks/blob-upload-batch.yml @@ -0,0 +1,10 @@ +--- +- name: create container in azure storage if it doesn't exist + include_role: + name: azure-cloud-storage + tasks_from: container-create.yml + +- name: upload files and folders from a local directory to azure storage container + shell: "az storage blob upload-batch --destination {{ blob_container_name }}{{ blob_container_folder_path }} --source {{ local_file_or_folder_path }} --account-name {{ storage_account_name }} --account-key {{ storage_account_key }}" + async: 3600 + poll: 10 \ No newline at end of file diff --git a/ansible/roles/azure-cloud-storage/tasks/blob-upload.yml b/ansible/roles/azure-cloud-storage/tasks/blob-upload.yml new file mode 100644 index 0000000000..4b493ffb73 --- /dev/null +++ b/ansible/roles/azure-cloud-storage/tasks/blob-upload.yml @@ -0,0 +1,10 @@ +--- +- name: create container in azure storage if it doesn't exist + include_role: + name: azure-cloud-storage + tasks_from: container-create.yml + +- name: upload file to azure storage container + shell: "az storage blob upload --container-name {{ blob_container_name }} --file {{ local_file_or_folder_path }} --name {{ blob_file_name }} --account-name {{ storage_account_name }} --account-key {{ storage_account_key }}" + async: 3600 + poll: 10 \ No newline at end of file diff --git a/ansible/roles/azure-cloud-storage/tasks/container-create.yml b/ansible/roles/azure-cloud-storage/tasks/container-create.yml new file mode 100644 index 0000000000..419510cc19 --- /dev/null +++ b/ansible/roles/azure-cloud-storage/tasks/container-create.yml @@ -0,0 +1,8 @@ +--- +- name: create container in azure storage if it doesn't exist + shell: "az storage container create --name {{ blob_container_name }} --public-access {{ container_public_access }} --account-name {{ storage_account_name }} --account-key {{ storage_account_key }}" + when: storage_account_key | length > 0 + +- name: create container in azure storage if it doesn't exist + shell: "az storage container create --name {{ blob_container_name }} --public-access {{ container_public_access }} --account-name {{ storage_account_name }} --sas-token '{{ storage_account_sas_token }}'" + when: storage_account_sas_token | length > 0 \ No newline at end of file diff --git a/ansible/roles/azure-cloud-storage/tasks/delete-using-azcopy.yml b/ansible/roles/azure-cloud-storage/tasks/delete-using-azcopy.yml new file mode 100644 index 0000000000..236169e86c --- /dev/null +++ b/ansible/roles/azure-cloud-storage/tasks/delete-using-azcopy.yml @@ -0,0 +1,7 @@ +--- +- name: delete files and folders from azure storage using azcopy + shell: "azcopy rm 'https://{{ storage_account_name }}.blob.core.windows.net/{{ blob_container_name }}{{ blob_container_folder_path }}{{ storage_account_sas_token }}' --recursive" + environment: + AZCOPY_CONCURRENT_FILES: "10" + async: 10800 + poll: 10 diff --git a/ansible/roles/azure-cloud-storage/tasks/main.yml b/ansible/roles/azure-cloud-storage/tasks/main.yml new file mode 100644 index 0000000000..eb435ecfe2 --- /dev/null +++ b/ansible/roles/azure-cloud-storage/tasks/main.yml @@ -0,0 +1,21 @@ +--- +- name: delete files and folders from azure storage container recursively + include: blob-delete-batch.yml + +- name: download a file from azure storage + include: blob-download.yml + +- name: upload files and folders from a local directory to azure storage container + include: blob-upload-batch.yml + +- name: upload file to azure storage container + include: blob-upload.yml + +- name: create container in azure storage if it doesn't exist + include: container-create.yml + +- name: delete files and folders from azure storage using azcopy + include: delete-using-azcopy.yml + +- name: upload files and folders to azure storage using azcopy + include: upload-using-azcopy.yml diff --git a/ansible/roles/azure-cloud-storage/tasks/upload-using-azcopy.yml b/ansible/roles/azure-cloud-storage/tasks/upload-using-azcopy.yml new file mode 100644 index 0000000000..99ab3c2bf8 --- /dev/null +++ b/ansible/roles/azure-cloud-storage/tasks/upload-using-azcopy.yml @@ -0,0 +1,12 @@ +--- +- name: create container in azure storage if it doesn't exist + include_role: + name: azure-cloud-storage + tasks_from: container-create.yml + +- name: upload files and folders to azure storage using azcopy + shell: "azcopy copy {{ local_file_or_folder_path }} 'https://{{ storage_account_name }}.blob.core.windows.net/{{ blob_container_name }}{{ blob_container_folder_path }}{{ storage_account_sas_token }}' --recursive" + environment: + AZCOPY_CONCURRENT_FILES: "10" + async: 10800 + poll: 10 \ No newline at end of file diff --git a/ansible/roles/cassandra-backup/defaults/main.yml b/ansible/roles/cassandra-backup/defaults/main.yml index f523c62620..00346662d8 100644 --- a/ansible/roles/cassandra-backup/defaults/main.yml +++ b/ansible/roles/cassandra-backup/defaults/main.yml @@ -2,3 +2,9 @@ cassandra_root_dir: /etc/cassandra cassandra_backup_dir: /data/cassandra/backup cassandra_backup_azure_container_name: lp-cassandra-backup +# This variable is added for the below reason - +# 1. Introduce a common variable for various clouds. In case of azure, it refers to container name, in case of aws / gcp, it refers to folder name +# 2. We want to avoid too many new variable introduction / replacement in first phase. Hence we will reuse the existing variable defined in private repo +# or other default files and just assign the value to the newly introduced common variable +# 3. After few releases, we will remove the older variables and use only the new variables across the repos +cassandra_backup_storage: "{{ cassandra_backup_azure_container_name }}" diff --git a/ansible/roles/cassandra-backup/tasks/main.yml b/ansible/roles/cassandra-backup/tasks/main.yml index 83a8bb263d..50bb248977 100755 --- a/ansible/roles/cassandra-backup/tasks/main.yml +++ b/ansible/roles/cassandra-backup/tasks/main.yml @@ -24,14 +24,19 @@ - name: print doc_root to console debug: var: doc_data - -- name: upload to azure + +- name: upload file to azure storage using azcopy include_role: - name: artifacts-upload-azure + name: azure-cloud-storage + tasks_from: upload-using-azcopy.yml vars: - artifact: "{{ cassandra_backup_gzip_file_name }}" - artifact_path: "{{ cassandra_backup_gzip_file_path }}" - artifacts_container: "{{ cassandra_backup_azure_container_name }}" - + blob_container_name: "{{ cassandra_backup_storage }}" + container_public_access: "off" + blob_container_folder_path: "" + local_file_or_folder_path: "{{ cassandra_backup_gzip_file_path }}" + storage_account_name: "{{ azure_management_storage_account_name }}" + storage_account_sas_token: "{{ azure_management_storage_account_sas }}" + when: cloud_service_provider == "azure" + - name: clean up backup dir after upload file: path={{ cassandra_backup_dir }} state=absent diff --git a/ansible/roles/cassandra-restore/defaults/main.yml b/ansible/roles/cassandra-restore/defaults/main.yml index db4bea795c..d061cd50e9 100644 --- a/ansible/roles/cassandra-restore/defaults/main.yml +++ b/ansible/roles/cassandra-restore/defaults/main.yml @@ -4,3 +4,11 @@ user: "{{ ansible_ssh_user }}" restore_path: /home/{{user}} backup_folder_name: cassandra_backup backup_dir: "{{restore_path}}/cassandra_backup" + +# This variable is added for the below reason - +# 1. Introduce a common variable for various clouds. In case of azure, it refers to container name, in case of aws / gcp, it refers to folder name +# 2. We want to avoid too many new variable introduction / replacement in first phase. Hence we will reuse the existing variable defined in private repo +# or other default files and just assign the value to the newly introduced common variable +# 3. After few releases, we will remove the older variables and use only the new variables across the repos +cassandra_backup_storage: "{{ cassandra_backup_azure_container_name }}" + diff --git a/ansible/roles/cassandra-restore/tasks/main.yml b/ansible/roles/cassandra-restore/tasks/main.yml index 22e5f41614..42e118f1d3 100755 --- a/ansible/roles/cassandra-restore/tasks/main.yml +++ b/ansible/roles/cassandra-restore/tasks/main.yml @@ -5,11 +5,19 @@ - set_fact: cassandra_restore_gzip_file_path: "{{ restore_path }}/{{ cassandra_restore_file_name }}" -- name: Download backup from azure - command: az storage blob download -c {{ cassandra_backup_azure_container_name }} --name {{ cassandra_restore_file_name }} -f {{ cassandra_restore_file_name }} - args: - chdir: "{{ restore_path }}" - become_user: "{{user}}" +- name: download a file from azure storage + become: true + include_role: + name: azure-cloud-storage + tasks_from: blob-download.yml + vars: + blob_container_name: "{{ cassandra_backup_storage }}" + blob_file_name: "{{ cassandra_restore_file_name }}" + local_file_or_folder_path: "{{restore_path}}/{{ cassandra_restore_file_name }}" + storage_account_name: "{{ azure_management_storage_account_name }}" + storage_account_key: "{{ azure_management_storage_account_key }}" + when: cloud_service_provider == "azure" + - name: unarchieve backup file unarchive: src={{restore_path}}/{{ cassandra_restore_file_name }} dest={{restore_path}}/ copy=no diff --git a/ansible/roles/es-azure-restore/tasks/main.yml b/ansible/roles/es-azure-restore/tasks/main.yml index f45a97587b..ff71c19a11 100644 --- a/ansible/roles/es-azure-restore/tasks/main.yml +++ b/ansible/roles/es-azure-restore/tasks/main.yml @@ -1,7 +1,7 @@ --- - name: Set azure snapshot for the first time uri: - url: "http://{{ es_restore_host }}:9200/_snapshot/azurebackup" + url: "http://{{ es_restore_host }}:9200/_snapshot/{{ snapshot_base_path }}" method: PUT body: "{{ snapshot_create_request_body | to_json }}" headers: @@ -9,12 +9,12 @@ - name: Restore ES from Azure backup uri: - url: "http://{{ es_restore_host }}:9200/_snapshot/azurebackup/{{snapshot_number}}/_restore" + url: "http://{{ es_restore_host }}:9200/_snapshot/{{ snapshot_base_path }}/{{ snapshot_number }}/_restore" method: POST - name: "Wait for restore to be completed" uri: - url: "http://{{ es_restore_host }}:9200/_snapshot/azurebackup/{{snapshot_number}}/_status" + url: "http://{{ es_restore_host }}:9200/_snapshot/{{ snapshot_base_path }}/{{ snapshot_number }}/_status" method: GET return_content: yes status_code: 200 diff --git a/ansible/roles/es-azure-snapshot/defaults/main.yml b/ansible/roles/es-azure-snapshot/defaults/main.yml index 0c6d6d90ff..a623c56a01 100644 --- a/ansible/roles/es-azure-snapshot/defaults/main.yml +++ b/ansible/roles/es-azure-snapshot/defaults/main.yml @@ -1,7 +1,20 @@ snapshot_create_request_body: { type: azure, settings: { - container: "elasticsearch-snapshots", + container: "{{ es_backup_storage }}", base_path: "{{ snapshot_base_path }}_{{ base_path_date }}" } } + +# Override these values +es_snapshot_host: "localhost" +snapshot_base_path: "default" + +es_azure_backup_container_name: "elasticsearch-snapshots" + +# This variable is added for the below reason - +# 1. Introduce a common variable for various clouds. In case of azure, it refers to container name, in case of aws / gcp, it refers to folder name +# 2. We want to avoid too many new variable introduction / replacement in first phase. Hence we will reuse the existing variable defined in private repo +# or other default files and just assign the value to the newly introduced common variable +# 3. After few releases, we will remove the older variables and use only the new variables across the repos +es_backup_storage: "{{ es_azure_backup_container_name }}" diff --git a/ansible/roles/es-azure-snapshot/tasks/main.yml b/ansible/roles/es-azure-snapshot/tasks/main.yml index 03986ff04d..c3e2ba3343 100644 --- a/ansible/roles/es-azure-snapshot/tasks/main.yml +++ b/ansible/roles/es-azure-snapshot/tasks/main.yml @@ -1,9 +1,9 @@ --- - set_fact: base_path_date="{{ lookup('pipe','date +%Y-%m') }}" -- name: Create azure snapshot +- name: Create Azure Repository uri: - url: "http://{{ es_snapshot_host }}:9200/_snapshot/azurebackup" + url: "http://{{ es_snapshot_host }}:9200/_snapshot/{{ snapshot_base_path }}" method: PUT body: "{{ snapshot_create_request_body | to_json }}" headers: @@ -13,7 +13,7 @@ - name: Take new snapshot uri: - url: "http://{{ es_snapshot_host }}:9200/_snapshot/azurebackup/{{snapshot_number}}" + url: "http://{{ es_snapshot_host }}:9200/_snapshot/{{ snapshot_base_path }}/{{snapshot_number}}" method: PUT body: > {"indices":"*","include_global_state":false} @@ -22,17 +22,17 @@ - name: Print all snapshots uri: - url: "http://{{ es_snapshot_host }}:9200/_snapshot/azurebackup/_all" + url: "http://{{ es_snapshot_host }}:9200/_snapshot/{{ snapshot_base_path }}/_all" method: GET - name: Print status of current snapshot uri: - url: "http://{{ es_snapshot_host }}:9200/_snapshot/azurebackup/{{snapshot_number}}" + url: "http://{{ es_snapshot_host }}:9200/_snapshot/{{ snapshot_base_path }}/{{snapshot_number}}" method: GET - name: "Wait for backup to be completed" uri: - url: "http://{{ es_snapshot_host }}:9200/_snapshot/azurebackup/{{snapshot_number}}" + url: "http://{{ es_snapshot_host }}:9200/_snapshot/{{ snapshot_base_path }}/{{snapshot_number}}" method: GET return_content: yes status_code: 200 diff --git a/ansible/roles/es-gcs-snapshot/defaults/main.yml b/ansible/roles/es-gcs-snapshot/defaults/main.yml new file mode 100644 index 0000000000..5e3cbece6f --- /dev/null +++ b/ansible/roles/es-gcs-snapshot/defaults/main.yml @@ -0,0 +1,12 @@ +snapshot_create_request_body: { + type: gcs, + settings: { + bucket: "{{ gcs_management_bucket_name }}", + base_path: "{{ es_backup_storage }}/{{ snapshot_base_path }}_{{ base_path_date }}" + } +} + +# Override these values +es_snapshot_host: "localhost" +snapshot_base_path: "default" +es_backup_storage: "elasticsearch-snapshots" \ No newline at end of file diff --git a/ansible/roles/es-gcs-snapshot/tasks/main.yml b/ansible/roles/es-gcs-snapshot/tasks/main.yml new file mode 100644 index 0000000000..55f50b17ad --- /dev/null +++ b/ansible/roles/es-gcs-snapshot/tasks/main.yml @@ -0,0 +1,42 @@ +--- + +- set_fact: base_path_date="{{ lookup('pipe','date +%Y-%m') }}" + +- set_fact: snapshot_number="snapshot_{{ lookup('pipe','date +%s') }}" + +- name: Create GCS Repository + uri: + url: "http://{{ es_snapshot_host }}:9200/_snapshot/{{ snapshot_base_path }}" + method: PUT + body: "{{ snapshot_create_request_body | to_json }}" + headers: + Content-Type: "application/json" + +- name: Take new snapshot + uri: + url: "http://{{ es_snapshot_host }}:9200/_snapshot/{{ snapshot_base_path }}/{{ snapshot_number }}" + method: PUT + headers: + Content-Type: "application/json" + +- name: Print all snapshots + uri: + url: "http://{{ es_snapshot_host }}:9200/_snapshot/{{ snapshot_base_path }}/_all" + method: GET + +- name: Print status of current snapshot + uri: + url: "http://{{ es_snapshot_host }}:9200/_snapshot/{{ snapshot_base_path }}/{{ snapshot_number }}" + method: GET + +- name: "Wait for backup to be completed" + uri: + url: "http://{{ es_snapshot_host }}:9200/_snapshot/{{ snapshot_base_path }}/{{ snapshot_number }}" + method: GET + return_content: yes + status_code: 200 + body_format: json + register: result + until: result.json.snapshots[0].state == 'SUCCESS' + retries: 120 + delay: 10 diff --git a/ansible/roles/es-s3-snapshot/defaults/main.yml b/ansible/roles/es-s3-snapshot/defaults/main.yml new file mode 100644 index 0000000000..7ddda6ebd0 --- /dev/null +++ b/ansible/roles/es-s3-snapshot/defaults/main.yml @@ -0,0 +1,12 @@ +snapshot_create_request_body: { + type: s3, + settings: { + bucket: "{{ aws_management_bucket_name }}", + base_path: "{{ es_backup_storage }}/{{ snapshot_base_path }}_{{ base_path_date }}" + } +} + +# Override these values +es_snapshot_host: "localhost" +snapshot_base_path: "default" +es_backup_storage: "elasticsearch-snapshots" \ No newline at end of file diff --git a/ansible/roles/es-s3-snapshot/tasks/main.yml b/ansible/roles/es-s3-snapshot/tasks/main.yml new file mode 100644 index 0000000000..aee768626c --- /dev/null +++ b/ansible/roles/es-s3-snapshot/tasks/main.yml @@ -0,0 +1,42 @@ +--- + +- set_fact: base_path_date="{{ lookup('pipe','date +%Y-%m') }}" + +- set_fact: snapshot_number="snapshot_{{ lookup('pipe','date +%s') }}" + +- name: Create S3 Repository + uri: + url: "http://{{ es_snapshot_host }}:9200/_snapshot/{{ snapshot_base_path }}" + method: PUT + body: "{{ snapshot_create_request_body | to_json }}" + headers: + Content-Type: "application/json" + +- name: Take new snapshot + uri: + url: "http://{{ es_snapshot_host }}:9200/_snapshot/{{ snapshot_base_path }}/{{ snapshot_number }}" + method: PUT + headers: + Content-Type: "application/json" + +- name: Print all snapshots + uri: + url: "http://{{ es_snapshot_host }}:9200/_snapshot/{{ snapshot_base_path }}/_all" + method: GET + +- name: Print status of current snapshot + uri: + url: "http://{{ es_snapshot_host }}:9200/_snapshot/{{ snapshot_base_path }}/{{ snapshot_number }}" + method: GET + +- name: "Wait for backup to be completed" + uri: + url: "http://{{ es_snapshot_host }}:9200/_snapshot/{{ snapshot_base_path }}/{{ snapshot_number }}" + method: GET + return_content: yes + status_code: 200 + body_format: json + register: result + until: result.json.snapshots[0].state == 'SUCCESS' + retries: 120 + delay: 10 diff --git a/ansible/roles/neo4j-backup/defaults/main.yml b/ansible/roles/neo4j-backup/defaults/main.yml index b2bb981a3a..a36838b35e 100644 --- a/ansible/roles/neo4j-backup/defaults/main.yml +++ b/ansible/roles/neo4j-backup/defaults/main.yml @@ -7,3 +7,10 @@ var1: "_graph" service: learning graph_machine: "{{service}}{{var1}}" neo4j_backup_azure_container_name: neo4j-backup + +# This variable is added for the below reason - +# 1. Introduce a common variable for various clouds. In case of azure, it refers to container name, in case of aws / gcp, it refers to folder name +# 2. We want to avoid too many new variable introduction / replacement in first phase. Hence we will reuse the existing variable defined in private repo +# or other default files and just assign the value to the newly introduced common variable +# 3. After few releases, we will remove the older variables and use only the new variables across the repos +neo4j_backup_storage: "{{ neo4j_backup_azure_container_name }}" diff --git a/ansible/roles/neo4j-backup/tasks/main.yml b/ansible/roles/neo4j-backup/tasks/main.yml index 59f4a29954..80719e76c2 100755 --- a/ansible/roles/neo4j-backup/tasks/main.yml +++ b/ansible/roles/neo4j-backup/tasks/main.yml @@ -29,20 +29,28 @@ var: var1.stdout - name: Ensure azure blob storage container exists - command: az storage container create --name {{ neo4j_backup_azure_container_name }} - ignore_errors: true - - #environment: - # AZURE_STORAGE_ACCOUNT: "{{ backup_azure_storage_account_name }}" - # AZURE_STORAGE_KEY: "{{ backup_azure_storage_access_key }}" - -- name: Upload to azure blob storage - command: "az storage blob upload --name {{ var1.stdout }} --file /home/learning/backup/{{ var1.stdout }} --container-name {{ neo4j_backup_azure_container_name }}" - #environment: - # AZURE_STORAGE_ACCOUNT: "{{ backup_azure_storage_account_name }}" - # AZURE_STORAGE_KEY: "{{ backup_azure_storage_access_key }}" - async: 3600 - poll: 10 + include_role: + name: azure-cloud-storage + tasks_from: container-create.yml + vars: + blob_container_name: "{{ neo4j_backup_storage }}" + container_public_access: "off" + storage_account_name: "{{ azure_management_storage_account_name }}" + storage_account_sas_token: "{{ azure_management_storage_account_sas }}" + when: cloud_service_provider == "azure" + +- name: upload file to azure storage using azcopy + include_role: + name: azure-cloud-storage + tasks_from: upload-using-azcopy.yml + vars: + blob_container_name: "{{ neo4j_backup_storage }}" + container_public_access: "off" + blob_container_folder_path: "" + local_file_or_folder_path: "/home/learning/backup/{{ var1.stdout }}" + storage_account_name: "{{ azure_management_storage_account_name }}" + storage_account_sas_token: "{{ azure_management_storage_account_sas }}" + when: cloud_service_provider == "azure" - name: clean up backup dir after upload file: path={{learner_user_home}}/backup state=absent diff --git a/ansible/roles/neo4j-restore/defaults/main.yml b/ansible/roles/neo4j-restore/defaults/main.yml index 4676400443..52e004992f 100644 --- a/ansible/roles/neo4j-restore/defaults/main.yml +++ b/ansible/roles/neo4j-restore/defaults/main.yml @@ -8,4 +8,11 @@ path_to_neo4j_db: "{{neo4j_home}}/data/databases" ##### #neo4j_backup_file_name: input from jenkins job #backup_azure_storage_account_name: defined in private repo -#backup_azure_storage_access_key: defined in secrets.yml \ No newline at end of file +#backup_azure_storage_access_key: defined in secrets.yml + +# This variable is added for the below reason - +# 1. Introduce a common variable for various clouds. In case of azure, it refers to container name, in case of aws / gcp, it refers to folder name +# 2. We want to avoid too many new variable introduction / replacement in first phase. Hence we will reuse the existing variable defined in private repo +# or other default files and just assign the value to the newly introduced common variable +# 3. After few releases, we will remove the older variables and use only the new variables across the repos +neo4j_backup_storage: "{{ neo4j_backup_azure_container_name }}" diff --git a/ansible/roles/neo4j-restore/tasks/main.yml b/ansible/roles/neo4j-restore/tasks/main.yml index 5d3edcfd47..e7f9ca4e38 100644 --- a/ansible/roles/neo4j-restore/tasks/main.yml +++ b/ansible/roles/neo4j-restore/tasks/main.yml @@ -4,13 +4,19 @@ - set_fact: neo4j_backup_file_path: "{{ neo4j_restore_dir }}/{{ neo4j_backup_file_name }}" -- name: Download restore file from azure - command: az storage blob download --container-name {{ neo4j_backup_azure_container_name }} --name {{ neo4j_backup_file_name }} --file {{ neo4j_backup_file_path }} - environment: - AZURE_STORAGE_ACCOUNT: "{{sunbird_management_storage_account_name}}" - AZURE_STORAGE_KEY: "{{sunbird_management_storage_account_key}}" - async: 3600 - poll: 10 +- name: download a file from azure storage + become: true + include_role: + name: azure-cloud-storage + tasks_from: blob-download.yml + vars: + blob_container_name: "{{ neo4j_backup_storage }}" + blob_file_name: "{{ neo4j_backup_file_name }}" + local_file_or_folder_path: "{{ neo4j_backup_file_path }}" + storage_account_name: "{{ azure_management_storage_account_name }}" + storage_account_key: "{{ azure_management_storage_account_key }}" + when: cloud_service_provider == "azure" + - name: Check if neo4j is running become_user: "{{ learner_user }}" diff --git a/ansible/roles/redis-backup/defaults/main.yml b/ansible/roles/redis-backup/defaults/main.yml index 2bb4e0e31c..ea52f764a4 100644 --- a/ansible/roles/redis-backup/defaults/main.yml +++ b/ansible/roles/redis-backup/defaults/main.yml @@ -4,3 +4,10 @@ learner_user: learning redis_data_dir: /data redis_version: 6.2.5 redis_dir: "/home/{{ learner_user }}/redis-{{ redis_version }}" + +# This variable is added for the below reason - +# 1. Introduce a common variable for various clouds. In case of azure, it refers to container name, in case of aws / gcp, it refers to folder name +# 2. We want to avoid too many new variable introduction / replacement in first phase. Hence we will reuse the existing variable defined in private repo +# or other default files and just assign the value to the newly introduced common variable +# 3. After few releases, we will remove the older variables and use only the new variables across the repos +redis_backup_storage: "{{ redis_backup_azure_container_name }}" diff --git a/ansible/roles/redis-backup/tasks/main.yml b/ansible/roles/redis-backup/tasks/main.yml index b18cf1412f..de533bb666 100644 --- a/ansible/roles/redis-backup/tasks/main.yml +++ b/ansible/roles/redis-backup/tasks/main.yml @@ -14,15 +14,19 @@ src: "{{ redis_data_dir }}/dump.rdb" dest: "{{ redis_backup_dir }}/{{ redis_backup_file_name }}" remote_src: yes - -- name: upload to azure +- name: upload file to azure storage include_role: - name: artifacts-upload-azure + name: azure-cloud-storage + tasks_from: blob-upload.yml vars: - artifact: "{{ redis_backup_file_name }}" - artifact_path: "{{ redis_backup_file_path }}" - artifacts_container: "{{ redis_backup_azure_container_name }}" + blob_container_name: "{{ redis_backup_storage }}" + container_public_access: "off" + blob_file_name: "{{ redis_backup_file_name }}" + local_file_or_folder_path: "{{ redis_backup_file_path }}" + storage_account_name: "{{ azure_management_storage_account_name }}" + storage_account_key: "{{ azure_management_storage_account_key }}" + when: cloud_service_provider == "azure" - name: clean up backup dir after upload file: path={{ redis_backup_dir }} state=absent diff --git a/ansible/roles/redis-restore/defaults/main.yml b/ansible/roles/redis-restore/defaults/main.yml index fec539e0d6..452d62b67f 100644 --- a/ansible/roles/redis-restore/defaults/main.yml +++ b/ansible/roles/redis-restore/defaults/main.yml @@ -1,2 +1,9 @@ redis_backup_azure_container_name: redis-backup learning_user_home: /home/learning + +# This variable is added for the below reason - +# 1. Introduce a common variable for various clouds. In case of azure, it refers to container name, in case of aws / gcp, it refers to folder name +# 2. We want to avoid too many new variable introduction / replacement in first phase. Hence we will reuse the existing variable defined in private repo +# or other default files and just assign the value to the newly introduced common variable +# 3. After few releases, we will remove the older variables and use only the new variables across the repos +redis_backup_storage: "{{ redis_backup_azure_container_name }}" diff --git a/ansible/roles/redis-restore/tasks/main.yml b/ansible/roles/redis-restore/tasks/main.yml index e435883030..0a75e9c32b 100644 --- a/ansible/roles/redis-restore/tasks/main.yml +++ b/ansible/roles/redis-restore/tasks/main.yml @@ -1,9 +1,16 @@ --- -- name: Download backup file - shell: "az storage blob download --container-name {{ redis_backup_azure_container_name }} --file {{ redis_restore_file_name }} --name {{ redis_restore_file_name }} --account-name {{sunbird_management_storage_account_name}} --account-key {{sunbird_management_storage_account_key}}" - args: - chdir: /tmp/ +- name: download a file from azure storage + include_role: + name: azure-cloud-storage + tasks_from: blob-download.yml + vars: + blob_container_name: "{{ redis_backup_storage }}" + blob_file_name: "{{ redis_restore_file_name }}" + local_file_or_folder_path: "/tmp/{{ redis_restore_file_name }}" + storage_account_name: "{{ azure_management_storage_account_name }}" + storage_account_key: "{{ azure_management_storage_account_key }}" + when: cloud_service_provider == "azure" - name: stop redis to take backup become: yes From 16e8e5ce7d44181a40422426785e467ee35ca23c Mon Sep 17 00:00:00 2001 From: saiakhil46 Date: Thu, 10 Nov 2022 19:55:21 +0530 Subject: [PATCH 069/222] updated azure-cli install role --- ansible/roles/azure-cli/tasks/main.yml | 54 ++++++++++++-------------- 1 file changed, 25 insertions(+), 29 deletions(-) diff --git a/ansible/roles/azure-cli/tasks/main.yml b/ansible/roles/azure-cli/tasks/main.yml index 50088a1bf0..7dde9dd425 100644 --- a/ansible/roles/azure-cli/tasks/main.yml +++ b/ansible/roles/azure-cli/tasks/main.yml @@ -1,32 +1,28 @@ -- name: Import Azure signing key - become: yes - become_user: root - shell: curl -L https://packages.microsoft.com/keys/microsoft.asc | apt-key add - +--- +- name: Add Microsfot signing key + apt_key: + url: https://packages.microsoft.com/keys/microsoft.asc + state: present -- name: Add Azure apt repository - become: yes - become_user: root - apt_repository: repo='deb [arch=amd64] https://packages.microsoft.com/repos/azure-cli/ {{ ansible_distribution_release }} main' state=present +- name: Add Microsfot repository into sources list + apt_repository: + repo: "deb [arch=amd64] https://packages.microsoft.com/repos/azure-cli/ {{ ansible_distribution_release | lower }} main" + state: present -- name: Add distribution release security apt repository - become: yes - become_user: root - apt_repository: repo='deb http://security.ubuntu.com/ubuntu bionic-security main' state=present +- name: Install azue cli and dependent packages + apt: + pkg: + - ca-certificates + - curl + - apt-transport-https + - lsb-release + - gnupg + - "azure-cli=2.33.1-1~{{ ansible_distribution_release | lower }}" -- name: install azure cli dependency - become: yes - become_user: root - apt: name={{ item }} state=present update_cache=yes - #allow_unauthenticated: yes - with_items: - - libssl1.0-dev - when: ansible_distribution_release == "focal" - -- name: ensure azure-cli and apt-transport-https is installed - become: yes - become_user: root - apt: name={{ item }} state=present update_cache=yes - #allow_unauthenticated: yes - with_items: - - apt-transport-https - - azure-cli \ No newline at end of file +- name: Install azcopy + shell: | + which azcopy || ( \ + mkdir /tmp/azcopy && cd /tmp/azcopy && \ + wget -O azcopy_v10.tar.gz https://aka.ms/downloadazcopy-v10-linux && tar -xf azcopy_v10.tar.gz --strip-components=1 \ + && mv azcopy /usr/local/bin \ + && rm -rf /tmp/azcopy ) \ No newline at end of file From ad25e9b65ef65bef763b874ef35b6b6123bc4479 Mon Sep 17 00:00:00 2001 From: saiakhil46 Date: Thu, 10 Nov 2022 19:58:27 +0530 Subject: [PATCH 070/222] updated azure-cli install role --- ansible/roles/azure-cli/tasks/main.yml | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/ansible/roles/azure-cli/tasks/main.yml b/ansible/roles/azure-cli/tasks/main.yml index 7dde9dd425..4286be19a7 100644 --- a/ansible/roles/azure-cli/tasks/main.yml +++ b/ansible/roles/azure-cli/tasks/main.yml @@ -1,15 +1,18 @@ --- - name: Add Microsfot signing key + become: yes apt_key: url: https://packages.microsoft.com/keys/microsoft.asc state: present -- name: Add Microsfot repository into sources list +- name: Add Microsoft repository into sources list + become: yes apt_repository: repo: "deb [arch=amd64] https://packages.microsoft.com/repos/azure-cli/ {{ ansible_distribution_release | lower }} main" state: present - name: Install azue cli and dependent packages + become: yes apt: pkg: - ca-certificates @@ -20,9 +23,10 @@ - "azure-cli=2.33.1-1~{{ ansible_distribution_release | lower }}" - name: Install azcopy + become: yes shell: | which azcopy || ( \ mkdir /tmp/azcopy && cd /tmp/azcopy && \ wget -O azcopy_v10.tar.gz https://aka.ms/downloadazcopy-v10-linux && tar -xf azcopy_v10.tar.gz --strip-components=1 \ && mv azcopy /usr/local/bin \ - && rm -rf /tmp/azcopy ) \ No newline at end of file + && rm -rf /tmp/azcopy ) From 77f73617a63a8eb06efbaa43d135c5227d67ac8c Mon Sep 17 00:00:00 2001 From: saiakhil46 Date: Thu, 10 Nov 2022 20:02:08 +0530 Subject: [PATCH 071/222] updated azure-cli install role --- ansible/roles/azure-cli/tasks/main.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/ansible/roles/azure-cli/tasks/main.yml b/ansible/roles/azure-cli/tasks/main.yml index 4286be19a7..abcad5025b 100644 --- a/ansible/roles/azure-cli/tasks/main.yml +++ b/ansible/roles/azure-cli/tasks/main.yml @@ -1,18 +1,21 @@ --- - name: Add Microsfot signing key become: yes + become_user: root apt_key: url: https://packages.microsoft.com/keys/microsoft.asc state: present - name: Add Microsoft repository into sources list become: yes + become_user: root apt_repository: repo: "deb [arch=amd64] https://packages.microsoft.com/repos/azure-cli/ {{ ansible_distribution_release | lower }} main" state: present - name: Install azue cli and dependent packages become: yes + become_user: root apt: pkg: - ca-certificates @@ -24,6 +27,7 @@ - name: Install azcopy become: yes + become_user: root shell: | which azcopy || ( \ mkdir /tmp/azcopy && cd /tmp/azcopy && \ From def236b1e3476b2af803dce6b30b057fba02e29e Mon Sep 17 00:00:00 2001 From: saiakhil46 Date: Thu, 10 Nov 2022 21:01:04 +0530 Subject: [PATCH 072/222] updated neo4j-backup role --- ansible/roles/azure-cli/tasks/main.yml | 12 ++++-------- ansible/roles/neo4j-backup/tasks/main.yml | 17 +++-------------- 2 files changed, 7 insertions(+), 22 deletions(-) diff --git a/ansible/roles/azure-cli/tasks/main.yml b/ansible/roles/azure-cli/tasks/main.yml index abcad5025b..bf3b28d019 100644 --- a/ansible/roles/azure-cli/tasks/main.yml +++ b/ansible/roles/azure-cli/tasks/main.yml @@ -1,21 +1,18 @@ --- - name: Add Microsfot signing key - become: yes - become_user: root + become: true apt_key: url: https://packages.microsoft.com/keys/microsoft.asc state: present - name: Add Microsoft repository into sources list - become: yes - become_user: root + become: true apt_repository: repo: "deb [arch=amd64] https://packages.microsoft.com/repos/azure-cli/ {{ ansible_distribution_release | lower }} main" state: present - name: Install azue cli and dependent packages - become: yes - become_user: root + become: true apt: pkg: - ca-certificates @@ -26,8 +23,7 @@ - "azure-cli=2.33.1-1~{{ ansible_distribution_release | lower }}" - name: Install azcopy - become: yes - become_user: root + become: true shell: | which azcopy || ( \ mkdir /tmp/azcopy && cd /tmp/azcopy && \ diff --git a/ansible/roles/neo4j-backup/tasks/main.yml b/ansible/roles/neo4j-backup/tasks/main.yml index 80719e76c2..820179369c 100755 --- a/ansible/roles/neo4j-backup/tasks/main.yml +++ b/ansible/roles/neo4j-backup/tasks/main.yml @@ -26,27 +26,16 @@ - name: debugging variable debug: - var: var1.stdout - -- name: Ensure azure blob storage container exists - include_role: - name: azure-cloud-storage - tasks_from: container-create.yml - vars: - blob_container_name: "{{ neo4j_backup_storage }}" - container_public_access: "off" - storage_account_name: "{{ azure_management_storage_account_name }}" - storage_account_sas_token: "{{ azure_management_storage_account_sas }}" - when: cloud_service_provider == "azure" + var: var1.stdout - name: upload file to azure storage using azcopy include_role: name: azure-cloud-storage - tasks_from: upload-using-azcopy.yml + tasks_from: blob-upload.yml vars: blob_container_name: "{{ neo4j_backup_storage }}" + blob_file_name: "{{ var1.stdout }}" container_public_access: "off" - blob_container_folder_path: "" local_file_or_folder_path: "/home/learning/backup/{{ var1.stdout }}" storage_account_name: "{{ azure_management_storage_account_name }}" storage_account_sas_token: "{{ azure_management_storage_account_sas }}" From 3bd526ae3894447c4af4ec6ccf8c23f05766c641 Mon Sep 17 00:00:00 2001 From: saiakhil46 Date: Thu, 10 Nov 2022 21:04:04 +0530 Subject: [PATCH 073/222] updated azure-cli install role --- ansible/roles/azure-cli/tasks/main.yml | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/ansible/roles/azure-cli/tasks/main.yml b/ansible/roles/azure-cli/tasks/main.yml index bf3b28d019..abcad5025b 100644 --- a/ansible/roles/azure-cli/tasks/main.yml +++ b/ansible/roles/azure-cli/tasks/main.yml @@ -1,18 +1,21 @@ --- - name: Add Microsfot signing key - become: true + become: yes + become_user: root apt_key: url: https://packages.microsoft.com/keys/microsoft.asc state: present - name: Add Microsoft repository into sources list - become: true + become: yes + become_user: root apt_repository: repo: "deb [arch=amd64] https://packages.microsoft.com/repos/azure-cli/ {{ ansible_distribution_release | lower }} main" state: present - name: Install azue cli and dependent packages - become: true + become: yes + become_user: root apt: pkg: - ca-certificates @@ -23,7 +26,8 @@ - "azure-cli=2.33.1-1~{{ ansible_distribution_release | lower }}" - name: Install azcopy - become: true + become: yes + become_user: root shell: | which azcopy || ( \ mkdir /tmp/azcopy && cd /tmp/azcopy && \ From af4821bc27d050a867a4cfcfae135b8747b8b84c Mon Sep 17 00:00:00 2001 From: saiakhil46 Date: Thu, 10 Nov 2022 21:23:14 +0530 Subject: [PATCH 074/222] updated neo4j-backup role --- ansible/roles/neo4j-backup/tasks/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ansible/roles/neo4j-backup/tasks/main.yml b/ansible/roles/neo4j-backup/tasks/main.yml index 820179369c..a8c23b6acf 100755 --- a/ansible/roles/neo4j-backup/tasks/main.yml +++ b/ansible/roles/neo4j-backup/tasks/main.yml @@ -38,7 +38,7 @@ container_public_access: "off" local_file_or_folder_path: "/home/learning/backup/{{ var1.stdout }}" storage_account_name: "{{ azure_management_storage_account_name }}" - storage_account_sas_token: "{{ azure_management_storage_account_sas }}" + storage_account_key: "{{ azure_management_storage_account_key }}" when: cloud_service_provider == "azure" - name: clean up backup dir after upload From 36c563e0a852a04d5a0843ced138bcf5925c68d3 Mon Sep 17 00:00:00 2001 From: saiakhil46 Date: Thu, 10 Nov 2022 23:21:32 +0530 Subject: [PATCH 075/222] updated cassandra-backup role --- .../roles/cassandra-backup/defaults/main.yml | 1 + ansible/roles/cassandra-backup/tasks/main.yml | 45 ++-- .../templates/cassandra_backup.j2 | 202 +++++++++++++++--- pipelines/backup/cassandra-backup/Jenkinsfile | 2 +- 4 files changed, 195 insertions(+), 55 deletions(-) diff --git a/ansible/roles/cassandra-backup/defaults/main.yml b/ansible/roles/cassandra-backup/defaults/main.yml index 00346662d8..65daa78122 100644 --- a/ansible/roles/cassandra-backup/defaults/main.yml +++ b/ansible/roles/cassandra-backup/defaults/main.yml @@ -1,5 +1,6 @@ cassandra_root_dir: /etc/cassandra cassandra_backup_dir: /data/cassandra/backup +data_dir: '/var/lib/cassandra/data' cassandra_backup_azure_container_name: lp-cassandra-backup # This variable is added for the below reason - diff --git a/ansible/roles/cassandra-backup/tasks/main.yml b/ansible/roles/cassandra-backup/tasks/main.yml index 50bb248977..cbd956b234 100755 --- a/ansible/roles/cassandra-backup/tasks/main.yml +++ b/ansible/roles/cassandra-backup/tasks/main.yml @@ -1,30 +1,37 @@ +- name: Make sure backup dir is empty + file: path="{{ cassandra_backup_dir }}" state=absent + ignore_errors: true + - name: Create the directory - file: path={{ cassandra_backup_dir }} state=directory recurse=yes - -- name: copy the backup script - template: src=cassandra_backup.j2 dest={{ cassandra_backup_dir }}/cassandra_backup.py mode=0755 + become: true + file: path=/data/cassandra/backup state=directory recurse=yes + +- name: copy the backup script + become: true + template: + src: cassandra_backup.j2 + dest: /data/cassandra/backup/cassandra_backup.py + mode: 0755 - set_fact: - cassandra_backup_gzip_file_name: "cassandra-backup-{{ lookup('pipe', 'date +%Y%m%d') }}.tar.gz" - -- set_fact: - cassandra_backup_gzip_file_path: "{{ cassandra_backup_dir }}/{{ cassandra_backup_gzip_file_name }}.tar.gz" + cassandra_backup_folder_name: "cassandra-backup-{{ lookup('pipe', 'date +%Y%m%d') }}-{{ ansible_hostname }}-new" - name: run the backup script - shell: python cassandra_backup.py {{ cassandra_backup_gzip_file_name }} -d {{ data_dir }} + become: true + shell: python3 cassandra_backup.py --snapshotname "{{ cassandra_backup_folder_name }}" --snapshotdirectory "{{ cassandra_backup_folder_name }}" "{{additional_arguments|d('')}}" args: - chdir: "{{ cassandra_backup_dir }}" - async: 3600 - poll: 10 - + chdir: /data/cassandra/backup + async: 14400 + poll: 30 + - name: Check doc_root path - shell: ls -all {{ cassandra_backup_dir }} + shell: ls -all /data/cassandra/backup/ register: doc_data - name: print doc_root to console debug: - var: doc_data - + var: doc_data + - name: upload file to azure storage using azcopy include_role: name: azure-cloud-storage @@ -33,10 +40,10 @@ blob_container_name: "{{ cassandra_backup_storage }}" container_public_access: "off" blob_container_folder_path: "" - local_file_or_folder_path: "{{ cassandra_backup_gzip_file_path }}" + local_file_or_folder_path: "/data/cassandra/backup/{{ cassandra_backup_folder_name }}" storage_account_name: "{{ azure_management_storage_account_name }}" storage_account_sas_token: "{{ azure_management_storage_account_sas }}" when: cloud_service_provider == "azure" - + - name: clean up backup dir after upload - file: path={{ cassandra_backup_dir }} state=absent + file: path="{{ cassandra_backup_dir }}" state=absent diff --git a/ansible/roles/cassandra-backup/templates/cassandra_backup.j2 b/ansible/roles/cassandra-backup/templates/cassandra_backup.j2 index b077bc9ac2..dc581d042a 100644 --- a/ansible/roles/cassandra-backup/templates/cassandra_backup.j2 +++ b/ansible/roles/cassandra-backup/templates/cassandra_backup.j2 @@ -1,68 +1,200 @@ + #!/usr/bin/env python3 # Author: Rajesh Rajendran ''' -Create a snapshot and create tar ball in targetdirectory name +Create cassandra snapshot with specified name, +and create tar ball in targetdirectory name + +By default + +Cassandra data directory : /var/lib/cassandra/data +Snapshot name : cassandra_backup-YYYY-MM-DD +Backup name : cassandra_backup-YYYY-MM-DD.tar.gz usage: script snapshot_name -eg: ./cassandra_backup.py my_snapshot +eg: ./cassandra_backup.py + +for help ./cassandra_backup.py -h ''' -from os import path, walk, sep, system, getcwd, makedirs -from argparse import ArgumentParser -from shutil import rmtree, ignore_patterns, copytree -from re import match, compile +from argparse import ArgumentParser +import concurrent.futures +from os import cpu_count, getcwd, link, makedirs, makedirs, sep, system, walk, path +from re import compile, match +from shutil import copytree, ignore_patterns, rmtree +import socket +from subprocess import check_output from sys import exit -from tempfile import mkdtemp +from time import strftime + + +''' +Returns the ip address of current host machine +''' +def get_ip(): + s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + try: + # doesn't even have to be reachable + s.connect(('10.255.255.255', 1)) + IP = s.getsockname()[0] + except Exception: + print("Couldn't get the correct, please pass the current node's cassandra ip address using flag '--host '") + raise + finally: + s.close() + return str(IP) + +# Create temporary directory to copy data +default_snapshot_name = "cassandra_backup" + strftime("%Y-%m-%d-%H%M%S") +tmpdir = getcwd()+sep+default_snapshot_name parser = ArgumentParser(description="Create a snapshot and create tar ball inside tardirectory") -parser.add_argument("-d","--datadirectory", metavar="datadir", default='/var/lib/cassandra/data', help="path to create the tarball. Default /var/lib/cassadra/data") -parser.add_argument("snapshotname", help="name in which you want to take the snapshot") -parser.add_argument("-t","--tardirectory", metavar="tardir", default=getcwd(), help="path to create the tarball. Default {}".format(getcwd())) +parser.add_argument("-d", "--datadirectory", metavar="datadir", default='/var/lib/cassandra/data', + help="Path to cassadandra keyspaces. Default /var/lib/cassadra/data") +parser.add_argument("-s", "--snapshotdirectory", metavar="snapdir", default=tmpdir, + help="Path to take cassandra snapshot. Default {}".format(tmpdir)) +parser.add_argument("-n", "--snapshotname", metavar="snapshotname", + default="cassandra_backup-"+strftime("%Y-%m-%d"), + help="Name with which snapshot to be taken. Default {}".format(default_snapshot_name)) +parser.add_argument("-t", "--tardirectory", metavar="tardir", + default='', help="Path to create the tarball. Disabled by Default") +parser.add_argument("-w", "--workers", metavar="workers", + default=cpu_count(), help="Number of workers to use. Default same as cpu cores {}".format(cpu_count())) +parser.add_argument("--disablesnapshot", action="store_true", + help="disable taking snapshot, snapshot name can be given via -s flag") +parser.add_argument("--host", default=get_ip(), metavar="< Default: "+get_ip()+" >", help="ip address of cassandra instance. Used to take the token ring info. If the ip address is not correct, Please update the ip address, else your token ring won't be correct.") args = parser.parse_args() -# Create temporary directory to copy data -tmpdir=mkdtemp() -makedirs(tmpdir+sep+"cassandra_backup") +tmpdir = args.snapshotdirectory +# Trying to create the directory if not exists +try: + makedirs(tmpdir+sep+"cassandra_backup") +except OSError as e: + raise + +def customCopy(root, root_target_dir): + print("copying {} to {}".format(root, root_target_dir)) + copytree(src=root, dst=root_target_dir, copy_function=link, ignore=ignore_patterns('.*')) + +# Names of the keyspaces to take schema backup +ignore_keyspace_names = [] def copy(): ''' Copying the data sanpshots to the target directory ''' - root_levels = args.datadirectory.count(sep) - ignore_list = compile(tmpdir+sep+"cassandra_backup"+sep+'(system|system|systemtauth|system_traces|system_schema|system_distributed)') - + root_levels = args.datadirectory.rstrip('/').count(sep) + # List of system keyspaces, which we don't need. + # We need system_schema keyspace, as it has all the keyspace,trigger,types information. + ignore_keyspaces = ["system", "system_auth", "system_traces", "system_distributed", "lock_db"] + # ignore_list = compile('^'+tmpdir+sep+"cassandra_backup"+sep+'(system|system_auth|system_traces|system_distributed|lock_db)/.*$') + ignore_list = compile('^'+tmpdir+sep+"cassandra_backup"+sep+"("+"|".join(ignore_keyspaces)+')/.*$') + # List of the threds running in background + futures = [] try: - for root, dirs, files in walk(args.datadirectory): - root_target_dir=tmpdir+sep+"cassandra_backup"+sep+sep.join(root.split(sep)[root_levels+1:-2]) - if match(ignore_list, root_target_dir): - continue - if root.split(sep)[-1] == args.snapshotname: - copytree(src=root, dst=root_target_dir, ignore=ignore_patterns('.*')) + with concurrent.futures.ThreadPoolExecutor(max_workers=args.workers) as executor: + for root, _, _ in walk(args.datadirectory): + keyspace = sep+sep.join(root.split(sep)[root_levels+1:-2]) + # We don't need tables and other inner directories for keyspace. + if len(keyspace.split('/')) != 3: + continue + root_target_dir = tmpdir+sep+"cassandra_backup"+keyspace + if match(ignore_list, root_target_dir): + continue + if root.split(sep)[-1] == args.snapshotname: + # Keeping copy operation in background with threads + tmp_arr = [root, root_target_dir] + futures.append( executor.submit( lambda p: customCopy(*p), tmp_arr)) except Exception as e: print(e) + # Checking status of the copy operation + for future in concurrent.futures.as_completed(futures): + try: + print("Task completed. Result: {}".format(future.result())) + except Exception as e: + print(e) + +keyspaces_schema_dict = {} +def create_schema(schema_file): + cmd = "cqlsh -e 'SELECT * from system_schema.keyspaces;' | tail -n +4 | head -n -2" + output = check_output('{}'.format(cmd), shell=True).decode().strip() + for line in output.split('\n'): + tmpline = line.split("|") + keyspaces_schema_dict[tmpline[0].strip()] = {"durable_writes": tmpline[1].strip(),"replication": tmpline[2].strip()} -# Creating schema -command = "cqlsh -e 'DESC SCHEMA' > {}/cassandra_backup/db_schema.cql".format(tmpdir) + # Creating table schema + for root, _, files in walk(tmpdir): + for file in files: + if file.endswith(".cql"): + with open(path.join(root, file),'r') as f: + with open(schema_file,'a') as w: + w.write(f.read()) + w.write('\n') + +# Creating complete schema +# For `ALTER DROP COLUMN`, +# This schema will have issues. +# So you'll have to create and drop the column. +# For details about that table/column, look at snapshot_table_schema.sql +command = "cqlsh -e 'DESC SCHEMA' > {}/cassandra_backup/complete_db_schema.cql".format(tmpdir) rc = system(command) if rc != 0: print("Couldn't backup schema, exiting...") exit(1) -print("Schema backup completed. saved in {}/cassandra_backup/db_schema.sql".format(tmpdir)) -# Cleaning all old snapshots -command = "nodetool clearsnapshot" -system(command) -# Creating snapshots -command = "nodetool snapshot -t {}".format(args.snapshotname) +print("Schema backup completed. saved in {}/cassandra_backup/complete_db_schema.sql".format(tmpdir)) + +# Backing up tokenring +command = """ nodetool ring | grep ^""" + get_ip() + """ | awk '{print $NF ","}' | xargs | tee -a """ + tmpdir + """/cassandra_backup/tokenring.txt """ #.format(args.host, tmpdir) +print(command) rc = system(command) -if rc == 0: +if rc != 0: + print("Couldn't backup tokenring, exiting...") + exit(1) +print("Token ring backup completed. saved in {}/cassandra_backup/tokenring.txt".format(tmpdir)) + +# Creating snapshots +if not args.disablesnapshot: + # Cleaning all old snapshots + command = "nodetool clearsnapshot" + system(command) + # Taking new snapshot + command = "nodetool snapshot -t {}".format(args.snapshotname) + rc = system(command) + if rc != 0: + print("Backup failed") + exit(1) print("Snapshot taken.") - copy() + +# Copying the snapshot to proper folder structure, this is not a copy but a hard link. +copy() + +# Dropping unwanted keyspace schema +# Including system schemas +## deduplicating Ignore Keyspace list +ignore_keyspace_names = list(dict.fromkeys(ignore_keyspace_names)) +# Creating schema for keyspaces. +create_schema("{}/cassandra_backup/snapshot_table_schema.cql".format(tmpdir)) + +# Clearing the snapshot. +# We've the data now available in the copied directory. +command = "nodetool clearsnapshot -t {}".format(args.snapshotname) +print("Clearing snapshot {} ...".format(args.snapshotname)) +rc = system(command) +if rc != 0: + print("Clearing snapshot {} failed".format(args.snapshotname)) + exit(1) + +# Creating tarball +if args.tardirectory: print("Making a tarball: {}.tar.gz".format(args.snapshotname)) - command = "cd {} && tar -czvf {}/{}.tar.gz *".format(tmpdir, args.tardirectory, args.snapshotname) - system(command) + command = "cd {} && tar --remove-files -czvf {}/{}.tar.gz *".format(tmpdir, args.tardirectory, args.snapshotname) + rc = system(command) + if rc != 0: + print("Creation of tar failed") + exit(1) # Cleaning up backup directory rmtree(tmpdir) - print("Cassandra backup completed and stored in {}/{}.tar.gz".format(args.tardirectory,args.snapshotname)) \ No newline at end of file + print("Cassandra backup completed and stored in {}/{}.tar.gz".format(args.tardirectory, args.snapshotname)) \ No newline at end of file diff --git a/pipelines/backup/cassandra-backup/Jenkinsfile b/pipelines/backup/cassandra-backup/Jenkinsfile index 227dbd6f09..412f97658f 100644 --- a/pipelines/backup/cassandra-backup/Jenkinsfile +++ b/pipelines/backup/cassandra-backup/Jenkinsfile @@ -25,7 +25,7 @@ node() { jobName = sh(returnStdout: true, script: "echo $JOB_NAME").split('/')[-1].trim() currentWs = sh(returnStdout: true, script: 'pwd').trim() ansiblePlaybook = "${currentWs}/ansible/cassandra-backup.yml" - ansibleExtraArgs = "--extra-vars \"remote=${params.remote} data_dir=${params.data_dir}\" --vault-password-file /var/lib/jenkins/secrets/vault-pass" + ansibleExtraArgs = "--vault-password-file /var/lib/jenkins/secrets/vault-pass" values.put('currentWs', currentWs) values.put('env', envDir) values.put('module', module) From b344a15b2807a50985267ed49456fb646d2925bc Mon Sep 17 00:00:00 2001 From: saiakhil46 Date: Thu, 10 Nov 2022 23:24:10 +0530 Subject: [PATCH 076/222] updated cassandra-backup role --- ansible/roles/cassandra-backup/tasks/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ansible/roles/cassandra-backup/tasks/main.yml b/ansible/roles/cassandra-backup/tasks/main.yml index cbd956b234..f4a8dd16dc 100755 --- a/ansible/roles/cassandra-backup/tasks/main.yml +++ b/ansible/roles/cassandra-backup/tasks/main.yml @@ -18,7 +18,7 @@ - name: run the backup script become: true - shell: python3 cassandra_backup.py --snapshotname "{{ cassandra_backup_folder_name }}" --snapshotdirectory "{{ cassandra_backup_folder_name }}" "{{additional_arguments|d('')}}" + shell: python3 cassandra_backup.py --snapshotname "{{ cassandra_backup_folder_name }}" --snapshotdirectory "{{ cassandra_backup_folder_name }}" args: chdir: /data/cassandra/backup async: 14400 From 661ed56ad9b1fc22894fa121cc90a7ca5b415a10 Mon Sep 17 00:00:00 2001 From: saiakhil46 Date: Thu, 10 Nov 2022 23:34:48 +0530 Subject: [PATCH 077/222] deleted meta folders --- ansible/roles/cassandra-backup/meta/main.yml | 2 -- ansible/roles/cassandra-restore/meta/main.yml | 2 -- ansible/roles/neo4j-backup/meta/main.yml | 2 -- ansible/roles/neo4j-restore/meta/main.yml | 2 -- ansible/roles/redis-backup/meta/main.yml | 2 -- ansible/roles/redis-restore/meta/main.yml | 2 -- 6 files changed, 12 deletions(-) delete mode 100644 ansible/roles/cassandra-backup/meta/main.yml delete mode 100644 ansible/roles/cassandra-restore/meta/main.yml delete mode 100644 ansible/roles/neo4j-backup/meta/main.yml delete mode 100644 ansible/roles/neo4j-restore/meta/main.yml delete mode 100644 ansible/roles/redis-backup/meta/main.yml delete mode 100644 ansible/roles/redis-restore/meta/main.yml diff --git a/ansible/roles/cassandra-backup/meta/main.yml b/ansible/roles/cassandra-backup/meta/main.yml deleted file mode 100644 index 23b18a800a..0000000000 --- a/ansible/roles/cassandra-backup/meta/main.yml +++ /dev/null @@ -1,2 +0,0 @@ -dependencies: - - azure-cli \ No newline at end of file diff --git a/ansible/roles/cassandra-restore/meta/main.yml b/ansible/roles/cassandra-restore/meta/main.yml deleted file mode 100644 index 23b18a800a..0000000000 --- a/ansible/roles/cassandra-restore/meta/main.yml +++ /dev/null @@ -1,2 +0,0 @@ -dependencies: - - azure-cli \ No newline at end of file diff --git a/ansible/roles/neo4j-backup/meta/main.yml b/ansible/roles/neo4j-backup/meta/main.yml deleted file mode 100644 index 23b18a800a..0000000000 --- a/ansible/roles/neo4j-backup/meta/main.yml +++ /dev/null @@ -1,2 +0,0 @@ -dependencies: - - azure-cli \ No newline at end of file diff --git a/ansible/roles/neo4j-restore/meta/main.yml b/ansible/roles/neo4j-restore/meta/main.yml deleted file mode 100644 index 23b18a800a..0000000000 --- a/ansible/roles/neo4j-restore/meta/main.yml +++ /dev/null @@ -1,2 +0,0 @@ -dependencies: - - azure-cli \ No newline at end of file diff --git a/ansible/roles/redis-backup/meta/main.yml b/ansible/roles/redis-backup/meta/main.yml deleted file mode 100644 index a124d4f7cb..0000000000 --- a/ansible/roles/redis-backup/meta/main.yml +++ /dev/null @@ -1,2 +0,0 @@ -dependencies: - - azure-cli diff --git a/ansible/roles/redis-restore/meta/main.yml b/ansible/roles/redis-restore/meta/main.yml deleted file mode 100644 index a124d4f7cb..0000000000 --- a/ansible/roles/redis-restore/meta/main.yml +++ /dev/null @@ -1,2 +0,0 @@ -dependencies: - - azure-cli From 0275947ea38a935d342bd8a059dc430f2404ce5d Mon Sep 17 00:00:00 2001 From: saiakhil46 Date: Fri, 11 Nov 2022 16:30:22 +0530 Subject: [PATCH 078/222] delete azure artifact upload and download roles --- ansible/artifacts-download.yml | 10 ---------- ansible/artifacts-upload.yml | 10 ---------- ansible/roles/artifacts-download-azure/tasks/main.yml | 8 -------- ansible/roles/artifacts-upload-azure/tasks/main.yml | 8 -------- 4 files changed, 36 deletions(-) delete mode 100644 ansible/artifacts-download.yml delete mode 100644 ansible/artifacts-upload.yml delete mode 100644 ansible/roles/artifacts-download-azure/tasks/main.yml delete mode 100644 ansible/roles/artifacts-upload-azure/tasks/main.yml diff --git a/ansible/artifacts-download.yml b/ansible/artifacts-download.yml deleted file mode 100644 index bf675c8bd3..0000000000 --- a/ansible/artifacts-download.yml +++ /dev/null @@ -1,10 +0,0 @@ ---- -- hosts: local - become: yes - vars_files: - - "{{inventory_dir}}/secrets.yml" - environment: - AZURE_STORAGE_ACCOUNT: "{{sunbird_artifact_storage_account_name}}" - AZURE_STORAGE_SAS_TOKEN: "{{sunbird_artifact_storage_account_sas}}" - roles: - - artifacts-download-azure \ No newline at end of file diff --git a/ansible/artifacts-upload.yml b/ansible/artifacts-upload.yml deleted file mode 100644 index 4b651f6dd0..0000000000 --- a/ansible/artifacts-upload.yml +++ /dev/null @@ -1,10 +0,0 @@ ---- -- hosts: local - become: yes - vars_files: - - "{{inventory_dir}}/secrets.yml" - environment: - AZURE_STORAGE_ACCOUNT: "{{sunbird_artifact_storage_account_name}}" - AZURE_STORAGE_SAS_TOKEN: "{{sunbird_artifact_storage_account_sas}}" - roles: - - artifacts-upload-azure \ No newline at end of file diff --git a/ansible/roles/artifacts-download-azure/tasks/main.yml b/ansible/roles/artifacts-download-azure/tasks/main.yml deleted file mode 100644 index db79bc213f..0000000000 --- a/ansible/roles/artifacts-download-azure/tasks/main.yml +++ /dev/null @@ -1,8 +0,0 @@ ---- -- name: Ensure azure blob storage container exists - command: az storage container exists --name {{ artifacts_container }} - -- name: Download from azure blob storage - command: az storage blob download -c {{ artifacts_container }} --name {{ artifact }} -f {{ artifact_path }} - async: 3600 - poll: 10 diff --git a/ansible/roles/artifacts-upload-azure/tasks/main.yml b/ansible/roles/artifacts-upload-azure/tasks/main.yml deleted file mode 100644 index 785dc1a455..0000000000 --- a/ansible/roles/artifacts-upload-azure/tasks/main.yml +++ /dev/null @@ -1,8 +0,0 @@ ---- -- name: Ensure azure blob storage container exists - command: az storage container create --name {{ artifacts_container }} - -- name: Upload to azure blob storage - command: az storage blob upload -c {{ artifacts_container }} --name {{ artifact }} -f {{ artifact_path }} - async: 3600 - poll: 10 From 4073a7b2fa562edf2436a21a03027a37abcc210c Mon Sep 17 00:00:00 2001 From: saiakhil46 Date: Fri, 11 Nov 2022 16:58:45 +0530 Subject: [PATCH 079/222] Revert "delete azure artifact upload and download roles" This reverts commit 0275947ea38a935d342bd8a059dc430f2404ce5d. --- ansible/artifacts-download.yml | 10 ++++++++++ ansible/artifacts-upload.yml | 10 ++++++++++ ansible/roles/artifacts-download-azure/tasks/main.yml | 8 ++++++++ ansible/roles/artifacts-upload-azure/tasks/main.yml | 8 ++++++++ 4 files changed, 36 insertions(+) create mode 100644 ansible/artifacts-download.yml create mode 100644 ansible/artifacts-upload.yml create mode 100644 ansible/roles/artifacts-download-azure/tasks/main.yml create mode 100644 ansible/roles/artifacts-upload-azure/tasks/main.yml diff --git a/ansible/artifacts-download.yml b/ansible/artifacts-download.yml new file mode 100644 index 0000000000..bf675c8bd3 --- /dev/null +++ b/ansible/artifacts-download.yml @@ -0,0 +1,10 @@ +--- +- hosts: local + become: yes + vars_files: + - "{{inventory_dir}}/secrets.yml" + environment: + AZURE_STORAGE_ACCOUNT: "{{sunbird_artifact_storage_account_name}}" + AZURE_STORAGE_SAS_TOKEN: "{{sunbird_artifact_storage_account_sas}}" + roles: + - artifacts-download-azure \ No newline at end of file diff --git a/ansible/artifacts-upload.yml b/ansible/artifacts-upload.yml new file mode 100644 index 0000000000..4b651f6dd0 --- /dev/null +++ b/ansible/artifacts-upload.yml @@ -0,0 +1,10 @@ +--- +- hosts: local + become: yes + vars_files: + - "{{inventory_dir}}/secrets.yml" + environment: + AZURE_STORAGE_ACCOUNT: "{{sunbird_artifact_storage_account_name}}" + AZURE_STORAGE_SAS_TOKEN: "{{sunbird_artifact_storage_account_sas}}" + roles: + - artifacts-upload-azure \ No newline at end of file diff --git a/ansible/roles/artifacts-download-azure/tasks/main.yml b/ansible/roles/artifacts-download-azure/tasks/main.yml new file mode 100644 index 0000000000..db79bc213f --- /dev/null +++ b/ansible/roles/artifacts-download-azure/tasks/main.yml @@ -0,0 +1,8 @@ +--- +- name: Ensure azure blob storage container exists + command: az storage container exists --name {{ artifacts_container }} + +- name: Download from azure blob storage + command: az storage blob download -c {{ artifacts_container }} --name {{ artifact }} -f {{ artifact_path }} + async: 3600 + poll: 10 diff --git a/ansible/roles/artifacts-upload-azure/tasks/main.yml b/ansible/roles/artifacts-upload-azure/tasks/main.yml new file mode 100644 index 0000000000..785dc1a455 --- /dev/null +++ b/ansible/roles/artifacts-upload-azure/tasks/main.yml @@ -0,0 +1,8 @@ +--- +- name: Ensure azure blob storage container exists + command: az storage container create --name {{ artifacts_container }} + +- name: Upload to azure blob storage + command: az storage blob upload -c {{ artifacts_container }} --name {{ artifact }} -f {{ artifact_path }} + async: 3600 + poll: 10 From 29ab3cd3c7cbf0430e0095b25491ed54c4d3b278 Mon Sep 17 00:00:00 2001 From: Jayaprakash8887 Date: Tue, 15 Nov 2022 15:28:42 +0530 Subject: [PATCH 080/222] Issue #KN-439 feat: CSP Migration Job --- .../roles/flink-jobs-deploy/defaults/main.yml | 29 ++++++++ .../helm_charts/datapipeline_jobs/values.j2 | 71 +++++++++++++++++++ 2 files changed, 100 insertions(+) diff --git a/kubernetes/ansible/roles/flink-jobs-deploy/defaults/main.yml b/kubernetes/ansible/roles/flink-jobs-deploy/defaults/main.yml index c88444ecf1..e25f2c62c1 100644 --- a/kubernetes/ansible/roles/flink-jobs-deploy/defaults/main.yml +++ b/kubernetes/ansible/roles/flink-jobs-deploy/defaults/main.yml @@ -265,6 +265,27 @@ flink_job_names: taskmanager_memory: 1024m taskslots: 1 cpu_requests: 0.3 + csp-migrator: + job_class_name: 'org.sunbird.job.cspmigrator.task.CSPMigratorStreamTask' + replica: 1 + jobmanager_memory: 1024m + taskmanager_memory: 1024m + taskslots: 1 + cpu_requests: 0.3 + live-node-publisher: + job_class_name: 'org.sunbird.job.livenodepublisher.task.LiveNodePublisherStreamTask' + replica: 1 + jobmanager_memory: 1024m + taskmanager_memory: 1024m + taskslots: 1 + cpu_requests: 0.3 + live-video-stream-generator: + job_class_name: 'org.sunbird.job.livevideostream.task.LiveVideoStreamGeneratorStreamTask' + replica: 1 + jobmanager_memory: 1024m + taskmanager_memory: 1024m + taskslots: 1 + cpu_requests: 0.3 ### Global vars middleware_course_keyspace: "sunbird_courses" @@ -341,3 +362,11 @@ aws_mediaconvert_access_secret: "" aws_mediaconvert_api_endpoint: "" aws_mediaconvert_queue_id: "" aws_mediaconvert_role_name: "" + + +### csp-migrator related vars +csp_migrator_parallelism: 1 +csp_migrator_timer_duration: 1800 +csp_migrator_max_retries: 10 +csp_migrator_consumer_parallelism: 1 +csp_migrator_cassandra_parallelism: 1 diff --git a/kubernetes/helm_charts/datapipeline_jobs/values.j2 b/kubernetes/helm_charts/datapipeline_jobs/values.j2 index 87cd57de66..9412a18e62 100644 --- a/kubernetes/helm_charts/datapipeline_jobs/values.j2 +++ b/kubernetes/helm_charts/datapipeline_jobs/values.j2 @@ -1314,3 +1314,74 @@ live-video-stream-generator: parallelism.default: 1 jobmanager.execution.failover-strategy: region taskmanager.memory.network.fraction: 0.1 + +csp-migrator: + csp-migrator: |+ + include file("/data/flink/conf/base-config.conf") + kafka { + input.topic = "{{ env_name }}.csp.migration.request" + groupId = "{{ env_name }}-csp-migrator-group" + failed.topic = "{{ env_name }}.csp.migration.job.request.failed" + live_video_stream.topic = "{{ env_name }}.live.video.stream.request" + live_content_node_republish.topic = "{{ env_name }}.republish.job.request" + } + task { + timer.duration = {{ csp_migrator_timer_duration }} + consumer.parallelism = {{ csp_migrator_consumer_parallelism }} + parallelism = {{ csp_migrator_parallelism }} + max.retries = {{ csp_migrator_max_retries }} + cassandra-migrator.parallelism = {{csp_migrator_cassandra_parallelism}} + } + redis { + database { + relationCache.id = 10 + collectionCache.id = 5 + } + } + + hierarchy { + keyspace = "{{ hierarchy_keyspace_name }}" + table = "content_hierarchy" + } + + content { + keyspace = "{{ content_keyspace_name }}" + content_table = "content_data" + assessment_table = "question_data" + } + + key_value_strings_to_migrate = { + "https://sunbirddev.blob.core.windows.net/": "https://obj.dev.sunbirded.org", + "https://ekstep-public-dev.s3-ap-south-1.amazonaws.com/": "https://obj.dev.sunbirded.org", + "https://community.ekstep.in/assets/public/": "https://obj.dev.sunbirded.org" + } + + neo4j_fields_to_migrate = { + "asset": ["artifactUrl","thumbnail"], + "content": ["appIcon","artifactUrl", "posterImage", "previewUrl", "thumbnail", "assetsMap", "certTemplate", "itemSetPreviewUrl", "grayScaleAppIcon","sourceURL"], + "contentimage": ["appIcon","artifactUrl", "posterImage", "previewUrl", "thumbnail", "assetsMap", "certTemplate", "itemSetPreviewUrl", "grayScaleAppIcon","sourceURL"], + "collection": ["appIcon","artifactUrl", "posterImage", "previewUrl", "thumbnail", "toc_url", "grayScaleAppIcon"], + "collectionimage": ["appIcon","artifactUrl", "posterImage", "previewUrl", "thumbnail", "toc_url", "grayScaleAppIcon"], + "plugins": ["artifactUrl"], + "itemset": ["previewUrl"], + "assessmentitem": ["data", "question", "solutions", "editorState", "media"] + } + + cassandra_fields_to_migrate = { + "assessmentitem": ["body", "editorState", "answer", "solutions", "instructions", "media"] + } + + migrationVersion = 1 + video_stream_regeneration_enable = true + live_node_republish_enable = true + copy_missing_files_to_cloud = true + download_path = /tmp + + + flink-conf: |+ + jobmanager.memory.flink.size: {{ flink_job_names['video-stream-generator'].jobmanager_memory }} + taskmanager.memory.flink.size: {{ flink_job_names['video-stream-generator'].taskmanager_memory }} + taskmanager.numberOfTaskSlots: {{ flink_job_names['video-stream-generator'].taskslots }} + parallelism.default: 1 + jobmanager.execution.failover-strategy: region + taskmanager.memory.network.fraction: 0.1 \ No newline at end of file From 4377cce43be2db9aae2a67b7ded7c837196433ff Mon Sep 17 00:00:00 2001 From: Keshav Prasad Date: Tue, 15 Nov 2022 16:45:01 +0530 Subject: [PATCH 081/222] fix: using cloud_storage_url instead of hard coded url Signed-off-by: Keshav Prasad --- ansible/inventory/env/group_vars/all.yml | 3 +++ ansible/roles/lp-contenttool/templates/application.conf.j2 | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/ansible/inventory/env/group_vars/all.yml b/ansible/inventory/env/group_vars/all.yml index 9b0ecd87f0..75ae4c49db 100644 --- a/ansible/inventory/env/group_vars/all.yml +++ b/ansible/inventory/env/group_vars/all.yml @@ -119,3 +119,6 @@ download_neo4j: true neo4j_upstream_download: false enable_suppress_exception: false enable_rc_certificate: true + +# SB-31155 +plugin_storage: "{{ plugin_container_name }}" \ No newline at end of file diff --git a/ansible/roles/lp-contenttool/templates/application.conf.j2 b/ansible/roles/lp-contenttool/templates/application.conf.j2 index 7444400ad2..7a990df718 100644 --- a/ansible/roles/lp-contenttool/templates/application.conf.j2 +++ b/ansible/roles/lp-contenttool/templates/application.conf.j2 @@ -16,7 +16,7 @@ content.extract_mimetype="application/vnd.ekstep.h5p-archive,application/vnd.eks cloud.src.baseurl="https://ekstep-public-{{ ekstep_env_name }}.s3-ap-south-1.amazonaws.com" -cloud.dest.baseurl="https://{{ sunbird_public_storage_account_name }}.blob.core.windows.net/{{ azure_public_container }}" +cloud.dest.baseurl="{{ cloud_storage_url }}/{{ plugin_storage }}" aws_storage_key="" aws_storage_secret="" From 20b0fa4a03a9d7c5e9d91f2037bce3dee8bb87db Mon Sep 17 00:00:00 2001 From: Kumar Gauraw Date: Wed, 16 Nov 2022 12:31:32 +0530 Subject: [PATCH 082/222] Issue #IQ-149 feat: sync tool changes for csp migration --- .../org/sunbird/graph/dac/model/Filter.java | 2 + .../graph/dac/model/SearchConditions.java | 2 + .../sunbird/learning/util/ControllerUtil.java | 55 +++++ .../mgr/CSPMigrationMessageGenerator.java | 210 ++++++++++++++++++ .../tool/shell/MigrateCSPDataCommand.java | 41 ++++ .../org/sunbird/sync/tool/util/KafkaUtil.java | 85 +++++++ .../src/main/resources/application.conf | 5 +- 7 files changed, 399 insertions(+), 1 deletion(-) create mode 100644 platform-tools/spikes/sync-tool/src/main/java/org/sunbird/sync/tool/mgr/CSPMigrationMessageGenerator.java create mode 100644 platform-tools/spikes/sync-tool/src/main/java/org/sunbird/sync/tool/shell/MigrateCSPDataCommand.java create mode 100644 platform-tools/spikes/sync-tool/src/main/java/org/sunbird/sync/tool/util/KafkaUtil.java diff --git a/platform-core/graph-engine/module/graph-dac-api/src/main/java/org/sunbird/graph/dac/model/Filter.java b/platform-core/graph-engine/module/graph-dac-api/src/main/java/org/sunbird/graph/dac/model/Filter.java index f7e0564958..ff5cef741e 100644 --- a/platform-core/graph-engine/module/graph-dac-api/src/main/java/org/sunbird/graph/dac/model/Filter.java +++ b/platform-core/graph-engine/module/graph-dac-api/src/main/java/org/sunbird/graph/dac/model/Filter.java @@ -104,6 +104,8 @@ public String getCypher(SearchCriteria sc, String param) { sb.append(" ").append(param).append(property).append(" in {").append(pIndex).append("} "); sc.params.put("" + pIndex, value); pIndex += 1; + } else if (SearchConditions.OP_IS.equals(getOperator())) { + sb.append(" ").append(param).append(property).append(" is ").append(value).append(" "); } sc.pIndex = pIndex; return sb.toString(); diff --git a/platform-core/graph-engine/module/graph-dac-api/src/main/java/org/sunbird/graph/dac/model/SearchConditions.java b/platform-core/graph-engine/module/graph-dac-api/src/main/java/org/sunbird/graph/dac/model/SearchConditions.java index b2b182c210..07095d7c40 100644 --- a/platform-core/graph-engine/module/graph-dac-api/src/main/java/org/sunbird/graph/dac/model/SearchConditions.java +++ b/platform-core/graph-engine/module/graph-dac-api/src/main/java/org/sunbird/graph/dac/model/SearchConditions.java @@ -20,6 +20,7 @@ public class SearchConditions implements Serializable { public static final String OP_LESS_OR_EQUAL = "<="; public static final String OP_NOT_EQUAL = "!="; public static final String OP_IN = "in"; + public static final String OP_IS = "is"; static List operators = new ArrayList(); @@ -34,5 +35,6 @@ public class SearchConditions implements Serializable { operators.add(OP_LESS_OR_EQUAL); operators.add(OP_NOT_EQUAL); operators.add(OP_IN); + operators.add(OP_IS); } } diff --git a/platform-modules/actors/src/main/java/org/sunbird/learning/util/ControllerUtil.java b/platform-modules/actors/src/main/java/org/sunbird/learning/util/ControllerUtil.java index 807e0794a1..b0b77942ff 100644 --- a/platform-modules/actors/src/main/java/org/sunbird/learning/util/ControllerUtil.java +++ b/platform-modules/actors/src/main/java/org/sunbird/learning/util/ControllerUtil.java @@ -4,6 +4,8 @@ import org.apache.commons.collections.MapUtils; import org.apache.commons.lang.StringUtils; import org.codehaus.jackson.map.ObjectMapper; +import org.json.JSONArray; +import org.neo4j.driver.v1.Values; import org.sunbird.common.Platform; import org.sunbird.common.dto.NodeDTO; import org.sunbird.common.dto.Request; @@ -16,7 +18,11 @@ import org.sunbird.common.mgr.ConvertGraphNode; import org.sunbird.graph.common.enums.GraphHeaderParams; import org.sunbird.graph.dac.enums.GraphDACParams; +import org.sunbird.graph.dac.enums.SystemNodeTypes; +import org.sunbird.graph.dac.model.Filter; +import org.sunbird.graph.dac.model.MetadataCriterion; import org.sunbird.graph.dac.model.Node; +import org.sunbird.graph.dac.model.SearchConditions; import org.sunbird.graph.dac.model.SearchCriteria; import org.sunbird.graph.engine.mgr.impl.NodeManager; import org.sunbird.graph.engine.router.GraphEngineManagers; @@ -406,6 +412,32 @@ public List getNodes(String graphId, String objectType, int startPosition, } } + public List getNodes(String graphId, String objectType, List mimeTypes, List status, int startPosition, int batchSize) { + List filters = new ArrayList(); + if(!mimeTypes.isEmpty()) + filters.add(new Filter("mimeType", SearchConditions.OP_IN, mimeTypes)); + if(!status.isEmpty()) + filters.add(new Filter("status", SearchConditions.OP_IN, status)); + filters.add(new Filter("migrationVersion", SearchConditions.OP_IS, Values.NULL)); + SearchCriteria sc = new SearchCriteria(); + sc.setNodeType(SystemNodeTypes.DATA_NODE.name()); + sc.setObjectType(objectType); + sc.setResultSize(batchSize); + sc.setStartPosition(startPosition); + if(!filters.isEmpty() && filters.size()>0) + sc.addMetadata(MetadataCriterion.create(filters)); + Request req = getRequest(graphId, GraphEngineManagers.SEARCH_MANAGER, "searchNodes", + GraphDACParams.search_criteria.name(), sc); + req.put(GraphDACParams.get_tags.name(), true); + Response listRes = getResponse(req); + if (checkError(listRes)) + return null; + else { + List nodes = (List) listRes.get(GraphDACParams.node_list.name()); + return nodes; + } + } + public List getNodesWithInDateRange(String graphId, String objectType, String startDate, String endDate) { List nodeIds = new ArrayList<>(); @@ -758,4 +790,27 @@ public void hierarchyCleanUp(Map map) { } } + public Map getCSPMigrationObjectCount(String graphId, List objectTypes) { + Map counts = new HashMap(); + Request request = getRequest(graphId, GraphEngineManagers.SEARCH_MANAGER, "executeQueryForProps"); + request.put(GraphDACParams.query.name(), MessageFormat.format("MATCH (n:{0}) WHERE EXISTS(n.IL_FUNC_OBJECT_TYPE) AND n.IL_SYS_NODE_TYPE=\"DATA_NODE\" AND n.IL_FUNC_OBJECT_TYPE IN {1} AND NOT EXISTS(n.migrationVersion) RETURN n.IL_FUNC_OBJECT_TYPE AS objectType, COUNT(n) AS count;", graphId, new JSONArray(objectTypes))); + List props = new ArrayList(); + props.add("objectType"); + props.add("count"); + request.put(GraphDACParams.property_keys.name(), props); + Response response = getResponse(request); + if (!checkError(response)) { + Map result = response.getResult(); + List> list = (List>) result.get("properties"); + if (null != list && !list.isEmpty()) { + for (int i = 0; i < list.size(); i++) { + Map properties = list.get(i); + counts.put((String) properties.get("objectType"), (Long) properties.get("count")); + } + } + + } + return counts; + } + } diff --git a/platform-tools/spikes/sync-tool/src/main/java/org/sunbird/sync/tool/mgr/CSPMigrationMessageGenerator.java b/platform-tools/spikes/sync-tool/src/main/java/org/sunbird/sync/tool/mgr/CSPMigrationMessageGenerator.java new file mode 100644 index 0000000000..9579728a60 --- /dev/null +++ b/platform-tools/spikes/sync-tool/src/main/java/org/sunbird/sync/tool/mgr/CSPMigrationMessageGenerator.java @@ -0,0 +1,210 @@ +package org.sunbird.sync.tool.mgr; + +import org.apache.commons.collections.CollectionUtils; +import org.apache.commons.lang3.StringUtils; +import org.codehaus.jackson.map.ObjectMapper; +import org.springframework.stereotype.Component; +import org.sunbird.common.Platform; +import org.sunbird.common.exception.ClientException; +import org.sunbird.common.exception.ResourceNotFoundException; +import org.sunbird.graph.dac.enums.SystemNodeTypes; +import org.sunbird.graph.dac.model.Node; +import org.sunbird.learning.util.ControllerUtil; +import org.sunbird.sync.tool.util.KafkaUtil; +import org.sunbird.telemetry.util.LogTelemetryEventUtil; + +import javax.annotation.PostConstruct; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +@Component +public class CSPMigrationMessageGenerator { + + private ControllerUtil util = new ControllerUtil(); + private static int batchSize = 100; + private ObjectMapper mapper = new ObjectMapper(); + private static String actorId = "csp-migration"; + private static String actorType = "System"; + private static String pdataId = "org.sunbird.platform"; + private static String pdataVersion = "1.0"; + private static String action = "csp-migration"; + private static String migrationTopicName = Platform.config.getString("csp.migration.request.topic"); + + @PostConstruct + private void init() throws Exception { + int batch = Platform.config.hasPath("csp.migration.batch.size") ? Platform.config.getInt("csp.migration.batch.size") : 100; + batchSize = batch; + } + + public void generateMgrMsg(String graphId, String[] objectTypes, String[] mimeTypes, String[] status, Integer limit, Integer delay) throws Exception { + if (StringUtils.isBlank(graphId)) + throw new ClientException("ERR_INVALID_GRAPH_ID", "Graph Id is blank."); + if (null == objectTypes || objectTypes.length == 0) + throw new ClientException("ERR_EMPTY_OBJECT_TYPE", "Object Type is blank."); + List mimeTypeList = new ArrayList(); + List statusList = new ArrayList(); + if (null != mimeTypes && mimeTypes.length > 0) + mimeTypeList = Arrays.asList(mimeTypes); + if (null != status && status.length > 0) + statusList = Arrays.asList(status); + Map errors = new HashMap<>(); + long startTime = System.currentTimeMillis(); + System.out.println("-----------------------------------------"); + System.out.println("\nMigration Event Generation starting at " + startTime); + Map counts = util.getCSPMigrationObjectCount(graphId, Arrays.asList(objectTypes)); + if (counts.isEmpty()) { + System.out.println("No objects found in this graph."); + } else { + List objTypes = counts.keySet().stream().filter(key -> Arrays.asList(objectTypes).contains(key)).collect(Collectors.toList()); + for (String objectType : objTypes) { + Long count = counts.get(objectType); + System.out.println(count + " - " + objectType + " nodes available for migration"); + } + } + for (String objectType : objectTypes) { + Long count = counts.get(objectType); + if (count > 0) { + System.out.println("-----------------------------------------"); + System.out.println("\nGenerating event for object of type " + objectType + " with batch size of " + batchSize + " having delay " + delay + "ms for each batch.\n"); + int start = 0; + int current = 0; + long total = counts.get(objectType); + long stopLimit; + if (limit > 0) { + if (limit < batchSize || limit % batchSize != 0) { + System.out.println("Limit value should be minimum " + batchSize + ". The limit value should be multiple of " + batchSize + ". Setting limit to minimum value. i.e " + batchSize); + stopLimit = batchSize; + } else stopLimit = limit; + } else stopLimit = total; + boolean found = true; + while (found && start < stopLimit) { + List nodes = null; + try { + nodes = util.getNodes(graphId, objectType.trim(), mimeTypeList, statusList, start, batchSize); + } catch (ResourceNotFoundException e) { + System.out.println("Error while fetching neo4j records for objectType=" + objectType + ", start=" + start + ",batchSize=" + batchSize); + start += batchSize; + continue; + } + if (CollectionUtils.isNotEmpty(nodes)) { + start += batchSize; + Map events = generateMigrationEvent(nodes, errors); + sendEvent(events, errors); + current += events.size(); + printProgress(startTime, total, current); + if (delay > 0) { + Thread.sleep(delay); + } + } else { + found = false; + break; + } + } + if (!errors.isEmpty()) + System.out.println("Error! while generating migration event data from nodes, below nodes are ignored. \n" + errors); + long endTime = System.currentTimeMillis(); + System.out.println("\nMigration Event Generation completed for object of type " + objectType + " in: " + (endTime - startTime) + "ms"); + } else { + System.out.println("\nSkipped Generating migration event for objectType: " + objectType); + } + } + System.out.println("-----------------------------------------"); + long endTime = System.currentTimeMillis(); + System.out.println("Migration Event Generation completed at " + endTime); + System.out.println("Time taken to generate Events: " + (endTime - startTime) + "ms"); + } + + private void sendEvent(Map events, Map errors) { + for (String id : events.keySet()) { + try { + KafkaUtil.send(events.get(id), migrationTopicName); + } catch (Exception e) { + e.printStackTrace(); + System.out.println("Error Message :"+e.getMessage() ); + errors.put(id, "Error While Sending Migration Event for " + id); + } + } + } + + private Map generateMigrationEvent(List nodes, Map errors) { + Map events = new HashMap(); + for (Node node : nodes) { + String message = getEvent(node, errors); + if (StringUtils.isNotBlank(message)) + events.put(node.getIdentifier(), message); + } + return events; + } + + private String getEvent(Node node, Map errors) { + Map actor = new HashMap() {{ + put("id", actorId); + put("type", actorType); + }}; + Map context = new HashMap() {{ + put("channel", node.getMetadata().getOrDefault("channel", "")); + put("pdata", new HashMap() {{ + put("id", pdataId); + put("ver", pdataVersion); + }}); + }}; + if (Platform.config.hasPath("cloud_storage.env")) { + String env = Platform.config.getString("cloud_storage.env"); + context.put("env", env); + } + Map object = new HashMap() {{ + put("id", node.getIdentifier()); + put("ver", node.getMetadata().get("versionKey")); + }}; + Map edata = new HashMap() {{ + put("action", action); + put("metadata", new HashMap() {{ + put("pkgVersion", node.getMetadata().get("pkgVersion")); + put("mimeType", node.getMetadata().get("mimeType")); + put("status", node.getMetadata().get("status")); + put("identifier", node.getIdentifier()); + put("objectType", node.getObjectType()); + }}); + }}; + String beJobRequestEvent = LogTelemetryEventUtil.logInstructionEvent(actor, context, object, edata); + if (StringUtils.isBlank(beJobRequestEvent)) { + errors.put(node.getIdentifier(), "Error While Generating Migration Event for " + node.getIdentifier()); + } + return beJobRequestEvent; + } + + private static void printProgress(long startTime, long total, long current) { + long eta = current == 0 ? 0 : + (total - current) * (System.currentTimeMillis() - startTime) / current; + + String etaHms = current == 0 ? "N/A" : + String.format("%02d:%02d:%02d", TimeUnit.MILLISECONDS.toHours(eta), + TimeUnit.MILLISECONDS.toMinutes(eta) % TimeUnit.HOURS.toMinutes(1), + TimeUnit.MILLISECONDS.toSeconds(eta) % TimeUnit.MINUTES.toSeconds(1)); + + StringBuilder string = new StringBuilder(140); + int percent = (int) (current * 100 / total); + string + .append('\r') + .append(String.join("", Collections.nCopies(percent == 0 ? 2 : 2 - (int) (Math.log10(percent)), " "))) + .append(String.format(" %d%% [", percent)) + .append(String.join("", Collections.nCopies(percent, "="))) + .append('>') + .append(String.join("", Collections.nCopies(100 - percent, " "))) + .append(']') + .append(String.join("", Collections.nCopies((int) (Math.log10(total)) - (int) (Math.log10(current)), " "))) + .append(String.format(" %d/%d, ETA: %s", current, total, etaHms)); + + System.out.print(string); + } + + public static void filterMigrationNodes(List nodes, Integer limit) { + nodes.removeIf(n -> SystemNodeTypes.DEFINITION_NODE.name().equals(n.getNodeType())); + } +} diff --git a/platform-tools/spikes/sync-tool/src/main/java/org/sunbird/sync/tool/shell/MigrateCSPDataCommand.java b/platform-tools/spikes/sync-tool/src/main/java/org/sunbird/sync/tool/shell/MigrateCSPDataCommand.java new file mode 100644 index 0000000000..4606e9eafa --- /dev/null +++ b/platform-tools/spikes/sync-tool/src/main/java/org/sunbird/sync/tool/shell/MigrateCSPDataCommand.java @@ -0,0 +1,41 @@ +package org.sunbird.sync.tool.shell; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.shell.core.CommandMarker; +import org.springframework.shell.core.annotation.CliCommand; +import org.springframework.shell.core.annotation.CliOption; +import org.springframework.stereotype.Component; +import org.sunbird.sync.tool.mgr.CSPMigrationMessageGenerator; + +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; + +@Component +public class MigrateCSPDataCommand implements CommandMarker { + + @Autowired + CSPMigrationMessageGenerator cspMsgGenerator; + + @CliCommand(value = "migratecspdata", help = "Generate CSP Data Migration Event") + public void migrateCSPData( + @CliOption(key = {"graphId"}, mandatory = false, unspecifiedDefaultValue = "domain", help = "graphId of the object") final String graphId, + @CliOption(key = {"objectType"}, mandatory = true, help = "Object Type is Required") final String[] objectType, + @CliOption(key = {"mimeType"}, mandatory = false, help = "mimeTypes can be provided") final String[] mimeType, + @CliOption(key = {"status"}, mandatory = false, help = "Specific Status can be passed") final String[] status, + @CliOption(key = {"limit"}, mandatory = false, unspecifiedDefaultValue = "0", help = "Specific Limit can be passed") final Integer limit, + @CliOption(key = {"delay"}, mandatory = false, unspecifiedDefaultValue = "10", help = "time gap between each batch") final Integer delay) + throws Exception { + + long startTime = System.currentTimeMillis(); + DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm:ss"); + LocalDateTime start = LocalDateTime.now(); + cspMsgGenerator.generateMgrMsg(graphId, objectType, mimeType, status, limit, delay); + long endTime = System.currentTimeMillis(); + long exeTime = endTime - startTime; + System.out.println("Total time of execution: " + exeTime + "ms"); + LocalDateTime end = LocalDateTime.now(); + System.out.println("START_TIME: " + dtf.format(start) + ", END_TIME: " + dtf.format(end)); + } + + +} diff --git a/platform-tools/spikes/sync-tool/src/main/java/org/sunbird/sync/tool/util/KafkaUtil.java b/platform-tools/spikes/sync-tool/src/main/java/org/sunbird/sync/tool/util/KafkaUtil.java new file mode 100644 index 0000000000..c77b578809 --- /dev/null +++ b/platform-tools/spikes/sync-tool/src/main/java/org/sunbird/sync/tool/util/KafkaUtil.java @@ -0,0 +1,85 @@ +package org.sunbird.sync.tool.util; + +import org.apache.kafka.clients.consumer.Consumer; +import org.apache.kafka.clients.consumer.ConsumerConfig; +import org.apache.kafka.clients.consumer.KafkaConsumer; +import org.apache.kafka.clients.producer.KafkaProducer; +import org.apache.kafka.clients.producer.Producer; +import org.apache.kafka.clients.producer.ProducerConfig; +import org.apache.kafka.clients.producer.ProducerRecord; +import org.apache.kafka.common.PartitionInfo; +import org.apache.kafka.common.serialization.LongDeserializer; +import org.apache.kafka.common.serialization.LongSerializer; +import org.apache.kafka.common.serialization.StringDeserializer; +import org.apache.kafka.common.serialization.StringSerializer; +import org.sunbird.common.Platform; +import org.sunbird.common.exception.ClientException; +import org.sunbird.telemetry.logger.TelemetryManager; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Properties; + +public class KafkaUtil { + + private final static String BOOTSTRAP_SERVERS = Platform.config.getString("kafka.urls"); + private static Producer producer; + private static Consumer consumer; + private static Map topicCheckResult = new HashMap(); + private static boolean isTopicCheckReq = Platform.config.hasPath("kafka.topic.send.enable") ? Platform.config.getBoolean("kafka.topic.send.enable") : true; + + static { + loadProducerProperties(); + loadConsumerProperties(); + } + + private static void loadProducerProperties() { + Properties props = new Properties(); + props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, BOOTSTRAP_SERVERS); + props.put(ProducerConfig.CLIENT_ID_CONFIG, "KafkaClientProducer"); + props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, LongSerializer.class.getName()); + props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName()); + producer = new KafkaProducer(props); + } + + private static void loadConsumerProperties() { + Properties props = new Properties(); + props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, BOOTSTRAP_SERVERS); + props.put(ConsumerConfig.CLIENT_ID_CONFIG, "KafkaClientConsumer"); + props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, LongDeserializer.class.getName()); + props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName()); + consumer = new KafkaConsumer<>(props); + } + + private static Producer getProducer() { + return producer; + } + + private static Consumer getConsumer() { + return consumer; + } + + public static void send(String event, String topic) throws Exception { + if (topicCheckResult.getOrDefault(topic, false)) { + final Producer producer = getProducer(); + ProducerRecord record = new ProducerRecord(topic, event); + producer.send(record); + } else if (validate(topic)) { + final Producer producer = getProducer(); + ProducerRecord record = new ProducerRecord(topic, event); + producer.send(record); + } else { + System.err.println("Topic id: " + topic + ", does not exists."); + throw new ClientException("TOPIC_NOT_EXISTS_EXCEPTION", "Topic id: " + topic + ", does not exists."); + } + } + + public static boolean validate(String topic) throws Exception { + Consumer consumer = getConsumer(); + Map> topics = consumer.listTopics(); + Boolean result = topics.keySet().contains(topic); + topicCheckResult.put(topic, result); + return result; + } +} diff --git a/platform-tools/spikes/sync-tool/src/main/resources/application.conf b/platform-tools/spikes/sync-tool/src/main/resources/application.conf index ca4f2c7f1a..e741eb105e 100644 --- a/platform-tools/spikes/sync-tool/src/main/resources/application.conf +++ b/platform-tools/spikes/sync-tool/src/main/resources/application.conf @@ -93,4 +93,7 @@ contentTypeToPrimaryCategory { LessonPlanUnit: "Lesson Plan Unit" CourseUnit: "Course Unit" TextBookUnit: "Textbook Unit" -} \ No newline at end of file +} + +csp.migration.request.topic="dev.csp.migration.job.request" +csp.migration.batch.size=50 \ No newline at end of file From 43e0d297fd9dc0892b5ddc0366a0ceca290f86d1 Mon Sep 17 00:00:00 2001 From: Kumar Gauraw Date: Wed, 16 Nov 2022 13:34:45 +0530 Subject: [PATCH 083/222] Issue #IQ-149 feat: added sync tool & kafka config --- ansible/roles/lp-synctool-deploy/defaults/main.yml | 4 +++- .../lp-synctool-deploy/templates/application.conf.j2 | 4 +++- ansible/roles/setup-kafka/defaults/main.yml | 8 +++++++- 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/ansible/roles/lp-synctool-deploy/defaults/main.yml b/ansible/roles/lp-synctool-deploy/defaults/main.yml index b9919c28d4..4ac452d9d8 100644 --- a/ansible/roles/lp-synctool-deploy/defaults/main.yml +++ b/ansible/roles/lp-synctool-deploy/defaults/main.yml @@ -25,4 +25,6 @@ search_lms_index_host: "{{ groups['core-es']|join(':9200,')}}:9200" cloud_store: azure azure_public_container: -azure_account_name: \ No newline at end of file +azure_account_name: + +csp_migration_batch_size: 100 \ No newline at end of file diff --git a/ansible/roles/lp-synctool-deploy/templates/application.conf.j2 b/ansible/roles/lp-synctool-deploy/templates/application.conf.j2 index 686acdda15..a64e6272d3 100644 --- a/ansible/roles/lp-synctool-deploy/templates/application.conf.j2 +++ b/ansible/roles/lp-synctool-deploy/templates/application.conf.j2 @@ -115,4 +115,6 @@ contentTypeToPrimaryCategory { LessonPlanUnit: "Lesson Plan Unit" CourseUnit: "Course Unit" TextBookUnit: "Textbook Unit" -} \ No newline at end of file +} +csp.migration.request.topic="{{ env }}.csp.migration.job.request" +csp.migration.batch.size={{ csp_migration_batch_size }} \ No newline at end of file diff --git a/ansible/roles/setup-kafka/defaults/main.yml b/ansible/roles/setup-kafka/defaults/main.yml index 6e70a29d56..c1a1585a49 100644 --- a/ansible/roles/setup-kafka/defaults/main.yml +++ b/ansible/roles/setup-kafka/defaults/main.yml @@ -135,6 +135,9 @@ processing_kafka_topics: - name: dialcode.context.job.request.failed num_of_partitions: 1 replication_factor: 1 + - name: csp.migration.job.request + num_of_partitions: 1 + replication_factor: 1 processing_kafka_overriden_topics: - name: telemetry.raw @@ -263,4 +266,7 @@ processing_kafka_overriden_topics: replication_factor: 1 - name: dialcode.context.job.request.failed retention_time: 1209600000 - replication_factor: 1 \ No newline at end of file + replication_factor: 1 + - name: csp.migration.job.request + retention_time: 1209600000 + replication_factor: 1 \ No newline at end of file From a68269c6296f70b18b868ca0594ff853747a600c Mon Sep 17 00:00:00 2001 From: Jayaprakash8887 Date: Wed, 16 Nov 2022 13:43:15 +0530 Subject: [PATCH 084/222] Issue #KN-439 feat: CSP Migration Job --- kubernetes/helm_charts/datapipeline_jobs/values.j2 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kubernetes/helm_charts/datapipeline_jobs/values.j2 b/kubernetes/helm_charts/datapipeline_jobs/values.j2 index 9412a18e62..b2fd5da2dd 100644 --- a/kubernetes/helm_charts/datapipeline_jobs/values.j2 +++ b/kubernetes/helm_charts/datapipeline_jobs/values.j2 @@ -1319,7 +1319,7 @@ csp-migrator: csp-migrator: |+ include file("/data/flink/conf/base-config.conf") kafka { - input.topic = "{{ env_name }}.csp.migration.request" + input.topic = "{{ env_name }}.csp.migration.job.request" groupId = "{{ env_name }}-csp-migrator-group" failed.topic = "{{ env_name }}.csp.migration.job.request.failed" live_video_stream.topic = "{{ env_name }}.live.video.stream.request" From ab2c19d8bad94cb0a50a50c71383202f47f18c25 Mon Sep 17 00:00:00 2001 From: Jayaprakash8887 Date: Wed, 16 Nov 2022 13:53:56 +0530 Subject: [PATCH 085/222] Issue #KN-439 feat: CSP Migration Job --- ansible/roles/setup-kafka/defaults/main.yml | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/ansible/roles/setup-kafka/defaults/main.yml b/ansible/roles/setup-kafka/defaults/main.yml index 6e70a29d56..ca18466e13 100644 --- a/ansible/roles/setup-kafka/defaults/main.yml +++ b/ansible/roles/setup-kafka/defaults/main.yml @@ -135,6 +135,12 @@ processing_kafka_topics: - name: dialcode.context.job.request.failed num_of_partitions: 1 replication_factor: 1 + - name: live.video.stream.request + num_of_partitions: 1 + replication_factor: 1 + - name: republish.job.request + num_of_partitions: 1 + replication_factor: 1 processing_kafka_overriden_topics: - name: telemetry.raw @@ -262,5 +268,11 @@ processing_kafka_overriden_topics: retention_time: 1209600000 replication_factor: 1 - name: dialcode.context.job.request.failed + retention_time: 1209600000 + replication_factor: 1 + - name: live.video.stream.request + retention_time: 1209600000 + replication_factor: 1 + - name: republish.job.request retention_time: 1209600000 replication_factor: 1 \ No newline at end of file From 63161c817426b4844a108bf82d940ffbfd0eb5b1 Mon Sep 17 00:00:00 2001 From: saiakhil46 Date: Thu, 17 Nov 2022 13:33:21 +0530 Subject: [PATCH 086/222] added es5-snapshot-purge role --- es5-snapshot-purge/defaults/main.yml | 5 +++++ es5-snapshot-purge/meta/main.yml | 3 +++ es5-snapshot-purge/tasks/main.yml | 14 ++++++++++++++ es5-snapshot-purge/templates/curator.yml | 10 ++++++++++ .../templates/snapshot-purge-action.yml | 13 +++++++++++++ 5 files changed, 45 insertions(+) create mode 100644 es5-snapshot-purge/defaults/main.yml create mode 100644 es5-snapshot-purge/meta/main.yml create mode 100644 es5-snapshot-purge/tasks/main.yml create mode 100644 es5-snapshot-purge/templates/curator.yml create mode 100644 es5-snapshot-purge/templates/snapshot-purge-action.yml diff --git a/es5-snapshot-purge/defaults/main.yml b/es5-snapshot-purge/defaults/main.yml new file mode 100644 index 0000000000..cab1182a3d --- /dev/null +++ b/es5-snapshot-purge/defaults/main.yml @@ -0,0 +1,5 @@ +es_snapshot_host: localhost +es_snapshot_repository: "{{ snapshot_base_path }}" +es_snapshot_retention_days: 30 +es_curator_config_dir: /etc/curator +es_curator_config_file: "{{ es_curator_config_dir }}/curator.yml" diff --git a/es5-snapshot-purge/meta/main.yml b/es5-snapshot-purge/meta/main.yml new file mode 100644 index 0000000000..8b4e268b5d --- /dev/null +++ b/es5-snapshot-purge/meta/main.yml @@ -0,0 +1,3 @@ +--- +dependencies: + - { role: es-curator, es_curator_major_version: 5, es_curator_version: 5.8.4 } diff --git a/es5-snapshot-purge/tasks/main.yml b/es5-snapshot-purge/tasks/main.yml new file mode 100644 index 0000000000..21364440ce --- /dev/null +++ b/es5-snapshot-purge/tasks/main.yml @@ -0,0 +1,14 @@ +# See meta folder for curator installation +- name: Ensure curator config dir exists + file: dest="{{es_curator_config_dir}}" state=directory + +- name: Create curator.yml + template: src=curator.yml dest="{{es_curator_config_file}}" + +- name: Create snapshot-purge-action.yml + template: src=snapshot-purge-action.yml dest="{{es_curator_config_dir}}/snapshot-purge-action.yml" + +- name: Delete snapshots older than {{ es_snapshot_retention_days }} days + shell: "curator --config {{ es_curator_config_file }} {{es_curator_config_dir}}/snapshot-purge-action.yml" + async: 800 + poll: 20 diff --git a/es5-snapshot-purge/templates/curator.yml b/es5-snapshot-purge/templates/curator.yml new file mode 100644 index 0000000000..6c9c6c9976 --- /dev/null +++ b/es5-snapshot-purge/templates/curator.yml @@ -0,0 +1,10 @@ +client: + hosts: + - {{ es_snapshot_host }} + port: 9200 + timeout: 100 +logging: + loglevel: INFO + logfile: + logformat: default + blacklist: ['elasticsearch', 'urllib3'] \ No newline at end of file diff --git a/es5-snapshot-purge/templates/snapshot-purge-action.yml b/es5-snapshot-purge/templates/snapshot-purge-action.yml new file mode 100644 index 0000000000..bd6cc05b06 --- /dev/null +++ b/es5-snapshot-purge/templates/snapshot-purge-action.yml @@ -0,0 +1,13 @@ +actions: + 1: + action: delete_snapshots + description: Delete snapshots older than {{ es_snapshot_retention_days }} days + options: + repository: {{ es_snapshot_repository }} + ignore_empty_list: True + filters: + - filtertype: age + source: creation_date + direction: older + unit: days + unit_count: {{ es_snapshot_retention_days }} From 20ec9a86168fe7ef5096019190719883c7b8621f Mon Sep 17 00:00:00 2001 From: saiakhil46 Date: Thu, 17 Nov 2022 13:38:27 +0530 Subject: [PATCH 087/222] added es-curator role --- es-curator/defaults/main.yml | 2 ++ es-curator/tasks/main.yml | 13 +++++++++++++ 2 files changed, 15 insertions(+) create mode 100644 es-curator/defaults/main.yml create mode 100644 es-curator/tasks/main.yml diff --git a/es-curator/defaults/main.yml b/es-curator/defaults/main.yml new file mode 100644 index 0000000000..9fd4efe2c8 --- /dev/null +++ b/es-curator/defaults/main.yml @@ -0,0 +1,2 @@ +es_curator_major_version: 5 +es_curator_version: 5.8.4 \ No newline at end of file diff --git a/es-curator/tasks/main.yml b/es-curator/tasks/main.yml new file mode 100644 index 0000000000..c4a8bacee7 --- /dev/null +++ b/es-curator/tasks/main.yml @@ -0,0 +1,13 @@ +- name: Debian - Add Elasticsearch repository key + apt_key: url="https://artifacts.elastic.co/GPG-KEY-elasticsearch" state=present + +- name: Add curator {{ es_curator_major_version }} repo + apt_repository: repo='deb [arch=amd64] http://packages.elastic.co/curator/{{ es_curator_major_version }}/debian stable main' state=present update_cache=yes + +- debug: + msg: "{{ es_curator_version }}" + +- name: Install elasticsearch curator + apt: + name: elasticsearch-curator={{ es_curator_version }} + force: yes From 15335bf4530bb8abb58e7e9af1ab2297fb7ef5ae Mon Sep 17 00:00:00 2001 From: saiakhil46 Date: Thu, 17 Nov 2022 14:03:06 +0530 Subject: [PATCH 088/222] added es5-snapshot-purge es-curator roles --- {es-curator => ansible/roles/es-curator}/defaults/main.yml | 0 {es-curator => ansible/roles/es-curator}/tasks/main.yml | 0 .../roles/es5-snapshot-purge}/defaults/main.yml | 0 .../roles/es5-snapshot-purge}/meta/main.yml | 0 .../roles/es5-snapshot-purge}/tasks/main.yml | 0 .../roles/es5-snapshot-purge}/templates/curator.yml | 0 .../roles/es5-snapshot-purge}/templates/snapshot-purge-action.yml | 0 7 files changed, 0 insertions(+), 0 deletions(-) rename {es-curator => ansible/roles/es-curator}/defaults/main.yml (100%) rename {es-curator => ansible/roles/es-curator}/tasks/main.yml (100%) rename {es5-snapshot-purge => ansible/roles/es5-snapshot-purge}/defaults/main.yml (100%) rename {es5-snapshot-purge => ansible/roles/es5-snapshot-purge}/meta/main.yml (100%) rename {es5-snapshot-purge => ansible/roles/es5-snapshot-purge}/tasks/main.yml (100%) rename {es5-snapshot-purge => ansible/roles/es5-snapshot-purge}/templates/curator.yml (100%) rename {es5-snapshot-purge => ansible/roles/es5-snapshot-purge}/templates/snapshot-purge-action.yml (100%) diff --git a/es-curator/defaults/main.yml b/ansible/roles/es-curator/defaults/main.yml similarity index 100% rename from es-curator/defaults/main.yml rename to ansible/roles/es-curator/defaults/main.yml diff --git a/es-curator/tasks/main.yml b/ansible/roles/es-curator/tasks/main.yml similarity index 100% rename from es-curator/tasks/main.yml rename to ansible/roles/es-curator/tasks/main.yml diff --git a/es5-snapshot-purge/defaults/main.yml b/ansible/roles/es5-snapshot-purge/defaults/main.yml similarity index 100% rename from es5-snapshot-purge/defaults/main.yml rename to ansible/roles/es5-snapshot-purge/defaults/main.yml diff --git a/es5-snapshot-purge/meta/main.yml b/ansible/roles/es5-snapshot-purge/meta/main.yml similarity index 100% rename from es5-snapshot-purge/meta/main.yml rename to ansible/roles/es5-snapshot-purge/meta/main.yml diff --git a/es5-snapshot-purge/tasks/main.yml b/ansible/roles/es5-snapshot-purge/tasks/main.yml similarity index 100% rename from es5-snapshot-purge/tasks/main.yml rename to ansible/roles/es5-snapshot-purge/tasks/main.yml diff --git a/es5-snapshot-purge/templates/curator.yml b/ansible/roles/es5-snapshot-purge/templates/curator.yml similarity index 100% rename from es5-snapshot-purge/templates/curator.yml rename to ansible/roles/es5-snapshot-purge/templates/curator.yml diff --git a/es5-snapshot-purge/templates/snapshot-purge-action.yml b/ansible/roles/es5-snapshot-purge/templates/snapshot-purge-action.yml similarity index 100% rename from es5-snapshot-purge/templates/snapshot-purge-action.yml rename to ansible/roles/es5-snapshot-purge/templates/snapshot-purge-action.yml From 661ccc0c9b6923478922d8460006f2f90babbb6d Mon Sep 17 00:00:00 2001 From: Jayaprakash8887 Date: Fri, 18 Nov 2022 13:45:07 +0530 Subject: [PATCH 089/222] Issue #KN-439 feat: CSP Migration Job --- kubernetes/helm_charts/datapipeline_jobs/values.j2 | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/kubernetes/helm_charts/datapipeline_jobs/values.j2 b/kubernetes/helm_charts/datapipeline_jobs/values.j2 index ee289c43c8..f7b7fdbd24 100644 --- a/kubernetes/helm_charts/datapipeline_jobs/values.j2 +++ b/kubernetes/helm_charts/datapipeline_jobs/values.j2 @@ -1351,9 +1351,10 @@ csp-migrator: } key_value_strings_to_migrate = { - "https://sunbirddev.blob.core.windows.net/": "https://obj.dev.sunbirded.org", - "https://ekstep-public-dev.s3-ap-south-1.amazonaws.com/": "https://obj.dev.sunbirded.org", - "https://community.ekstep.in/assets/public/": "https://obj.dev.sunbirded.org" + "https://sunbirddev.blob.core.windows.net/": "Knowlg_Store", + "https://ekstep-public-dev.s3-ap-south-1.amazonaws.com": "Knowlg_Store", + "https://community.ekstep.in/assets/public": "Knowlg_Store", + "https://ntpproductionall.blob.core.windows.net/ntp-content-production": "Knowlg_Store" } neo4j_fields_to_migrate = { From c48107bcaeb2ca855a4a0d33897d8076a685d2a6 Mon Sep 17 00:00:00 2001 From: Jayaprakash8887 Date: Fri, 18 Nov 2022 13:50:57 +0530 Subject: [PATCH 090/222] Issue #KN-439 feat: CSP Migration Job --- ansible/roles/setup-kafka/defaults/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ansible/roles/setup-kafka/defaults/main.yml b/ansible/roles/setup-kafka/defaults/main.yml index ca18466e13..82d0e762c8 100644 --- a/ansible/roles/setup-kafka/defaults/main.yml +++ b/ansible/roles/setup-kafka/defaults/main.yml @@ -275,4 +275,4 @@ processing_kafka_overriden_topics: replication_factor: 1 - name: republish.job.request retention_time: 1209600000 - replication_factor: 1 \ No newline at end of file + replication_factor: 1 From 749816d1ec0c30e3f1e45695464886c385fca3d6 Mon Sep 17 00:00:00 2001 From: Jayaprakash8887 Date: Fri, 18 Nov 2022 13:51:56 +0530 Subject: [PATCH 091/222] Issue #KN-439 feat: CSP Migration Job --- ansible/roles/setup-kafka/defaults/main.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ansible/roles/setup-kafka/defaults/main.yml b/ansible/roles/setup-kafka/defaults/main.yml index 82d0e762c8..37ae9bc8f8 100644 --- a/ansible/roles/setup-kafka/defaults/main.yml +++ b/ansible/roles/setup-kafka/defaults/main.yml @@ -139,8 +139,8 @@ processing_kafka_topics: num_of_partitions: 1 replication_factor: 1 - name: republish.job.request - num_of_partitions: 1 - replication_factor: 1 + num_of_partitions: 1 + replication_factor: 1 processing_kafka_overriden_topics: - name: telemetry.raw From cb629335f5af7a63884d3f2a3ba8450b6813e410 Mon Sep 17 00:00:00 2001 From: Jayaprakash8887 Date: Fri, 18 Nov 2022 16:04:19 +0530 Subject: [PATCH 092/222] Issue #KN-439 feat: CSP Migration Job --- kubernetes/helm_charts/datapipeline_jobs/values.j2 | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/kubernetes/helm_charts/datapipeline_jobs/values.j2 b/kubernetes/helm_charts/datapipeline_jobs/values.j2 index f7b7fdbd24..4055ef0efa 100644 --- a/kubernetes/helm_charts/datapipeline_jobs/values.j2 +++ b/kubernetes/helm_charts/datapipeline_jobs/values.j2 @@ -1354,7 +1354,8 @@ csp-migrator: "https://sunbirddev.blob.core.windows.net/": "Knowlg_Store", "https://ekstep-public-dev.s3-ap-south-1.amazonaws.com": "Knowlg_Store", "https://community.ekstep.in/assets/public": "Knowlg_Store", - "https://ntpproductionall.blob.core.windows.net/ntp-content-production": "Knowlg_Store" + "https://ntpproductionall.blob.core.windows.net/ntp-content-production": "Knowlg_Store", + "https://vdn.diksha.gov.in/assets/public": "Knowlg_Store" } neo4j_fields_to_migrate = { From 16ebfd9fba9f504d6b376691138928543d80c364 Mon Sep 17 00:00:00 2001 From: Jayaprakash8887 Date: Fri, 18 Nov 2022 16:41:16 +0530 Subject: [PATCH 093/222] Issue #KN-439 feat: CSP Migration Job --- ansible/roles/setup-kafka/defaults/main.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/ansible/roles/setup-kafka/defaults/main.yml b/ansible/roles/setup-kafka/defaults/main.yml index 37ae9bc8f8..8f60db9ebc 100644 --- a/ansible/roles/setup-kafka/defaults/main.yml +++ b/ansible/roles/setup-kafka/defaults/main.yml @@ -135,6 +135,9 @@ processing_kafka_topics: - name: dialcode.context.job.request.failed num_of_partitions: 1 replication_factor: 1 + - name: csp.migration.job.request + num_of_partitions: 1 + replication_factor: 1 - name: live.video.stream.request num_of_partitions: 1 replication_factor: 1 @@ -270,6 +273,9 @@ processing_kafka_overriden_topics: - name: dialcode.context.job.request.failed retention_time: 1209600000 replication_factor: 1 + - name: csp.migration.job.request + retention_time: 1209600000 + replication_factor: 1 - name: live.video.stream.request retention_time: 1209600000 replication_factor: 1 From 50d053d1c9682910d3573dfc21d085fb9a60996e Mon Sep 17 00:00:00 2001 From: Jayaprakash8887 Date: Fri, 18 Nov 2022 16:47:14 +0530 Subject: [PATCH 094/222] Issue #KN-439 feat: CSP Migration Job --- kubernetes/helm_charts/datapipeline_jobs/values.j2 | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/kubernetes/helm_charts/datapipeline_jobs/values.j2 b/kubernetes/helm_charts/datapipeline_jobs/values.j2 index 4055ef0efa..a9224dce4a 100644 --- a/kubernetes/helm_charts/datapipeline_jobs/values.j2 +++ b/kubernetes/helm_charts/datapipeline_jobs/values.j2 @@ -1351,11 +1351,12 @@ csp-migrator: } key_value_strings_to_migrate = { - "https://sunbirddev.blob.core.windows.net/": "Knowlg_Store", - "https://ekstep-public-dev.s3-ap-south-1.amazonaws.com": "Knowlg_Store", - "https://community.ekstep.in/assets/public": "Knowlg_Store", - "https://ntpproductionall.blob.core.windows.net/ntp-content-production": "Knowlg_Store", - "https://vdn.diksha.gov.in/assets/public": "Knowlg_Store" + "https://sunbirddev.blob.core.windows.net": "$Knowlg_Store$", + "https://ekstep-public-dev.s3-ap-south-1.amazonaws.com": "$Knowlg_Store$", + "https://community.ekstep.in/assets/public": "$Knowlg_Store$", + "https://ntpproductionall.blob.core.windows.net/ntp-content-production": "$Knowlg_Store$", + "https://vdn.diksha.gov.in/assets/public": "$Knowlg_Store$", + "https://sunbirdstagingpublic.blob.core.windows.net/sunbird-content-staging": "$Knowlg_Store$" } neo4j_fields_to_migrate = { @@ -1374,7 +1375,7 @@ csp-migrator: } migrationVersion = 1 - video_stream_regeneration_enable = true + video_stream_regeneration_enable = false live_node_republish_enable = true copy_missing_files_to_cloud = true download_path = /tmp From b20cae64740ada1318e5f16f5b980a29ceacb210 Mon Sep 17 00:00:00 2001 From: Kumar Gauraw Date: Fri, 18 Nov 2022 16:48:19 +0530 Subject: [PATCH 095/222] Issue #IQ-149 fix: updated kafka config --- ansible/roles/setup-kafka/defaults/main.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/ansible/roles/setup-kafka/defaults/main.yml b/ansible/roles/setup-kafka/defaults/main.yml index c1a1585a49..615d9f8a81 100644 --- a/ansible/roles/setup-kafka/defaults/main.yml +++ b/ansible/roles/setup-kafka/defaults/main.yml @@ -136,8 +136,8 @@ processing_kafka_topics: num_of_partitions: 1 replication_factor: 1 - name: csp.migration.job.request - num_of_partitions: 1 - replication_factor: 1 + num_of_partitions: 1 + replication_factor: 1 processing_kafka_overriden_topics: - name: telemetry.raw @@ -268,5 +268,5 @@ processing_kafka_overriden_topics: retention_time: 1209600000 replication_factor: 1 - name: csp.migration.job.request - retention_time: 1209600000 - replication_factor: 1 \ No newline at end of file + retention_time: 1209600000 + replication_factor: 1 \ No newline at end of file From 28cf5ec2f9241213abccceb52bc0b7ca34c021ac Mon Sep 17 00:00:00 2001 From: Jayaprakash8887 Date: Mon, 21 Nov 2022 19:25:49 +0530 Subject: [PATCH 096/222] Issue #KN-439 feat: CSP Migration Job --- kubernetes/helm_charts/datapipeline_jobs/values.j2 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kubernetes/helm_charts/datapipeline_jobs/values.j2 b/kubernetes/helm_charts/datapipeline_jobs/values.j2 index a9224dce4a..a72f3f7b14 100644 --- a/kubernetes/helm_charts/datapipeline_jobs/values.j2 +++ b/kubernetes/helm_charts/datapipeline_jobs/values.j2 @@ -1377,7 +1377,7 @@ csp-migrator: migrationVersion = 1 video_stream_regeneration_enable = false live_node_republish_enable = true - copy_missing_files_to_cloud = true + copy_missing_files_to_cloud = false download_path = /tmp From 23a95dc153675ebfa866d295109cbcb116709614 Mon Sep 17 00:00:00 2001 From: Jayaprakash8887 Date: Mon, 21 Nov 2022 21:59:54 +0530 Subject: [PATCH 097/222] Issue #KN-439 feat: CSP Migration Job --- kubernetes/helm_charts/datapipeline_jobs/values.j2 | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/kubernetes/helm_charts/datapipeline_jobs/values.j2 b/kubernetes/helm_charts/datapipeline_jobs/values.j2 index a72f3f7b14..6f607f5809 100644 --- a/kubernetes/helm_charts/datapipeline_jobs/values.j2 +++ b/kubernetes/helm_charts/datapipeline_jobs/values.j2 @@ -1361,10 +1361,10 @@ csp-migrator: neo4j_fields_to_migrate = { "asset": ["artifactUrl","thumbnail"], - "content": ["appIcon","artifactUrl", "posterImage", "previewUrl", "thumbnail", "assetsMap", "certTemplate", "itemSetPreviewUrl", "grayScaleAppIcon","sourceURL"], - "contentimage": ["appIcon","artifactUrl", "posterImage", "previewUrl", "thumbnail", "assetsMap", "certTemplate", "itemSetPreviewUrl", "grayScaleAppIcon","sourceURL"], - "collection": ["appIcon","artifactUrl", "posterImage", "previewUrl", "thumbnail", "toc_url", "grayScaleAppIcon"], - "collectionimage": ["appIcon","artifactUrl", "posterImage", "previewUrl", "thumbnail", "toc_url", "grayScaleAppIcon"], + "content": ["appIcon","artifactUrl", "posterImage", "previewUrl", "thumbnail", "assetsMap", "certTemplate", "itemSetPreviewUrl", "grayScaleAppIcon", "sourceURL", "variants", "downloadUrl"], + "contentimage": ["appIcon","artifactUrl", "posterImage", "previewUrl", "thumbnail", "assetsMap", "certTemplate", "itemSetPreviewUrl", "grayScaleAppIcon", "sourceURL", "variants", "downloadUrl"], + "collection": ["appIcon","artifactUrl", "posterImage", "previewUrl", "thumbnail", "toc_url", "grayScaleAppIcon", "variants"], + "collectionimage": ["appIcon","artifactUrl", "posterImage", "previewUrl", "thumbnail", "toc_url", "grayScaleAppIcon", "variants"], "plugins": ["artifactUrl"], "itemset": ["previewUrl"], "assessmentitem": ["data", "question", "solutions", "editorState", "media"] From 8e42caad07d9897660d31231845366547f683768 Mon Sep 17 00:00:00 2001 From: Jayaprakash8887 Date: Mon, 21 Nov 2022 22:00:43 +0530 Subject: [PATCH 098/222] Issue #KN-439 feat: CSP Migration Job --- kubernetes/helm_charts/datapipeline_jobs/values.j2 | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/kubernetes/helm_charts/datapipeline_jobs/values.j2 b/kubernetes/helm_charts/datapipeline_jobs/values.j2 index 6f607f5809..d0c84f59a3 100644 --- a/kubernetes/helm_charts/datapipeline_jobs/values.j2 +++ b/kubernetes/helm_charts/datapipeline_jobs/values.j2 @@ -1360,13 +1360,13 @@ csp-migrator: } neo4j_fields_to_migrate = { - "asset": ["artifactUrl","thumbnail"], - "content": ["appIcon","artifactUrl", "posterImage", "previewUrl", "thumbnail", "assetsMap", "certTemplate", "itemSetPreviewUrl", "grayScaleAppIcon", "sourceURL", "variants", "downloadUrl"], - "contentimage": ["appIcon","artifactUrl", "posterImage", "previewUrl", "thumbnail", "assetsMap", "certTemplate", "itemSetPreviewUrl", "grayScaleAppIcon", "sourceURL", "variants", "downloadUrl"], - "collection": ["appIcon","artifactUrl", "posterImage", "previewUrl", "thumbnail", "toc_url", "grayScaleAppIcon", "variants"], - "collectionimage": ["appIcon","artifactUrl", "posterImage", "previewUrl", "thumbnail", "toc_url", "grayScaleAppIcon", "variants"], + "asset": ["artifactUrl", "thumbnail", "downloadUrl"], + "content": ["appIcon", "artifactUrl", "posterImage", "previewUrl", "thumbnail", "assetsMap", "certTemplate", "itemSetPreviewUrl", "grayScaleAppIcon", "sourceURL", "variants", "downloadUrl"], + "contentimage": ["appIcon", "artifactUrl", "posterImage", "previewUrl", "thumbnail", "assetsMap", "certTemplate", "itemSetPreviewUrl", "grayScaleAppIcon", "sourceURL", "variants", "downloadUrl"], + "collection": ["appIcon", "artifactUrl", "posterImage", "previewUrl", "thumbnail", "toc_url", "grayScaleAppIcon", "variants"], + "collectionimage": ["appIcon", "artifactUrl", "posterImage", "previewUrl", "thumbnail", "toc_url", "grayScaleAppIcon", "variants"], "plugins": ["artifactUrl"], - "itemset": ["previewUrl"], + "itemset": ["previewUrl", "downloadUrl"], "assessmentitem": ["data", "question", "solutions", "editorState", "media"] } From 26a8d7a67f526345c35af17d3e981e4da40ec6ea Mon Sep 17 00:00:00 2001 From: Jayaprakash8887 Date: Mon, 21 Nov 2022 22:09:03 +0530 Subject: [PATCH 099/222] Issue #KN-439 feat: CSP Migration Job --- kubernetes/helm_charts/datapipeline_jobs/values.j2 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/kubernetes/helm_charts/datapipeline_jobs/values.j2 b/kubernetes/helm_charts/datapipeline_jobs/values.j2 index d0c84f59a3..077974882f 100644 --- a/kubernetes/helm_charts/datapipeline_jobs/values.j2 +++ b/kubernetes/helm_charts/datapipeline_jobs/values.j2 @@ -1363,8 +1363,8 @@ csp-migrator: "asset": ["artifactUrl", "thumbnail", "downloadUrl"], "content": ["appIcon", "artifactUrl", "posterImage", "previewUrl", "thumbnail", "assetsMap", "certTemplate", "itemSetPreviewUrl", "grayScaleAppIcon", "sourceURL", "variants", "downloadUrl"], "contentimage": ["appIcon", "artifactUrl", "posterImage", "previewUrl", "thumbnail", "assetsMap", "certTemplate", "itemSetPreviewUrl", "grayScaleAppIcon", "sourceURL", "variants", "downloadUrl"], - "collection": ["appIcon", "artifactUrl", "posterImage", "previewUrl", "thumbnail", "toc_url", "grayScaleAppIcon", "variants"], - "collectionimage": ["appIcon", "artifactUrl", "posterImage", "previewUrl", "thumbnail", "toc_url", "grayScaleAppIcon", "variants"], + "collection": ["appIcon", "artifactUrl", "posterImage", "previewUrl", "thumbnail", "toc_url", "grayScaleAppIcon", "variants", "downloadUrl"], + "collectionimage": ["appIcon", "artifactUrl", "posterImage", "previewUrl", "thumbnail", "toc_url", "grayScaleAppIcon", "variants", "downloadUrl"], "plugins": ["artifactUrl"], "itemset": ["previewUrl", "downloadUrl"], "assessmentitem": ["data", "question", "solutions", "editorState", "media"] From a6a5490f00da60a92a5ffb1287919c58d179104a Mon Sep 17 00:00:00 2001 From: Kumar Gauraw Date: Thu, 24 Nov 2022 17:04:28 +0530 Subject: [PATCH 100/222] Issue #IQ-194 feat: added config and kafka topic --- ansible/roles/setup-kafka/defaults/main.yml | 6 +++++ .../helm_charts/datapipeline_jobs/values.j2 | 26 ++++++++++++------- 2 files changed, 23 insertions(+), 9 deletions(-) diff --git a/ansible/roles/setup-kafka/defaults/main.yml b/ansible/roles/setup-kafka/defaults/main.yml index 0daf58a6d6..c9a784aa1f 100644 --- a/ansible/roles/setup-kafka/defaults/main.yml +++ b/ansible/roles/setup-kafka/defaults/main.yml @@ -144,6 +144,9 @@ processing_kafka_topics: - name: republish.job.request num_of_partitions: 1 replication_factor: 1 + - name: assessment.republish.request + num_of_partitions: 1 + replication_factor: 1 processing_kafka_overriden_topics: - name: telemetry.raw @@ -282,4 +285,7 @@ processing_kafka_overriden_topics: - name: republish.job.request retention_time: 1209600000 replication_factor: 1 + - name: assessment.republish.request + retention_time: 1209600000 + replication_factor: 1 diff --git a/kubernetes/helm_charts/datapipeline_jobs/values.j2 b/kubernetes/helm_charts/datapipeline_jobs/values.j2 index 077974882f..d5faf33662 100644 --- a/kubernetes/helm_charts/datapipeline_jobs/values.j2 +++ b/kubernetes/helm_charts/datapipeline_jobs/values.j2 @@ -1324,6 +1324,7 @@ csp-migrator: failed.topic = "{{ env_name }}.csp.migration.job.request.failed" live_video_stream.topic = "{{ env_name }}.live.video.stream.request" live_content_node_republish.topic = "{{ env_name }}.republish.job.request" + live_question_node_republish.topic = "{{ env_name }}.assessment.republish.request" } task { timer.duration = {{ csp_migrator_timer_duration }} @@ -1350,24 +1351,31 @@ csp-migrator: assessment_table = "question_data" } + questionset.hierarchy.keyspace="{{ hierarchy_keyspace_name }}" + questionset.hierarchy.table="questionset_hierarchy" + key_value_strings_to_migrate = { - "https://sunbirddev.blob.core.windows.net": "$Knowlg_Store$", - "https://ekstep-public-dev.s3-ap-south-1.amazonaws.com": "$Knowlg_Store$", - "https://community.ekstep.in/assets/public": "$Knowlg_Store$", - "https://ntpproductionall.blob.core.windows.net/ntp-content-production": "$Knowlg_Store$", - "https://vdn.diksha.gov.in/assets/public": "$Knowlg_Store$", - "https://sunbirdstagingpublic.blob.core.windows.net/sunbird-content-staging": "$Knowlg_Store$" + "https://community.ekstep.in/assets/public": "CLOUD_STORAGE_BASE_PATH", + "https://sunbirddev.blob.core.windows.net": "CLOUD_STORAGE_BASE_PATH", + "https://ekstep-public-dev.s3-ap-south-1.amazonaws.com": "CLOUD_STORAGE_BASE_PATH", + "https://ntpproductionall.blob.core.windows.net/ntp-content-production": "CLOUD_STORAGE_BASE_PATH", + "https://vdn.diksha.gov.in/assets/public": "CLOUD_STORAGE_BASE_PATH", + "https://sunbirdstagingpublic.blob.core.windows.net/sunbird-content-staging": "CLOUD_STORAGE_BASE_PATH" } neo4j_fields_to_migrate = { "asset": ["artifactUrl", "thumbnail", "downloadUrl"], - "content": ["appIcon", "artifactUrl", "posterImage", "previewUrl", "thumbnail", "assetsMap", "certTemplate", "itemSetPreviewUrl", "grayScaleAppIcon", "sourceURL", "variants", "downloadUrl"], - "contentimage": ["appIcon", "artifactUrl", "posterImage", "previewUrl", "thumbnail", "assetsMap", "certTemplate", "itemSetPreviewUrl", "grayScaleAppIcon", "sourceURL", "variants", "downloadUrl"], + "content": ["appIcon", "artifactUrl", "posterImage", "previewUrl", "thumbnail", "assetsMap", "certTemplate", "itemSetPreviewUrl", "grayScaleAppIcon", "sourceURL", "variants", "downloadUrl","streamingUrl"], + "contentimage": ["appIcon", "artifactUrl", "posterImage", "previewUrl", "thumbnail", "assetsMap", "certTemplate", "itemSetPreviewUrl", "grayScaleAppIcon", "sourceURL", "variants", "downloadUrl","streamingUrl"], "collection": ["appIcon", "artifactUrl", "posterImage", "previewUrl", "thumbnail", "toc_url", "grayScaleAppIcon", "variants", "downloadUrl"], "collectionimage": ["appIcon", "artifactUrl", "posterImage", "previewUrl", "thumbnail", "toc_url", "grayScaleAppIcon", "variants", "downloadUrl"], "plugins": ["artifactUrl"], "itemset": ["previewUrl", "downloadUrl"], - "assessmentitem": ["data", "question", "solutions", "editorState", "media"] + "assessmentitem": ["data", "question", "solutions", "editorState", "media"], + "question": ["appIcon","artifactUrl", "posterImage", "previewUrl","downloadUrl", "variants","pdfUrl"], + "questionimage": ["appIcon","artifactUrl", "posterImage", "previewUrl","downloadUrl", "variants","pdfUrl"], + "questionset": ["appIcon","artifactUrl", "posterImage", "previewUrl","downloadUrl", "variants","pdfUrl"], + "questionsetimage": ["appIcon","artifactUrl", "posterImage", "previewUrl","downloadUrl", "variants","pdfUrl"] } cassandra_fields_to_migrate = { From 862fc852400e9034c1a8d65aba9ba59598118ed8 Mon Sep 17 00:00:00 2001 From: anilgupta Date: Mon, 28 Nov 2022 20:17:45 +0530 Subject: [PATCH 101/222] Issue #KN-439 feat: Cloud agnostic changes --- .../templates/application.conf.j2 | 10 ++++----- .../templates/application.conf.j2 | 10 ++++----- .../org/sunbird/learning/util/CloudStore.java | 21 +++++-------------- platform-modules/pom.xml | 2 +- 4 files changed, 16 insertions(+), 27 deletions(-) diff --git a/ansible/roles/learning-service/templates/application.conf.j2 b/ansible/roles/learning-service/templates/application.conf.j2 index 6fa2f4e0c3..fd0be0da3b 100644 --- a/ansible/roles/learning-service/templates/application.conf.j2 +++ b/ansible/roles/learning-service/templates/application.conf.j2 @@ -248,11 +248,11 @@ learning.content.type.not.copied.list=["Asset"] #Youtube License Validation Regex Pattern youtube.license.regex.pattern=["\\?vi?=([^&]*)", "watch\\?.*v=([^&]*)", "(?:embed|vi?)/([^/?]*)","^([A-Za-z0-9\\-\\_]*)"] -#Azure Storage details -cloud_storage_type="{{ cloud_store }}" -azure_storage_key="{{sunbird_public_storage_account_name}}" -azure_storage_secret="{{sunbird_public_storage_account_key}}" -azure_storage_container="{{ azure_public_container }}" +#Cloud Storage details +cloud_storage_type="{{ cloud_service_provider }}" +cloud_storage_key="{{ cloud_public_storage_accountname }}" +cloud_storage_secret="{{ cloud_public_storage_secret }}" +cloud_storage_container="{{ cloud_storage_content_bucketname }}" installation.id="{{ instance_name }}" diff --git a/ansible/roles/lp-synctool-deploy/templates/application.conf.j2 b/ansible/roles/lp-synctool-deploy/templates/application.conf.j2 index a64e6272d3..aa4ea50435 100644 --- a/ansible/roles/lp-synctool-deploy/templates/application.conf.j2 +++ b/ansible/roles/lp-synctool-deploy/templates/application.conf.j2 @@ -76,11 +76,11 @@ content.postpublish.topic="{{ env }}.content.postpublish.request" search.lms_es_conn_info="{{ search_lms_index_host }}" -#Azure Storage details -cloud_storage_type="{{ cloud_store }}" -azure_storage_key="{{sunbird_public_storage_account_name}}" -azure_storage_secret="{{sunbird_public_storage_account_key}}" -azure_storage_container="{{ azure_public_container }}" +#Cloud Storage details +cloud_storage_type="{{ cloud_service_provider }}" +cloud_storage_key="{{ cloud_public_storage_accountname }}" +cloud_storage_secret="{{ cloud_public_storage_secret }}" +cloud_storage_container="{{ cloud_storage_content_bucketname }}" contentTypeToPrimaryCategory { ClassroomTeachingVideo: "Explanation Content" diff --git a/platform-modules/actors/src/main/java/org/sunbird/learning/util/CloudStore.java b/platform-modules/actors/src/main/java/org/sunbird/learning/util/CloudStore.java index 32643d61e8..3e24ead27d 100644 --- a/platform-modules/actors/src/main/java/org/sunbird/learning/util/CloudStore.java +++ b/platform-modules/actors/src/main/java/org/sunbird/learning/util/CloudStore.java @@ -24,18 +24,9 @@ public class CloudStore { private static String cloudStoreType = Platform.config.getString("cloud_storage_type"); static { - - if(StringUtils.equalsIgnoreCase(cloudStoreType, "azure")) { - String storageKey = Platform.config.getString("azure_storage_key"); - String storageSecret = Platform.config.getString("azure_storage_secret"); - storageService = StorageServiceFactory.getStorageService(new StorageConfig(cloudStoreType, storageKey, storageSecret)); - }else if(StringUtils.equalsIgnoreCase(cloudStoreType, "aws")) { - String storageKey = Platform.config.getString("aws_storage_key"); - String storageSecret = Platform.config.getString("aws_storage_secret"); - storageService = StorageServiceFactory.getStorageService(new StorageConfig(cloudStoreType, storageKey, storageSecret)); - }else { - throw new ServerException("ERR_INVALID_CLOUD_STORAGE", "Error while initialising cloud storage"); - } + String storageKey = Platform.config.getString("cloud_storage_key"); + String storageSecret = Platform.config.getString("cloud_storage_secret"); + storageService = StorageServiceFactory.getStorageService(new StorageConfig(cloudStoreType, storageKey, storageSecret)); } public static BaseStorageService getCloudStoreService() { @@ -43,10 +34,8 @@ public static BaseStorageService getCloudStoreService() { } public static String getContainerName() { - if(StringUtils.equalsIgnoreCase(cloudStoreType, "azure")) { - return Platform.config.getString("azure_storage_container"); - }else if(StringUtils.equalsIgnoreCase(cloudStoreType, "aws")) { - return S3PropertyReader.getProperty("aws_storage_container"); + if(Platform.config.hasPath("cloud_storage_container") && !Platform.config.getString("cloud_storage_container").equalsIgnoreCase("")) { + return Platform.config.getString("cloud_storage_container"); }else { throw new ServerException("ERR_INVALID_CLOUD_STORAGE", "Error while getting container name"); } diff --git a/platform-modules/pom.xml b/platform-modules/pom.xml index 94cdc3d8ee..20ee3a4651 100644 --- a/platform-modules/pom.xml +++ b/platform-modules/pom.xml @@ -19,7 +19,7 @@ 2.3.1 1.8 1.8 - 1.2.8 + 1.4.3 From 40cf7ae48d37fbd28909c24cee1570a9db36a0a0 Mon Sep 17 00:00:00 2001 From: anilgupta Date: Thu, 8 Dec 2022 08:51:29 +0530 Subject: [PATCH 102/222] Issue #KN-439 merge: merging from release-5.2.0-knowlg to release-5.2.0 --- ansible/inventory/env/group_vars/all.yml | 4 +- .../lp-synctool-deploy/defaults/main.yml | 3 +- .../templates/application.conf.j2 | 2 +- ansible/roles/setup-kafka/defaults/main.yml | 11 +- .../roles/flink-jobs-deploy/defaults/main.yml | 24 + .../helm_charts/datapipeline_jobs/values.j2 | 467 ++++++------------ pom.xml | 2 +- 7 files changed, 193 insertions(+), 320 deletions(-) diff --git a/ansible/inventory/env/group_vars/all.yml b/ansible/inventory/env/group_vars/all.yml index 75ae4c49db..3bf75b51b6 100644 --- a/ansible/inventory/env/group_vars/all.yml +++ b/ansible/inventory/env/group_vars/all.yml @@ -85,8 +85,8 @@ search_es7_host: "{{ groups['es7']|join(':9200,')}}:9200" mlworkbench: "{{ groups['mlworkbench'][0]}}" -azure_account: "{{ sunbird_public_storage_account_name }}" -azure_secret: "{{ sunbird_public_storage_account_key }}" +azure_account: "{{ cloud_public_storage_accountname }}" +azure_secret: "{{ cloud_public_storage_secret }}" dedup_redis_host: "{{ dp_redis_host }}" kp_redis_host: "{{ groups['redisall'][0] }}" neo4j_route_path: "bolt://{{ groups['learning-neo4j-node1'][0] }}:7687" diff --git a/ansible/roles/lp-synctool-deploy/defaults/main.yml b/ansible/roles/lp-synctool-deploy/defaults/main.yml index 4ac452d9d8..b98b48a3e7 100644 --- a/ansible/roles/lp-synctool-deploy/defaults/main.yml +++ b/ansible/roles/lp-synctool-deploy/defaults/main.yml @@ -27,4 +27,5 @@ cloud_store: azure azure_public_container: azure_account_name: -csp_migration_batch_size: 100 \ No newline at end of file +csp_migration_batch_size: 100 +csp_migration_topic_name: "{{ env }}.csp.migration.job.request" diff --git a/ansible/roles/lp-synctool-deploy/templates/application.conf.j2 b/ansible/roles/lp-synctool-deploy/templates/application.conf.j2 index aa4ea50435..98703b4a64 100644 --- a/ansible/roles/lp-synctool-deploy/templates/application.conf.j2 +++ b/ansible/roles/lp-synctool-deploy/templates/application.conf.j2 @@ -116,5 +116,5 @@ contentTypeToPrimaryCategory { CourseUnit: "Course Unit" TextBookUnit: "Textbook Unit" } -csp.migration.request.topic="{{ env }}.csp.migration.job.request" +csp.migration.request.topic="{{ csp_migration_topic_name }}" csp.migration.batch.size={{ csp_migration_batch_size }} \ No newline at end of file diff --git a/ansible/roles/setup-kafka/defaults/main.yml b/ansible/roles/setup-kafka/defaults/main.yml index c9a784aa1f..91e15dc276 100644 --- a/ansible/roles/setup-kafka/defaults/main.yml +++ b/ansible/roles/setup-kafka/defaults/main.yml @@ -144,7 +144,10 @@ processing_kafka_topics: - name: republish.job.request num_of_partitions: 1 replication_factor: 1 - - name: assessment.republish.request + - name: cassandra.data.migration.request + num_of_partitions: 1 + replication_factor: 1 + - name: cassandra.data.migration.job.request.failed num_of_partitions: 1 replication_factor: 1 @@ -285,7 +288,9 @@ processing_kafka_overriden_topics: - name: republish.job.request retention_time: 1209600000 replication_factor: 1 - - name: assessment.republish.request + - name: cassandra.data.migration.request + retention_time: 1209600000 + replication_factor: 1 + - name: cassandra.data.migration.job.request.failed retention_time: 1209600000 replication_factor: 1 - diff --git a/kubernetes/ansible/roles/flink-jobs-deploy/defaults/main.yml b/kubernetes/ansible/roles/flink-jobs-deploy/defaults/main.yml index 79600d3e11..aa07726095 100644 --- a/kubernetes/ansible/roles/flink-jobs-deploy/defaults/main.yml +++ b/kubernetes/ansible/roles/flink-jobs-deploy/defaults/main.yml @@ -286,6 +286,13 @@ flink_job_names: taskmanager_memory: 1024m taskslots: 1 cpu_requests: 0.3 + cassandra-data-migration: + job_class_name: 'org.sunbird.job.task.CassandraDataMigrationStreamTask' + replica: 1 + jobmanager_memory: 1024m + taskmanager_memory: 1024m + taskslots: 1 + cpu_requests: 0.3 ### Global vars middleware_course_keyspace: "sunbird_courses" @@ -299,6 +306,11 @@ composite_search_indexer_parallelism: 1 dialcode_external_indexer_parallelism: 1 dialcode_metric_indexer_parallelism: 1 schema_definition_cache_expiry_in_sec: 14400 +search_indexer_topic_name: "{{ env_name }}.learning.graph.events" +search_indexer_failed_topic_name: "{{ env_name }}.learning.events.failed" +search_indexer_group_name: "{{ env_name }}-search-indexer-group" +search_indexer_es_index_name: "{{ compositesearch_index_name }}" +dialcode_es_index_name: "dialcode" search_indexer_ignored_fields: ["responseDeclaration", "body", "options", "lastStatusChangedOn", "SYS_INTERNAL_LAST_UPDATED_ON", "sYS_INTERNAL_LAST_UPDATED_ON","branchingLogic"] search_indexer_restrict_object_types: ["EventSet", "EventSetImage", "Event", "EventImage", "Questionnaire", "Misconception", "FrameworkType", "Concept", "Misconception", "Language", "Reference", "Dimension", "Method", "Library", "Domain", "Api"] @@ -376,3 +388,15 @@ csp_migrator_timer_duration: 1800 csp_migrator_max_retries: 10 csp_migrator_consumer_parallelism: 1 csp_migrator_cassandra_parallelism: 1 +csp_migration_topic_name: "{{ env_name }}.csp.migration.job.request" +csp_migrator_group_name: "{{ env_name }}-csp-migrator-group" +csp_migrator_failed_topic_name: "{{ env_name }}.csp.migration.job.request.failed" +question_republish_topic_name: "{{ env_name }}.assessment.republish.request" +content_republish_topic_name: "{{ env_name }}.republish.job.request" +video_stream_topic_name: "{{ env_name }}.live.video.stream.request" +content_hierarchy_keyspace_name: "{{ hierarchy_keyspace_name }}" +questionset_hierarchy_keyspace_name: "{{ hierarchy_keyspace_name }}" + +cloudstorage_relative_path_prefix_content: "CLOUD_STORAGE_BASE_PATH" +cloudstorage_relative_path_prefix_dial: "DIAL_STORAGE_BASE_PATH" +cloudstorage_metadata_list: '["appIcon", "artifactUrl", "posterImage", "previewUrl", "thumbnail", "assetsMap", "certTemplate", "itemSetPreviewUrl", "grayScaleAppIcon", "sourceURL", "variants", "downloadUrl", "streamingUrl", "toc_url", "data", "question", "solutions", "editorState", "media", "pdfUrl"]' \ No newline at end of file diff --git a/kubernetes/helm_charts/datapipeline_jobs/values.j2 b/kubernetes/helm_charts/datapipeline_jobs/values.j2 index d5faf33662..f17dc6f2bf 100644 --- a/kubernetes/helm_charts/datapipeline_jobs/values.j2 +++ b/kubernetes/helm_charts/datapipeline_jobs/values.j2 @@ -116,98 +116,6 @@ base_config: | } } -activity-aggregate-updater: - activity-aggregate-updater: |+ - include file("/data/flink/conf/base-config.conf") - kafka { - input.topic = {{ env_name }}.coursebatch.job.request - output.audit.topic = {{ env_name }}.telemetry.raw - output.failed.topic = {{ env_name }}.activity.agg.failed - output.certissue.topic = {{ env_name }}.issue.certificate.request - groupId = {{ env_name }}-activity-aggregate-group - } - task { - window.shards = {{ activity_agg_window_shards }} - checkpointing.interval = {{ activity_agg_checkpointing_interval }} - checkpointing.pause.between.seconds = {{ activity_agg_checkpointing_pause_interval }} - restart-strategy.attempts = {{ restart_attempts }} # max 3 restart attempts - restart-strategy.delay = 240000 # in milli-seconds # on max restarts (3) within 4 min the job will fail. - consumer.parallelism = {{ activity_agg_consumer_parallelism }} - dedup.parallelism = {{ activity_agg_dedup_parallelism }} - activity.agg.parallelism = {{ activity_agg_parallelism }} - enrolment.complete.parallelism = {{ enrolment_complete_parallelism }} - } - lms-cassandra { - keyspace = "{{ middleware_course_keyspace }}" - consumption.table = "{{ middleware_consumption_table }}" - user_activity_agg.table = "{{ middleware_user_activity_agg_table }}" - user_enrolments.table = "user_enrolments" - } - redis { - database { - relationCache.id = 10 - } - } - dedup-redis { - host = {{ dedup_redis_host }} - port = 6379 - database.index = {{ activity_agg_dedup_index }} - database.expiry = {{ activity_agg_dedup_expiry }} - } - threshold.batch.read.interval = {{ activity_agg_batch_interval }} - threshold.batch.read.size = {{ activity_agg_batch_read_size }} - threshold.batch.write.size = {{ activity_agg_batch_write_size }} - activity { - module.aggs.enabled = true - input.dedup.enabled = true - filter.processed.enrolments = {{ activity_agg_enrolment_filter_processe_enabled | lower }} - collection.status.cache.expiry = {{ activity_agg_collection_status_cache_expiry_time }} - } - service { - search.basePath = "{{ kp_search_service_base_url }}" - } - - - flink-conf: |+ - jobmanager.memory.flink.size: {{ flink_job_names['activity-aggregate-updater'].jobmanager_memory }} - taskmanager.memory.flink.size: {{ flink_job_names['activity-aggregate-updater'].taskmanager_memory }} - taskmanager.numberOfTaskSlots: {{ flink_job_names['activity-aggregate-updater'].taskslots }} - parallelism.default: 1 - jobmanager.execution.failover-strategy: region - taskmanager.memory.network.fraction: 0.1 - -relation-cache-updater: - relation-cache-updater: |+ - include file("/data/flink/conf/base-config.conf") - kafka { - input.topic = {{ env_name }}.content.postpublish.request - groupId = {{ env_name }}-relation-cache-updater-group - } - task { - consumer.parallelism = {{ relation_cache_updater_consumer_parallelism }} - parallelism = {{ relation_cache_updater_parallelism }} - } - lms-cassandra { - keyspace = "{{ middleware_hierarchy_keyspace }}" - table = "{{ middleware_content_hierarchy_table }}" - } - redis { - database.index = 10 - } - dp-redis { - host = {{ dp_redis_host }} - port = 6379 - database.index = 5 - } - - flink-conf: |+ - jobmanager.memory.flink.size: {{ flink_job_names['relation-cache-updater'].jobmanager_memory }} - taskmanager.memory.flink.size: {{ flink_job_names['relation-cache-updater'].taskmanager_memory }} - taskmanager.numberOfTaskSlots: {{ flink_job_names['relation-cache-updater'].taskslots }} - parallelism.default: 1 - jobmanager.execution.failover-strategy: region - taskmanager.memory.network.fraction: 0.1 - post-publish-processor: post-publish-processor: |+ include file("/data/flink/conf/base-config.conf") @@ -241,6 +149,14 @@ post-publish-processor: dial.basePath = "https://{{domain_name}}/dial/" } + cloudstorage { + metadata.replace_absolute_path={{ cloudstorage_replace_absolute_path | default('false') }} + relative_path_prefix={{ cloudstorage_relative_path_prefix_content }} + metadata.list={{ cloudstorage_metadata_list }} + read_base_path="{{ cloudstorage_base_path }}" + write_base_path={{ valid_cloudstorage_base_urls }} + } + flink-conf: |+ jobmanager.memory.flink.size: {{ flink_job_names['post-publish-processor'].jobmanager_memory }} taskmanager.memory.flink.size: {{ flink_job_names['post-publish-processor'].taskmanager_memory }} @@ -249,43 +165,6 @@ post-publish-processor: jobmanager.execution.failover-strategy: region taskmanager.memory.network.fraction: 0.1 -questionset-publish: - questionset-publish: |+ - include file("/data/flink/conf/base-config.conf") - kafka { - input.topic = {{ env_name }}.assessment.publish.request - post_publish.topic = {{ env_name }}.content.postpublish.request - groupId = {{ env_name }}-questionset-publish-group - } - task { - consumer.parallelism = 1 - parallelism = 1 - router.parallelism = 1 - } - question { - keyspace = "{{ assessment_keyspace_name }}" - table = "question_data" - } - questionset { - keyspace = "{{ hierarchy_keyspace_name }}" - table = "questionset_hierarchy" - } - print_service.base_url = "{{ kp_print_service_base_url }}" - cloud_storage_type="{{ cloud_store }}" - cloud_storage_key="{{ cloud_storage_key }}" - cloud_storage_secret="{{ cloud_storage_secret }}" - cloud_storage_container="{{ cloud_storage_container }}" - - master.category.validation.enabled ="{{ master_category_validation_enabled }}" - - flink-conf: |+ - jobmanager.memory.flink.size: {{ flink_job_names['questionset-publish'].jobmanager_memory }} - taskmanager.memory.flink.size: {{ flink_job_names['questionset-publish'].taskmanager_memory }} - taskmanager.numberOfTaskSlots: {{ flink_job_names['questionset-publish'].taskslots }} - parallelism.default: 1 - jobmanager.execution.failover-strategy: region - taskmanager.memory.network.fraction: 0.1 - video-stream-generator: video-stream-generator: |+ include file("/data/flink/conf/base-config.conf") @@ -367,9 +246,9 @@ search-indexer: search-indexer: |+ include file("/data/flink/conf/base-config.conf") kafka { - input.topic = "{{ env_name }}.learning.graph.events" - error.topic = "{{ env_name }}.learning.events.failed" - groupId = "{{ env_name }}-search-indexer-group" + input.topic = "{{ search_indexer_topic_name }}" + error.topic = "{{ search_indexer_failed_topic_name }}" + groupId = "{{ search_indexer_group_name }}" } task { consumer.parallelism = {{ search_indexer_consumer_parallelism }} @@ -378,8 +257,8 @@ search-indexer: dialcodeIndexer.parallelism = {{ dialcode_external_indexer_parallelism }} dialcodemetricsIndexer.parallelism = {{ dialcode_metric_indexer_parallelism }} } - compositesearch.index.name = "compositesearch" - dialcode.index.name = "dialcode" + compositesearch.index.name = "{{ search_indexer_es_index_name }}" + dialcode.index.name = "{{ dialcode_es_index_name }}" dailcodemetrics.index.name = "dialcodemetrics" restrict.metadata.objectTypes = [] nested.fields = ["badgeAssertions", "targets", "badgeAssociations", "plugins", "me_totalTimeSpent", "me_totalPlaySessionCount", "me_totalTimeSpentInSec", "batches", "trackable", "credentials", "discussionForum", "provider", "osMetadata", "actions", "transcripts", "accessibility"] @@ -387,6 +266,13 @@ search-indexer: restrict.objectTypes = {{ search_indexer_restrict_object_types | to_json }} ignored.fields={{ search_indexer_ignored_fields | to_json }} + cloudstorage { + metadata.replace_absolute_path={{ cloudstorage_replace_absolute_path | default('false') }} + relative_path_prefix={{ cloudstorage_relative_path_prefix_content }} + metadata.list={{ cloudstorage_metadata_list }} + read_base_path="{{ cloudstorage_base_path }}" + } + flink-conf: |+ jobmanager.memory.flink.size: {{ flink_job_names['search-indexer'].jobmanager_memory }} taskmanager.memory.flink.size: {{ flink_job_names['search-indexer'].taskmanager_memory }} @@ -395,52 +281,6 @@ search-indexer: jobmanager.execution.failover-strategy: region taskmanager.memory.network.fraction: 0.1 -enrolment-reconciliation: - enrolment-reconciliation: |+ - include file("/data/flink/conf/base-config.conf") - kafka { - input.topic = {{ env_name }}.batch.enrolment.sync.request - output.audit.topic = {{ env_name }}.telemetry.raw - output.failed.topic = {{ env_name }}.activity.agg.failed - output.certissue.topic = {{ env_name }}.issue.certificate.request - groupId = {{ env_name }}-enrolment-reconciliation-group - } - task { - restart-strategy.attempts = {{ restart_attempts }} # max 3 restart attempts - restart-strategy.delay = 240000 # in milli-seconds # on max restarts (3) within 4 min the job will fail. - consumer.parallelism = {{ enrolment_reconciliation_consumer_parallelism }} - enrolment.reconciliation.parallelism = {{ enrolment_reconciliation_parallelism }} - enrolment.complete.parallelism = {{ enrolment_complete_parallelism }} - } - lms-cassandra { - keyspace = "{{ middleware_course_keyspace }}" - consumption.table = "{{ middleware_consumption_table }}" - user_activity_agg.table = "{{ middleware_user_activity_agg_table }}" - user_enrolments.table = "user_enrolments" - } - redis { - database { - relationCache.id = 10 - } - } - threshold.batch.write.size = {{ enrolment_reconciliation_batch_write_size }} - activity { - module.aggs.enabled = true - collection.status.cache.expiry = {{ enrolment_reconciliation_collection_status_cache_expiry_time }} - } - service { - search.basePath = "{{ kp_search_service_base_url }}" - } - - - flink-conf: |+ - jobmanager.memory.flink.size: {{ flink_job_names['enrolment-reconciliation'].jobmanager_memory }} - taskmanager.memory.flink.size: {{ flink_job_names['enrolment-reconciliation'].taskmanager_memory }} - taskmanager.numberOfTaskSlots: {{ flink_job_names['enrolment-reconciliation'].taskslots }} - parallelism.default: 1 - jobmanager.execution.failover-strategy: region - taskmanager.memory.network.fraction: 0.1 - asset-enrichment: asset-enrichment: |+ include file("/data/flink/conf/base-config.conf") @@ -475,10 +315,18 @@ asset-enrichment: size.pixel = 150 } content_youtube_apikey="{{ youtube_api_key }}" - cloud_storage_type="{{ cloud_store }}" - cloud_storage_key="{{ cloud_storage_key }}" - cloud_storage_secret="{{ cloud_storage_secret }}" - cloud_storage_container="{{ cloud_storage_container }}" + cloud_storage_type="{{ cloud_service_provider }}" + cloud_storage_key="{{ cloud_public_storage_accountname }}" + cloud_storage_secret="{{ cloud_public_storage_secret }}" + cloud_storage_container="{{ cloud_storage_content_bucketname }}" + + cloudstorage { + metadata.replace_absolute_path={{ cloudstorage_replace_absolute_path | default('false') }} + relative_path_prefix={{ cloudstorage_relative_path_prefix_content }} + metadata.list={{ cloudstorage_metadata_list }} + read_base_path="{{ cloudstorage_base_path }}" + write_base_path={{ valid_cloudstorage_base_urls }} + } flink-conf: |+ jobmanager.memory.flink.size: {{ flink_job_names['asset-enrichment'].jobmanager_memory }} @@ -583,10 +431,18 @@ content-auto-creator: learning_service.basePath = "{{ kp_learning_service_base_url }}" } - cloud_storage_type="{{ cloud_store }}" - cloud_storage_key="{{ cloud_storage_key }}" - cloud_storage_secret="{{ cloud_storage_secret }}" - cloud_storage_container="{{ cloud_storage_container }}" + cloud_storage_type="{{ cloud_service_provider }}" + cloud_storage_key="{{ cloud_public_storage_accountname }}" + cloud_storage_secret="{{ cloud_public_storage_secret }}" + cloud_storage_container="{{ cloud_storage_content_bucketname }}" + + cloudstorage { + metadata.replace_absolute_path={{ cloudstorage_replace_absolute_path | default('false') }} + relative_path_prefix={{ cloudstorage_relative_path_prefix_content }} + metadata.list={{ cloudstorage_metadata_list }} + read_base_path="{{ cloudstorage_base_path }}" + write_base_path={{ valid_cloudstorage_base_urls }} + } content_auto_creator { actions=auto-create @@ -680,102 +536,6 @@ metrics-data-transformer: jobmanager.execution.failover-strategy: region taskmanager.memory.network.fraction: 0.1 -collection-cert-pre-processor: - collection-cert-pre-processor: |+ - include file("/data/flink/conf/base-config.conf") - kafka { - input.topic = {{ env_name }}.issue.certificate.request - output.topic = {{ env_name }}.generate.certificate.request - output.failed.topic = {{ env_name }}.issue.certificate.failed - groupId = {{ env_name }}-collection-cert-pre-processor-group - } - task { - restart-strategy.attempts = {{ restart_attempts }} # max 3 restart attempts - restart-strategy.delay = 240000 # in milli-seconds # on max restarts (3) within 4 min the job will fail. - parallelism = {{collection_cert_pre_processor_consumer_parallelism}} - consumer.parallelism = {{ collection_cert_pre_processor_consumer_parallelism }} - generate_certificate.parallelism = {{generate_certificate_parallelism}} - } - lms-cassandra { - keyspace = "{{ middleware_course_keyspace }}" - consumption.table = "{{ middleware_consumption_table }}" - user_enrolments.table = "{{ middleware_user_enrolments_table }}" - course_batch.table = "{{ middleware_course_batch_table }}" - assessment_aggregator.table = "{{ middleware_assessment_aggregator_table }}" - user_activity_agg.table = "{{ middleware_user_activity_agg_table }}" - } - cert_domain_url = "{{ cert_domain_url }}" - user_read_api = "/private/user/v1/read" - content_read_api = "/content/v3/read" - service { - content.basePath = "{{ content_service_base_url }}" - learner.basePath = "{{ learner_service_base_url }}" - } - enable.suppress.exception = {{ collection_certificate_pre_processor_enable_suppress_exception | lower }} - redis-meta { - {% if metadata2_redis_host is defined %} - host = {{ metadata2_redis_host }} - {% else %} - host = {{ redis_host }} - {% endif %} - port = 6379 - } - assessment.metrics.supported.contenttype = ["SelfAssess"] - - flink-conf: |+ - jobmanager.memory.flink.size: {{ flink_job_names['collection-cert-pre-processor'].jobmanager_memory }} - taskmanager.memory.flink.size: {{ flink_job_names['collection-cert-pre-processor'].taskmanager_memory }} - taskmanager.numberOfTaskSlots: {{ flink_job_names['collection-cert-pre-processor'].taskslots }} - parallelism.default: 1 - jobmanager.execution.failover-strategy: region - taskmanager.memory.network.fraction: 0.1 - -collection-certificate-generator: - collection-certificate-generator: |+ - include file("/data/flink/conf/base-config.conf") - kafka { - input.topic = {{ env_name }}.generate.certificate.request - output.audit.topic = {{ env_name }}.telemetry.raw - groupId = {{ env_name }}-certificate-generator-group - } - task { - restart-strategy.attempts = {{ restart_attempts }} # max 3 restart attempts - restart-strategy.delay = 240000 # in milli-seconds # on max restarts (3) within 4 min the job will fail. - consumer.parallelism = {{ collection_certificate_generator_consumer_parallelism }} - parallelism = {{ collection_certificate_generator_parallelism }} - } - lms-cassandra { - keyspace = "{{ middleware_course_keyspace }}" - user_enrolments.table = "{{ middleware_user_enrolments_table }}" - course_batch.table = "{{ middleware_course_batch_table }}" - sbkeyspace = "{{ registry_sunbird_keyspace }}" - certreg.table ="{{ cert_registry_table }}" - } - cert_domain_url = "{{ cert_domain_url }}" - cert_container_name = "{{ cert_container_name }}" - cert_cloud_storage_type = "{{ cert_cloud_storage_type }}" - cert_azure_storage_secret = "{{ cert_azure_storage_secret }}" - cert_azure_storage_key = "{{ cert_azure_storage_key }}" - service { - certreg.basePath = "{{ cert_reg_service_base_url }}" - learner.basePath = "{{ learner_service_base_url }}" - enc.basePath = "{{ enc_service_base_url }}" - rc.basePath = "{{ cert_rc_base_url }}" - rc.entity = "{{ cert_rc_entity }}" - } - enable.suppress.exception = {{ collection_certificate_generator_enable_suppress_exception | lower }} - enable.rc.certificate = {{ collection_certificate_generator_enable_rc_certificate | lower }} - task.rc.badcharlist = {{ collection_certificate_generator_rc_badcharlist }} - - - flink-conf: |+ - jobmanager.memory.flink.size: {{ flink_job_names['collection-certificate-generator'].jobmanager_memory }} - taskmanager.memory.flink.size: {{ flink_job_names['collection-certificate-generator'].taskmanager_memory }} - taskmanager.numberOfTaskSlots: {{ flink_job_names['collection-certificate-generator'].taskslots }} - parallelism.default: 1 - jobmanager.execution.failover-strategy: region - taskmanager.memory.network.fraction: 0.1 - mvc-indexer: mvc-indexer: |+ include "base-config.conf" @@ -963,14 +723,22 @@ content-publish: Asset: "Certificate Template" } - compositesearch.index.name = "compositesearch" + compositesearch.index.name = "{{ compositesearch_index_name }}" search.document.type = "cs" enableDIALContextUpdate = "Yes" - cloud_storage_type="{{ cloud_store }}" - cloud_storage_key="{{ cloud_storage_key }}" - cloud_storage_secret="{{ cloud_storage_secret }}" - cloud_storage_container="{{ cloud_storage_container }}" + cloud_storage_type="{{ cloud_service_provider }}" + cloud_storage_key="{{ cloud_public_storage_accountname }}" + cloud_storage_secret="{{ cloud_public_storage_secret }}" + cloud_storage_container="{{ cloud_storage_content_bucketname }}" + + cloudstorage { + metadata.replace_absolute_path={{ cloudstorage_replace_absolute_path | default('false') }} + relative_path_prefix={{ cloudstorage_relative_path_prefix_content }} + metadata.list={{ cloudstorage_metadata_list }} + read_base_path="{{ cloudstorage_base_path }}" + write_base_path={{ valid_cloudstorage_base_urls }} + } master.category.validation.enabled ="{{ master_category_validation_enabled }}" service { @@ -1008,10 +776,18 @@ qrcode-image-generator: margin=1 } - cloud_storage_type="{{ cloud_store }}" - cloud_storage_key="{{ cloud_storage_key }}" - cloud_storage_secret="{{ cloud_storage_secret }}" - cloud_storage_container="{{ cloud_storage_container }}" + cloudstorage { + metadata.replace_absolute_path={{ cloudstorage_replace_absolute_path | default('false') }} + relative_path_prefix={{ cloudstorage_relative_path_prefix_dial }} + metadata.list={{ cloudstorage_metadata_list }} + read_base_path="{{ cloudstorage_base_path }}" + write_base_path={{ valid_cloudstorage_base_urls }} + } + + cloud_storage_type="{{ cloud_service_provider }}" + cloud_storage_key="{{ cloud_public_storage_accountname }}" + cloud_storage_secret="{{ cloud_public_storage_secret }}" + cloud_storage_container="{{ cloud_storage_dial_bucketname | default('dial') }}" lms-cassandra { keyspace = "dialcodes" @@ -1217,12 +993,11 @@ live-node-publisher: compositesearch.index.name = "compositesearch" search.document.type = "cs" - enableDIALContextUpdate = "Yes" - cloud_storage_type="{{ cloud_store }}" - azure_storage_key="{{ sunbird_public_storage_account_name }}" - azure_storage_secret="{{ sunbird_public_storage_account_key }}" - azure_storage_container="{{ azure_public_container }}" + cloud_storage_type="{{ cloud_service_provider }}" + cloud_storage_key="{{ cloud_public_storage_accountname }}" + cloud_storage_secret="{{ cloud_public_storage_secret }}" + cloud_storage_container="{{ cloud_storage_content_bucketname }}" master.category.validation.enabled ="{{ master_category_validation_enabled }}" service { @@ -1230,6 +1005,14 @@ live-node-publisher: search.basePath = "{{ kp_search_service_base_url }}" } + cloudstorage { + metadata.replace_absolute_path={{ cloudstorage_replace_absolute_path | default('false') }} + relative_path_prefix={{ cloudstorage_relative_path_prefix_content }} + metadata.list={{ cloudstorage_metadata_list }} + read_base_path="{{ cloudstorage_base_path }}" + write_base_path={{ valid_cloudstorage_base_urls }} + } + flink-conf: |+ jobmanager.memory.flink.size: {{ flink_job_names['content-publish'].jobmanager_memory }} taskmanager.memory.flink.size: {{ flink_job_names['content-publish'].taskmanager_memory }} @@ -1319,12 +1102,12 @@ csp-migrator: csp-migrator: |+ include file("/data/flink/conf/base-config.conf") kafka { - input.topic = "{{ env_name }}.csp.migration.job.request" - groupId = "{{ env_name }}-csp-migrator-group" - failed.topic = "{{ env_name }}.csp.migration.job.request.failed" - live_video_stream.topic = "{{ env_name }}.live.video.stream.request" - live_content_node_republish.topic = "{{ env_name }}.republish.job.request" - live_question_node_republish.topic = "{{ env_name }}.assessment.republish.request" + input.topic = "{{ csp_migration_topic_name }}" + groupId = "{{ csp_migrator_group_name }}" + failed.topic = "{{ csp_migrator_failed_topic_name }}" + live_video_stream.topic = "{{ video_stream_topic_name }}" + live_content_node_republish.topic = "{{ content_republish_topic_name }}" + live_question_node_republish.topic = "{{ question_republish_topic_name }}" } task { timer.duration = {{ csp_migrator_timer_duration }} @@ -1341,7 +1124,7 @@ csp-migrator: } hierarchy { - keyspace = "{{ hierarchy_keyspace_name }}" + keyspace = "{{ content_hierarchy_keyspace_name }}" table = "content_hierarchy" } @@ -1351,16 +1134,22 @@ csp-migrator: assessment_table = "question_data" } - questionset.hierarchy.keyspace="{{ hierarchy_keyspace_name }}" + questionset.hierarchy.keyspace="{{ questionset_hierarchy_keyspace_name }}" questionset.hierarchy.table="questionset_hierarchy" key_value_strings_to_migrate = { - "https://community.ekstep.in/assets/public": "CLOUD_STORAGE_BASE_PATH", - "https://sunbirddev.blob.core.windows.net": "CLOUD_STORAGE_BASE_PATH", - "https://ekstep-public-dev.s3-ap-south-1.amazonaws.com": "CLOUD_STORAGE_BASE_PATH", - "https://ntpproductionall.blob.core.windows.net/ntp-content-production": "CLOUD_STORAGE_BASE_PATH", - "https://vdn.diksha.gov.in/assets/public": "CLOUD_STORAGE_BASE_PATH", - "https://sunbirdstagingpublic.blob.core.windows.net/sunbird-content-staging": "CLOUD_STORAGE_BASE_PATH" + "https://qa.ekstep.in/assets/public": "{{ cloudstorage_relative_path_prefix_content }}" + "https://dev.ekstep.in/assets/public": "{{ cloudstorage_relative_path_prefix_content }}" + "https://community.ekstep.in/assets/public": "{{ cloudstorage_relative_path_prefix_content }}" + "https://community.ekstep.in:443": "{{ cloudstorage_relative_path_prefix_content }}" + "https://ekstep-public-qa.s3-ap-south-1.amazonaws.com": "{{ cloudstorage_relative_path_prefix_content }}" + "https://ekstep-public-dev.s3-ap-south-1.amazonaws.com": "{{ cloudstorage_relative_path_prefix_content }}" + "https://ekstep-public-preprod.s3-ap-south-1.amazonaws.com": "{{ cloudstorage_relative_path_prefix_content }}" + "https://ekstep-public-prod.s3-ap-south-1.amazonaws.com": "{{ cloudstorage_relative_path_prefix_content }}" + "https://sunbirddev.blob.core.windows.net/sunbird-content-dev": "{{ cloudstorage_relative_path_prefix_content }}" + "https://sunbirdstagingpublic.blob.core.windows.net/sunbird-content-staging": "{{ cloudstorage_relative_path_prefix_content }}" + "https://preprodall.blob.core.windows.net/ntp-content-preprod": "{{ cloudstorage_relative_path_prefix_content }}" + "https://ntpproductionall.blob.core.windows.net/ntp-content-production": "{{ cloudstorage_relative_path_prefix_content }}" } neo4j_fields_to_migrate = { @@ -1382,12 +1171,24 @@ csp-migrator: "assessmentitem": ["body", "editorState", "answer", "solutions", "instructions", "media"] } + cloudstorage { + metadata.replace_absolute_path=false + relative_path_prefix={{ cloudstorage_relative_path_prefix_content }} + metadata.list={{ cloudstorage_metadata_list }} + read_base_path="{{ cloudstorage_base_path }}" + write_base_path={{ valid_cloudstorage_base_urls }} + } + migrationVersion = 1 video_stream_regeneration_enable = false live_node_republish_enable = true copy_missing_files_to_cloud = false download_path = /tmp + cloud_storage_type="{{ cloud_service_provider }}" + cloud_storage_key="{{ cloud_public_storage_accountname }}" + cloud_storage_secret="{{ cloud_public_storage_secret }}" + cloud_storage_container="{{ cloud_storage_content_bucketname }}" flink-conf: |+ jobmanager.memory.flink.size: {{ flink_job_names['video-stream-generator'].jobmanager_memory }} @@ -1395,4 +1196,46 @@ csp-migrator: taskmanager.numberOfTaskSlots: {{ flink_job_names['video-stream-generator'].taskslots }} parallelism.default: 1 jobmanager.execution.failover-strategy: region + taskmanager.memory.network.fraction: 0.1 + +cassandra-data-migration: + cassandra-data-migration: |+ + include file("/data/flink/conf/base-config.conf") + kafka { + input.topic = "{{ env_name }}.cassandra.data.migration.request" + failed.topic = "{{ env_name }}.cassandra.data.migration.job.request.failed" + groupId = "{{ env_name }}-cassandra-data-migration-group" + } + + task { + consumer.parallelism = 1 + parallelism = 1 + } + + migrate = { + keyspace = "dialcodes" + table = "dialcode_batch" + primary_key_column = "processid" + primary_key_column_type = "UUID" + column_to_migrate = "url" + column_to_migrate_type = "string" + key_value_strings_to_migrate = { + "https://sunbirdstagingpublic.blob.core.windows.net/dial": "{{ cloudstorage_relative_path_prefix_dial }}" + } + } + + cloudstorage { + metadata.replace_absolute_path=false + relative_path_prefix={{ cloudstorage_relative_path_prefix_content }} + metadata.list={{ cloudstorage_metadata_list }} + read_base_path="{{ cloudstorage_base_path }}" + write_base_path={{ valid_cloudstorage_base_urls }} + } + + flink-conf: |+ + jobmanager.memory.flink.size: {{ flink_job_names['cassandra-data-migration'].jobmanager_memory }} + taskmanager.memory.flink.size: {{ flink_job_names['cassandra-data-migration'].taskmanager_memory }} + taskmanager.numberOfTaskSlots: {{ flink_job_names['cassandra-data-migration'].taskslots }} + parallelism.default: 1 + jobmanager.execution.failover-strategy: region taskmanager.memory.network.fraction: 0.1 \ No newline at end of file diff --git a/pom.xml b/pom.xml index 50ac54bcce..693f5fc003 100644 --- a/pom.xml +++ b/pom.xml @@ -29,7 +29,7 @@ platform-core platform-modules platform-tools/spikes/sync-tool - platform-tools/cassandra-extension + platform-tools/spikes/content-tool From 8664e2fbc2d3190ac02d12fb736580c38f6c597e Mon Sep 17 00:00:00 2001 From: anilgupta Date: Thu, 8 Dec 2022 11:00:46 +0530 Subject: [PATCH 103/222] Issue #KN-439 merge: merging from release-5.2.0-knowlg to release-5.2.0 --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 693f5fc003..50ac54bcce 100644 --- a/pom.xml +++ b/pom.xml @@ -29,7 +29,7 @@ platform-core platform-modules platform-tools/spikes/sync-tool - + platform-tools/cassandra-extension platform-tools/spikes/content-tool From 64f2ec8b0a0af33dcdc32bb2496ad3a731b45bad Mon Sep 17 00:00:00 2001 From: anilgupta Date: Thu, 8 Dec 2022 11:36:41 +0530 Subject: [PATCH 104/222] Issue #KN-439 merge: merging from release-5.2.0-knowlg to release-5.2.0 --- kubernetes/helm_charts/datapipeline_jobs/values.j2 | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/kubernetes/helm_charts/datapipeline_jobs/values.j2 b/kubernetes/helm_charts/datapipeline_jobs/values.j2 index f17dc6f2bf..50a268a4e1 100644 --- a/kubernetes/helm_charts/datapipeline_jobs/values.j2 +++ b/kubernetes/helm_charts/datapipeline_jobs/values.j2 @@ -383,10 +383,10 @@ auto-creator-v2: service { content.basePath = "{{ kp_content_service_base_url }}" } - cloud_storage_type="{{ cloud_store }}" - cloud_storage_key="{{ cloud_storage_key }}" - cloud_storage_secret="{{ cloud_storage_secret }}" - cloud_storage_container="{{ cloud_storage_container }}" + cloud_storage_type="{{ cloud_service_provider }}" + cloud_storage_key="{{ cloud_public_storage_accountname }}" + cloud_storage_secret="{{ cloud_public_storage_secret }}" + cloud_storage_container="{{ cloud_storage_content_bucketname }}" source { baseUrl="{{ source_base_url }}" From 98bba2145ea7626933522c2ade5de388c0869a7b Mon Sep 17 00:00:00 2001 From: Jayaprakash8887 Date: Fri, 9 Dec 2022 15:18:25 +0530 Subject: [PATCH 105/222] Issue #KN-427 debug: Sync Tool data --- .../main/java/org/sunbird/learning/util/ControllerUtil.java | 2 ++ .../sunbird/sync/tool/mgr/CSPMigrationMessageGenerator.java | 4 ++++ 2 files changed, 6 insertions(+) diff --git a/platform-modules/actors/src/main/java/org/sunbird/learning/util/ControllerUtil.java b/platform-modules/actors/src/main/java/org/sunbird/learning/util/ControllerUtil.java index b0b77942ff..43130db61b 100644 --- a/platform-modules/actors/src/main/java/org/sunbird/learning/util/ControllerUtil.java +++ b/platform-modules/actors/src/main/java/org/sunbird/learning/util/ControllerUtil.java @@ -426,10 +426,12 @@ public List getNodes(String graphId, String objectType, List mimeT sc.setStartPosition(startPosition); if(!filters.isEmpty() && filters.size()>0) sc.addMetadata(MetadataCriterion.create(filters)); + System.out.println("ControllerUtil:: getNodes:: sc:: " + sc.toString()); Request req = getRequest(graphId, GraphEngineManagers.SEARCH_MANAGER, "searchNodes", GraphDACParams.search_criteria.name(), sc); req.put(GraphDACParams.get_tags.name(), true); Response listRes = getResponse(req); + System.out.println("ControllerUtil:: getNodes:: listRes:: " + listRes); if (checkError(listRes)) return null; else { diff --git a/platform-tools/spikes/sync-tool/src/main/java/org/sunbird/sync/tool/mgr/CSPMigrationMessageGenerator.java b/platform-tools/spikes/sync-tool/src/main/java/org/sunbird/sync/tool/mgr/CSPMigrationMessageGenerator.java index 9579728a60..504f83ca2d 100644 --- a/platform-tools/spikes/sync-tool/src/main/java/org/sunbird/sync/tool/mgr/CSPMigrationMessageGenerator.java +++ b/platform-tools/spikes/sync-tool/src/main/java/org/sunbird/sync/tool/mgr/CSPMigrationMessageGenerator.java @@ -82,6 +82,9 @@ public void generateMgrMsg(String graphId, String[] objectTypes, String[] mimeTy stopLimit = batchSize; } else stopLimit = limit; } else stopLimit = total; + + System.out.println("CSPMigrationMessageGenerator:: generateMgrMsg:: stopLimit: " + stopLimit + " || total: " + total); + boolean found = true; while (found && start < stopLimit) { List nodes = null; @@ -102,6 +105,7 @@ public void generateMgrMsg(String graphId, String[] objectTypes, String[] mimeTy Thread.sleep(delay); } } else { + System.out.println("CSPMigrationMessageGenerator:: generateMgrMsg:: Breaking Event Generation Loop!"); found = false; break; } From 71b6f3757d521de5b8b027be34b4a43e998dea4b Mon Sep 17 00:00:00 2001 From: Jayaprakash8887 Date: Fri, 9 Dec 2022 15:59:16 +0530 Subject: [PATCH 106/222] Issue #KN-427 debug: Sync Tool data --- .../main/java/org/sunbird/learning/util/ControllerUtil.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/platform-modules/actors/src/main/java/org/sunbird/learning/util/ControllerUtil.java b/platform-modules/actors/src/main/java/org/sunbird/learning/util/ControllerUtil.java index 43130db61b..30c88f2e0d 100644 --- a/platform-modules/actors/src/main/java/org/sunbird/learning/util/ControllerUtil.java +++ b/platform-modules/actors/src/main/java/org/sunbird/learning/util/ControllerUtil.java @@ -426,12 +426,12 @@ public List getNodes(String graphId, String objectType, List mimeT sc.setStartPosition(startPosition); if(!filters.isEmpty() && filters.size()>0) sc.addMetadata(MetadataCriterion.create(filters)); - System.out.println("ControllerUtil:: getNodes:: sc:: " + sc.toString()); + System.out.println("ControllerUtil:: getNodes:: sc:: " + sc); Request req = getRequest(graphId, GraphEngineManagers.SEARCH_MANAGER, "searchNodes", GraphDACParams.search_criteria.name(), sc); req.put(GraphDACParams.get_tags.name(), true); Response listRes = getResponse(req); - System.out.println("ControllerUtil:: getNodes:: listRes:: " + listRes); + System.out.println("ControllerUtil:: getNodes:: listRes.params:: " + listRes.getParams()); if (checkError(listRes)) return null; else { From 1fd7586bdb4ab358e7d66e31332e9939b14c8f0c Mon Sep 17 00:00:00 2001 From: Jayaprakash8887 Date: Mon, 12 Dec 2022 17:06:05 +0530 Subject: [PATCH 107/222] Issue #KN-427 fix: Config update --- .../roles/flink-jobs-deploy/defaults/main.yml | 2 +- .../helm_charts/datapipeline_jobs/values.j2 | 25 ++++++++++--------- 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/kubernetes/ansible/roles/flink-jobs-deploy/defaults/main.yml b/kubernetes/ansible/roles/flink-jobs-deploy/defaults/main.yml index aa07726095..ac1397b6a9 100644 --- a/kubernetes/ansible/roles/flink-jobs-deploy/defaults/main.yml +++ b/kubernetes/ansible/roles/flink-jobs-deploy/defaults/main.yml @@ -397,6 +397,6 @@ video_stream_topic_name: "{{ env_name }}.live.video.stream.request" content_hierarchy_keyspace_name: "{{ hierarchy_keyspace_name }}" questionset_hierarchy_keyspace_name: "{{ hierarchy_keyspace_name }}" -cloudstorage_relative_path_prefix_content: "CLOUD_STORAGE_BASE_PATH" +cloudstorage_relative_path_prefix_content: "CONTENT_STORAGE_BASE_PATH" cloudstorage_relative_path_prefix_dial: "DIAL_STORAGE_BASE_PATH" cloudstorage_metadata_list: '["appIcon", "artifactUrl", "posterImage", "previewUrl", "thumbnail", "assetsMap", "certTemplate", "itemSetPreviewUrl", "grayScaleAppIcon", "sourceURL", "variants", "downloadUrl", "streamingUrl", "toc_url", "data", "question", "solutions", "editorState", "media", "pdfUrl"]' \ No newline at end of file diff --git a/kubernetes/helm_charts/datapipeline_jobs/values.j2 b/kubernetes/helm_charts/datapipeline_jobs/values.j2 index 50a268a4e1..462e95f0fa 100644 --- a/kubernetes/helm_charts/datapipeline_jobs/values.j2 +++ b/kubernetes/helm_charts/datapipeline_jobs/values.j2 @@ -1138,18 +1138,19 @@ csp-migrator: questionset.hierarchy.table="questionset_hierarchy" key_value_strings_to_migrate = { - "https://qa.ekstep.in/assets/public": "{{ cloudstorage_relative_path_prefix_content }}" - "https://dev.ekstep.in/assets/public": "{{ cloudstorage_relative_path_prefix_content }}" - "https://community.ekstep.in/assets/public": "{{ cloudstorage_relative_path_prefix_content }}" - "https://community.ekstep.in:443": "{{ cloudstorage_relative_path_prefix_content }}" - "https://ekstep-public-qa.s3-ap-south-1.amazonaws.com": "{{ cloudstorage_relative_path_prefix_content }}" - "https://ekstep-public-dev.s3-ap-south-1.amazonaws.com": "{{ cloudstorage_relative_path_prefix_content }}" - "https://ekstep-public-preprod.s3-ap-south-1.amazonaws.com": "{{ cloudstorage_relative_path_prefix_content }}" - "https://ekstep-public-prod.s3-ap-south-1.amazonaws.com": "{{ cloudstorage_relative_path_prefix_content }}" - "https://sunbirddev.blob.core.windows.net/sunbird-content-dev": "{{ cloudstorage_relative_path_prefix_content }}" - "https://sunbirdstagingpublic.blob.core.windows.net/sunbird-content-staging": "{{ cloudstorage_relative_path_prefix_content }}" - "https://preprodall.blob.core.windows.net/ntp-content-preprod": "{{ cloudstorage_relative_path_prefix_content }}" - "https://ntpproductionall.blob.core.windows.net/ntp-content-production": "{{ cloudstorage_relative_path_prefix_content }}" + "https://qa.ekstep.in/assets/public": "{{ cloudstorage_relative_path_prefix_content }}", + "https://dev.ekstep.in/assets/public": "{{ cloudstorage_relative_path_prefix_content }}", + "https://community.ekstep.in/assets/public": "{{ cloudstorage_relative_path_prefix_content }}", + "https://community.ekstep.in:443": "{{ cloudstorage_relative_path_prefix_content }}", + "https://ekstep-public-qa.s3-ap-south-1.amazonaws.com": "{{ cloudstorage_relative_path_prefix_content }}", + "https://ekstep-public-dev.s3-ap-south-1.amazonaws.com": "{{ cloudstorage_relative_path_prefix_content }}", + "https://ekstep-public-preprod.s3-ap-south-1.amazonaws.com": "{{ cloudstorage_relative_path_prefix_content }}", + "https://ekstep-public-prod.s3-ap-south-1.amazonaws.com": "{{ cloudstorage_relative_path_prefix_content }}", + "https://sunbirddev.blob.core.windows.net/sunbird-content-dev": "{{ cloudstorage_relative_path_prefix_content }}", + "https://sunbirdstagingpublic.blob.core.windows.net/sunbird-content-staging": "{{ cloudstorage_relative_path_prefix_content }}", + "https://preprodall.blob.core.windows.net/ntp-content-preprod": "{{ cloudstorage_relative_path_prefix_content }}", + "https://ntpproductionall.blob.core.windows.net/ntp-content-production": "{{ cloudstorage_relative_path_prefix_content }}", + "CLOUD_STORAGE_BASE_PATH": "{{ cloudstorage_relative_path_prefix_content }}" } neo4j_fields_to_migrate = { From 48e78a4857712fc1da526b15bc4c7ecc3ec8449d Mon Sep 17 00:00:00 2001 From: Jayaprakash8887 Date: Tue, 13 Dec 2022 11:47:09 +0530 Subject: [PATCH 108/222] Issue #KN-427 fix: Config update --- .../helm_charts/datapipeline_jobs/values.j2 | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/kubernetes/helm_charts/datapipeline_jobs/values.j2 b/kubernetes/helm_charts/datapipeline_jobs/values.j2 index 462e95f0fa..5dfae09836 100644 --- a/kubernetes/helm_charts/datapipeline_jobs/values.j2 +++ b/kubernetes/helm_charts/datapipeline_jobs/values.j2 @@ -834,9 +834,9 @@ dialcode-context-updater: es_sync_wait_time = 20000 flink-conf: |+ - jobmanager.memory.flink.size: {{ flink_job_names['qrcode-image-generator'].jobmanager_memory }} - taskmanager.memory.flink.size: {{ flink_job_names['qrcode-image-generator'].taskmanager_memory }} - taskmanager.numberOfTaskSlots: {{ flink_job_names['qrcode-image-generator'].taskslots }} + jobmanager.memory.flink.size: {{ flink_job_names['dialcode-context-updater'].jobmanager_memory }} + taskmanager.memory.flink.size: {{ flink_job_names['dialcode-context-updater'].taskmanager_memory }} + taskmanager.numberOfTaskSlots: {{ flink_job_names['dialcode-context-updater'].taskslots }} parallelism.default: 1 jobmanager.execution.failover-strategy: region taskmanager.memory.network.fraction: 0.1 @@ -1014,9 +1014,9 @@ live-node-publisher: } flink-conf: |+ - jobmanager.memory.flink.size: {{ flink_job_names['content-publish'].jobmanager_memory }} - taskmanager.memory.flink.size: {{ flink_job_names['content-publish'].taskmanager_memory }} - taskmanager.numberOfTaskSlots: {{ flink_job_names['content-publish'].taskslots }} + jobmanager.memory.flink.size: {{ flink_job_names['live-node-publisher'].jobmanager_memory }} + taskmanager.memory.flink.size: {{ flink_job_names['live-node-publisher'].taskmanager_memory }} + taskmanager.numberOfTaskSlots: {{ flink_job_names['live-node-publisher'].taskslots }} parallelism.default: 1 jobmanager.execution.failover-strategy: region taskmanager.memory.network.fraction: 0.1 @@ -1091,9 +1091,9 @@ live-video-stream-generator: flink-conf: |+ - jobmanager.memory.flink.size: {{ flink_job_names['video-stream-generator'].jobmanager_memory }} - taskmanager.memory.flink.size: {{ flink_job_names['video-stream-generator'].taskmanager_memory }} - taskmanager.numberOfTaskSlots: {{ flink_job_names['video-stream-generator'].taskslots }} + jobmanager.memory.flink.size: {{ flink_job_names['live-video-stream-generator'].jobmanager_memory }} + taskmanager.memory.flink.size: {{ flink_job_names['live-video-stream-generator'].taskmanager_memory }} + taskmanager.numberOfTaskSlots: {{ flink_job_names['live-video-stream-generator'].taskslots }} parallelism.default: 1 jobmanager.execution.failover-strategy: region taskmanager.memory.network.fraction: 0.1 @@ -1192,9 +1192,9 @@ csp-migrator: cloud_storage_container="{{ cloud_storage_content_bucketname }}" flink-conf: |+ - jobmanager.memory.flink.size: {{ flink_job_names['video-stream-generator'].jobmanager_memory }} - taskmanager.memory.flink.size: {{ flink_job_names['video-stream-generator'].taskmanager_memory }} - taskmanager.numberOfTaskSlots: {{ flink_job_names['video-stream-generator'].taskslots }} + jobmanager.memory.flink.size: {{ flink_job_names['csp-migrator'].jobmanager_memory }} + taskmanager.memory.flink.size: {{ flink_job_names['csp-migrator'].taskmanager_memory }} + taskmanager.numberOfTaskSlots: {{ flink_job_names['csp-migrator'].taskslots }} parallelism.default: 1 jobmanager.execution.failover-strategy: region taskmanager.memory.network.fraction: 0.1 From 9686cc94d597bf6699815a58b91c6c0e9f0f7493 Mon Sep 17 00:00:00 2001 From: Jayaprakash8887 Date: Tue, 13 Dec 2022 11:50:59 +0530 Subject: [PATCH 109/222] Issue #KN-427 fix: Config update --- .../src/main/java/org/sunbird/learning/util/ControllerUtil.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/platform-modules/actors/src/main/java/org/sunbird/learning/util/ControllerUtil.java b/platform-modules/actors/src/main/java/org/sunbird/learning/util/ControllerUtil.java index 30c88f2e0d..b0b77942ff 100644 --- a/platform-modules/actors/src/main/java/org/sunbird/learning/util/ControllerUtil.java +++ b/platform-modules/actors/src/main/java/org/sunbird/learning/util/ControllerUtil.java @@ -426,12 +426,10 @@ public List getNodes(String graphId, String objectType, List mimeT sc.setStartPosition(startPosition); if(!filters.isEmpty() && filters.size()>0) sc.addMetadata(MetadataCriterion.create(filters)); - System.out.println("ControllerUtil:: getNodes:: sc:: " + sc); Request req = getRequest(graphId, GraphEngineManagers.SEARCH_MANAGER, "searchNodes", GraphDACParams.search_criteria.name(), sc); req.put(GraphDACParams.get_tags.name(), true); Response listRes = getResponse(req); - System.out.println("ControllerUtil:: getNodes:: listRes.params:: " + listRes.getParams()); if (checkError(listRes)) return null; else { From 2a404530325b953b1d957ecb70514e2b9d1fcb7e Mon Sep 17 00:00:00 2001 From: Jayaprakash8887 Date: Tue, 13 Dec 2022 16:13:11 +0530 Subject: [PATCH 110/222] Issue #KN-427 fix: Config update --- kubernetes/helm_charts/datapipeline_jobs/values.j2 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kubernetes/helm_charts/datapipeline_jobs/values.j2 b/kubernetes/helm_charts/datapipeline_jobs/values.j2 index 5dfae09836..184f268937 100644 --- a/kubernetes/helm_charts/datapipeline_jobs/values.j2 +++ b/kubernetes/helm_charts/datapipeline_jobs/values.j2 @@ -1154,7 +1154,7 @@ csp-migrator: } neo4j_fields_to_migrate = { - "asset": ["artifactUrl", "thumbnail", "downloadUrl"], + "asset": ["artifactUrl", "thumbnail", "downloadUrl","variants"], "content": ["appIcon", "artifactUrl", "posterImage", "previewUrl", "thumbnail", "assetsMap", "certTemplate", "itemSetPreviewUrl", "grayScaleAppIcon", "sourceURL", "variants", "downloadUrl","streamingUrl"], "contentimage": ["appIcon", "artifactUrl", "posterImage", "previewUrl", "thumbnail", "assetsMap", "certTemplate", "itemSetPreviewUrl", "grayScaleAppIcon", "sourceURL", "variants", "downloadUrl","streamingUrl"], "collection": ["appIcon", "artifactUrl", "posterImage", "previewUrl", "thumbnail", "toc_url", "grayScaleAppIcon", "variants", "downloadUrl"], From 9eacf3520fa71aa4752665daed485fb1ca308290 Mon Sep 17 00:00:00 2001 From: G33tha Date: Tue, 13 Dec 2022 17:28:59 +0530 Subject: [PATCH 111/222] added asessment republish and postpublish kafka topic name --- ansible/roles/setup-kafka/defaults/main.yml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/ansible/roles/setup-kafka/defaults/main.yml b/ansible/roles/setup-kafka/defaults/main.yml index 91e15dc276..8aba456fe3 100644 --- a/ansible/roles/setup-kafka/defaults/main.yml +++ b/ansible/roles/setup-kafka/defaults/main.yml @@ -150,6 +150,13 @@ processing_kafka_topics: - name: cassandra.data.migration.job.request.failed num_of_partitions: 1 replication_factor: 1 + - name: assessment.republish.request + num_of_partitions: 1 + replication_factor: 1 + - name: assessment.postpublish.request + num_of_partitions: 1 + replication_factor: 1 + processing_kafka_overriden_topics: - name: telemetry.raw @@ -294,3 +301,10 @@ processing_kafka_overriden_topics: - name: cassandra.data.migration.job.request.failed retention_time: 1209600000 replication_factor: 1 + - name: assessment.republish.request + retention_time: 1209600000 + replication_factor: 1 + - name: assessment.postpublish.request + retention_time: 1209600000 + replication_factor: 1 + From 25f8eae02ecba2b265ce067edc0ceb34b4a084e9 Mon Sep 17 00:00:00 2001 From: Jayaprakash8887 Date: Tue, 13 Dec 2022 17:58:13 +0530 Subject: [PATCH 112/222] Issue #KN-427 fix: Config update --- kubernetes/helm_charts/datapipeline_jobs/values.j2 | 1 + 1 file changed, 1 insertion(+) diff --git a/kubernetes/helm_charts/datapipeline_jobs/values.j2 b/kubernetes/helm_charts/datapipeline_jobs/values.j2 index 184f268937..48c7d0e432 100644 --- a/kubernetes/helm_charts/datapipeline_jobs/values.j2 +++ b/kubernetes/helm_charts/datapipeline_jobs/values.j2 @@ -855,6 +855,7 @@ live-node-publisher: consumer.parallelism = 1 parallelism = 1 router.parallelism = 1 + checkpointing.interval = 300000 } redis { host={{redis_host}} From bfd2c58ed0889f07342d89ea21197fda2c033235 Mon Sep 17 00:00:00 2001 From: Jayaprakash8887 Date: Wed, 14 Dec 2022 11:31:34 +0530 Subject: [PATCH 113/222] Issue #KN-427 fix: Config update --- kubernetes/helm_charts/datapipeline_jobs/values.j2 | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/kubernetes/helm_charts/datapipeline_jobs/values.j2 b/kubernetes/helm_charts/datapipeline_jobs/values.j2 index 48c7d0e432..50360a468c 100644 --- a/kubernetes/helm_charts/datapipeline_jobs/values.j2 +++ b/kubernetes/helm_charts/datapipeline_jobs/values.j2 @@ -855,7 +855,8 @@ live-node-publisher: consumer.parallelism = 1 parallelism = 1 router.parallelism = 1 - checkpointing.interval = 300000 + window.time = 60 + checkpointing.timeout = 4200000 } redis { host={{redis_host}} From 9e502f811c9c01fcba112221410da782dbf4cb82 Mon Sep 17 00:00:00 2001 From: G33tha Date: Wed, 14 Dec 2022 16:03:43 +0530 Subject: [PATCH 114/222] updated search-indexer taskslots --- kubernetes/ansible/roles/flink-jobs-deploy/defaults/main.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/kubernetes/ansible/roles/flink-jobs-deploy/defaults/main.yml b/kubernetes/ansible/roles/flink-jobs-deploy/defaults/main.yml index ac1397b6a9..bc77d7c565 100644 --- a/kubernetes/ansible/roles/flink-jobs-deploy/defaults/main.yml +++ b/kubernetes/ansible/roles/flink-jobs-deploy/defaults/main.yml @@ -172,7 +172,7 @@ flink_job_names: replica: 1 jobmanager_memory: 1024m taskmanager_memory: 1024m - taskslots: 1 + taskslots: {{search-indexer-taskslots | default('1') }} cpu_requests: 0.3 enrolment-reconciliation: job_class_name: 'org.sunbird.job.recounciliation.task.EnrolmentReconciliationStreamTask' @@ -399,4 +399,4 @@ questionset_hierarchy_keyspace_name: "{{ hierarchy_keyspace_name }}" cloudstorage_relative_path_prefix_content: "CONTENT_STORAGE_BASE_PATH" cloudstorage_relative_path_prefix_dial: "DIAL_STORAGE_BASE_PATH" -cloudstorage_metadata_list: '["appIcon", "artifactUrl", "posterImage", "previewUrl", "thumbnail", "assetsMap", "certTemplate", "itemSetPreviewUrl", "grayScaleAppIcon", "sourceURL", "variants", "downloadUrl", "streamingUrl", "toc_url", "data", "question", "solutions", "editorState", "media", "pdfUrl"]' \ No newline at end of file +cloudstorage_metadata_list: '["appIcon", "artifactUrl", "posterImage", "previewUrl", "thumbnail", "assetsMap", "certTemplate", "itemSetPreviewUrl", "grayScaleAppIcon", "sourceURL", "variants", "downloadUrl", "streamingUrl", "toc_url", "data", "question", "solutions", "editorState", "media", "pdfUrl"]' From 6319df5e62e3aef6cf6317cc8ad0be2ffd704a09 Mon Sep 17 00:00:00 2001 From: anilgupta Date: Wed, 14 Dec 2022 16:09:36 +0530 Subject: [PATCH 115/222] Issue #KN-439 chore: Added the quote --- kubernetes/ansible/roles/flink-jobs-deploy/defaults/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kubernetes/ansible/roles/flink-jobs-deploy/defaults/main.yml b/kubernetes/ansible/roles/flink-jobs-deploy/defaults/main.yml index bc77d7c565..62eb2c06a6 100644 --- a/kubernetes/ansible/roles/flink-jobs-deploy/defaults/main.yml +++ b/kubernetes/ansible/roles/flink-jobs-deploy/defaults/main.yml @@ -172,7 +172,7 @@ flink_job_names: replica: 1 jobmanager_memory: 1024m taskmanager_memory: 1024m - taskslots: {{search-indexer-taskslots | default('1') }} + taskslots: "{{search-indexer-taskslots | default('1') }}" cpu_requests: 0.3 enrolment-reconciliation: job_class_name: 'org.sunbird.job.recounciliation.task.EnrolmentReconciliationStreamTask' From 453015348ca19bac293623549230b2590a0c5484 Mon Sep 17 00:00:00 2001 From: anilgupta Date: Wed, 14 Dec 2022 16:10:24 +0530 Subject: [PATCH 116/222] Issue #KN-439 chore: Added the quote --- kubernetes/ansible/roles/flink-jobs-deploy/defaults/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kubernetes/ansible/roles/flink-jobs-deploy/defaults/main.yml b/kubernetes/ansible/roles/flink-jobs-deploy/defaults/main.yml index 62eb2c06a6..c6d2c0fe9f 100644 --- a/kubernetes/ansible/roles/flink-jobs-deploy/defaults/main.yml +++ b/kubernetes/ansible/roles/flink-jobs-deploy/defaults/main.yml @@ -172,7 +172,7 @@ flink_job_names: replica: 1 jobmanager_memory: 1024m taskmanager_memory: 1024m - taskslots: "{{search-indexer-taskslots | default('1') }}" + taskslots: "{{search_indexer_taskslots | default('1') }}" cpu_requests: 0.3 enrolment-reconciliation: job_class_name: 'org.sunbird.job.recounciliation.task.EnrolmentReconciliationStreamTask' From 9066a740833d747c8ba9890cba8455dba5b580cd Mon Sep 17 00:00:00 2001 From: Jayaprakash8887 Date: Wed, 14 Dec 2022 16:39:44 +0530 Subject: [PATCH 117/222] Issue #KN-427 fix: Config update --- kubernetes/helm_charts/datapipeline_jobs/values.j2 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kubernetes/helm_charts/datapipeline_jobs/values.j2 b/kubernetes/helm_charts/datapipeline_jobs/values.j2 index 50360a468c..7b71e12942 100644 --- a/kubernetes/helm_charts/datapipeline_jobs/values.j2 +++ b/kubernetes/helm_charts/datapipeline_jobs/values.j2 @@ -1185,7 +1185,7 @@ csp-migrator: migrationVersion = 1 video_stream_regeneration_enable = false live_node_republish_enable = true - copy_missing_files_to_cloud = false + copy_missing_files_to_cloud = true download_path = /tmp cloud_storage_type="{{ cloud_service_provider }}" From e3aa64942808026b3d094941d40a2b86cea4d794 Mon Sep 17 00:00:00 2001 From: Jayaprakash8887 Date: Wed, 14 Dec 2022 18:23:31 +0530 Subject: [PATCH 118/222] Issue #KN-427 fix: Config update --- kubernetes/ansible/roles/flink-jobs-deploy/defaults/main.yml | 2 +- kubernetes/helm_charts/datapipeline_jobs/values.j2 | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/kubernetes/ansible/roles/flink-jobs-deploy/defaults/main.yml b/kubernetes/ansible/roles/flink-jobs-deploy/defaults/main.yml index c6d2c0fe9f..eec1d24cca 100644 --- a/kubernetes/ansible/roles/flink-jobs-deploy/defaults/main.yml +++ b/kubernetes/ansible/roles/flink-jobs-deploy/defaults/main.yml @@ -277,7 +277,7 @@ flink_job_names: replica: 1 jobmanager_memory: 1024m taskmanager_memory: 1024m - taskslots: 1 + taskslots: "{{search_indexer_taskslots | default('1') }}" cpu_requests: 0.3 live-video-stream-generator: job_class_name: 'org.sunbird.job.livevideostream.task.LiveVideoStreamGeneratorStreamTask' diff --git a/kubernetes/helm_charts/datapipeline_jobs/values.j2 b/kubernetes/helm_charts/datapipeline_jobs/values.j2 index 7b71e12942..45c32245ec 100644 --- a/kubernetes/helm_charts/datapipeline_jobs/values.j2 +++ b/kubernetes/helm_charts/datapipeline_jobs/values.j2 @@ -852,9 +852,9 @@ live-node-publisher: groupId = {{ env_name }}-content-republish-group } task { - consumer.parallelism = 1 parallelism = 1 - router.parallelism = 1 + consumer.parallelism = {{ search_indexer_consumer_parallelism }} + router.parallelism = {{ transaction_event_router_parallelism }} window.time = 60 checkpointing.timeout = 4200000 } From 2c8849dee6374d0719420f394567163e5fd18dca Mon Sep 17 00:00:00 2001 From: Jayaprakash8887 Date: Wed, 14 Dec 2022 19:14:14 +0530 Subject: [PATCH 119/222] Issue #KN-427 fix: Config update --- kubernetes/helm_charts/datapipeline_jobs/values.j2 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kubernetes/helm_charts/datapipeline_jobs/values.j2 b/kubernetes/helm_charts/datapipeline_jobs/values.j2 index 45c32245ec..e7c5b35afe 100644 --- a/kubernetes/helm_charts/datapipeline_jobs/values.j2 +++ b/kubernetes/helm_charts/datapipeline_jobs/values.j2 @@ -856,7 +856,7 @@ live-node-publisher: consumer.parallelism = {{ search_indexer_consumer_parallelism }} router.parallelism = {{ transaction_event_router_parallelism }} window.time = 60 - checkpointing.timeout = 4200000 + checkpointing.timeout = 300000 } redis { host={{redis_host}} From d6105a34947fc3b32928f24a334fa23478bc95fc Mon Sep 17 00:00:00 2001 From: Jayaprakash8887 Date: Wed, 14 Dec 2022 22:05:42 +0530 Subject: [PATCH 120/222] Issue #KN-427 fix: Config update --- kubernetes/helm_charts/datapipeline_jobs/values.j2 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kubernetes/helm_charts/datapipeline_jobs/values.j2 b/kubernetes/helm_charts/datapipeline_jobs/values.j2 index e7c5b35afe..45c32245ec 100644 --- a/kubernetes/helm_charts/datapipeline_jobs/values.j2 +++ b/kubernetes/helm_charts/datapipeline_jobs/values.j2 @@ -856,7 +856,7 @@ live-node-publisher: consumer.parallelism = {{ search_indexer_consumer_parallelism }} router.parallelism = {{ transaction_event_router_parallelism }} window.time = 60 - checkpointing.timeout = 300000 + checkpointing.timeout = 4200000 } redis { host={{redis_host}} From a1069de30a2c05db87391309e215f6bb071f4d76 Mon Sep 17 00:00:00 2001 From: Jayaprakash8887 Date: Wed, 14 Dec 2022 22:26:22 +0530 Subject: [PATCH 121/222] Issue #KN-427 fix: Config update --- kubernetes/helm_charts/datapipeline_jobs/values.j2 | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/kubernetes/helm_charts/datapipeline_jobs/values.j2 b/kubernetes/helm_charts/datapipeline_jobs/values.j2 index 45c32245ec..090402a46e 100644 --- a/kubernetes/helm_charts/datapipeline_jobs/values.j2 +++ b/kubernetes/helm_charts/datapipeline_jobs/values.j2 @@ -1016,9 +1016,9 @@ live-node-publisher: } flink-conf: |+ - jobmanager.memory.flink.size: {{ flink_job_names['live-node-publisher'].jobmanager_memory }} - taskmanager.memory.flink.size: {{ flink_job_names['live-node-publisher'].taskmanager_memory }} - taskmanager.numberOfTaskSlots: {{ flink_job_names['live-node-publisher'].taskslots }} + jobmanager.memory.flink.size: {{ flink_job_names['content-publish'].jobmanager_memory }} + taskmanager.memory.flink.size: {{ flink_job_names['content-publish'].taskmanager_memory }} + taskmanager.numberOfTaskSlots: {{ flink_job_names['content-publish'].taskslots }} parallelism.default: 1 jobmanager.execution.failover-strategy: region taskmanager.memory.network.fraction: 0.1 From 644fc67c6a18bc612224145fe646667ef90ac918 Mon Sep 17 00:00:00 2001 From: Jayaprakash8887 Date: Wed, 14 Dec 2022 22:27:44 +0530 Subject: [PATCH 122/222] Issue #KN-427 fix: Config update --- kubernetes/helm_charts/datapipeline_jobs/values.j2 | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/kubernetes/helm_charts/datapipeline_jobs/values.j2 b/kubernetes/helm_charts/datapipeline_jobs/values.j2 index 090402a46e..7b1ebffc0a 100644 --- a/kubernetes/helm_charts/datapipeline_jobs/values.j2 +++ b/kubernetes/helm_charts/datapipeline_jobs/values.j2 @@ -1017,8 +1017,8 @@ live-node-publisher: flink-conf: |+ jobmanager.memory.flink.size: {{ flink_job_names['content-publish'].jobmanager_memory }} - taskmanager.memory.flink.size: {{ flink_job_names['content-publish'].taskmanager_memory }} - taskmanager.numberOfTaskSlots: {{ flink_job_names['content-publish'].taskslots }} + taskmanager.memory.flink.size: {{ flink_job_names['content-publish'].taskmanager_memory }} + taskmanager.numberOfTaskSlots: {{ flink_job_names['content-publish'].taskslots }} parallelism.default: 1 jobmanager.execution.failover-strategy: region taskmanager.memory.network.fraction: 0.1 @@ -1194,9 +1194,9 @@ csp-migrator: cloud_storage_container="{{ cloud_storage_content_bucketname }}" flink-conf: |+ - jobmanager.memory.flink.size: {{ flink_job_names['csp-migrator'].jobmanager_memory }} - taskmanager.memory.flink.size: {{ flink_job_names['csp-migrator'].taskmanager_memory }} - taskmanager.numberOfTaskSlots: {{ flink_job_names['csp-migrator'].taskslots }} + jobmanager.memory.flink.size: {{ flink_job_names['content-publish'].jobmanager_memory }} + taskmanager.memory.flink.size: {{ flink_job_names['content-publish'].taskmanager_memory }} + taskmanager.numberOfTaskSlots: {{ flink_job_names['content-publish'].taskslots }} parallelism.default: 1 jobmanager.execution.failover-strategy: region taskmanager.memory.network.fraction: 0.1 From 48ee03cc55ac2eed6a7a7550b70771e8dcfaab8b Mon Sep 17 00:00:00 2001 From: Jayaprakash8887 Date: Wed, 14 Dec 2022 22:45:42 +0530 Subject: [PATCH 123/222] Issue #KN-427 fix: Config update --- kubernetes/helm_charts/datapipeline_jobs/values.j2 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kubernetes/helm_charts/datapipeline_jobs/values.j2 b/kubernetes/helm_charts/datapipeline_jobs/values.j2 index 7b1ebffc0a..79a888ece4 100644 --- a/kubernetes/helm_charts/datapipeline_jobs/values.j2 +++ b/kubernetes/helm_charts/datapipeline_jobs/values.j2 @@ -1185,7 +1185,7 @@ csp-migrator: migrationVersion = 1 video_stream_regeneration_enable = false live_node_republish_enable = true - copy_missing_files_to_cloud = true + copy_missing_files_to_cloud = false download_path = /tmp cloud_storage_type="{{ cloud_service_provider }}" From bae4d2decfd6e457f450a9f276ad407ca83ea041 Mon Sep 17 00:00:00 2001 From: Jayaprakash8887 Date: Wed, 14 Dec 2022 23:09:38 +0530 Subject: [PATCH 124/222] Issue #KN-427 fix: Config update --- kubernetes/helm_charts/datapipeline_jobs/values.j2 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kubernetes/helm_charts/datapipeline_jobs/values.j2 b/kubernetes/helm_charts/datapipeline_jobs/values.j2 index 79a888ece4..57a37bb093 100644 --- a/kubernetes/helm_charts/datapipeline_jobs/values.j2 +++ b/kubernetes/helm_charts/datapipeline_jobs/values.j2 @@ -1171,7 +1171,7 @@ csp-migrator: } cassandra_fields_to_migrate = { - "assessmentitem": ["body", "editorState", "answer", "solutions", "instructions", "media"] + "assessmentitem": ["body", "editorState", "solutions", "body"] } cloudstorage { From bdd6e75e2503c08cfb26a82d29edc2d5bb208866 Mon Sep 17 00:00:00 2001 From: Jayaprakash8887 Date: Thu, 15 Dec 2022 10:28:03 +0530 Subject: [PATCH 125/222] Issue #KN-427 fix: Config update --- kubernetes/helm_charts/datapipeline_jobs/values.j2 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kubernetes/helm_charts/datapipeline_jobs/values.j2 b/kubernetes/helm_charts/datapipeline_jobs/values.j2 index 57a37bb093..73b50fb834 100644 --- a/kubernetes/helm_charts/datapipeline_jobs/values.j2 +++ b/kubernetes/helm_charts/datapipeline_jobs/values.j2 @@ -1171,7 +1171,7 @@ csp-migrator: } cassandra_fields_to_migrate = { - "assessmentitem": ["body", "editorState", "solutions", "body"] + "assessmentitem": ["question", "editorState", "solutions", "body"] } cloudstorage { From 10ca308e288d3ffab78063af640581d59acfd316 Mon Sep 17 00:00:00 2001 From: Jayaprakash8887 Date: Thu, 15 Dec 2022 10:29:17 +0530 Subject: [PATCH 126/222] Issue #KN-427 fix: Config update --- kubernetes/ansible/roles/flink-jobs-deploy/defaults/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kubernetes/ansible/roles/flink-jobs-deploy/defaults/main.yml b/kubernetes/ansible/roles/flink-jobs-deploy/defaults/main.yml index eec1d24cca..c6d2c0fe9f 100644 --- a/kubernetes/ansible/roles/flink-jobs-deploy/defaults/main.yml +++ b/kubernetes/ansible/roles/flink-jobs-deploy/defaults/main.yml @@ -277,7 +277,7 @@ flink_job_names: replica: 1 jobmanager_memory: 1024m taskmanager_memory: 1024m - taskslots: "{{search_indexer_taskslots | default('1') }}" + taskslots: 1 cpu_requests: 0.3 live-video-stream-generator: job_class_name: 'org.sunbird.job.livevideostream.task.LiveVideoStreamGeneratorStreamTask' From 7360c9cea7944d3d28c180457cbeda67a40c7412 Mon Sep 17 00:00:00 2001 From: Jayaprakash8887 Date: Thu, 15 Dec 2022 12:02:59 +0530 Subject: [PATCH 127/222] Issue #KN-427 fix: Config update --- kubernetes/helm_charts/datapipeline_jobs/values.j2 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kubernetes/helm_charts/datapipeline_jobs/values.j2 b/kubernetes/helm_charts/datapipeline_jobs/values.j2 index 73b50fb834..f2f09db7f9 100644 --- a/kubernetes/helm_charts/datapipeline_jobs/values.j2 +++ b/kubernetes/helm_charts/datapipeline_jobs/values.j2 @@ -1171,7 +1171,7 @@ csp-migrator: } cassandra_fields_to_migrate = { - "assessmentitem": ["question", "editorState", "solutions", "body"] + "assessmentitem": ["question", "editorstate", "solutions", "body"] } cloudstorage { From d86105485f7599996a5e6e0b6d497d26b47b038c Mon Sep 17 00:00:00 2001 From: Jayaprakash8887 Date: Thu, 15 Dec 2022 12:07:32 +0530 Subject: [PATCH 128/222] Issue #KN-427 fix: Config update --- kubernetes/helm_charts/datapipeline_jobs/values.j2 | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/kubernetes/helm_charts/datapipeline_jobs/values.j2 b/kubernetes/helm_charts/datapipeline_jobs/values.j2 index f2f09db7f9..a9dc3f5caf 100644 --- a/kubernetes/helm_charts/datapipeline_jobs/values.j2 +++ b/kubernetes/helm_charts/datapipeline_jobs/values.j2 @@ -1016,9 +1016,9 @@ live-node-publisher: } flink-conf: |+ - jobmanager.memory.flink.size: {{ flink_job_names['content-publish'].jobmanager_memory }} - taskmanager.memory.flink.size: {{ flink_job_names['content-publish'].taskmanager_memory }} - taskmanager.numberOfTaskSlots: {{ flink_job_names['content-publish'].taskslots }} + jobmanager.memory.flink.size: {{ flink_job_names['live-node-publisher'].jobmanager_memory }} + taskmanager.memory.flink.size: {{ flink_job_names['live-node-publisher'].taskmanager_memory }} + taskmanager.numberOfTaskSlots: {{ flink_job_names['live-node-publisher'].taskslots }} parallelism.default: 1 jobmanager.execution.failover-strategy: region taskmanager.memory.network.fraction: 0.1 From 2c35184ba9f10352baec39a74cfa5f9e3dfc5c1f Mon Sep 17 00:00:00 2001 From: Jayaprakash8887 Date: Thu, 15 Dec 2022 12:17:03 +0530 Subject: [PATCH 129/222] Issue #KN-427 fix: Config update --- kubernetes/helm_charts/datapipeline_jobs/values.j2 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/kubernetes/helm_charts/datapipeline_jobs/values.j2 b/kubernetes/helm_charts/datapipeline_jobs/values.j2 index a9dc3f5caf..ff4a4591e5 100644 --- a/kubernetes/helm_charts/datapipeline_jobs/values.j2 +++ b/kubernetes/helm_charts/datapipeline_jobs/values.j2 @@ -853,8 +853,8 @@ live-node-publisher: } task { parallelism = 1 - consumer.parallelism = {{ search_indexer_consumer_parallelism }} - router.parallelism = {{ transaction_event_router_parallelism }} + consumer.parallelism = 1 + router.parallelism = 1 window.time = 60 checkpointing.timeout = 4200000 } From dcb07d07bb680902731282bbbbddd0eae37f8fa4 Mon Sep 17 00:00:00 2001 From: Jayaprakash8887 Date: Thu, 15 Dec 2022 12:37:42 +0530 Subject: [PATCH 130/222] Issue #KN-427 fix: Config update --- kubernetes/helm_charts/datapipeline_jobs/values.j2 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/kubernetes/helm_charts/datapipeline_jobs/values.j2 b/kubernetes/helm_charts/datapipeline_jobs/values.j2 index ff4a4591e5..fceb22cc19 100644 --- a/kubernetes/helm_charts/datapipeline_jobs/values.j2 +++ b/kubernetes/helm_charts/datapipeline_jobs/values.j2 @@ -1016,8 +1016,8 @@ live-node-publisher: } flink-conf: |+ - jobmanager.memory.flink.size: {{ flink_job_names['live-node-publisher'].jobmanager_memory }} - taskmanager.memory.flink.size: {{ flink_job_names['live-node-publisher'].taskmanager_memory }} + jobmanager.memory.flink.size: {{ flink_job_names['content-publish'].jobmanager_memory }} + taskmanager.memory.flink.size: {{ flink_job_names['content-publish'].taskmanager_memory }} taskmanager.numberOfTaskSlots: {{ flink_job_names['live-node-publisher'].taskslots }} parallelism.default: 1 jobmanager.execution.failover-strategy: region From d9e711852077f3b2ee8ae9c10c2a6da87bbe2bb4 Mon Sep 17 00:00:00 2001 From: Jayaprakash8887 Date: Thu, 15 Dec 2022 12:42:07 +0530 Subject: [PATCH 131/222] Issue #KN-427 fix: Config update --- .../ansible/roles/flink-jobs-deploy/defaults/main.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/kubernetes/ansible/roles/flink-jobs-deploy/defaults/main.yml b/kubernetes/ansible/roles/flink-jobs-deploy/defaults/main.yml index c6d2c0fe9f..8b31798e83 100644 --- a/kubernetes/ansible/roles/flink-jobs-deploy/defaults/main.yml +++ b/kubernetes/ansible/roles/flink-jobs-deploy/defaults/main.yml @@ -275,10 +275,10 @@ flink_job_names: live-node-publisher: job_class_name: 'org.sunbird.job.livenodepublisher.task.LiveNodePublisherStreamTask' replica: 1 - jobmanager_memory: 1024m - taskmanager_memory: 1024m + jobmanager_memory: 2048m + taskmanager_memory: 2048m taskslots: 1 - cpu_requests: 0.3 + cpu_requests: 0.7 live-video-stream-generator: job_class_name: 'org.sunbird.job.livevideostream.task.LiveVideoStreamGeneratorStreamTask' replica: 1 From 107ff1b032deec292cd7ef71705693aeb46e400f Mon Sep 17 00:00:00 2001 From: Jayaprakash8887 Date: Thu, 15 Dec 2022 13:25:13 +0530 Subject: [PATCH 132/222] Issue #KN-427 fix: Config update --- .../ansible/roles/flink-jobs-deploy/defaults/main.yml | 4 ++-- kubernetes/helm_charts/datapipeline_jobs/values.j2 | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/kubernetes/ansible/roles/flink-jobs-deploy/defaults/main.yml b/kubernetes/ansible/roles/flink-jobs-deploy/defaults/main.yml index 8b31798e83..b18f2fd330 100644 --- a/kubernetes/ansible/roles/flink-jobs-deploy/defaults/main.yml +++ b/kubernetes/ansible/roles/flink-jobs-deploy/defaults/main.yml @@ -270,14 +270,14 @@ flink_job_names: replica: 1 jobmanager_memory: 1024m taskmanager_memory: 1024m - taskslots: 1 + taskslots: cpu_requests: 0.3 live-node-publisher: job_class_name: 'org.sunbird.job.livenodepublisher.task.LiveNodePublisherStreamTask' replica: 1 jobmanager_memory: 2048m taskmanager_memory: 2048m - taskslots: 1 + taskslots: 3 cpu_requests: 0.7 live-video-stream-generator: job_class_name: 'org.sunbird.job.livevideostream.task.LiveVideoStreamGeneratorStreamTask' diff --git a/kubernetes/helm_charts/datapipeline_jobs/values.j2 b/kubernetes/helm_charts/datapipeline_jobs/values.j2 index fceb22cc19..05bf7be1be 100644 --- a/kubernetes/helm_charts/datapipeline_jobs/values.j2 +++ b/kubernetes/helm_charts/datapipeline_jobs/values.j2 @@ -853,10 +853,10 @@ live-node-publisher: } task { parallelism = 1 - consumer.parallelism = 1 - router.parallelism = 1 + consumer.parallelism = 2 + router.parallelism = 2 window.time = 60 - checkpointing.timeout = 4200000 + checkpointing.timeout = 900000 } redis { host={{redis_host}} @@ -1016,8 +1016,8 @@ live-node-publisher: } flink-conf: |+ - jobmanager.memory.flink.size: {{ flink_job_names['content-publish'].jobmanager_memory }} - taskmanager.memory.flink.size: {{ flink_job_names['content-publish'].taskmanager_memory }} + jobmanager.memory.flink.size: {{ flink_job_names['live-node-publisher'].jobmanager_memory }} + taskmanager.memory.flink.size: {{ flink_job_names['live-node-publisher'].taskmanager_memory }} taskmanager.numberOfTaskSlots: {{ flink_job_names['live-node-publisher'].taskslots }} parallelism.default: 1 jobmanager.execution.failover-strategy: region From b4db0e3a7e18266b164aa0a47744acb2a193312d Mon Sep 17 00:00:00 2001 From: Jayaprakash8887 Date: Thu, 15 Dec 2022 13:26:21 +0530 Subject: [PATCH 133/222] Issue #KN-427 fix: Config update --- kubernetes/ansible/roles/flink-jobs-deploy/defaults/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kubernetes/ansible/roles/flink-jobs-deploy/defaults/main.yml b/kubernetes/ansible/roles/flink-jobs-deploy/defaults/main.yml index b18f2fd330..a68560e0e6 100644 --- a/kubernetes/ansible/roles/flink-jobs-deploy/defaults/main.yml +++ b/kubernetes/ansible/roles/flink-jobs-deploy/defaults/main.yml @@ -270,7 +270,7 @@ flink_job_names: replica: 1 jobmanager_memory: 1024m taskmanager_memory: 1024m - taskslots: + taskslots: 1 cpu_requests: 0.3 live-node-publisher: job_class_name: 'org.sunbird.job.livenodepublisher.task.LiveNodePublisherStreamTask' From a24ea0cb5efc1bcd1639f8b5083ff3ed5854ff14 Mon Sep 17 00:00:00 2001 From: Jayaprakash8887 Date: Thu, 15 Dec 2022 13:31:28 +0530 Subject: [PATCH 134/222] Issue #KN-427 fix: Config update --- .../ansible/roles/flink-jobs-deploy/defaults/main.yml | 8 ++++---- kubernetes/helm_charts/datapipeline_jobs/values.j2 | 6 +++--- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/kubernetes/ansible/roles/flink-jobs-deploy/defaults/main.yml b/kubernetes/ansible/roles/flink-jobs-deploy/defaults/main.yml index a68560e0e6..ee64877a5e 100644 --- a/kubernetes/ansible/roles/flink-jobs-deploy/defaults/main.yml +++ b/kubernetes/ansible/roles/flink-jobs-deploy/defaults/main.yml @@ -275,10 +275,10 @@ flink_job_names: live-node-publisher: job_class_name: 'org.sunbird.job.livenodepublisher.task.LiveNodePublisherStreamTask' replica: 1 - jobmanager_memory: 2048m - taskmanager_memory: 2048m - taskslots: 3 - cpu_requests: 0.7 + jobmanager_memory: "{{live_node_publisher_job_memory | default('2048m') }}" + taskmanager_memory: "{{live_node_publisher_task_memory | default('2048m') }}" + taskslots: "{{live_node_publisher_taskslots | default('3') }}" + cpu_requests: "{{live_node_publisher_cpu_requests | default('0.7') }}" live-video-stream-generator: job_class_name: 'org.sunbird.job.livevideostream.task.LiveVideoStreamGeneratorStreamTask' replica: 1 diff --git a/kubernetes/helm_charts/datapipeline_jobs/values.j2 b/kubernetes/helm_charts/datapipeline_jobs/values.j2 index 05bf7be1be..570ddf4582 100644 --- a/kubernetes/helm_charts/datapipeline_jobs/values.j2 +++ b/kubernetes/helm_charts/datapipeline_jobs/values.j2 @@ -853,10 +853,10 @@ live-node-publisher: } task { parallelism = 1 - consumer.parallelism = 2 - router.parallelism = 2 + consumer.parallelism = {{live_node_publisher_consumer_parallelism | default('2') }} + router.parallelism = {{live_node_publisher_router_parallelism | default('2') }} window.time = 60 - checkpointing.timeout = 900000 + checkpointing.timeout = {{live_node_publisher_checkpointing_timeout | default('900000') }} } redis { host={{redis_host}} From 32e9c6da6f8ee0c0c36a389a4024124c3decea16 Mon Sep 17 00:00:00 2001 From: Jayaprakash8887 Date: Fri, 16 Dec 2022 17:13:54 +0530 Subject: [PATCH 135/222] Issue #KN-427 fix: Config update --- kubernetes/helm_charts/datapipeline_jobs/values.j2 | 6 ------ 1 file changed, 6 deletions(-) diff --git a/kubernetes/helm_charts/datapipeline_jobs/values.j2 b/kubernetes/helm_charts/datapipeline_jobs/values.j2 index 570ddf4582..82faca34b0 100644 --- a/kubernetes/helm_charts/datapipeline_jobs/values.j2 +++ b/kubernetes/helm_charts/datapipeline_jobs/values.j2 @@ -1216,12 +1216,6 @@ cassandra-data-migration: } migrate = { - keyspace = "dialcodes" - table = "dialcode_batch" - primary_key_column = "processid" - primary_key_column_type = "UUID" - column_to_migrate = "url" - column_to_migrate_type = "string" key_value_strings_to_migrate = { "https://sunbirdstagingpublic.blob.core.windows.net/dial": "{{ cloudstorage_relative_path_prefix_dial }}" } From 4867422714f595ce571094f9cb7ac7550b27b4df Mon Sep 17 00:00:00 2001 From: Jayaprakash8887 Date: Mon, 19 Dec 2022 10:18:06 +0530 Subject: [PATCH 136/222] Issue #KN-427 fix: Config update --- ansible/roles/setup-kafka/defaults/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ansible/roles/setup-kafka/defaults/main.yml b/ansible/roles/setup-kafka/defaults/main.yml index 8aba456fe3..e69e140e6f 100644 --- a/ansible/roles/setup-kafka/defaults/main.yml +++ b/ansible/roles/setup-kafka/defaults/main.yml @@ -142,7 +142,7 @@ processing_kafka_topics: num_of_partitions: 1 replication_factor: 1 - name: republish.job.request - num_of_partitions: 1 + num_of_partitions: 4 replication_factor: 1 - name: cassandra.data.migration.request num_of_partitions: 1 From a73aa249731e49e8bf0e2c2550a3c44955a81972 Mon Sep 17 00:00:00 2001 From: santhosh-tg Date: Wed, 14 Dec 2022 13:33:17 +0530 Subject: [PATCH 137/222] Generate sas_token on-demand --- ansible/es_backup.yml | 2 +- .../tasks/delete-using-azcopy.yml | 12 +++++++++++- .../tasks/upload-using-azcopy.yml | 15 +++++++++++++-- ansible/roles/cassandra-backup/tasks/main.yml | 2 +- 4 files changed, 26 insertions(+), 5 deletions(-) diff --git a/ansible/es_backup.yml b/ansible/es_backup.yml index fd47fa6ed3..2669eed28e 100644 --- a/ansible/es_backup.yml +++ b/ansible/es_backup.yml @@ -12,7 +12,7 @@ blob_container_name: "elasticsearch-snapshots" container_public_access: "off" storage_account_name: "{{ azure_management_storage_account_name }}" - storage_account_sas_token: "{{ azure_management_storage_account_sas }}" + storage_account_key: "{{ cloud_management_storage_secret }}" when: cloud_service_provider == "azure" - hosts: composite-search-cluster diff --git a/ansible/roles/azure-cloud-storage/tasks/delete-using-azcopy.yml b/ansible/roles/azure-cloud-storage/tasks/delete-using-azcopy.yml index 236169e86c..196de9c9b3 100644 --- a/ansible/roles/azure-cloud-storage/tasks/delete-using-azcopy.yml +++ b/ansible/roles/azure-cloud-storage/tasks/delete-using-azcopy.yml @@ -1,6 +1,16 @@ --- +- name: generate SAS token for azcopy + shell: | + sas_expiry=`date -u -d "1 hour" '+%Y-%m-%dT%H:%MZ'` + sas_token=?`az storage container generate-sas -n {{ blob_container_name }} --account-name {{ storage_account_name }} --account-key {{ storage_account_key }} --https-only --permissions dlrw --expiry $sas_expiry -o tsv` + echo $sas_token + register: sas_token + +- set_fact: + container_sas_token: "{{ sas_token.stdout}}" + - name: delete files and folders from azure storage using azcopy - shell: "azcopy rm 'https://{{ storage_account_name }}.blob.core.windows.net/{{ blob_container_name }}{{ blob_container_folder_path }}{{ storage_account_sas_token }}' --recursive" + shell: "azcopy rm 'https://{{ storage_account_name }}.blob.core.windows.net/{{ blob_container_name }}{{ blob_container_folder_path }}{{ container_sas_token }}' --recursive" environment: AZCOPY_CONCURRENT_FILES: "10" async: 10800 diff --git a/ansible/roles/azure-cloud-storage/tasks/upload-using-azcopy.yml b/ansible/roles/azure-cloud-storage/tasks/upload-using-azcopy.yml index 99ab3c2bf8..95da584c9b 100644 --- a/ansible/roles/azure-cloud-storage/tasks/upload-using-azcopy.yml +++ b/ansible/roles/azure-cloud-storage/tasks/upload-using-azcopy.yml @@ -1,12 +1,23 @@ --- +- name: generate SAS token for azcopy + shell: | + sas_expiry=`date -u -d "1 hour" '+%Y-%m-%dT%H:%MZ'` + sas_token=?`az storage container generate-sas -n {{ blob_container_name }} --account-name {{ storage_account_name }} --account-key {{ storage_account_key }} --https-only --permissions dlrw --expiry $sas_expiry -o tsv` + echo $sas_token + register: sas_token + +- set_fact: + container_sas_token: "{{ sas_token.stdout}}" + - name: create container in azure storage if it doesn't exist include_role: name: azure-cloud-storage tasks_from: container-create.yml + when: create_container == True - name: upload files and folders to azure storage using azcopy - shell: "azcopy copy {{ local_file_or_folder_path }} 'https://{{ storage_account_name }}.blob.core.windows.net/{{ blob_container_name }}{{ blob_container_folder_path }}{{ storage_account_sas_token }}' --recursive" + shell: "azcopy copy {{ local_file_or_folder_path }} 'https://{{ storage_account_name }}.blob.core.windows.net/{{ blob_container_name }}{{ blob_container_folder_path }}{{ container_sas_token }}' --recursive" environment: AZCOPY_CONCURRENT_FILES: "10" async: 10800 - poll: 10 \ No newline at end of file + poll: 10 diff --git a/ansible/roles/cassandra-backup/tasks/main.yml b/ansible/roles/cassandra-backup/tasks/main.yml index f4750c0aad..fa9afdc394 100755 --- a/ansible/roles/cassandra-backup/tasks/main.yml +++ b/ansible/roles/cassandra-backup/tasks/main.yml @@ -42,7 +42,7 @@ blob_container_folder_path: "" local_file_or_folder_path: "/data/cassandra/backup/{{ cassandra_backup_folder_name }}" storage_account_name: "{{ azure_management_storage_account_name }}" - storage_account_sas_token: "{{ azure_management_storage_account_sas }}" + storage_account_key: "{{ cloud_management_storage_secret }}" when: cloud_service_provider == "azure" - name: upload file to gcloud storage From a63de0faf0aac05fa73e1c5e04d6d4e3defc8274 Mon Sep 17 00:00:00 2001 From: santhosh-tg Date: Wed, 14 Dec 2022 14:05:18 +0530 Subject: [PATCH 138/222] Update generalized vars --- ansible/es_backup.yml | 2 +- ansible/roles/cassandra-backup/tasks/main.yml | 2 +- ansible/roles/cassandra-restore/tasks/main.yml | 4 ++-- ansible/roles/neo4j-backup/tasks/main.yml | 4 ++-- ansible/roles/neo4j-restore/tasks/main.yml | 4 ++-- ansible/roles/redis-backup/tasks/main.yml | 4 ++-- ansible/roles/redis-restore/tasks/main.yml | 4 ++-- 7 files changed, 12 insertions(+), 12 deletions(-) diff --git a/ansible/es_backup.yml b/ansible/es_backup.yml index 2669eed28e..8134759173 100644 --- a/ansible/es_backup.yml +++ b/ansible/es_backup.yml @@ -11,7 +11,7 @@ vars: blob_container_name: "elasticsearch-snapshots" container_public_access: "off" - storage_account_name: "{{ azure_management_storage_account_name }}" + storage_account_name: "{{ cloud_management_storage_accountname }}" storage_account_key: "{{ cloud_management_storage_secret }}" when: cloud_service_provider == "azure" diff --git a/ansible/roles/cassandra-backup/tasks/main.yml b/ansible/roles/cassandra-backup/tasks/main.yml index fa9afdc394..9d441410fe 100755 --- a/ansible/roles/cassandra-backup/tasks/main.yml +++ b/ansible/roles/cassandra-backup/tasks/main.yml @@ -41,7 +41,7 @@ container_public_access: "off" blob_container_folder_path: "" local_file_or_folder_path: "/data/cassandra/backup/{{ cassandra_backup_folder_name }}" - storage_account_name: "{{ azure_management_storage_account_name }}" + storage_account_name: "{{ cloud_management_storage_accountname }}" storage_account_key: "{{ cloud_management_storage_secret }}" when: cloud_service_provider == "azure" diff --git a/ansible/roles/cassandra-restore/tasks/main.yml b/ansible/roles/cassandra-restore/tasks/main.yml index 356397e56c..6562c77a63 100755 --- a/ansible/roles/cassandra-restore/tasks/main.yml +++ b/ansible/roles/cassandra-restore/tasks/main.yml @@ -14,8 +14,8 @@ blob_container_name: "{{ cassandra_backup_storage }}" blob_file_name: "{{ cassandra_restore_file_name }}" local_file_or_folder_path: "{{restore_path}}/{{ cassandra_restore_file_name }}" - storage_account_name: "{{ azure_management_storage_account_name }}" - storage_account_key: "{{ azure_management_storage_account_key }}" + storage_account_name: "{{ cloud_management_storage_accountname }}" + storage_account_key: "{{ cloud_management_storage_secret }}" when: cloud_service_provider == "azure" - name: download file from gcloud storage diff --git a/ansible/roles/neo4j-backup/tasks/main.yml b/ansible/roles/neo4j-backup/tasks/main.yml index e0af37b441..22a65f9f88 100755 --- a/ansible/roles/neo4j-backup/tasks/main.yml +++ b/ansible/roles/neo4j-backup/tasks/main.yml @@ -34,8 +34,8 @@ blob_file_name: "{{ var1.stdout }}" container_public_access: "off" local_file_or_folder_path: "/home/learning/backup/{{ var1.stdout }}" - storage_account_name: "{{ azure_management_storage_account_name }}" - storage_account_key: "{{ azure_management_storage_account_key }}" + storage_account_name: "{{ cloud_management_storage_accountname }}" + storage_account_key: "{{ cloud_management_storage_secret }}" when: cloud_service_provider == "azure" - name: upload file to gcloud storage diff --git a/ansible/roles/neo4j-restore/tasks/main.yml b/ansible/roles/neo4j-restore/tasks/main.yml index 94ded5e5db..e521404577 100644 --- a/ansible/roles/neo4j-restore/tasks/main.yml +++ b/ansible/roles/neo4j-restore/tasks/main.yml @@ -13,8 +13,8 @@ blob_container_name: "{{ neo4j_backup_storage }}" blob_file_name: "{{ neo4j_backup_file_name }}" local_file_or_folder_path: "{{ neo4j_backup_file_path }}" - storage_account_name: "{{ azure_management_storage_account_name }}" - storage_account_key: "{{ azure_management_storage_account_key }}" + storage_account_name: "{{ cloud_management_storage_accountname }}" + storage_account_key: "{{ cloud_management_storage_secret }}" when: cloud_service_provider == "azure" - name: download file from gcloud storage diff --git a/ansible/roles/redis-backup/tasks/main.yml b/ansible/roles/redis-backup/tasks/main.yml index e3cf9baa04..e3584d5603 100644 --- a/ansible/roles/redis-backup/tasks/main.yml +++ b/ansible/roles/redis-backup/tasks/main.yml @@ -24,8 +24,8 @@ container_public_access: "off" blob_file_name: "{{ redis_backup_file_name }}" local_file_or_folder_path: "{{ redis_backup_file_path }}" - storage_account_name: "{{ azure_management_storage_account_name }}" - storage_account_key: "{{ azure_management_storage_account_key }}" + storage_account_name: "{{ cloud_management_storage_accountname }}" + storage_account_key: "{{ cloud_management_storage_secret }}" when: cloud_service_provider == "azure" - name: upload file to gcloud storage diff --git a/ansible/roles/redis-restore/tasks/main.yml b/ansible/roles/redis-restore/tasks/main.yml index df86374a7d..a6d9c2d5f9 100644 --- a/ansible/roles/redis-restore/tasks/main.yml +++ b/ansible/roles/redis-restore/tasks/main.yml @@ -8,8 +8,8 @@ blob_container_name: "{{ redis_backup_storage }}" blob_file_name: "{{ redis_restore_file_name }}" local_file_or_folder_path: "/tmp/{{ redis_restore_file_name }}" - storage_account_name: "{{ azure_management_storage_account_name }}" - storage_account_key: "{{ azure_management_storage_account_key }}" + storage_account_name: "{{ cloud_management_storage_accountname }}" + storage_account_key: "{{ cloud_management_storage_secret }}" when: cloud_service_provider == "azure" - name: download file from gcloud storage From fea0f2c1163ac0f181207e83c94c5f4f8301c49b Mon Sep 17 00:00:00 2001 From: Jayaprakash8887 Date: Mon, 19 Dec 2022 15:39:32 +0530 Subject: [PATCH 139/222] Issue #KN-427 fix: Config update --- kubernetes/helm_charts/datapipeline_jobs/values.j2 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kubernetes/helm_charts/datapipeline_jobs/values.j2 b/kubernetes/helm_charts/datapipeline_jobs/values.j2 index 82faca34b0..cb1f253d87 100644 --- a/kubernetes/helm_charts/datapipeline_jobs/values.j2 +++ b/kubernetes/helm_charts/datapipeline_jobs/values.j2 @@ -1143,7 +1143,7 @@ csp-migrator: "https://qa.ekstep.in/assets/public": "{{ cloudstorage_relative_path_prefix_content }}", "https://dev.ekstep.in/assets/public": "{{ cloudstorage_relative_path_prefix_content }}", "https://community.ekstep.in/assets/public": "{{ cloudstorage_relative_path_prefix_content }}", - "https://community.ekstep.in:443": "{{ cloudstorage_relative_path_prefix_content }}", + "https://community.ekstep.in:443/assets/public": "{{ cloudstorage_relative_path_prefix_content }}", "https://ekstep-public-qa.s3-ap-south-1.amazonaws.com": "{{ cloudstorage_relative_path_prefix_content }}", "https://ekstep-public-dev.s3-ap-south-1.amazonaws.com": "{{ cloudstorage_relative_path_prefix_content }}", "https://ekstep-public-preprod.s3-ap-south-1.amazonaws.com": "{{ cloudstorage_relative_path_prefix_content }}", From 26854076ece75268e8164ac7d0a5ba511f763e18 Mon Sep 17 00:00:00 2001 From: santhosh-tg Date: Mon, 19 Dec 2022 16:41:08 +0530 Subject: [PATCH 140/222] Add cloud agnostic vars Add AWS role --- ansible/artifacts-download.yml | 31 +++++++++++++----- ansible/artifacts-upload.yml | 32 ++++++++++++++----- ansible/roles/aws-cli/defaults/main.yml | 1 + ansible/roles/aws-cli/tasks/main.yml | 24 ++++++++++++++ .../roles/aws-cloud-storage/defaults/main.yml | 3 ++ .../aws-cloud-storage/tasks/delete-folder.yml | 9 ++++++ .../roles/aws-cloud-storage/tasks/delete.yml | 9 ++++++ .../aws-cloud-storage/tasks/download.yml | 9 ++++++ .../roles/aws-cloud-storage/tasks/main.yml | 18 +++++++++++ .../aws-cloud-storage/tasks/upload-folder.yml | 9 ++++++ .../roles/aws-cloud-storage/tasks/upload.yml | 9 ++++++ .../roles/cassandra-backup/defaults/main.yml | 10 ++---- ansible/roles/cassandra-backup/tasks/main.yml | 20 +++++++++--- .../roles/cassandra-restore/defaults/main.yml | 10 ++---- .../roles/cassandra-restore/tasks/main.yml | 23 ++++++++++--- .../roles/gcp-cloud-storage/defaults/main.yml | 6 ++-- .../gcp-cloud-storage/tasks/download.yml | 4 +-- .../gcp-cloud-storage/tasks/upload-batch.yml | 2 +- .../roles/gcp-cloud-storage/tasks/upload.yml | 2 +- ansible/roles/neo4j-backup/defaults/main.yml | 10 ++---- ansible/roles/neo4j-backup/tasks/main.yml | 22 ++++++++++--- ansible/roles/neo4j-restore/defaults/main.yml | 9 ++---- ansible/roles/neo4j-restore/tasks/main.yml | 20 +++++++++--- ansible/roles/redis-backup/defaults/main.yml | 9 ++---- ansible/roles/redis-backup/tasks/main.yml | 20 +++++++++--- ansible/roles/redis-restore/defaults/main.yml | 9 ++---- ansible/roles/redis-restore/tasks/main.yml | 22 ++++++++++--- 27 files changed, 259 insertions(+), 93 deletions(-) create mode 100644 ansible/roles/aws-cli/defaults/main.yml create mode 100644 ansible/roles/aws-cli/tasks/main.yml create mode 100644 ansible/roles/aws-cloud-storage/defaults/main.yml create mode 100644 ansible/roles/aws-cloud-storage/tasks/delete-folder.yml create mode 100644 ansible/roles/aws-cloud-storage/tasks/delete.yml create mode 100644 ansible/roles/aws-cloud-storage/tasks/download.yml create mode 100644 ansible/roles/aws-cloud-storage/tasks/main.yml create mode 100644 ansible/roles/aws-cloud-storage/tasks/upload-folder.yml create mode 100644 ansible/roles/aws-cloud-storage/tasks/upload.yml diff --git a/ansible/artifacts-download.yml b/ansible/artifacts-download.yml index 5966c81220..ce8b9455fb 100644 --- a/ansible/artifacts-download.yml +++ b/ansible/artifacts-download.yml @@ -6,11 +6,14 @@ tasks: - name: download artifact from azure storage include_role: - name: artifacts-download-azure - apply: - environment: - AZURE_STORAGE_ACCOUNT: "{{sunbird_artifact_storage_account_name}}" - AZURE_STORAGE_SAS_TOKEN: "{{sunbird_artifact_storage_account_sas}}" + name: azure-cloud-storage + tasks_from: blob-download.yml + vars: + blob_container_name: "{{ cloud_storage_artifacts_bucketname }}" + blob_file_name: "{{ artifact }}" + local_file_or_folder_path: "{{ artifact_path }}" + storage_account_name: "{{ cloud_artifact_storage_accountname }}" + storage_account_key: "{{ cloud_artifact_storage_secret }}" when: cloud_service_provider == "azure" - name: download artifact from gcloud storage @@ -18,8 +21,20 @@ name: gcp-cloud-storage tasks_from: download.yml vars: - gcp_bucket_name: "{{ gcloud_artifact_bucket_name }}" - dest_folder_name: "{{ artifacts_container }}" - dest_file_name: "{{ artifact }}" + gcp_bucket_name: "{{ cloud_storage_artifacts_bucketname }}" + gcp_path: "{{ artifact }}" local_file_or_folder_path: "{{ artifact_path }}" when: cloud_service_provider == "gcloud" + + - name: download artifact from aws s3 + include_role: + name: aws-cloud-storage + tasks_from: download.yml + vars: + local_file_or_folder_path: "{{ artifact_path }}" + s3_bucket_name: "{{ cloud_storage_artifacts_bucketname }}" + s3_path: "{{ artifact }}" + aws_default_region: "{{ cloud_public_storage_region }}" + aws_access_key_id: "{{ cloud_artifact_storage_accountname }}" + aws_secret_access_key: "{{ cloud_artifact_storage_secret }}" + when: cloud_service_provider == "aws" diff --git a/ansible/artifacts-upload.yml b/ansible/artifacts-upload.yml index 2d3bf76414..acdf917c85 100644 --- a/ansible/artifacts-upload.yml +++ b/ansible/artifacts-upload.yml @@ -6,11 +6,15 @@ tasks: - name: upload artifact to azure storage include_role: - name: artifacts-upload-azure - apply: - environment: - AZURE_STORAGE_ACCOUNT: "{{sunbird_artifact_storage_account_name}}" - AZURE_STORAGE_SAS_TOKEN: "{{sunbird_artifact_storage_account_sas}}" + name: azure-cloud-storage + tasks_from: blob-upload.yml + vars: + blob_container_name: "{{ cloud_storage_artifacts_bucketname }}" + container_public_access: "off" + blob_file_name: "{{ artifact }}" + local_file_or_folder_path: "{{ artifact_path }}" + storage_account_name: "{{ cloud_artifact_storage_accountname }}" + storage_account_key: "{{ cloud_artifact_storage_secret }}" when: cloud_service_provider == "azure" - name: upload artifact to gcloud storage @@ -18,8 +22,20 @@ name: gcp-cloud-storage tasks_from: upload.yml vars: - gcp_bucket_name: "{{ gcloud_artifact_bucket_name }}" - dest_folder_name: "{{ artifacts_container }}" - dest_file_name: "{{ artifact }}" + gcp_bucket_name: "{{ cloud_storage_artifacts_bucketname }}" + gcp_path: "{{ artifact }}" local_file_or_folder_path: "{{ artifact_path }}" when: cloud_service_provider == "gcloud" + + - name: upload artifact to aws s3 + include_role: + name: aws-cloud-storage + tasks_from: upload.yml + vars: + local_file_or_folder_path: "{{ artifact_path }}" + s3_bucket_name: "{{ cloud_storage_artifacts_bucketname }}" + s3_path: "{{ artifact }}" + aws_default_region: "{{ cloud_public_storage_region }}" + aws_access_key_id: "{{ cloud_artifact_storage_accountname }}" + aws_secret_access_key: "{{ cloud_artifact_storage_secret }}" + when: cloud_service_provider == "aws" diff --git a/ansible/roles/aws-cli/defaults/main.yml b/ansible/roles/aws-cli/defaults/main.yml new file mode 100644 index 0000000000..53d866eafa --- /dev/null +++ b/ansible/roles/aws-cli/defaults/main.yml @@ -0,0 +1 @@ +aws_cli_url: https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip \ No newline at end of file diff --git a/ansible/roles/aws-cli/tasks/main.yml b/ansible/roles/aws-cli/tasks/main.yml new file mode 100644 index 0000000000..5907fb1aaf --- /dev/null +++ b/ansible/roles/aws-cli/tasks/main.yml @@ -0,0 +1,24 @@ +--- +- name: Download the installation file + get_url: + url: "{{ aws_cli_url }}" + dest: /tmp/awscliv2.zip + +- name: Installing unzip + apt: + name: "{{item}}" + state: latest + with_items: + - zip + - unzip + +- name: Unzip the installer + unarchive: + src: /tmp/awscliv2.zip + dest: /tmp/ + remote_src: yes + +- name: install aws cli + shell: ./aws/install + args: + chdir: /tmp/ diff --git a/ansible/roles/aws-cloud-storage/defaults/main.yml b/ansible/roles/aws-cloud-storage/defaults/main.yml new file mode 100644 index 0000000000..6f3f6f86d6 --- /dev/null +++ b/ansible/roles/aws-cloud-storage/defaults/main.yml @@ -0,0 +1,3 @@ +s3_bucket_name: "" +s3_path: "" +local_file_or_folder_path: "" diff --git a/ansible/roles/aws-cloud-storage/tasks/delete-folder.yml b/ansible/roles/aws-cloud-storage/tasks/delete-folder.yml new file mode 100644 index 0000000000..c912b14edb --- /dev/null +++ b/ansible/roles/aws-cloud-storage/tasks/delete-folder.yml @@ -0,0 +1,9 @@ +--- +- name: delete files and folders recursively + environment: + AWS_DEFAULT_REGION: "{{ aws_default_region }}" + AWS_ACCESS_KEY_ID: "{{ aws_access_key_id }}" + AWS_SECRET_ACCESS_KEY: "{{ aws_secret_access_key }}" + shell: "aws s3 rm s3://{{ s3_bucket_name }}/{{ s3_path }} --recursive" + async: 3600 + poll: 10 diff --git a/ansible/roles/aws-cloud-storage/tasks/delete.yml b/ansible/roles/aws-cloud-storage/tasks/delete.yml new file mode 100644 index 0000000000..414ea52e6b --- /dev/null +++ b/ansible/roles/aws-cloud-storage/tasks/delete.yml @@ -0,0 +1,9 @@ +--- +- name: delete files from s3 + environment: + AWS_DEFAULT_REGION: "{{ aws_default_region }}" + AWS_ACCESS_KEY_ID: "{{ aws_access_key_id }}" + AWS_SECRET_ACCESS_KEY: "{{ aws_secret_access_key }}" + shell: "aws s3 rm s3://{{ s3_bucket_name }}/{{ s3_path }}" + async: 3600 + poll: 10 diff --git a/ansible/roles/aws-cloud-storage/tasks/download.yml b/ansible/roles/aws-cloud-storage/tasks/download.yml new file mode 100644 index 0000000000..138024af78 --- /dev/null +++ b/ansible/roles/aws-cloud-storage/tasks/download.yml @@ -0,0 +1,9 @@ +--- +- name: download files to s3 + environment: + AWS_DEFAULT_REGION: "{{ aws_default_region }}" + AWS_ACCESS_KEY_ID: "{{ aws_access_key_id }}" + AWS_SECRET_ACCESS_KEY: "{{ aws_secret_access_key }}" + shell: "aws s3 cp s3://{{ s3_bucket_name }}/{{ s3_path }} {{ local_file_or_folder_path }}" + async: 3600 + poll: 10 diff --git a/ansible/roles/aws-cloud-storage/tasks/main.yml b/ansible/roles/aws-cloud-storage/tasks/main.yml new file mode 100644 index 0000000000..62f204a9d2 --- /dev/null +++ b/ansible/roles/aws-cloud-storage/tasks/main.yml @@ -0,0 +1,18 @@ +--- +- name: delete files from aws S3 bucket + include: delete.yml + +- name: delete folders from aws S3 bucket recursively + include: delete-folder.yml + + +- name: download file from S3 + include: download.yml + +- name: upload files from a local to aws S3 + include: upload.yml + +- name: upload files and folder from local directory to aws S3 + include: upload-folder.yml + + diff --git a/ansible/roles/aws-cloud-storage/tasks/upload-folder.yml b/ansible/roles/aws-cloud-storage/tasks/upload-folder.yml new file mode 100644 index 0000000000..3e03b068b7 --- /dev/null +++ b/ansible/roles/aws-cloud-storage/tasks/upload-folder.yml @@ -0,0 +1,9 @@ +--- +- name: upload folder to s3 + environment: + AWS_DEFAULT_REGION: "{{ aws_default_region }}" + AWS_ACCESS_KEY_ID: "{{ aws_access_key_id }}" + AWS_SECRET_ACCESS_KEY: "{{ aws_secret_access_key }}" + shell: "aws s3 cp {{ local_file_or_folder_path }} s3://{{ s3_bucket_name }}/{{ s3_path }} --recursive" + async: 3600 + poll: 10 diff --git a/ansible/roles/aws-cloud-storage/tasks/upload.yml b/ansible/roles/aws-cloud-storage/tasks/upload.yml new file mode 100644 index 0000000000..af8de990e2 --- /dev/null +++ b/ansible/roles/aws-cloud-storage/tasks/upload.yml @@ -0,0 +1,9 @@ +--- +- name: upload files to s3 + environment: + AWS_DEFAULT_REGION: "{{ aws_default_region }}" + AWS_ACCESS_KEY_ID: "{{ aws_access_key_id }}" + AWS_SECRET_ACCESS_KEY: "{{ aws_secret_access_key }}" + shell: "aws s3 cp {{ local_file_or_folder_path }} s3://{{ s3_bucket_name }}/{{ s3_path }}" + async: 3600 + poll: 10 diff --git a/ansible/roles/cassandra-backup/defaults/main.yml b/ansible/roles/cassandra-backup/defaults/main.yml index 65daa78122..ac9d48a629 100644 --- a/ansible/roles/cassandra-backup/defaults/main.yml +++ b/ansible/roles/cassandra-backup/defaults/main.yml @@ -1,11 +1,7 @@ cassandra_root_dir: /etc/cassandra cassandra_backup_dir: /data/cassandra/backup data_dir: '/var/lib/cassandra/data' -cassandra_backup_azure_container_name: lp-cassandra-backup -# This variable is added for the below reason - -# 1. Introduce a common variable for various clouds. In case of azure, it refers to container name, in case of aws / gcp, it refers to folder name -# 2. We want to avoid too many new variable introduction / replacement in first phase. Hence we will reuse the existing variable defined in private repo -# or other default files and just assign the value to the newly introduced common variable -# 3. After few releases, we will remove the older variables and use only the new variables across the repos -cassandra_backup_storage: "{{ cassandra_backup_azure_container_name }}" +cloud_storage_cassandrabackup_bucketname: "{{cloud_storage_management_bucketname}}" +cloud_storage_cassandrabackup_foldername: lp-cassandra-backup + diff --git a/ansible/roles/cassandra-backup/tasks/main.yml b/ansible/roles/cassandra-backup/tasks/main.yml index 9d441410fe..9906b6b3cc 100755 --- a/ansible/roles/cassandra-backup/tasks/main.yml +++ b/ansible/roles/cassandra-backup/tasks/main.yml @@ -37,22 +37,34 @@ name: azure-cloud-storage tasks_from: upload-using-azcopy.yml vars: - blob_container_name: "{{ cassandra_backup_storage }}" + blob_container_name: "{{ cloud_storage_cassandrabackup_foldername }}" container_public_access: "off" blob_container_folder_path: "" local_file_or_folder_path: "/data/cassandra/backup/{{ cassandra_backup_folder_name }}" storage_account_name: "{{ cloud_management_storage_accountname }}" storage_account_key: "{{ cloud_management_storage_secret }}" when: cloud_service_provider == "azure" + +- name: upload backup to S3 + include_role: + name: aws-cloud-storage + tasks_from: upload-folder.yml + vars: + local_file_or_folder_path: "/data/cassandra/backup/{{ cassandra_backup_folder_name }}" + s3_bucket_name: "{{ cloud_storage_cassandrabackup_bucketname }}" + s3_path: "{{ cloud_storage_cassandrabackup_foldername }}" + aws_default_region: "{{ cloud_public_storage_region }}" + aws_access_key_id: "{{ cloud_management_storage_accountname }}" + aws_secret_access_key: "{{ cloud_management_storage_secret }}" + when: cloud_service_provider == "aws" - name: upload file to gcloud storage include_role: name: gcp-cloud-storage tasks_from: upload-batch.yml vars: - gcp_bucket_name: "{{ gcloud_management_bucket_name }}" - dest_folder_name: "{{ cassandra_backup_storage }}" - dest_folder_path: "" + gcp_bucket_name: "{{ cloud_storage_cassandrabackup_bucketname }}" + gcp_path: "{{ cloud_storage_cassandrabackup_foldername }}" local_file_or_folder_path: "/data/cassandra/backup/{{ cassandra_backup_folder_name }}" when: cloud_service_provider == "gcloud" diff --git a/ansible/roles/cassandra-restore/defaults/main.yml b/ansible/roles/cassandra-restore/defaults/main.yml index c269c9f360..45a9017f2e 100644 --- a/ansible/roles/cassandra-restore/defaults/main.yml +++ b/ansible/roles/cassandra-restore/defaults/main.yml @@ -1,13 +1,7 @@ -cassandra_backup_azure_container_name: lp-cassandra-backup - user: "{{ ansible_ssh_user }}" restore_path: /home/{{user}} backup_folder_name: cassandra_backup backup_dir: "{{restore_path}}/cassandra_backup" -# This variable is added for the below reason - -# 1. Introduce a common variable for various clouds. In case of azure, it refers to container name, in case of aws / gcp, it refers to folder name -# 2. We want to avoid too many new variable introduction / replacement in first phase. Hence we will reuse the existing variable defined in private repo -# or other default files and just assign the value to the newly introduced common variable -# 3. After few releases, we will remove the older variables and use only the new variables across the repos -cassandra_backup_storage: "{{ cassandra_backup_azure_container_name }}" +cloud_storage_cassandrabackup_bucketname: "{{cloud_storage_management_bucketname}}" +cloud_storage_cassandrabackup_foldername: lp-cassandra-backup diff --git a/ansible/roles/cassandra-restore/tasks/main.yml b/ansible/roles/cassandra-restore/tasks/main.yml index 6562c77a63..42a69df939 100755 --- a/ansible/roles/cassandra-restore/tasks/main.yml +++ b/ansible/roles/cassandra-restore/tasks/main.yml @@ -11,22 +11,35 @@ name: azure-cloud-storage tasks_from: blob-download.yml vars: - blob_container_name: "{{ cassandra_backup_storage }}" + blob_container_name: "{{ cloud_storage_cassandrabackup_foldername }}" blob_file_name: "{{ cassandra_restore_file_name }}" local_file_or_folder_path: "{{restore_path}}/{{ cassandra_restore_file_name }}" storage_account_name: "{{ cloud_management_storage_accountname }}" storage_account_key: "{{ cloud_management_storage_secret }}" when: cloud_service_provider == "azure" +- name: download a file from aws s3 + become: true + include_role: + name: aws-cloud-storage + tasks_from: download.yml + vars: + s3_bucket_name: "{{ cloud_storage_cassandrabackup_bucketname }}" + aws_access_key_id: "{{ cloud_management_storage_accountname }}" + aws_secret_access_key: "{{ cloud_management_storage_secret }}" + aws_default_region: "{{ cloud_public_storage_region }}" + local_file_or_folder_path: "{{restore_path}}/{{ cassandra_restore_file_name }}" + s3_path: "{{ cloud_storage_cassandrabackup_foldername }}/{{ cassandra_restore_file_name }}" + when: cloud_service_provider == "aws" + - name: download file from gcloud storage include_role: name: gcp-cloud-storage tasks_from: download.yml vars: - gcp_bucket_name: "{{ gcloud_management_bucket_name }}" - dest_folder_name: "{{ cassandra_backup_storage }}" - dest_file_name: "{{ cassandra_restore_file_name }}" - local_file_or_folder_path: "{{ cassandra_restore_gzip_file_path }}" + gcp_bucket_name: "{{ cloud_storage_cassandrabackup_bucketname }}" + s3_path: "{{ cloud_storage_cassandrabackup_foldername }}/{{ cassandra_restore_file_name }}" + local_file_or_folder_path: "{{restore_path}}/{{ cassandra_restore_file_name }}" when: cloud_service_provider == "gcloud" - name: unarchieve backup file diff --git a/ansible/roles/gcp-cloud-storage/defaults/main.yml b/ansible/roles/gcp-cloud-storage/defaults/main.yml index 086cf9c50d..b0fd847b26 100644 --- a/ansible/roles/gcp-cloud-storage/defaults/main.yml +++ b/ansible/roles/gcp-cloud-storage/defaults/main.yml @@ -10,8 +10,8 @@ gcp_storage_key_file: "" # Folder name in GCP bucket # Example - -# dest_folder_name: "my-destination-folder" -dest_folder_name: "" +# gcp_path: "my-destination-folder" +gcp_path: "" # The delete pattern to delete files and folder # Example - @@ -36,7 +36,7 @@ dest_file_name: "" # The folder path in gcloud storage to upload the files starting from the root of the bucket # This path should start with / if we provide a value for this variable since we are going to append this path as below -# {{ bucket_name }}{{ dest_folder_name }} +# {{ bucket_name }}{{ gcp_path }} # The above translates to "my-bucket/my-folder-path" # Example - # dest_folder_path: "/my-folder/json-files-folder" diff --git a/ansible/roles/gcp-cloud-storage/tasks/download.yml b/ansible/roles/gcp-cloud-storage/tasks/download.yml index c8c6e956ad..73bf76bb04 100644 --- a/ansible/roles/gcp-cloud-storage/tasks/download.yml +++ b/ansible/roles/gcp-cloud-storage/tasks/download.yml @@ -3,9 +3,9 @@ include_tasks: gcloud-auth.yml - name: Download from gcloud storage - shell: gsutil cp "gs://{{ gcp_bucket_name }}/{{ dest_folder_name }}/{{ dest_file_name }}" "{{ local_file_or_folder_path }}" + shell: gsutil cp "gs://{{ gcp_bucket_name }}/{{ gcp_path }}" "{{ local_file_or_folder_path }}" async: 3600 poll: 10 - name: Revoke gcloud access - include_tasks: gcloud-revoke.yml \ No newline at end of file + include_tasks: gcloud-revoke.yml diff --git a/ansible/roles/gcp-cloud-storage/tasks/upload-batch.yml b/ansible/roles/gcp-cloud-storage/tasks/upload-batch.yml index 49abd5b822..dc103969aa 100644 --- a/ansible/roles/gcp-cloud-storage/tasks/upload-batch.yml +++ b/ansible/roles/gcp-cloud-storage/tasks/upload-batch.yml @@ -3,7 +3,7 @@ include_tasks: gcloud-auth.yml - name: Upload files from a local directory gcp storage - shell: gsutil -m cp -r "{{ local_file_or_folder_path }}" "gs://{{ gcp_bucket_name }}/{{ dest_folder_name }}/{{ dest_folder_path }}" + shell: gsutil -m cp -r "{{ local_file_or_folder_path }}" "gs://{{ gcp_bucket_name }}/{{ gcp_path}}" async: 3600 poll: 10 diff --git a/ansible/roles/gcp-cloud-storage/tasks/upload.yml b/ansible/roles/gcp-cloud-storage/tasks/upload.yml index 2f88d9407f..de766a94c7 100644 --- a/ansible/roles/gcp-cloud-storage/tasks/upload.yml +++ b/ansible/roles/gcp-cloud-storage/tasks/upload.yml @@ -3,7 +3,7 @@ include_tasks: gcloud-auth.yml - name: Upload to gcloud storage - shell: gsutil cp "{{ local_file_or_folder_path }}" "gs://{{ gcp_bucket_name }}/{{ dest_folder_name }}/{{ dest_file_name }}" + shell: gsutil cp "{{ local_file_or_folder_path }}" "gs://{{ gcp_bucket_name }}/{{ gcp_path }}" async: 3600 poll: 10 diff --git a/ansible/roles/neo4j-backup/defaults/main.yml b/ansible/roles/neo4j-backup/defaults/main.yml index e31639176a..ecc58634ee 100644 --- a/ansible/roles/neo4j-backup/defaults/main.yml +++ b/ansible/roles/neo4j-backup/defaults/main.yml @@ -6,13 +6,9 @@ backup_add: "127.0.0.1:7362" var1: "_graph" service: learning graph_machine: "{{service}}{{var1}}" -neo4j_backup_azure_container_name: neo4j-backup neo4j_backup_dir: "{{ learner_user_home }}/backup" -# This variable is added for the below reason - -# 1. Introduce a common variable for various clouds. In case of azure, it refers to container name, in case of aws / gcp, it refers to folder name -# 2. We want to avoid too many new variable introduction / replacement in first phase. Hence we will reuse the existing variable defined in private repo -# or other default files and just assign the value to the newly introduced common variable -# 3. After few releases, we will remove the older variables and use only the new variables across the repos -neo4j_backup_storage: "{{ neo4j_backup_azure_container_name }}" +cloud_storage_neo4jbackup_bucketname: "{{ cloud_storage_management_bucketname }}" +cloud_storage_neo4jbackup_foldername: neo4j-backup + diff --git a/ansible/roles/neo4j-backup/tasks/main.yml b/ansible/roles/neo4j-backup/tasks/main.yml index 22a65f9f88..395b77fd86 100755 --- a/ansible/roles/neo4j-backup/tasks/main.yml +++ b/ansible/roles/neo4j-backup/tasks/main.yml @@ -30,7 +30,7 @@ name: azure-cloud-storage tasks_from: blob-upload.yml vars: - blob_container_name: "{{ neo4j_backup_storage }}" + blob_container_name: "{{ cloud_storage_neo4jbackup_foldername }}" blob_file_name: "{{ var1.stdout }}" container_public_access: "off" local_file_or_folder_path: "/home/learning/backup/{{ var1.stdout }}" @@ -38,15 +38,27 @@ storage_account_key: "{{ cloud_management_storage_secret }}" when: cloud_service_provider == "azure" +- name: upload file to aws s3 + include_role: + name: aws-cloud-storage + tasks_from: upload.yml + vars: + s3_bucket_name: "{{ cloud_storage_neo4jbackup_bucketname }}" + aws_access_key_id: "{{ cloud_management_storage_accountname }}" + aws_secret_access_key: "{{ cloud_management_storage_secret }}" + aws_default_region: "{{ cloud_public_storage_region }}" + local_file_or_folder_path: "/home/learning/backup/{{ var1.stdout }}" + s3_path: "{{ cloud_storage_neo4jbackup_foldername }}/{{ var1.stdout }}" + when: cloud_service_provider == "aws" + - name: upload file to gcloud storage include_role: name: gcp-cloud-storage tasks_from: upload.yml vars: - gcp_bucket_name: "{{ gcloud_management_bucket_name }}" - dest_folder_name: "{{ neo4j_backup_storage }}" - dest_file_name: "{{ var1.stdout }}" - local_file_or_folder_path: "{{ neo4j_backup_dir }}/{{ var1.stdout }}" + gcp_bucket_name: "{{ cloud_storage_neo4jbackup_bucketname }}" + gcp_path: "{{ cloud_storage_neo4jbackup_foldername }}/{{ var1.stdout }}" + local_file_or_folder_path: "/home/learning/backup/{{ var1.stdout }}" when: cloud_service_provider == "gcloud" - name: clean up backup dir after upload diff --git a/ansible/roles/neo4j-restore/defaults/main.yml b/ansible/roles/neo4j-restore/defaults/main.yml index 52e004992f..e338cee980 100644 --- a/ansible/roles/neo4j-restore/defaults/main.yml +++ b/ansible/roles/neo4j-restore/defaults/main.yml @@ -1,6 +1,5 @@ neo4j_restore_dir: /home/{{learner_user}}/restore learner_user: learning -neo4j_backup_azure_container_name: neo4j-backup neo4j_home: "{{learner_user_home}}/neo4j-learning/neo4j-enterprise-3.3.0" learner_user_home: /home/{{learner_user}} path_to_neo4j_db: "{{neo4j_home}}/data/databases" @@ -10,9 +9,5 @@ path_to_neo4j_db: "{{neo4j_home}}/data/databases" #backup_azure_storage_account_name: defined in private repo #backup_azure_storage_access_key: defined in secrets.yml -# This variable is added for the below reason - -# 1. Introduce a common variable for various clouds. In case of azure, it refers to container name, in case of aws / gcp, it refers to folder name -# 2. We want to avoid too many new variable introduction / replacement in first phase. Hence we will reuse the existing variable defined in private repo -# or other default files and just assign the value to the newly introduced common variable -# 3. After few releases, we will remove the older variables and use only the new variables across the repos -neo4j_backup_storage: "{{ neo4j_backup_azure_container_name }}" +cloud_storage_neo4jbackup_bucketname: "{{ cloud_storage_management_bucketname }}" +cloud_storage_neo4jbackup_foldername: neo4j-backup diff --git a/ansible/roles/neo4j-restore/tasks/main.yml b/ansible/roles/neo4j-restore/tasks/main.yml index e521404577..82a485f09e 100644 --- a/ansible/roles/neo4j-restore/tasks/main.yml +++ b/ansible/roles/neo4j-restore/tasks/main.yml @@ -10,21 +10,33 @@ name: azure-cloud-storage tasks_from: blob-download.yml vars: - blob_container_name: "{{ neo4j_backup_storage }}" + blob_container_name: "{{ cloud_storage_neo4jbackup_foldername }}" blob_file_name: "{{ neo4j_backup_file_name }}" local_file_or_folder_path: "{{ neo4j_backup_file_path }}" storage_account_name: "{{ cloud_management_storage_accountname }}" storage_account_key: "{{ cloud_management_storage_secret }}" when: cloud_service_provider == "azure" +- name: download file from aws s3 + include_role: + name: aws-cloud-storage + tasks_from: download.yml + vars: + s3_bucket_name: "{{ cloud_storage_neo4jbackup_bucketname }}" + aws_access_key_id: "{{ cloud_management_storage_accountname }}" + aws_secret_access_key: "{{ cloud_management_storage_secret }}" + aws_default_region: "{{ cloud_public_storage_region }}" + local_file_or_folder_path: "{{ neo4j_backup_file_path }}" + s3_path: "{{ cloud_storage_neo4jbackup_foldername }}/{{ neo4j_backup_file_name }}" + when: cloud_service_provider == "aws" + - name: download file from gcloud storage include_role: name: gcp-cloud-storage tasks_from: download.yml vars: - gcp_bucket_name: "{{ gcloud_management_bucket_name }}" - dest_folder_name: "{{ neo4j_backup_storage }}" - dest_file_name: "{{ neo4j_backup_file_name }}" + gcp_bucket_name: "{{ cloud_storage_neo4jbackup_bucketname }}" + gcp_path: "{{ cloud_storage_neo4jbackup_foldername }}/{{ neo4j_backup_file_name }}" local_file_or_folder_path: "{{ neo4j_backup_file_path }}" when: cloud_service_provider == "gcloud" diff --git a/ansible/roles/redis-backup/defaults/main.yml b/ansible/roles/redis-backup/defaults/main.yml index ea52f764a4..234379b85c 100644 --- a/ansible/roles/redis-backup/defaults/main.yml +++ b/ansible/roles/redis-backup/defaults/main.yml @@ -1,13 +1,8 @@ redis_backup_dir: /tmp/redis-backup -redis_backup_azure_container_name: redis-backup learner_user: learning redis_data_dir: /data redis_version: 6.2.5 redis_dir: "/home/{{ learner_user }}/redis-{{ redis_version }}" -# This variable is added for the below reason - -# 1. Introduce a common variable for various clouds. In case of azure, it refers to container name, in case of aws / gcp, it refers to folder name -# 2. We want to avoid too many new variable introduction / replacement in first phase. Hence we will reuse the existing variable defined in private repo -# or other default files and just assign the value to the newly introduced common variable -# 3. After few releases, we will remove the older variables and use only the new variables across the repos -redis_backup_storage: "{{ redis_backup_azure_container_name }}" +cloud_storage_redisbackup_bucketname: "{{ cloud_storage_management_bucketname }}" +cloud_storage_redisbackup_foldername: redis-backup diff --git a/ansible/roles/redis-backup/tasks/main.yml b/ansible/roles/redis-backup/tasks/main.yml index e3584d5603..fe49b512b7 100644 --- a/ansible/roles/redis-backup/tasks/main.yml +++ b/ansible/roles/redis-backup/tasks/main.yml @@ -20,7 +20,7 @@ name: azure-cloud-storage tasks_from: blob-upload.yml vars: - blob_container_name: "{{ redis_backup_storage }}" + blob_container_name: "{{ cloud_storage_redisbackup_foldername }}" container_public_access: "off" blob_file_name: "{{ redis_backup_file_name }}" local_file_or_folder_path: "{{ redis_backup_file_path }}" @@ -28,14 +28,26 @@ storage_account_key: "{{ cloud_management_storage_secret }}" when: cloud_service_provider == "azure" +- name: upload file to aws s3 + include_role: + name: aws-cloud-storage + tasks_from: upload.yml + vars: + s3_bucket_name: "{{ cloud_storage_redisbackup_bucketname }}" + aws_access_key_id: "{{ cloud_management_storage_accountname }}" + aws_secret_access_key: "{{ cloud_management_storage_secret }}" + aws_default_region: "{{ cloud_public_storage_region }}" + local_file_or_folder_path: "{{ redis_backup_file_path }}" + s3_path: "{{ cloud_storage_redisbackup_foldername }}/{{ redis_backup_file_name }}" + when: cloud_service_provider == "aws" + - name: upload file to gcloud storage include_role: name: gcp-cloud-storage tasks_from: upload.yml vars: - gcp_bucket_name: "{{ gcloud_management_bucket_name }}" - dest_folder_name: "{{ redis_backup_storage }}" - dest_file_name: "{{ redis_backup_file_name }}" + gcp_bucket_name: "{{ cloud_storage_redisbackup_bucketname }}" + gcp_path: "{{ cloud_storage_redisbackup_foldername }}/{{ redis_backup_file_name }}" local_file_or_folder_path: "{{ redis_backup_file_path }}" when: cloud_service_provider == "gcloud" diff --git a/ansible/roles/redis-restore/defaults/main.yml b/ansible/roles/redis-restore/defaults/main.yml index 452d62b67f..5861b60c55 100644 --- a/ansible/roles/redis-restore/defaults/main.yml +++ b/ansible/roles/redis-restore/defaults/main.yml @@ -1,9 +1,4 @@ -redis_backup_azure_container_name: redis-backup learning_user_home: /home/learning -# This variable is added for the below reason - -# 1. Introduce a common variable for various clouds. In case of azure, it refers to container name, in case of aws / gcp, it refers to folder name -# 2. We want to avoid too many new variable introduction / replacement in first phase. Hence we will reuse the existing variable defined in private repo -# or other default files and just assign the value to the newly introduced common variable -# 3. After few releases, we will remove the older variables and use only the new variables across the repos -redis_backup_storage: "{{ redis_backup_azure_container_name }}" +cloud_storage_redisbackup_bucketname: "{{ cloud_storage_management_bucketname }}" +cloud_storage_redisbackup_foldername: redis-backup diff --git a/ansible/roles/redis-restore/tasks/main.yml b/ansible/roles/redis-restore/tasks/main.yml index a6d9c2d5f9..4b8916872c 100644 --- a/ansible/roles/redis-restore/tasks/main.yml +++ b/ansible/roles/redis-restore/tasks/main.yml @@ -5,22 +5,34 @@ name: azure-cloud-storage tasks_from: blob-download.yml vars: - blob_container_name: "{{ redis_backup_storage }}" + blob_container_name: "{{ cloud_storage_redisbackup_foldername }}" blob_file_name: "{{ redis_restore_file_name }}" local_file_or_folder_path: "/tmp/{{ redis_restore_file_name }}" storage_account_name: "{{ cloud_management_storage_accountname }}" storage_account_key: "{{ cloud_management_storage_secret }}" when: cloud_service_provider == "azure" +- name: download file from aws s3 + include_role: + name: aws-cloud-storage + tasks_from: download.yml + vars: + s3_bucket_name: "{{ cloud_storage_redisbackup_bucketname }}" + aws_access_key_id: "{{ cloud_management_storage_accountname }}" + aws_secret_access_key: "{{ cloud_management_storage_secret }}" + aws_default_region: "{{ cloud_public_storage_region }}" + local_file_or_folder_path: "/tmp/{{ redis_restore_file_name }}" + s3_path: "{{ cloud_storage_redisbackup_foldername }}/{{ redis_restore_file_name }}" + when: cloud_service_provider == "aws" + - name: download file from gcloud storage include_role: name: gcp-cloud-storage tasks_from: download.yml vars: - gcp_bucket_name: "{{ gcloud_management_bucket_name }}" - dest_folder_name: "{{ redis_backup_storage }}" - dest_file_name: "{{ redis_restore_file_name }}" - local_file_or_folder_path: "/tmp" + gcp_bucket_name: "{{ cloud_storage_redisbackup_bucketname }}" + gcp_path: "{{ cloud_storage_redisbackup_foldername }}/{{ redis_restore_file_name }}" + local_file_or_folder_path: "/tmp/{{ redis_restore_file_name }}" when: cloud_service_provider == "gcloud" - name: stop redis to take backup From e18e452607eabb6a9f6bce97df9af718ad833554 Mon Sep 17 00:00:00 2001 From: santhosh-tg Date: Mon, 19 Dec 2022 17:01:58 +0530 Subject: [PATCH 141/222] Add missing var --- ansible/roles/azure-cloud-storage/defaults/main.yml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/ansible/roles/azure-cloud-storage/defaults/main.yml b/ansible/roles/azure-cloud-storage/defaults/main.yml index 0e4e45bf95..ceef7bd46e 100644 --- a/ansible/roles/azure-cloud-storage/defaults/main.yml +++ b/ansible/roles/azure-cloud-storage/defaults/main.yml @@ -64,4 +64,9 @@ blob_container_folder_path: "" # This variable affects only new containers and has no affect on a container if it already exists # If the container already exists, the access level will not be changed # You will need to change the access level from Azure portal or using az storage container set-permission command -container_public_access: "" \ No newline at end of file +container_public_access: "" + +# Creates the container by default before running the specific azure blob tasks +# If you would like to skip container creation (in case of a looped execution), +# you can set this value to False in order to skip the contatiner creation task for every iteration +create_container: True From b68e194d35e032ab2c35c5d729eb0c76409d4ec2 Mon Sep 17 00:00:00 2001 From: Jayaprakash8887 Date: Tue, 20 Dec 2022 11:29:27 +0530 Subject: [PATCH 142/222] Issue #KN-427 fix: Config update --- kubernetes/helm_charts/datapipeline_jobs/values.j2 | 1 + 1 file changed, 1 insertion(+) diff --git a/kubernetes/helm_charts/datapipeline_jobs/values.j2 b/kubernetes/helm_charts/datapipeline_jobs/values.j2 index cb1f253d87..efc8e9f991 100644 --- a/kubernetes/helm_charts/datapipeline_jobs/values.j2 +++ b/kubernetes/helm_charts/datapipeline_jobs/values.j2 @@ -265,6 +265,7 @@ search-indexer: schema.definition_cache.expiry = {{ schema_definition_cache_expiry_in_sec }} restrict.objectTypes = {{ search_indexer_restrict_object_types | to_json }} ignored.fields={{ search_indexer_ignored_fields | to_json }} + cloud_storage_container="{{ cloud_storage_content_bucketname }}" cloudstorage { metadata.replace_absolute_path={{ cloudstorage_replace_absolute_path | default('false') }} From f1230dbc31824425eba0329b5eec98117ec0f76d Mon Sep 17 00:00:00 2001 From: santhosh-tg Date: Tue, 20 Dec 2022 14:19:11 +0530 Subject: [PATCH 143/222] Update es backup role --- ansible/roles/es-azure-snapshot/defaults/main.yml | 12 +++--------- ansible/roles/es-gcs-snapshot/defaults/main.yml | 8 +++++--- ansible/roles/es-s3-snapshot/defaults/main.yml | 8 +++++--- 3 files changed, 13 insertions(+), 15 deletions(-) diff --git a/ansible/roles/es-azure-snapshot/defaults/main.yml b/ansible/roles/es-azure-snapshot/defaults/main.yml index a623c56a01..df52870977 100644 --- a/ansible/roles/es-azure-snapshot/defaults/main.yml +++ b/ansible/roles/es-azure-snapshot/defaults/main.yml @@ -1,7 +1,7 @@ snapshot_create_request_body: { type: azure, settings: { - container: "{{ es_backup_storage }}", + container: "{{ cloud_storage_esbackup_foldername }}", base_path: "{{ snapshot_base_path }}_{{ base_path_date }}" } } @@ -10,11 +10,5 @@ snapshot_create_request_body: { es_snapshot_host: "localhost" snapshot_base_path: "default" -es_azure_backup_container_name: "elasticsearch-snapshots" - -# This variable is added for the below reason - -# 1. Introduce a common variable for various clouds. In case of azure, it refers to container name, in case of aws / gcp, it refers to folder name -# 2. We want to avoid too many new variable introduction / replacement in first phase. Hence we will reuse the existing variable defined in private repo -# or other default files and just assign the value to the newly introduced common variable -# 3. After few releases, we will remove the older variables and use only the new variables across the repos -es_backup_storage: "{{ es_azure_backup_container_name }}" +cloud_storage_esbackup_bucketname: "{{ cloud_storage_management_bucketname }}" +cloud_storage_esbackup_foldername: "elasticsearch-snapshots" diff --git a/ansible/roles/es-gcs-snapshot/defaults/main.yml b/ansible/roles/es-gcs-snapshot/defaults/main.yml index 5e3cbece6f..7222b0c06b 100644 --- a/ansible/roles/es-gcs-snapshot/defaults/main.yml +++ b/ansible/roles/es-gcs-snapshot/defaults/main.yml @@ -1,12 +1,14 @@ snapshot_create_request_body: { type: gcs, settings: { - bucket: "{{ gcs_management_bucket_name }}", - base_path: "{{ es_backup_storage }}/{{ snapshot_base_path }}_{{ base_path_date }}" + bucket: "{{ cloud_storage_management_bucketname }}", + base_path: "{{ cloud_storage_esbackup_foldername }}/{{ snapshot_base_path }}_{{ base_path_date }}" } } # Override these values es_snapshot_host: "localhost" snapshot_base_path: "default" -es_backup_storage: "elasticsearch-snapshots" \ No newline at end of file + +cloud_storage_esbackup_bucketname: "{{ cloud_storage_management_bucketname }}" +cloud_storage_esbackup_foldername: "elasticsearch-snapshots" diff --git a/ansible/roles/es-s3-snapshot/defaults/main.yml b/ansible/roles/es-s3-snapshot/defaults/main.yml index 7ddda6ebd0..316ae512fb 100644 --- a/ansible/roles/es-s3-snapshot/defaults/main.yml +++ b/ansible/roles/es-s3-snapshot/defaults/main.yml @@ -1,12 +1,14 @@ snapshot_create_request_body: { type: s3, settings: { - bucket: "{{ aws_management_bucket_name }}", - base_path: "{{ es_backup_storage }}/{{ snapshot_base_path }}_{{ base_path_date }}" + bucket: "{{ cloud_storage_esbackup_bucketname }}", + base_path: "{{ cloud_storage_esbackup_foldername }}/{{ snapshot_base_path }}_{{ base_path_date }}" } } # Override these values es_snapshot_host: "localhost" snapshot_base_path: "default" -es_backup_storage: "elasticsearch-snapshots" \ No newline at end of file + +cloud_storage_esbackup_bucketname: "{{ cloud_storage_management_bucketname }}" +cloud_storage_esbackup_foldername: "elasticsearch-snapshots" From 1760bbf8f3c229e7ff71269b3f0d36045c06b830 Mon Sep 17 00:00:00 2001 From: anilgupta Date: Tue, 20 Dec 2022 16:31:23 +0530 Subject: [PATCH 144/222] Issue #KN-439 feat: Adding the replace string feature. --- .../templates/application.conf.j2 | 5 ++- .../org/sunbird/sync/tool/util/JSONUtils.java | 34 +++++++++++++++++++ .../sync/tool/util/SyncMessageGenerator.java | 14 ++++++++ .../src/main/resources/application.conf | 6 +++- 4 files changed, 57 insertions(+), 2 deletions(-) create mode 100644 platform-tools/spikes/sync-tool/src/main/java/org/sunbird/sync/tool/util/JSONUtils.java diff --git a/ansible/roles/lp-synctool-deploy/templates/application.conf.j2 b/ansible/roles/lp-synctool-deploy/templates/application.conf.j2 index 98703b4a64..aa6e2c3920 100644 --- a/ansible/roles/lp-synctool-deploy/templates/application.conf.j2 +++ b/ansible/roles/lp-synctool-deploy/templates/application.conf.j2 @@ -117,4 +117,7 @@ contentTypeToPrimaryCategory { TextBookUnit: "Textbook Unit" } csp.migration.request.topic="{{ csp_migration_topic_name }}" -csp.migration.batch.size={{ csp_migration_batch_size }} \ No newline at end of file +csp.migration.batch.size={{ csp_migration_batch_size }} +is_replace_string={{ sync_tool_is_replace_string | default('false') }} +replace_src_string= "{{ sync_tool_replace_src_string | default('') }}" +replace_dest_string="{{ sync_tool_replace_dest_string | default('') }}" \ No newline at end of file diff --git a/platform-tools/spikes/sync-tool/src/main/java/org/sunbird/sync/tool/util/JSONUtils.java b/platform-tools/spikes/sync-tool/src/main/java/org/sunbird/sync/tool/util/JSONUtils.java new file mode 100644 index 0000000000..35cc7f7871 --- /dev/null +++ b/platform-tools/spikes/sync-tool/src/main/java/org/sunbird/sync/tool/util/JSONUtils.java @@ -0,0 +1,34 @@ +package org.sunbird.sync.tool.util; + +import org.apache.commons.lang3.StringUtils; +import org.codehaus.jackson.map.ObjectMapper; + +import java.util.List; +import java.util.Map; + +public class JSONUtils { + + private static ObjectMapper mapper = new ObjectMapper();; + + public static String serialize(Object object) throws Exception { + return mapper.writeValueAsString(object); + } + + public static Object convertJSONString(String value) { + if (StringUtils.isNotBlank(value)) { + com.fasterxml.jackson.databind.ObjectMapper mapper = new com.fasterxml.jackson.databind.ObjectMapper(); + try { + Map map = mapper.readValue(value, Map.class); + return map; + } catch (Exception e) { + try { + List list = mapper.readValue(value, List.class); + return list; + } catch (Exception ex) { + //suppress error due to invalid map while converting JSON and return null + } + } + } + return null; + } +} \ No newline at end of file diff --git a/platform-tools/spikes/sync-tool/src/main/java/org/sunbird/sync/tool/util/SyncMessageGenerator.java b/platform-tools/spikes/sync-tool/src/main/java/org/sunbird/sync/tool/util/SyncMessageGenerator.java index 81b63497e8..4e41e996a1 100644 --- a/platform-tools/spikes/sync-tool/src/main/java/org/sunbird/sync/tool/util/SyncMessageGenerator.java +++ b/platform-tools/spikes/sync-tool/src/main/java/org/sunbird/sync/tool/util/SyncMessageGenerator.java @@ -28,6 +28,9 @@ public class SyncMessageGenerator { private static Map definitionObjectMap = new HashMap<>(); private static ControllerUtil util = new ControllerUtil(); private static List nestedFields = Platform.config.getStringList("nested.fields"); + private static boolean isReplaceString = Platform.config.getBoolean("is_replace_string"); + private static String replaceSrcString = Platform.config.getString("replace_src_string"); + private static String replaceDestString = Platform.config.getString("replace_dest_string"); public static Map getMessages(List nodes, String objectType, Map errors) throws Exception { @@ -112,6 +115,17 @@ public static Map getMessage(Node node) { Map transactionData = new HashMap(); if (null != node.getMetadata() && !node.getMetadata().isEmpty()) { Map propertyMap = new HashMap(); + if(isReplaceString) { + try { + String metadataStr = JSONUtils.serialize(node.getMetadata()); + String updatedMetadataStr = StringUtils.replaceEach(metadataStr, new String[]{replaceSrcString}, new String[]{replaceDestString}); + Map updatedMetadata = (Map) JSONUtils.convertJSONString(updatedMetadataStr); + node.setMetadata(updatedMetadata); + } catch (Exception e) { + System.out.println("SyncMessageGenerator:getMessage: While replacing string " + e.getMessage()); + e.printStackTrace(); + } + } for (Entry entry : node.getMetadata().entrySet()) { String key = entry.getKey(); if (StringUtils.isNotBlank(key)) { diff --git a/platform-tools/spikes/sync-tool/src/main/resources/application.conf b/platform-tools/spikes/sync-tool/src/main/resources/application.conf index e741eb105e..66c49bdd9f 100644 --- a/platform-tools/spikes/sync-tool/src/main/resources/application.conf +++ b/platform-tools/spikes/sync-tool/src/main/resources/application.conf @@ -96,4 +96,8 @@ contentTypeToPrimaryCategory { } csp.migration.request.topic="dev.csp.migration.job.request" -csp.migration.batch.size=50 \ No newline at end of file +csp.migration.batch.size=50 + +is_replace_string=false +replace_src_string= "" +replace_dest_string="" From 949727ab2731d2ffa0425d755df9e33fee39419c Mon Sep 17 00:00:00 2001 From: Jayaprakash8887 Date: Tue, 20 Dec 2022 17:08:24 +0530 Subject: [PATCH 145/222] Issue #KN-427 feat: Sync Tool Update --- .../java/org/sunbird/sync/tool/util/SyncMessageGenerator.java | 2 ++ .../sunbird/searchindex/elasticsearch/ElasticSearchUtil.java | 1 + 2 files changed, 3 insertions(+) diff --git a/platform-tools/spikes/sync-tool/src/main/java/org/sunbird/sync/tool/util/SyncMessageGenerator.java b/platform-tools/spikes/sync-tool/src/main/java/org/sunbird/sync/tool/util/SyncMessageGenerator.java index 4e41e996a1..d8b9cbc2da 100644 --- a/platform-tools/spikes/sync-tool/src/main/java/org/sunbird/sync/tool/util/SyncMessageGenerator.java +++ b/platform-tools/spikes/sync-tool/src/main/java/org/sunbird/sync/tool/util/SyncMessageGenerator.java @@ -61,6 +61,7 @@ public static Map getMessages(List nodes, String objectTyp errors.put(node.getIdentifier(), e.getMessage()); } } + System.out.println("SyncMessageGenerator: getMessage: message:: " + messages); return messages; } @@ -119,6 +120,7 @@ public static Map getMessage(Node node) { try { String metadataStr = JSONUtils.serialize(node.getMetadata()); String updatedMetadataStr = StringUtils.replaceEach(metadataStr, new String[]{replaceSrcString}, new String[]{replaceDestString}); + System.out.println("SyncMessageGenerator:getMessage: updatedMetadataStr " + updatedMetadataStr); Map updatedMetadata = (Map) JSONUtils.convertJSONString(updatedMetadataStr); node.setMetadata(updatedMetadata); } catch (Exception e) { diff --git a/searchIndex-platform/module/searchindex-elasticsearch/src/main/java/org/sunbird/searchindex/elasticsearch/ElasticSearchUtil.java b/searchIndex-platform/module/searchindex-elasticsearch/src/main/java/org/sunbird/searchindex/elasticsearch/ElasticSearchUtil.java index 771a009476..72e3914707 100644 --- a/searchIndex-platform/module/searchindex-elasticsearch/src/main/java/org/sunbird/searchindex/elasticsearch/ElasticSearchUtil.java +++ b/searchIndex-platform/module/searchindex-elasticsearch/src/main/java/org/sunbird/searchindex/elasticsearch/ElasticSearchUtil.java @@ -275,6 +275,7 @@ public static void bulkIndexWithIndexId(String indexName, String documentType, M .source((Map) jsonObjects.get(key))); if (count % BATCH_SIZE == 0 || (count % BATCH_SIZE < BATCH_SIZE && count == jsonObjects.size())) { BulkResponse bulkResponse = client.bulk(request); + System.out.println("ElasticSearchUtil: bulkIndexWithIndexId: bulkResponse: " + bulkResponse.status()); if (bulkResponse.hasFailures()) { TelemetryManager .log("Failures in Elasticsearch bulkIndex : " + bulkResponse.buildFailureMessage()); From 8842941c7a1d422a6f7ddbdd9bf07c042b222246 Mon Sep 17 00:00:00 2001 From: Jayaprakash8887 Date: Tue, 20 Dec 2022 18:23:37 +0530 Subject: [PATCH 146/222] Issue #KN-427 feat: Sync Tool Update --- .../org/sunbird/searchindex/elasticsearch/ElasticSearchUtil.java | 1 + 1 file changed, 1 insertion(+) diff --git a/searchIndex-platform/module/searchindex-elasticsearch/src/main/java/org/sunbird/searchindex/elasticsearch/ElasticSearchUtil.java b/searchIndex-platform/module/searchindex-elasticsearch/src/main/java/org/sunbird/searchindex/elasticsearch/ElasticSearchUtil.java index 72e3914707..066fe07d48 100644 --- a/searchIndex-platform/module/searchindex-elasticsearch/src/main/java/org/sunbird/searchindex/elasticsearch/ElasticSearchUtil.java +++ b/searchIndex-platform/module/searchindex-elasticsearch/src/main/java/org/sunbird/searchindex/elasticsearch/ElasticSearchUtil.java @@ -274,6 +274,7 @@ public static void bulkIndexWithIndexId(String indexName, String documentType, M request.add(new IndexRequest(indexName, documentType, key) .source((Map) jsonObjects.get(key))); if (count % BATCH_SIZE == 0 || (count % BATCH_SIZE < BATCH_SIZE && count == jsonObjects.size())) { + System.out.println("ElasticSearchUtil: bulkIndexWithIndexId: request: " + request); BulkResponse bulkResponse = client.bulk(request); System.out.println("ElasticSearchUtil: bulkIndexWithIndexId: bulkResponse: " + bulkResponse.status()); if (bulkResponse.hasFailures()) { From f8580707c9a0edc315201c2f3a98d18d6d92459b Mon Sep 17 00:00:00 2001 From: Jayaprakash8887 Date: Tue, 20 Dec 2022 18:25:59 +0530 Subject: [PATCH 147/222] Issue #KN-427 feat: Sync Tool Update --- .../org/sunbird/searchindex/elasticsearch/ElasticSearchUtil.java | 1 + 1 file changed, 1 insertion(+) diff --git a/searchIndex-platform/module/searchindex-elasticsearch/src/main/java/org/sunbird/searchindex/elasticsearch/ElasticSearchUtil.java b/searchIndex-platform/module/searchindex-elasticsearch/src/main/java/org/sunbird/searchindex/elasticsearch/ElasticSearchUtil.java index 066fe07d48..920ab0560a 100644 --- a/searchIndex-platform/module/searchindex-elasticsearch/src/main/java/org/sunbird/searchindex/elasticsearch/ElasticSearchUtil.java +++ b/searchIndex-platform/module/searchindex-elasticsearch/src/main/java/org/sunbird/searchindex/elasticsearch/ElasticSearchUtil.java @@ -265,6 +265,7 @@ public static List getMultiDocumentAsStringByIdList(String indexName, St public static void bulkIndexWithIndexId(String indexName, String documentType, Map jsonObjects) throws Exception { if (isIndexExists(indexName)) { + System.out.println("ElasticSearchUtil: bulkIndexWithIndexId: indexName: " + indexName); RestHighLevelClient client = getClient(indexName); if (!jsonObjects.isEmpty()) { int count = 0; From 430d20799fda2884f15e0843f1647a3fa81a7ca7 Mon Sep 17 00:00:00 2001 From: Jayaprakash8887 Date: Wed, 21 Dec 2022 11:53:20 +0530 Subject: [PATCH 148/222] Issue #KN-427 feat: Sync Tool Update --- ansible/roles/lp-synctool-deploy/templates/application.conf.j2 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ansible/roles/lp-synctool-deploy/templates/application.conf.j2 b/ansible/roles/lp-synctool-deploy/templates/application.conf.j2 index aa6e2c3920..5b590cd909 100644 --- a/ansible/roles/lp-synctool-deploy/templates/application.conf.j2 +++ b/ansible/roles/lp-synctool-deploy/templates/application.conf.j2 @@ -46,7 +46,7 @@ search.batch.size=500 search.connection.timeout=30 search.index.name="{{ compositesearch_index_name }}" -nested.fields=["badgeAssertions","targets","badgeAssociations","plugins","me_totalTimeSpent","me_totalPlaySessionCount","me_totalTimeSpentInSec","batches","trackable","credentials"] +nested.fields=["badgeAssertions", "targets", "badgeAssociations", "plugins", "me_totalTimeSpent", "me_totalPlaySessionCount", "me_totalTimeSpentInSec", "batches", "trackable", "credentials", "discussionForum", "provider", "osMetadata", "actions", "transcripts", "accessibility"] channel.default="in.ekstep" # Cassandra Configurations From 2a59f1c8da3231ba0218cbe73f638a72c5e22519 Mon Sep 17 00:00:00 2001 From: Jayaprakash8887 Date: Thu, 22 Dec 2022 18:13:21 +0530 Subject: [PATCH 149/222] Issue #ED-591 fix: Content publish API fix --- .../sunbird/content/validator/ContentValidator.java | 7 +++++++ .../service/src/main/resources/application.conf | 10 ++++++++++ 2 files changed, 17 insertions(+) diff --git a/platform-modules/content-manager/src/main/java/org/sunbird/content/validator/ContentValidator.java b/platform-modules/content-manager/src/main/java/org/sunbird/content/validator/ContentValidator.java index 7b15a87825..3567ad7aaa 100644 --- a/platform-modules/content-manager/src/main/java/org/sunbird/content/validator/ContentValidator.java +++ b/platform-modules/content-manager/src/main/java/org/sunbird/content/validator/ContentValidator.java @@ -478,6 +478,13 @@ private boolean isAllRequiredFieldsAvailable(Node node) { */ public Boolean isValidUrl(String fileURL, String mimeType) { Boolean isValid = false; + + String strBlobPrefix = Platform.config.hasPath("cloudstorage.relative_path_prefix")? Platform.config.getString("cloudstorage.relative_path_prefix"): "CONTENT_STORAGE_BASE_PATH"; + if(fileURL.contains(strBlobPrefix)) { + String absolutePath = Platform.config.getString("cloudstorage.read_base_path") + java.io.File.separator + Platform.config.getString("cloud_storage_container"); + fileURL = StringUtils.replace(fileURL,strBlobPrefix,absolutePath); + } + File file = HttpDownloadUtility.downloadFile(fileURL, BUNDLE_PATH); try { if (exceptionChecks(mimeType, file)) { diff --git a/platform-modules/service/src/main/resources/application.conf b/platform-modules/service/src/main/resources/application.conf index 5c057273cd..8daa6343de 100644 --- a/platform-modules/service/src/main/resources/application.conf +++ b/platform-modules/service/src/main/resources/application.conf @@ -221,3 +221,13 @@ content.tagging.property="subject,medium" # This is added to handle large artifacts sizes differently content.artifact.size.for_online=209715200 + + +cloudstorage { + metadata.replace_absolute_path=false + relative_path_prefix={{ cloudstorage_relative_path_prefix_content }} + metadata.list={{ cloudstorage_metadata_list }} + read_base_path="{{ cloudstorage_base_path }}" + write_base_path={{ valid_cloudstorage_base_urls }} +} +cloud_storage_container="{{ cloud_storage_content_bucketname }}" \ No newline at end of file From f0e6bd81557391828d8d2bcb3d21f2e078c2d5d5 Mon Sep 17 00:00:00 2001 From: Jayaprakash8887 Date: Fri, 23 Dec 2022 09:58:20 +0530 Subject: [PATCH 150/222] Issue #KN-445 fix: Collection publish API fix --- ansible/roles/setup-kafka/defaults/main.yml | 16 +++++++++++++--- .../helm_charts/datapipeline_jobs/values.j2 | 4 ++-- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/ansible/roles/setup-kafka/defaults/main.yml b/ansible/roles/setup-kafka/defaults/main.yml index e69e140e6f..cee780aab3 100644 --- a/ansible/roles/setup-kafka/defaults/main.yml +++ b/ansible/roles/setup-kafka/defaults/main.yml @@ -155,8 +155,13 @@ processing_kafka_topics: replication_factor: 1 - name: assessment.postpublish.request num_of_partitions: 1 - replication_factor: 1 - + replication_factor: 1 + - name: republish.events.failed + num_of_partitions: 1 + replication_factor: 1 + - name: republish.events.skipped + num_of_partitions: 1 + replication_factor: 1 processing_kafka_overriden_topics: - name: telemetry.raw @@ -307,4 +312,9 @@ processing_kafka_overriden_topics: - name: assessment.postpublish.request retention_time: 1209600000 replication_factor: 1 - + - name: republish.events.failed + retention_time: 604800000 + replication_factor: 1 + - name: republish.events.skipped + retention_time: 604800000 + replication_factor: 1 \ No newline at end of file diff --git a/kubernetes/helm_charts/datapipeline_jobs/values.j2 b/kubernetes/helm_charts/datapipeline_jobs/values.j2 index efc8e9f991..991f4469e7 100644 --- a/kubernetes/helm_charts/datapipeline_jobs/values.j2 +++ b/kubernetes/helm_charts/datapipeline_jobs/values.j2 @@ -848,8 +848,8 @@ live-node-publisher: kafka { input.topic = {{ env_name }}.republish.job.request live_video_stream.topic = "{{ env_name }}.live.video.stream.request" - error.topic = "{{ env_name }}.learning.events.failed" - skipped.topic = "{{ env_name }}.learning.events.skipped" + error.topic = "{{ env_name }}.republish.events.failed" + skipped.topic = "{{ env_name }}.republish.events.skipped" groupId = {{ env_name }}-content-republish-group } task { From f8611bb122f15bceb8b7bb21e0f78da8c0c73454 Mon Sep 17 00:00:00 2001 From: Jayaprakash8887 Date: Thu, 29 Dec 2022 12:47:41 +0530 Subject: [PATCH 151/222] Issue #KN-445 fix: MigrateCSPDataCommand enhancement with migrationVersion Input --- .../sunbird/learning/util/ControllerUtil.java | 24 +++++++++++++++---- .../mgr/CSPMigrationMessageGenerator.java | 6 ++--- .../tool/shell/MigrateCSPDataCommand.java | 3 ++- 3 files changed, 25 insertions(+), 8 deletions(-) diff --git a/platform-modules/actors/src/main/java/org/sunbird/learning/util/ControllerUtil.java b/platform-modules/actors/src/main/java/org/sunbird/learning/util/ControllerUtil.java index b0b77942ff..baf5a113db 100644 --- a/platform-modules/actors/src/main/java/org/sunbird/learning/util/ControllerUtil.java +++ b/platform-modules/actors/src/main/java/org/sunbird/learning/util/ControllerUtil.java @@ -412,13 +412,15 @@ public List getNodes(String graphId, String objectType, int startPosition, } } - public List getNodes(String graphId, String objectType, List mimeTypes, List status, int startPosition, int batchSize) { + public List getNodes(String graphId, String objectType, List mimeTypes, List status, double migrationVersion, int startPosition, int batchSize) { List filters = new ArrayList(); if(!mimeTypes.isEmpty()) filters.add(new Filter("mimeType", SearchConditions.OP_IN, mimeTypes)); if(!status.isEmpty()) filters.add(new Filter("status", SearchConditions.OP_IN, status)); - filters.add(new Filter("migrationVersion", SearchConditions.OP_IS, Values.NULL)); + if(migrationVersion == 0) filters.add(new Filter("migrationVersion", SearchConditions.OP_IS, Values.NULL)); + else filters.add(new Filter("migrationVersion", SearchConditions.OP_EQUAL, migrationVersion)); + SearchCriteria sc = new SearchCriteria(); sc.setNodeType(SystemNodeTypes.DATA_NODE.name()); sc.setObjectType(objectType); @@ -790,10 +792,24 @@ public void hierarchyCleanUp(Map map) { } } - public Map getCSPMigrationObjectCount(String graphId, List objectTypes) { + public Map getCSPMigrationObjectCount(String graphId, List objectTypes, List mimeTypeList, List statusList, double migrationVersion) { Map counts = new HashMap(); Request request = getRequest(graphId, GraphEngineManagers.SEARCH_MANAGER, "executeQueryForProps"); - request.put(GraphDACParams.query.name(), MessageFormat.format("MATCH (n:{0}) WHERE EXISTS(n.IL_FUNC_OBJECT_TYPE) AND n.IL_SYS_NODE_TYPE=\"DATA_NODE\" AND n.IL_FUNC_OBJECT_TYPE IN {1} AND NOT EXISTS(n.migrationVersion) RETURN n.IL_FUNC_OBJECT_TYPE AS objectType, COUNT(n) AS count;", graphId, new JSONArray(objectTypes))); + StringBuilder queryString = new StringBuilder(); + queryString.append("MATCH (n:{0}) WHERE EXISTS(n.IL_FUNC_OBJECT_TYPE) AND n.IL_SYS_NODE_TYPE=\"DATA_NODE\" AND n.IL_FUNC_OBJECT_TYPE IN {1} "); + + if(migrationVersion == 0) queryString.append(" AND NOT EXISTS(n.migrationVersion) "); + else queryString.append(" AND n.migrationVersion={4} "); + + if(mimeTypeList!=null && !mimeTypeList.isEmpty()) + queryString.append(" AND n.mimeType IN {2} "); + + if(statusList!=null && !statusList.isEmpty()) + queryString.append(" AND n.status IN {3} "); + + queryString.append("RETURN n.IL_FUNC_OBJECT_TYPE AS objectType, COUNT(n) AS count;"); + request.put(GraphDACParams.query.name(), MessageFormat.format(queryString.toString(), graphId, new JSONArray(objectTypes), new JSONArray(mimeTypeList), new JSONArray(statusList), migrationVersion)); + List props = new ArrayList(); props.add("objectType"); props.add("count"); diff --git a/platform-tools/spikes/sync-tool/src/main/java/org/sunbird/sync/tool/mgr/CSPMigrationMessageGenerator.java b/platform-tools/spikes/sync-tool/src/main/java/org/sunbird/sync/tool/mgr/CSPMigrationMessageGenerator.java index 504f83ca2d..5d4d5fe393 100644 --- a/platform-tools/spikes/sync-tool/src/main/java/org/sunbird/sync/tool/mgr/CSPMigrationMessageGenerator.java +++ b/platform-tools/spikes/sync-tool/src/main/java/org/sunbird/sync/tool/mgr/CSPMigrationMessageGenerator.java @@ -42,7 +42,7 @@ private void init() throws Exception { batchSize = batch; } - public void generateMgrMsg(String graphId, String[] objectTypes, String[] mimeTypes, String[] status, Integer limit, Integer delay) throws Exception { + public void generateMgrMsg(String graphId, String[] objectTypes, String[] mimeTypes, String[] status, double migrationVersion, Integer limit, Integer delay) throws Exception { if (StringUtils.isBlank(graphId)) throw new ClientException("ERR_INVALID_GRAPH_ID", "Graph Id is blank."); if (null == objectTypes || objectTypes.length == 0) @@ -57,7 +57,7 @@ public void generateMgrMsg(String graphId, String[] objectTypes, String[] mimeTy long startTime = System.currentTimeMillis(); System.out.println("-----------------------------------------"); System.out.println("\nMigration Event Generation starting at " + startTime); - Map counts = util.getCSPMigrationObjectCount(graphId, Arrays.asList(objectTypes)); + Map counts = util.getCSPMigrationObjectCount(graphId, Arrays.asList(objectTypes), mimeTypeList, statusList, migrationVersion); if (counts.isEmpty()) { System.out.println("No objects found in this graph."); } else { @@ -89,7 +89,7 @@ public void generateMgrMsg(String graphId, String[] objectTypes, String[] mimeTy while (found && start < stopLimit) { List nodes = null; try { - nodes = util.getNodes(graphId, objectType.trim(), mimeTypeList, statusList, start, batchSize); + nodes = util.getNodes(graphId, objectType.trim(), mimeTypeList, statusList, migrationVersion, start, batchSize); } catch (ResourceNotFoundException e) { System.out.println("Error while fetching neo4j records for objectType=" + objectType + ", start=" + start + ",batchSize=" + batchSize); start += batchSize; diff --git a/platform-tools/spikes/sync-tool/src/main/java/org/sunbird/sync/tool/shell/MigrateCSPDataCommand.java b/platform-tools/spikes/sync-tool/src/main/java/org/sunbird/sync/tool/shell/MigrateCSPDataCommand.java index 4606e9eafa..fe990fed8a 100644 --- a/platform-tools/spikes/sync-tool/src/main/java/org/sunbird/sync/tool/shell/MigrateCSPDataCommand.java +++ b/platform-tools/spikes/sync-tool/src/main/java/org/sunbird/sync/tool/shell/MigrateCSPDataCommand.java @@ -22,6 +22,7 @@ public void migrateCSPData( @CliOption(key = {"objectType"}, mandatory = true, help = "Object Type is Required") final String[] objectType, @CliOption(key = {"mimeType"}, mandatory = false, help = "mimeTypes can be provided") final String[] mimeType, @CliOption(key = {"status"}, mandatory = false, help = "Specific Status can be passed") final String[] status, + @CliOption(key = {"migrationVersion"}, mandatory = false, unspecifiedDefaultValue = "0", help = "Specific migration version can be passed") final double migrationVersion, @CliOption(key = {"limit"}, mandatory = false, unspecifiedDefaultValue = "0", help = "Specific Limit can be passed") final Integer limit, @CliOption(key = {"delay"}, mandatory = false, unspecifiedDefaultValue = "10", help = "time gap between each batch") final Integer delay) throws Exception { @@ -29,7 +30,7 @@ public void migrateCSPData( long startTime = System.currentTimeMillis(); DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm:ss"); LocalDateTime start = LocalDateTime.now(); - cspMsgGenerator.generateMgrMsg(graphId, objectType, mimeType, status, limit, delay); + cspMsgGenerator.generateMgrMsg(graphId, objectType, mimeType, status, migrationVersion, limit, delay); long endTime = System.currentTimeMillis(); long exeTime = endTime - startTime; System.out.println("Total time of execution: " + exeTime + "ms"); From e224866253927edbd9a2a8d9e67e5994852c12a5 Mon Sep 17 00:00:00 2001 From: santhosh-tg Date: Thu, 29 Dec 2022 18:13:33 +0530 Subject: [PATCH 152/222] Add gcp service account --- ansible/artifacts-download.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ansible/artifacts-download.yml b/ansible/artifacts-download.yml index ce8b9455fb..4477368ec4 100644 --- a/ansible/artifacts-download.yml +++ b/ansible/artifacts-download.yml @@ -21,6 +21,8 @@ name: gcp-cloud-storage tasks_from: download.yml vars: + gcp_storage_service_account_name: "{{ cloud_artifact_storage_accountname }}" + gcp_storage_key_file: "{{ cloud_artifact_storage_secret }}" gcp_bucket_name: "{{ cloud_storage_artifacts_bucketname }}" gcp_path: "{{ artifact }}" local_file_or_folder_path: "{{ artifact_path }}" From 23360584406760231ab96632be591aa054d9ac99 Mon Sep 17 00:00:00 2001 From: anilgupta Date: Thu, 29 Dec 2022 18:39:46 +0530 Subject: [PATCH 153/222] Issue #KN-439 chore: Added the transcripts in cloudstorage_metadata_list. --- kubernetes/ansible/roles/flink-jobs-deploy/defaults/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kubernetes/ansible/roles/flink-jobs-deploy/defaults/main.yml b/kubernetes/ansible/roles/flink-jobs-deploy/defaults/main.yml index ee64877a5e..4edf7c30f4 100644 --- a/kubernetes/ansible/roles/flink-jobs-deploy/defaults/main.yml +++ b/kubernetes/ansible/roles/flink-jobs-deploy/defaults/main.yml @@ -399,4 +399,4 @@ questionset_hierarchy_keyspace_name: "{{ hierarchy_keyspace_name }}" cloudstorage_relative_path_prefix_content: "CONTENT_STORAGE_BASE_PATH" cloudstorage_relative_path_prefix_dial: "DIAL_STORAGE_BASE_PATH" -cloudstorage_metadata_list: '["appIcon", "artifactUrl", "posterImage", "previewUrl", "thumbnail", "assetsMap", "certTemplate", "itemSetPreviewUrl", "grayScaleAppIcon", "sourceURL", "variants", "downloadUrl", "streamingUrl", "toc_url", "data", "question", "solutions", "editorState", "media", "pdfUrl"]' +cloudstorage_metadata_list: '["appIcon", "artifactUrl", "posterImage", "previewUrl", "thumbnail", "assetsMap", "certTemplate", "itemSetPreviewUrl", "grayScaleAppIcon", "sourceURL", "variants", "downloadUrl", "streamingUrl", "toc_url", "data", "question", "solutions", "editorState", "media", "pdfUrl", "transcripts"]' From 7b5510d0bdc9fd715fa33785cc2677b5e51bf673 Mon Sep 17 00:00:00 2001 From: santhosh-tg Date: Fri, 30 Dec 2022 10:17:55 +0530 Subject: [PATCH 154/222] Add gcp vars for service accounts --- ansible/artifacts-upload.yml | 2 ++ ansible/roles/cassandra-backup/tasks/main.yml | 2 ++ ansible/roles/cassandra-restore/tasks/main.yml | 4 +++- ansible/roles/gcp-cloud-storage/defaults/main.yml | 5 +++++ ansible/roles/neo4j-backup/tasks/main.yml | 2 ++ ansible/roles/neo4j-restore/tasks/main.yml | 2 ++ ansible/roles/redis-backup/tasks/main.yml | 2 ++ ansible/roles/redis-restore/tasks/main.yml | 2 ++ 8 files changed, 20 insertions(+), 1 deletion(-) diff --git a/ansible/artifacts-upload.yml b/ansible/artifacts-upload.yml index acdf917c85..99833f9d10 100644 --- a/ansible/artifacts-upload.yml +++ b/ansible/artifacts-upload.yml @@ -22,6 +22,8 @@ name: gcp-cloud-storage tasks_from: upload.yml vars: + gcp_storage_service_account_name: "{{ cloud_artifact_storage_accountname }}" + gcp_storage_key_file: "{{ cloud_artifact_storage_secret }}" gcp_bucket_name: "{{ cloud_storage_artifacts_bucketname }}" gcp_path: "{{ artifact }}" local_file_or_folder_path: "{{ artifact_path }}" diff --git a/ansible/roles/cassandra-backup/tasks/main.yml b/ansible/roles/cassandra-backup/tasks/main.yml index 9906b6b3cc..9b0e0875ff 100755 --- a/ansible/roles/cassandra-backup/tasks/main.yml +++ b/ansible/roles/cassandra-backup/tasks/main.yml @@ -63,6 +63,8 @@ name: gcp-cloud-storage tasks_from: upload-batch.yml vars: + gcp_storage_service_account_name: "{{ cloud_management_storage_accountname }}" + gcp_storage_key_file: "{{ cloud_management_storage_secret }}" gcp_bucket_name: "{{ cloud_storage_cassandrabackup_bucketname }}" gcp_path: "{{ cloud_storage_cassandrabackup_foldername }}" local_file_or_folder_path: "/data/cassandra/backup/{{ cassandra_backup_folder_name }}" diff --git a/ansible/roles/cassandra-restore/tasks/main.yml b/ansible/roles/cassandra-restore/tasks/main.yml index 42a69df939..95a193be8f 100755 --- a/ansible/roles/cassandra-restore/tasks/main.yml +++ b/ansible/roles/cassandra-restore/tasks/main.yml @@ -37,8 +37,10 @@ name: gcp-cloud-storage tasks_from: download.yml vars: + gcp_storage_service_account_name: "{{ cloud_management_storage_accountname }}" + gcp_storage_key_file: "{{ cloud_management_storage_secret }}" gcp_bucket_name: "{{ cloud_storage_cassandrabackup_bucketname }}" - s3_path: "{{ cloud_storage_cassandrabackup_foldername }}/{{ cassandra_restore_file_name }}" + gcp_path: "{{ cloud_storage_cassandrabackup_foldername }}/{{ cassandra_restore_file_name }}" local_file_or_folder_path: "{{restore_path}}/{{ cassandra_restore_file_name }}" when: cloud_service_provider == "gcloud" diff --git a/ansible/roles/gcp-cloud-storage/defaults/main.yml b/ansible/roles/gcp-cloud-storage/defaults/main.yml index b0fd847b26..a9f4247d42 100644 --- a/ansible/roles/gcp-cloud-storage/defaults/main.yml +++ b/ansible/roles/gcp-cloud-storage/defaults/main.yml @@ -1,3 +1,8 @@ +# GCP service account name +# Example - +# gcp_storage_service_account_name: test@sunbird.iam.gserviceaccount.com +gcp_storage_service_account_name: "" + # GCP bucket name # Example - # bucket_name: "sunbird-dev-public" diff --git a/ansible/roles/neo4j-backup/tasks/main.yml b/ansible/roles/neo4j-backup/tasks/main.yml index 395b77fd86..fa230974d7 100755 --- a/ansible/roles/neo4j-backup/tasks/main.yml +++ b/ansible/roles/neo4j-backup/tasks/main.yml @@ -56,6 +56,8 @@ name: gcp-cloud-storage tasks_from: upload.yml vars: + gcp_storage_service_account_name: "{{ cloud_management_storage_accountname }}" + gcp_storage_key_file: "{{ cloud_management_storage_secret }}" gcp_bucket_name: "{{ cloud_storage_neo4jbackup_bucketname }}" gcp_path: "{{ cloud_storage_neo4jbackup_foldername }}/{{ var1.stdout }}" local_file_or_folder_path: "/home/learning/backup/{{ var1.stdout }}" diff --git a/ansible/roles/neo4j-restore/tasks/main.yml b/ansible/roles/neo4j-restore/tasks/main.yml index 82a485f09e..1e79c759b1 100644 --- a/ansible/roles/neo4j-restore/tasks/main.yml +++ b/ansible/roles/neo4j-restore/tasks/main.yml @@ -35,6 +35,8 @@ name: gcp-cloud-storage tasks_from: download.yml vars: + gcp_storage_service_account_name: "{{ cloud_management_storage_accountname }}" + gcp_storage_key_file: "{{ cloud_management_storage_secret }}" gcp_bucket_name: "{{ cloud_storage_neo4jbackup_bucketname }}" gcp_path: "{{ cloud_storage_neo4jbackup_foldername }}/{{ neo4j_backup_file_name }}" local_file_or_folder_path: "{{ neo4j_backup_file_path }}" diff --git a/ansible/roles/redis-backup/tasks/main.yml b/ansible/roles/redis-backup/tasks/main.yml index fe49b512b7..c73c524221 100644 --- a/ansible/roles/redis-backup/tasks/main.yml +++ b/ansible/roles/redis-backup/tasks/main.yml @@ -46,6 +46,8 @@ name: gcp-cloud-storage tasks_from: upload.yml vars: + gcp_storage_service_account_name: "{{ cloud_management_storage_accountname }}" + gcp_storage_key_file: "{{ cloud_management_storage_secret }}" gcp_bucket_name: "{{ cloud_storage_redisbackup_bucketname }}" gcp_path: "{{ cloud_storage_redisbackup_foldername }}/{{ redis_backup_file_name }}" local_file_or_folder_path: "{{ redis_backup_file_path }}" diff --git a/ansible/roles/redis-restore/tasks/main.yml b/ansible/roles/redis-restore/tasks/main.yml index 4b8916872c..e073c1d178 100644 --- a/ansible/roles/redis-restore/tasks/main.yml +++ b/ansible/roles/redis-restore/tasks/main.yml @@ -30,6 +30,8 @@ name: gcp-cloud-storage tasks_from: download.yml vars: + gcp_storage_service_account_name: "{{ cloud_management_storage_accountname }}" + gcp_storage_key_file: "{{ cloud_management_storage_secret }}" gcp_bucket_name: "{{ cloud_storage_redisbackup_bucketname }}" gcp_path: "{{ cloud_storage_redisbackup_foldername }}/{{ redis_restore_file_name }}" local_file_or_folder_path: "/tmp/{{ redis_restore_file_name }}" From ce23f479c8f34bf27c3f4b6f19f196dfc21e45c1 Mon Sep 17 00:00:00 2001 From: Jayaprakash8887 Date: Fri, 30 Dec 2022 12:20:56 +0530 Subject: [PATCH 155/222] Issue #CO-192 fix: Removing posterImage from VDN import metadata --- kubernetes/helm_charts/datapipeline_jobs/values.j2 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kubernetes/helm_charts/datapipeline_jobs/values.j2 b/kubernetes/helm_charts/datapipeline_jobs/values.j2 index 991f4469e7..46c6e33903 100644 --- a/kubernetes/helm_charts/datapipeline_jobs/values.j2 +++ b/kubernetes/helm_charts/datapipeline_jobs/values.j2 @@ -450,7 +450,7 @@ content-auto-creator: allowed_object_types=["Content"] allowed_content_stages=["create","upload","review","publish"] content_mandatory_fields=["name","code","mimeType","primaryCategory","artifactUrl","lastPublishedBy"] - content_props_to_removed=["identifier","downloadUrl","variants","createdOn","collections","children","lastUpdatedOn","SYS_INTERNAL_LAST_UPDATED_ON","versionKey","s3Key","status","pkgVersion","toc_url","mimeTypesCount","contentTypesCount","leafNodesCount","childNodes","prevState","lastPublishedOn","flagReasons","compatibilityLevel","size","publishChecklist","publishComment","lastPublishedBy","rejectReasons","rejectComment","badgeAssertions","leafNodes","sYS_INTERNAL_LAST_UPDATED_ON","previewUrl","channel","objectType","visibility","version","pragma","prevStatus","streamingUrl","idealScreenSize","contentDisposition","lastStatusChangedOn","idealScreenDensity","lastSubmittedOn","publishError","flaggedBy","flags","lastFlaggedOn","publisher","lastUpdatedBy","lastSubmittedBy","uploadError","lockKey","publish_type","reviewError","totalCompressedSize","origin","originData","importError","questions"] + content_props_to_removed=["identifier","downloadUrl","variants","createdOn","collections","children","lastUpdatedOn","SYS_INTERNAL_LAST_UPDATED_ON","versionKey","s3Key","status","pkgVersion","toc_url","mimeTypesCount","contentTypesCount","leafNodesCount","childNodes","prevState","lastPublishedOn","flagReasons","compatibilityLevel","size","publishChecklist","publishComment","lastPublishedBy","rejectReasons","rejectComment","badgeAssertions","leafNodes","sYS_INTERNAL_LAST_UPDATED_ON","previewUrl","channel","objectType","visibility","version","pragma","prevStatus","streamingUrl","idealScreenSize","contentDisposition","lastStatusChangedOn","idealScreenDensity","lastSubmittedOn","publishError","flaggedBy","flags","lastFlaggedOn","publisher","lastUpdatedBy","lastSubmittedBy","uploadError","lockKey","publish_type","reviewError","totalCompressedSize","origin","originData","importError","questions","posterImage"] bulk_upload_mime_types=["video/mp4"] artifact_upload_max_size=157286400 content_create_props=["name","code","mimeType","contentType","framework","processId","primaryCategory"] From 1d770ed4eaeaae3e0752612a1ae4a61e1462c3f0 Mon Sep 17 00:00:00 2001 From: Jayaprakash8887 Date: Fri, 30 Dec 2022 12:49:12 +0530 Subject: [PATCH 156/222] Issue #CO-192 fix: Removing posterImage from VDN import metadata --- kubernetes/helm_charts/datapipeline_jobs/values.j2 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kubernetes/helm_charts/datapipeline_jobs/values.j2 b/kubernetes/helm_charts/datapipeline_jobs/values.j2 index 991f4469e7..b51e617495 100644 --- a/kubernetes/helm_charts/datapipeline_jobs/values.j2 +++ b/kubernetes/helm_charts/datapipeline_jobs/values.j2 @@ -450,7 +450,7 @@ content-auto-creator: allowed_object_types=["Content"] allowed_content_stages=["create","upload","review","publish"] content_mandatory_fields=["name","code","mimeType","primaryCategory","artifactUrl","lastPublishedBy"] - content_props_to_removed=["identifier","downloadUrl","variants","createdOn","collections","children","lastUpdatedOn","SYS_INTERNAL_LAST_UPDATED_ON","versionKey","s3Key","status","pkgVersion","toc_url","mimeTypesCount","contentTypesCount","leafNodesCount","childNodes","prevState","lastPublishedOn","flagReasons","compatibilityLevel","size","publishChecklist","publishComment","lastPublishedBy","rejectReasons","rejectComment","badgeAssertions","leafNodes","sYS_INTERNAL_LAST_UPDATED_ON","previewUrl","channel","objectType","visibility","version","pragma","prevStatus","streamingUrl","idealScreenSize","contentDisposition","lastStatusChangedOn","idealScreenDensity","lastSubmittedOn","publishError","flaggedBy","flags","lastFlaggedOn","publisher","lastUpdatedBy","lastSubmittedBy","uploadError","lockKey","publish_type","reviewError","totalCompressedSize","origin","originData","importError","questions"] + content_props_to_removed=["identifier","downloadUrl","variants","createdOn","collections","children","lastUpdatedOn","SYS_INTERNAL_LAST_UPDATED_ON","versionKey","s3Key","status","pkgVersion","toc_url","mimeTypesCount","contentTypesCount","leafNodesCount","childNodes","prevState","lastPublishedOn","flagReasons","compatibilityLevel","size","publishChecklist","publishComment","lastPublishedBy","rejectReasons","rejectComment","badgeAssertions","leafNodes","sYS_INTERNAL_LAST_UPDATED_ON","previewUrl","channel","objectType","visibility","version","pragma","prevStatus","streamingUrl","idealScreenSize","contentDisposition","lastStatusChangedOn","idealScreenDensity","lastSubmittedOn","publishError","flaggedBy","flags","lastFlaggedOn","publisher","lastUpdatedBy","lastSubmittedBy","uploadError","lockKey","publish_type","reviewError","totalCompressedSize","origin","originData","importError","questions","posterImage] bulk_upload_mime_types=["video/mp4"] artifact_upload_max_size=157286400 content_create_props=["name","code","mimeType","contentType","framework","processId","primaryCategory"] From 8c0f2ae5ba5e8211c1c7e96ef00394382f436b72 Mon Sep 17 00:00:00 2001 From: Jayaprakash8887 Date: Mon, 2 Jan 2023 15:22:45 +0530 Subject: [PATCH 157/222] Issue #KN-445 fix: MigrateCSPDataCommand enhancement with migrationVersion --- .../java/org/sunbird/learning/util/ControllerUtil.java | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/platform-modules/actors/src/main/java/org/sunbird/learning/util/ControllerUtil.java b/platform-modules/actors/src/main/java/org/sunbird/learning/util/ControllerUtil.java index baf5a113db..448a95085b 100644 --- a/platform-modules/actors/src/main/java/org/sunbird/learning/util/ControllerUtil.java +++ b/platform-modules/actors/src/main/java/org/sunbird/learning/util/ControllerUtil.java @@ -798,16 +798,19 @@ public Map getCSPMigrationObjectCount(String graphId, List StringBuilder queryString = new StringBuilder(); queryString.append("MATCH (n:{0}) WHERE EXISTS(n.IL_FUNC_OBJECT_TYPE) AND n.IL_SYS_NODE_TYPE=\"DATA_NODE\" AND n.IL_FUNC_OBJECT_TYPE IN {1} "); - if(migrationVersion == 0) queryString.append(" AND NOT EXISTS(n.migrationVersion) "); - else queryString.append(" AND n.migrationVersion={4} "); - if(mimeTypeList!=null && !mimeTypeList.isEmpty()) queryString.append(" AND n.mimeType IN {2} "); if(statusList!=null && !statusList.isEmpty()) queryString.append(" AND n.status IN {3} "); + if(migrationVersion == 0) queryString.append(" AND NOT EXISTS(n.migrationVersion) "); + else queryString.append(" AND n.migrationVersion={4} "); + queryString.append("RETURN n.IL_FUNC_OBJECT_TYPE AS objectType, COUNT(n) AS count;"); + + System.out.println("Count queryString:: " + MessageFormat.format(queryString.toString(), graphId, new JSONArray(objectTypes), new JSONArray(mimeTypeList), new JSONArray(statusList), migrationVersion)); + request.put(GraphDACParams.query.name(), MessageFormat.format(queryString.toString(), graphId, new JSONArray(objectTypes), new JSONArray(mimeTypeList), new JSONArray(statusList), migrationVersion)); List props = new ArrayList(); From d75afbf9196d487722e67156ed1fc0731005cdd8 Mon Sep 17 00:00:00 2001 From: Jayaprakash8887 Date: Mon, 2 Jan 2023 21:57:39 +0530 Subject: [PATCH 158/222] Issue #CO-192 fix: Removing posterImage from VDN import metadata --- kubernetes/helm_charts/datapipeline_jobs/values.j2 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kubernetes/helm_charts/datapipeline_jobs/values.j2 b/kubernetes/helm_charts/datapipeline_jobs/values.j2 index b51e617495..46c6e33903 100644 --- a/kubernetes/helm_charts/datapipeline_jobs/values.j2 +++ b/kubernetes/helm_charts/datapipeline_jobs/values.j2 @@ -450,7 +450,7 @@ content-auto-creator: allowed_object_types=["Content"] allowed_content_stages=["create","upload","review","publish"] content_mandatory_fields=["name","code","mimeType","primaryCategory","artifactUrl","lastPublishedBy"] - content_props_to_removed=["identifier","downloadUrl","variants","createdOn","collections","children","lastUpdatedOn","SYS_INTERNAL_LAST_UPDATED_ON","versionKey","s3Key","status","pkgVersion","toc_url","mimeTypesCount","contentTypesCount","leafNodesCount","childNodes","prevState","lastPublishedOn","flagReasons","compatibilityLevel","size","publishChecklist","publishComment","lastPublishedBy","rejectReasons","rejectComment","badgeAssertions","leafNodes","sYS_INTERNAL_LAST_UPDATED_ON","previewUrl","channel","objectType","visibility","version","pragma","prevStatus","streamingUrl","idealScreenSize","contentDisposition","lastStatusChangedOn","idealScreenDensity","lastSubmittedOn","publishError","flaggedBy","flags","lastFlaggedOn","publisher","lastUpdatedBy","lastSubmittedBy","uploadError","lockKey","publish_type","reviewError","totalCompressedSize","origin","originData","importError","questions","posterImage] + content_props_to_removed=["identifier","downloadUrl","variants","createdOn","collections","children","lastUpdatedOn","SYS_INTERNAL_LAST_UPDATED_ON","versionKey","s3Key","status","pkgVersion","toc_url","mimeTypesCount","contentTypesCount","leafNodesCount","childNodes","prevState","lastPublishedOn","flagReasons","compatibilityLevel","size","publishChecklist","publishComment","lastPublishedBy","rejectReasons","rejectComment","badgeAssertions","leafNodes","sYS_INTERNAL_LAST_UPDATED_ON","previewUrl","channel","objectType","visibility","version","pragma","prevStatus","streamingUrl","idealScreenSize","contentDisposition","lastStatusChangedOn","idealScreenDensity","lastSubmittedOn","publishError","flaggedBy","flags","lastFlaggedOn","publisher","lastUpdatedBy","lastSubmittedBy","uploadError","lockKey","publish_type","reviewError","totalCompressedSize","origin","originData","importError","questions","posterImage"] bulk_upload_mime_types=["video/mp4"] artifact_upload_max_size=157286400 content_create_props=["name","code","mimeType","contentType","framework","processId","primaryCategory"] From 4b33d522e77b552af1a9ee3217b41a029cf055e3 Mon Sep 17 00:00:00 2001 From: Jayaprakash8887 Date: Tue, 3 Jan 2023 14:50:49 +0530 Subject: [PATCH 159/222] Issue #KN-427 fix: Bundle API issue fix. --- .../templates/application.conf.j2 | 9 +++++ .../dac/mgr/impl/Neo4JBoltSearchMgrImpl.java | 33 +++++++++++++++++++ .../src/main/resources/application.conf | 2 +- 3 files changed, 43 insertions(+), 1 deletion(-) diff --git a/ansible/roles/learning-service/templates/application.conf.j2 b/ansible/roles/learning-service/templates/application.conf.j2 index fd0be0da3b..27feedd67e 100644 --- a/ansible/roles/learning-service/templates/application.conf.j2 +++ b/ansible/roles/learning-service/templates/application.conf.j2 @@ -304,3 +304,12 @@ content.tagging.property="subject,medium" # Search Service Config kp.search_service.base_url="{{ kp_search_service_base_url }}" + +# CNAME migration variables +cloudstorage { + metadata.replace_absolute_path=true + relative_path_prefix={{ cloudstorage_relative_path_prefix_content }} + metadata.list={{ cloudstorage_metadata_list }} + read_base_path="{{ cloudstorage_base_path }}" + write_base_path={{ valid_cloudstorage_base_urls }} +} \ No newline at end of file diff --git a/platform-core/graph-engine/module/graph-dac/src/main/java/org/sunbird/graph/dac/mgr/impl/Neo4JBoltSearchMgrImpl.java b/platform-core/graph-engine/module/graph-dac/src/main/java/org/sunbird/graph/dac/mgr/impl/Neo4JBoltSearchMgrImpl.java index b330fd4328..d4b8a8a6c5 100644 --- a/platform-core/graph-engine/module/graph-dac/src/main/java/org/sunbird/graph/dac/mgr/impl/Neo4JBoltSearchMgrImpl.java +++ b/platform-core/graph-engine/module/graph-dac/src/main/java/org/sunbird/graph/dac/mgr/impl/Neo4JBoltSearchMgrImpl.java @@ -4,6 +4,7 @@ import java.util.List; import java.util.Map; +import org.sunbird.common.Platform; import org.sunbird.common.dto.Property; import org.sunbird.common.dto.Request; import org.sunbird.common.dto.Response; @@ -247,6 +248,8 @@ public Response searchNodes(Request request) { } else { try { List nodes = Neo4JBoltSearchOperations.searchNodes(graphId, searchCriteria, getTags, request); + boolean isrRelativePathEnabled = Platform.config.getBoolean("cloudstorage.metadata.replace_absolute_path"); + if(isrRelativePathEnabled) updateAbsolutePath(nodes); return OK(GraphDACParams.node_list.name(), nodes); } catch (Exception e) { return ERROR(e); @@ -321,4 +324,34 @@ public Response getSubGraph(Request request) { } } + private Node updateAbsolutePath(Node node) { + Map metadata = updateAbsolutePath(node.getMetadata()); + node.setMetadata(metadata); + return node; + } + + private java.util.List updateAbsolutePath(java.util.List nodes) { + for(Node node: nodes) { + updateAbsolutePath(node); + } + return nodes; + } + + private Map updateAbsolutePath(Map data) { + String relativePathPrefix = Platform.config.getString("cloudstorage.relative_path_prefix"); + List cspMeta = Platform.config.getStringList("cloudstorage.metadata.list"); + String absolutePath = Platform.config.getString("cloudstorage.read_base_path") + java.io.File.separator + Platform.config.getString("cloud_storage_container"); + if (data !=null && !data.isEmpty()) { + for (Map.Entry entry : data.entrySet()) { + if(cspMeta.contains(entry.getKey())) { + if(entry.getValue() instanceof String) { + data.replace(entry.getKey(), ((String) entry.getValue()).replaceAll(relativePathPrefix, absolutePath)); + } + } + } + } + return data; + } + + } diff --git a/platform-modules/service/src/main/resources/application.conf b/platform-modules/service/src/main/resources/application.conf index 8daa6343de..8e34631cff 100644 --- a/platform-modules/service/src/main/resources/application.conf +++ b/platform-modules/service/src/main/resources/application.conf @@ -224,7 +224,7 @@ content.artifact.size.for_online=209715200 cloudstorage { - metadata.replace_absolute_path=false + metadata.replace_absolute_path=true relative_path_prefix={{ cloudstorage_relative_path_prefix_content }} metadata.list={{ cloudstorage_metadata_list }} read_base_path="{{ cloudstorage_base_path }}" From 6ba10a4de2ec5afd36bcc623113b607bd29f5b4b Mon Sep 17 00:00:00 2001 From: Jayaprakash8887 Date: Tue, 3 Jan 2023 16:46:24 +0530 Subject: [PATCH 160/222] Issue #KN-427 fix: Bundle API issue fix. --- ansible/roles/learning-service/defaults/main.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/ansible/roles/learning-service/defaults/main.yml b/ansible/roles/learning-service/defaults/main.yml index ced3460453..91338cac25 100644 --- a/ansible/roles/learning-service/defaults/main.yml +++ b/ansible/roles/learning-service/defaults/main.yml @@ -51,3 +51,8 @@ tomcat_init_mem: -Xms1024m tomcat_max_mem: -Xmx4096m search_index_host: "{{ groups['composite-search-cluster']|join(':9200,')}}:9200" compositesearch_index_name: "compositesearch" + + +cloudstorage_relative_path_prefix_content: "CONTENT_STORAGE_BASE_PATH" +cloudstorage_relative_path_prefix_dial: "DIAL_STORAGE_BASE_PATH" +cloudstorage_metadata_list: '["appIcon", "artifactUrl", "posterImage", "previewUrl", "thumbnail", "assetsMap", "certTemplate", "itemSetPreviewUrl", "grayScaleAppIcon", "sourceURL", "variants", "downloadUrl", "streamingUrl", "toc_url", "data", "question", "solutions", "editorState", "media", "pdfUrl", "transcripts"]' \ No newline at end of file From f40b6196552bac6f6e585b4a20e85849bd457a8c Mon Sep 17 00:00:00 2001 From: Jayaprakash8887 Date: Tue, 3 Jan 2023 16:47:21 +0530 Subject: [PATCH 161/222] Issue #KN-427 fix: Bundle API issue fix. --- ansible/roles/learning-service/defaults/main.yml | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/ansible/roles/learning-service/defaults/main.yml b/ansible/roles/learning-service/defaults/main.yml index 91338cac25..4787dcb15a 100644 --- a/ansible/roles/learning-service/defaults/main.yml +++ b/ansible/roles/learning-service/defaults/main.yml @@ -50,9 +50,4 @@ cloud_storage_config_environment: "{{env}}" tomcat_init_mem: -Xms1024m tomcat_max_mem: -Xmx4096m search_index_host: "{{ groups['composite-search-cluster']|join(':9200,')}}:9200" -compositesearch_index_name: "compositesearch" - - -cloudstorage_relative_path_prefix_content: "CONTENT_STORAGE_BASE_PATH" -cloudstorage_relative_path_prefix_dial: "DIAL_STORAGE_BASE_PATH" -cloudstorage_metadata_list: '["appIcon", "artifactUrl", "posterImage", "previewUrl", "thumbnail", "assetsMap", "certTemplate", "itemSetPreviewUrl", "grayScaleAppIcon", "sourceURL", "variants", "downloadUrl", "streamingUrl", "toc_url", "data", "question", "solutions", "editorState", "media", "pdfUrl", "transcripts"]' \ No newline at end of file +compositesearch_index_name: "compositesearch" \ No newline at end of file From 4f9c363b5d0111f03e341a9910a8a8bd806d5cf8 Mon Sep 17 00:00:00 2001 From: Jayaprakash8887 Date: Tue, 3 Jan 2023 16:47:51 +0530 Subject: [PATCH 162/222] Issue #KN-427 fix: Bundle API issue fix. --- ansible/inventory/env/group_vars/all.yml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/ansible/inventory/env/group_vars/all.yml b/ansible/inventory/env/group_vars/all.yml index 3bf75b51b6..e022d3f800 100644 --- a/ansible/inventory/env/group_vars/all.yml +++ b/ansible/inventory/env/group_vars/all.yml @@ -121,4 +121,9 @@ enable_suppress_exception: false enable_rc_certificate: true # SB-31155 -plugin_storage: "{{ plugin_container_name }}" \ No newline at end of file +plugin_storage: "{{ plugin_container_name }}" + + +cloudstorage_relative_path_prefix_content: "CONTENT_STORAGE_BASE_PATH" +cloudstorage_relative_path_prefix_dial: "DIAL_STORAGE_BASE_PATH" +cloudstorage_metadata_list: '["appIcon", "artifactUrl", "posterImage", "previewUrl", "thumbnail", "assetsMap", "certTemplate", "itemSetPreviewUrl", "grayScaleAppIcon", "sourceURL", "variants", "downloadUrl", "streamingUrl", "toc_url", "data", "question", "solutions", "editorState", "media", "pdfUrl", "transcripts"]' \ No newline at end of file From f8280492a0b87ac231681c5f71a336f664d31fba Mon Sep 17 00:00:00 2001 From: Jayaprakash8887 Date: Tue, 3 Jan 2023 16:49:14 +0530 Subject: [PATCH 163/222] Issue #KN-427 fix: Common Vars addition --- kubernetes/ansible/roles/flink-jobs-deploy/defaults/main.yml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/kubernetes/ansible/roles/flink-jobs-deploy/defaults/main.yml b/kubernetes/ansible/roles/flink-jobs-deploy/defaults/main.yml index 4edf7c30f4..0bf436db09 100644 --- a/kubernetes/ansible/roles/flink-jobs-deploy/defaults/main.yml +++ b/kubernetes/ansible/roles/flink-jobs-deploy/defaults/main.yml @@ -396,7 +396,3 @@ content_republish_topic_name: "{{ env_name }}.republish.job.request" video_stream_topic_name: "{{ env_name }}.live.video.stream.request" content_hierarchy_keyspace_name: "{{ hierarchy_keyspace_name }}" questionset_hierarchy_keyspace_name: "{{ hierarchy_keyspace_name }}" - -cloudstorage_relative_path_prefix_content: "CONTENT_STORAGE_BASE_PATH" -cloudstorage_relative_path_prefix_dial: "DIAL_STORAGE_BASE_PATH" -cloudstorage_metadata_list: '["appIcon", "artifactUrl", "posterImage", "previewUrl", "thumbnail", "assetsMap", "certTemplate", "itemSetPreviewUrl", "grayScaleAppIcon", "sourceURL", "variants", "downloadUrl", "streamingUrl", "toc_url", "data", "question", "solutions", "editorState", "media", "pdfUrl", "transcripts"]' From 8f954683fb9e77079f6109e3399147fbbe2075af Mon Sep 17 00:00:00 2001 From: Jayaprakash8887 Date: Tue, 10 Jan 2023 16:06:47 +0530 Subject: [PATCH 164/222] Issue #KN-445 fix: MigrateCSPDataCommand enhancement with content Ids --- .../org/sunbird/learning/util/ControllerUtil.java | 13 +++++++++---- .../sync/tool/mgr/CSPMigrationMessageGenerator.java | 10 +++++++--- .../sync/tool/shell/MigrateCSPDataCommand.java | 3 ++- 3 files changed, 18 insertions(+), 8 deletions(-) diff --git a/platform-modules/actors/src/main/java/org/sunbird/learning/util/ControllerUtil.java b/platform-modules/actors/src/main/java/org/sunbird/learning/util/ControllerUtil.java index 448a95085b..a21c1df96f 100644 --- a/platform-modules/actors/src/main/java/org/sunbird/learning/util/ControllerUtil.java +++ b/platform-modules/actors/src/main/java/org/sunbird/learning/util/ControllerUtil.java @@ -412,12 +412,14 @@ public List getNodes(String graphId, String objectType, int startPosition, } } - public List getNodes(String graphId, String objectType, List mimeTypes, List status, double migrationVersion, int startPosition, int batchSize) { + public List getNodes(String graphId, String objectType, List mimeTypes, List status, List contentIdsList, double migrationVersion, int startPosition, int batchSize) { List filters = new ArrayList(); if(!mimeTypes.isEmpty()) filters.add(new Filter("mimeType", SearchConditions.OP_IN, mimeTypes)); if(!status.isEmpty()) filters.add(new Filter("status", SearchConditions.OP_IN, status)); + if(!contentIdsList.isEmpty()) + filters.add(new Filter("IL_UNIQUE_ID", SearchConditions.OP_IN, contentIdsList)); if(migrationVersion == 0) filters.add(new Filter("migrationVersion", SearchConditions.OP_IS, Values.NULL)); else filters.add(new Filter("migrationVersion", SearchConditions.OP_EQUAL, migrationVersion)); @@ -792,7 +794,7 @@ public void hierarchyCleanUp(Map map) { } } - public Map getCSPMigrationObjectCount(String graphId, List objectTypes, List mimeTypeList, List statusList, double migrationVersion) { + public Map getCSPMigrationObjectCount(String graphId, List objectTypes, List mimeTypeList, List statusList, List contentIdsList, double migrationVersion) { Map counts = new HashMap(); Request request = getRequest(graphId, GraphEngineManagers.SEARCH_MANAGER, "executeQueryForProps"); StringBuilder queryString = new StringBuilder(); @@ -804,14 +806,17 @@ public Map getCSPMigrationObjectCount(String graphId, List if(statusList!=null && !statusList.isEmpty()) queryString.append(" AND n.status IN {3} "); + if(contentIdsList!=null && !contentIdsList.isEmpty()) + queryString.append(" AND n.IL_UNIQUE_ID IN {5} "); + if(migrationVersion == 0) queryString.append(" AND NOT EXISTS(n.migrationVersion) "); else queryString.append(" AND n.migrationVersion={4} "); queryString.append("RETURN n.IL_FUNC_OBJECT_TYPE AS objectType, COUNT(n) AS count;"); - System.out.println("Count queryString:: " + MessageFormat.format(queryString.toString(), graphId, new JSONArray(objectTypes), new JSONArray(mimeTypeList), new JSONArray(statusList), migrationVersion)); + System.out.println("Count queryString:: " + MessageFormat.format(queryString.toString(), graphId, new JSONArray(objectTypes), new JSONArray(mimeTypeList), new JSONArray(statusList), migrationVersion, new JSONArray(contentIdsList))); - request.put(GraphDACParams.query.name(), MessageFormat.format(queryString.toString(), graphId, new JSONArray(objectTypes), new JSONArray(mimeTypeList), new JSONArray(statusList), migrationVersion)); + request.put(GraphDACParams.query.name(), MessageFormat.format(queryString.toString(), graphId, new JSONArray(objectTypes), new JSONArray(mimeTypeList), new JSONArray(statusList), migrationVersion, new JSONArray(contentIdsList))); List props = new ArrayList(); props.add("objectType"); diff --git a/platform-tools/spikes/sync-tool/src/main/java/org/sunbird/sync/tool/mgr/CSPMigrationMessageGenerator.java b/platform-tools/spikes/sync-tool/src/main/java/org/sunbird/sync/tool/mgr/CSPMigrationMessageGenerator.java index 5d4d5fe393..099ca05f27 100644 --- a/platform-tools/spikes/sync-tool/src/main/java/org/sunbird/sync/tool/mgr/CSPMigrationMessageGenerator.java +++ b/platform-tools/spikes/sync-tool/src/main/java/org/sunbird/sync/tool/mgr/CSPMigrationMessageGenerator.java @@ -42,22 +42,26 @@ private void init() throws Exception { batchSize = batch; } - public void generateMgrMsg(String graphId, String[] objectTypes, String[] mimeTypes, String[] status, double migrationVersion, Integer limit, Integer delay) throws Exception { + public void generateMgrMsg(String graphId, String[] objectTypes, String[] mimeTypes, String[] status, String[] contentIds, double migrationVersion, Integer limit, Integer delay) throws Exception { if (StringUtils.isBlank(graphId)) throw new ClientException("ERR_INVALID_GRAPH_ID", "Graph Id is blank."); if (null == objectTypes || objectTypes.length == 0) throw new ClientException("ERR_EMPTY_OBJECT_TYPE", "Object Type is blank."); List mimeTypeList = new ArrayList(); List statusList = new ArrayList(); + List contentIdsList = new ArrayList(); if (null != mimeTypes && mimeTypes.length > 0) mimeTypeList = Arrays.asList(mimeTypes); if (null != status && status.length > 0) statusList = Arrays.asList(status); + if (null != contentIds && contentIds.length > 0) + contentIdsList = Arrays.asList(contentIds); + Map errors = new HashMap<>(); long startTime = System.currentTimeMillis(); System.out.println("-----------------------------------------"); System.out.println("\nMigration Event Generation starting at " + startTime); - Map counts = util.getCSPMigrationObjectCount(graphId, Arrays.asList(objectTypes), mimeTypeList, statusList, migrationVersion); + Map counts = util.getCSPMigrationObjectCount(graphId, Arrays.asList(objectTypes), mimeTypeList, statusList, contentIdsList, migrationVersion); if (counts.isEmpty()) { System.out.println("No objects found in this graph."); } else { @@ -89,7 +93,7 @@ public void generateMgrMsg(String graphId, String[] objectTypes, String[] mimeTy while (found && start < stopLimit) { List nodes = null; try { - nodes = util.getNodes(graphId, objectType.trim(), mimeTypeList, statusList, migrationVersion, start, batchSize); + nodes = util.getNodes(graphId, objectType.trim(), mimeTypeList, statusList, contentIdsList, migrationVersion, start, batchSize); } catch (ResourceNotFoundException e) { System.out.println("Error while fetching neo4j records for objectType=" + objectType + ", start=" + start + ",batchSize=" + batchSize); start += batchSize; diff --git a/platform-tools/spikes/sync-tool/src/main/java/org/sunbird/sync/tool/shell/MigrateCSPDataCommand.java b/platform-tools/spikes/sync-tool/src/main/java/org/sunbird/sync/tool/shell/MigrateCSPDataCommand.java index fe990fed8a..fbf055dc68 100644 --- a/platform-tools/spikes/sync-tool/src/main/java/org/sunbird/sync/tool/shell/MigrateCSPDataCommand.java +++ b/platform-tools/spikes/sync-tool/src/main/java/org/sunbird/sync/tool/shell/MigrateCSPDataCommand.java @@ -22,6 +22,7 @@ public void migrateCSPData( @CliOption(key = {"objectType"}, mandatory = true, help = "Object Type is Required") final String[] objectType, @CliOption(key = {"mimeType"}, mandatory = false, help = "mimeTypes can be provided") final String[] mimeType, @CliOption(key = {"status"}, mandatory = false, help = "Specific Status can be passed") final String[] status, + @CliOption(key = {"ids"}, mandatory = false, help = "Specific content Ids can be passed") final String[] contentIds, @CliOption(key = {"migrationVersion"}, mandatory = false, unspecifiedDefaultValue = "0", help = "Specific migration version can be passed") final double migrationVersion, @CliOption(key = {"limit"}, mandatory = false, unspecifiedDefaultValue = "0", help = "Specific Limit can be passed") final Integer limit, @CliOption(key = {"delay"}, mandatory = false, unspecifiedDefaultValue = "10", help = "time gap between each batch") final Integer delay) @@ -30,7 +31,7 @@ public void migrateCSPData( long startTime = System.currentTimeMillis(); DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm:ss"); LocalDateTime start = LocalDateTime.now(); - cspMsgGenerator.generateMgrMsg(graphId, objectType, mimeType, status, migrationVersion, limit, delay); + cspMsgGenerator.generateMgrMsg(graphId, objectType, mimeType, status, contentIds, migrationVersion, limit, delay); long endTime = System.currentTimeMillis(); long exeTime = endTime - startTime; System.out.println("Total time of execution: " + exeTime + "ms"); From 57f1f8fda71d25c693e1eee766ef8144e78aee6f Mon Sep 17 00:00:00 2001 From: Jayaprakash8887 Date: Tue, 10 Jan 2023 18:00:01 +0530 Subject: [PATCH 165/222] Issue #KN-445 fix: Revert MigrateCSPDataCommand enhancement with content Ids --- .../java/org/sunbird/learning/util/ControllerUtil.java | 9 ++------- .../sync/tool/mgr/CSPMigrationMessageGenerator.java | 9 +++------ .../sunbird/sync/tool/shell/MigrateCSPDataCommand.java | 3 +-- 3 files changed, 6 insertions(+), 15 deletions(-) diff --git a/platform-modules/actors/src/main/java/org/sunbird/learning/util/ControllerUtil.java b/platform-modules/actors/src/main/java/org/sunbird/learning/util/ControllerUtil.java index a21c1df96f..d667c6ea48 100644 --- a/platform-modules/actors/src/main/java/org/sunbird/learning/util/ControllerUtil.java +++ b/platform-modules/actors/src/main/java/org/sunbird/learning/util/ControllerUtil.java @@ -412,14 +412,12 @@ public List getNodes(String graphId, String objectType, int startPosition, } } - public List getNodes(String graphId, String objectType, List mimeTypes, List status, List contentIdsList, double migrationVersion, int startPosition, int batchSize) { + public List getNodes(String graphId, String objectType, List mimeTypes, List status, double migrationVersion, int startPosition, int batchSize) { List filters = new ArrayList(); if(!mimeTypes.isEmpty()) filters.add(new Filter("mimeType", SearchConditions.OP_IN, mimeTypes)); if(!status.isEmpty()) filters.add(new Filter("status", SearchConditions.OP_IN, status)); - if(!contentIdsList.isEmpty()) - filters.add(new Filter("IL_UNIQUE_ID", SearchConditions.OP_IN, contentIdsList)); if(migrationVersion == 0) filters.add(new Filter("migrationVersion", SearchConditions.OP_IS, Values.NULL)); else filters.add(new Filter("migrationVersion", SearchConditions.OP_EQUAL, migrationVersion)); @@ -794,7 +792,7 @@ public void hierarchyCleanUp(Map map) { } } - public Map getCSPMigrationObjectCount(String graphId, List objectTypes, List mimeTypeList, List statusList, List contentIdsList, double migrationVersion) { + public Map getCSPMigrationObjectCount(String graphId, List objectTypes, List mimeTypeList, List statusList, double migrationVersion) { Map counts = new HashMap(); Request request = getRequest(graphId, GraphEngineManagers.SEARCH_MANAGER, "executeQueryForProps"); StringBuilder queryString = new StringBuilder(); @@ -806,9 +804,6 @@ public Map getCSPMigrationObjectCount(String graphId, List if(statusList!=null && !statusList.isEmpty()) queryString.append(" AND n.status IN {3} "); - if(contentIdsList!=null && !contentIdsList.isEmpty()) - queryString.append(" AND n.IL_UNIQUE_ID IN {5} "); - if(migrationVersion == 0) queryString.append(" AND NOT EXISTS(n.migrationVersion) "); else queryString.append(" AND n.migrationVersion={4} "); diff --git a/platform-tools/spikes/sync-tool/src/main/java/org/sunbird/sync/tool/mgr/CSPMigrationMessageGenerator.java b/platform-tools/spikes/sync-tool/src/main/java/org/sunbird/sync/tool/mgr/CSPMigrationMessageGenerator.java index 099ca05f27..1815f6226f 100644 --- a/platform-tools/spikes/sync-tool/src/main/java/org/sunbird/sync/tool/mgr/CSPMigrationMessageGenerator.java +++ b/platform-tools/spikes/sync-tool/src/main/java/org/sunbird/sync/tool/mgr/CSPMigrationMessageGenerator.java @@ -42,26 +42,23 @@ private void init() throws Exception { batchSize = batch; } - public void generateMgrMsg(String graphId, String[] objectTypes, String[] mimeTypes, String[] status, String[] contentIds, double migrationVersion, Integer limit, Integer delay) throws Exception { + public void generateMgrMsg(String graphId, String[] objectTypes, String[] mimeTypes, String[] status, double migrationVersion, Integer limit, Integer delay) throws Exception { if (StringUtils.isBlank(graphId)) throw new ClientException("ERR_INVALID_GRAPH_ID", "Graph Id is blank."); if (null == objectTypes || objectTypes.length == 0) throw new ClientException("ERR_EMPTY_OBJECT_TYPE", "Object Type is blank."); List mimeTypeList = new ArrayList(); List statusList = new ArrayList(); - List contentIdsList = new ArrayList(); if (null != mimeTypes && mimeTypes.length > 0) mimeTypeList = Arrays.asList(mimeTypes); if (null != status && status.length > 0) statusList = Arrays.asList(status); - if (null != contentIds && contentIds.length > 0) - contentIdsList = Arrays.asList(contentIds); Map errors = new HashMap<>(); long startTime = System.currentTimeMillis(); System.out.println("-----------------------------------------"); System.out.println("\nMigration Event Generation starting at " + startTime); - Map counts = util.getCSPMigrationObjectCount(graphId, Arrays.asList(objectTypes), mimeTypeList, statusList, contentIdsList, migrationVersion); + Map counts = util.getCSPMigrationObjectCount(graphId, Arrays.asList(objectTypes), mimeTypeList, statusList, migrationVersion); if (counts.isEmpty()) { System.out.println("No objects found in this graph."); } else { @@ -93,7 +90,7 @@ public void generateMgrMsg(String graphId, String[] objectTypes, String[] mimeTy while (found && start < stopLimit) { List nodes = null; try { - nodes = util.getNodes(graphId, objectType.trim(), mimeTypeList, statusList, contentIdsList, migrationVersion, start, batchSize); + nodes = util.getNodes(graphId, objectType.trim(), mimeTypeList, statusList, migrationVersion, start, batchSize); } catch (ResourceNotFoundException e) { System.out.println("Error while fetching neo4j records for objectType=" + objectType + ", start=" + start + ",batchSize=" + batchSize); start += batchSize; diff --git a/platform-tools/spikes/sync-tool/src/main/java/org/sunbird/sync/tool/shell/MigrateCSPDataCommand.java b/platform-tools/spikes/sync-tool/src/main/java/org/sunbird/sync/tool/shell/MigrateCSPDataCommand.java index fbf055dc68..fe990fed8a 100644 --- a/platform-tools/spikes/sync-tool/src/main/java/org/sunbird/sync/tool/shell/MigrateCSPDataCommand.java +++ b/platform-tools/spikes/sync-tool/src/main/java/org/sunbird/sync/tool/shell/MigrateCSPDataCommand.java @@ -22,7 +22,6 @@ public void migrateCSPData( @CliOption(key = {"objectType"}, mandatory = true, help = "Object Type is Required") final String[] objectType, @CliOption(key = {"mimeType"}, mandatory = false, help = "mimeTypes can be provided") final String[] mimeType, @CliOption(key = {"status"}, mandatory = false, help = "Specific Status can be passed") final String[] status, - @CliOption(key = {"ids"}, mandatory = false, help = "Specific content Ids can be passed") final String[] contentIds, @CliOption(key = {"migrationVersion"}, mandatory = false, unspecifiedDefaultValue = "0", help = "Specific migration version can be passed") final double migrationVersion, @CliOption(key = {"limit"}, mandatory = false, unspecifiedDefaultValue = "0", help = "Specific Limit can be passed") final Integer limit, @CliOption(key = {"delay"}, mandatory = false, unspecifiedDefaultValue = "10", help = "time gap between each batch") final Integer delay) @@ -31,7 +30,7 @@ public void migrateCSPData( long startTime = System.currentTimeMillis(); DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm:ss"); LocalDateTime start = LocalDateTime.now(); - cspMsgGenerator.generateMgrMsg(graphId, objectType, mimeType, status, contentIds, migrationVersion, limit, delay); + cspMsgGenerator.generateMgrMsg(graphId, objectType, mimeType, status, migrationVersion, limit, delay); long endTime = System.currentTimeMillis(); long exeTime = endTime - startTime; System.out.println("Total time of execution: " + exeTime + "ms"); From 50e58b36822fe2b57222abc536e32a9744f23dba Mon Sep 17 00:00:00 2001 From: Jayaprakash8887 Date: Tue, 10 Jan 2023 19:01:51 +0530 Subject: [PATCH 166/222] Issue #KN-445 fix: Revert MigrateCSPDataCommand enhancement with content Ids --- .../main/java/org/sunbird/learning/util/ControllerUtil.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/platform-modules/actors/src/main/java/org/sunbird/learning/util/ControllerUtil.java b/platform-modules/actors/src/main/java/org/sunbird/learning/util/ControllerUtil.java index d667c6ea48..448a95085b 100644 --- a/platform-modules/actors/src/main/java/org/sunbird/learning/util/ControllerUtil.java +++ b/platform-modules/actors/src/main/java/org/sunbird/learning/util/ControllerUtil.java @@ -809,9 +809,9 @@ public Map getCSPMigrationObjectCount(String graphId, List queryString.append("RETURN n.IL_FUNC_OBJECT_TYPE AS objectType, COUNT(n) AS count;"); - System.out.println("Count queryString:: " + MessageFormat.format(queryString.toString(), graphId, new JSONArray(objectTypes), new JSONArray(mimeTypeList), new JSONArray(statusList), migrationVersion, new JSONArray(contentIdsList))); + System.out.println("Count queryString:: " + MessageFormat.format(queryString.toString(), graphId, new JSONArray(objectTypes), new JSONArray(mimeTypeList), new JSONArray(statusList), migrationVersion)); - request.put(GraphDACParams.query.name(), MessageFormat.format(queryString.toString(), graphId, new JSONArray(objectTypes), new JSONArray(mimeTypeList), new JSONArray(statusList), migrationVersion, new JSONArray(contentIdsList))); + request.put(GraphDACParams.query.name(), MessageFormat.format(queryString.toString(), graphId, new JSONArray(objectTypes), new JSONArray(mimeTypeList), new JSONArray(statusList), migrationVersion)); List props = new ArrayList(); props.add("objectType"); From 94440e8ca538edf0da795f1237fd7e83db66257b Mon Sep 17 00:00:00 2001 From: Jayaprakash8887 Date: Wed, 11 Jan 2023 13:17:04 +0530 Subject: [PATCH 167/222] Issue #KN-445 fix: MigrateCSPDataCommand enhancement with content Ids --- .../dac/mgr/impl/Neo4JBoltSearchMgrImpl.java | 2 +- .../sunbird/learning/util/ControllerUtil.java | 115 +++++++++--------- .../mgr/CSPMigrationMessageGenerator.java | 37 +++--- .../tool/shell/MigrateCSPDataCommand.java | 21 ++-- 4 files changed, 92 insertions(+), 83 deletions(-) diff --git a/platform-core/graph-engine/module/graph-dac/src/main/java/org/sunbird/graph/dac/mgr/impl/Neo4JBoltSearchMgrImpl.java b/platform-core/graph-engine/module/graph-dac/src/main/java/org/sunbird/graph/dac/mgr/impl/Neo4JBoltSearchMgrImpl.java index d4b8a8a6c5..dfb20b65be 100644 --- a/platform-core/graph-engine/module/graph-dac/src/main/java/org/sunbird/graph/dac/mgr/impl/Neo4JBoltSearchMgrImpl.java +++ b/platform-core/graph-engine/module/graph-dac/src/main/java/org/sunbird/graph/dac/mgr/impl/Neo4JBoltSearchMgrImpl.java @@ -248,7 +248,7 @@ public Response searchNodes(Request request) { } else { try { List nodes = Neo4JBoltSearchOperations.searchNodes(graphId, searchCriteria, getTags, request); - boolean isrRelativePathEnabled = Platform.config.getBoolean("cloudstorage.metadata.replace_absolute_path"); + boolean isrRelativePathEnabled = Platform.config.hasPath("cloudstorage.metadata.replace_absolute_path")?Platform.config.getBoolean("cloudstorage.metadata.replace_absolute_path"):false; if(isrRelativePathEnabled) updateAbsolutePath(nodes); return OK(GraphDACParams.node_list.name(), nodes); } catch (Exception e) { diff --git a/platform-modules/actors/src/main/java/org/sunbird/learning/util/ControllerUtil.java b/platform-modules/actors/src/main/java/org/sunbird/learning/util/ControllerUtil.java index 448a95085b..f0a09239a6 100644 --- a/platform-modules/actors/src/main/java/org/sunbird/learning/util/ControllerUtil.java +++ b/platform-modules/actors/src/main/java/org/sunbird/learning/util/ControllerUtil.java @@ -155,25 +155,25 @@ public DefinitionDTO getDefinition(String taxonomyId, String objectType) { } return null; } - + public DefinitionDTO getDefinition(String taxonomyId, String objectType, boolean disableAkka) { - DefinitionDTO definition = null; - if(disableAkka) { - try { - Request request = new Request(); - request.getContext().put(GraphHeaderParams.graph_id.name(), TAXONOMY_ID); - request.put(GraphDACParams.object_type.name(), objectType); - NodeManager nodeManager = new NodeManager(); - definition = nodeManager.getNodeDefinition(request); - }catch (Exception e) { - throw new ServerException(TaxonomyErrorCodes.SYSTEM_ERROR.name(), e.getMessage() + ". Please Try Again After Sometime!"); - } - }else { - definition = getDefinition(taxonomyId, objectType); - } - return definition; - - } + DefinitionDTO definition = null; + if(disableAkka) { + try { + Request request = new Request(); + request.getContext().put(GraphHeaderParams.graph_id.name(), TAXONOMY_ID); + request.put(GraphDACParams.object_type.name(), objectType); + NodeManager nodeManager = new NodeManager(); + definition = nodeManager.getNodeDefinition(request); + }catch (Exception e) { + throw new ServerException(TaxonomyErrorCodes.SYSTEM_ERROR.name(), e.getMessage() + ". Please Try Again After Sometime!"); + } + }else { + definition = getDefinition(taxonomyId, objectType); + } + return definition; + + } /** * Gets all the definitions @@ -412,12 +412,14 @@ public List getNodes(String graphId, String objectType, int startPosition, } } - public List getNodes(String graphId, String objectType, List mimeTypes, List status, double migrationVersion, int startPosition, int batchSize) { + public List getNodes(String graphId, String objectType, List mimeTypes, List status, List contentIdsList, double migrationVersion, int startPosition, int batchSize) { List filters = new ArrayList(); if(!mimeTypes.isEmpty()) filters.add(new Filter("mimeType", SearchConditions.OP_IN, mimeTypes)); if(!status.isEmpty()) filters.add(new Filter("status", SearchConditions.OP_IN, status)); + if(!contentIdsList.isEmpty()) + filters.add(new Filter("IL_UNIQUE_ID", SearchConditions.OP_IN, contentIdsList)); if(migrationVersion == 0) filters.add(new Filter("migrationVersion", SearchConditions.OP_IS, Values.NULL)); else filters.add(new Filter("migrationVersion", SearchConditions.OP_EQUAL, migrationVersion)); @@ -426,7 +428,7 @@ public List getNodes(String graphId, String objectType, List mimeT sc.setObjectType(objectType); sc.setResultSize(batchSize); sc.setStartPosition(startPosition); - if(!filters.isEmpty() && filters.size()>0) + if(!filters.isEmpty() && filters.size()>0) sc.addMetadata(MetadataCriterion.create(filters)); Request req = getRequest(graphId, GraphEngineManagers.SEARCH_MANAGER, "searchNodes", GraphDACParams.search_criteria.name(), sc); @@ -587,15 +589,15 @@ public Map constructHierarchy(List> list) { Map>> currentLevelNodes = new HashMap<>(); list.stream().filter(e -> ((Number) e.get("depth")).intValue() == depth) .collect(Collectors.toList()).forEach(e -> { - String id = (String) e.get("identifier"); - List> nodes = currentLevelNodes.get(id); - if (CollectionUtils.isEmpty(nodes)) { - nodes = new ArrayList<>(); - currentLevelNodes.put((String) e.get("identifier"), nodes); - } - nodes.add(e); + String id = (String) e.get("identifier"); + List> nodes = currentLevelNodes.get(id); + if (CollectionUtils.isEmpty(nodes)) { + nodes = new ArrayList<>(); + currentLevelNodes.put((String) e.get("identifier"), nodes); + } + nodes.add(e); - }); + }); List> nextLevelNodes = list.stream().filter(e -> ((Number) e.get("depth")).intValue() == depth + 1) .collect(Collectors.toList()); @@ -697,19 +699,19 @@ public Map getHierarchyMap(String graphId, String contentId, Def // startTime = System.currentTimeMillis(); Response getList = getDataNodes(graphId, ids); // System.out.println("Time to get required data nodes: " + (System.currentTimeMillis() - startTime)); - if (null != getList && !checkError(getList)) { - List nodeList = (List) getList.get("node_list"); - Map> contentsWithMetadata = nodeList.stream().map(n -> ConvertGraphNode.convertGraphNode - (n, graphId, definition, fields)).map(contentMap -> { - contentMap.remove("collections"); - contentMap.remove("children"); - contentMap.remove("usedByContent"); - contentMap.remove("item_sets"); - contentMap.remove("methods"); - contentMap.remove("libraries"); - contentMap.remove("editorState"); - return contentMap; - }).collect(Collectors.toMap(e -> (String) e.get("identifier"), e -> e)); + if (null != getList && !checkError(getList)) { + List nodeList = (List) getList.get("node_list"); + Map> contentsWithMetadata = nodeList.stream().map(n -> ConvertGraphNode.convertGraphNode + (n, graphId, definition, fields)).map(contentMap -> { + contentMap.remove("collections"); + contentMap.remove("children"); + contentMap.remove("usedByContent"); + contentMap.remove("item_sets"); + contentMap.remove("methods"); + contentMap.remove("libraries"); + contentMap.remove("editorState"); + return contentMap; + }).collect(Collectors.toMap(e -> (String) e.get("identifier"), e -> e)); contentList = contentList.stream().map(n -> { n.putAll(contentsWithMetadata.get(n.get("identifier"))); @@ -718,16 +720,16 @@ public Map getHierarchyMap(String graphId, String contentId, Def // startTime = System.currentTimeMillis(); collectionHierarchy = contentCleanUp(constructHierarchy(contentList)); // System.out.println("Time to construct hierarchy: " + (System.currentTimeMillis() - startTime)); - } else { - if (null != getList && getList.getResponseCode() == ResponseCode.CLIENT_ERROR) { - throw new ClientException(ContentErrorCodes.ERR_INVALID_INPUT.name(), getList.getParams().getErrmsg()); - } else { - throw new ServerException(ContentAPIParams.SERVER_ERROR.name(), getList.getParams().getErrmsg()); - } - } - hierarchyCleanUp(collectionHierarchy); - return collectionHierarchy; - } + } else { + if (null != getList && getList.getResponseCode() == ResponseCode.CLIENT_ERROR) { + throw new ClientException(ContentErrorCodes.ERR_INVALID_INPUT.name(), getList.getParams().getErrmsg()); + } else { + throw new ServerException(ContentAPIParams.SERVER_ERROR.name(), getList.getParams().getErrmsg()); + } + } + hierarchyCleanUp(collectionHierarchy); + return collectionHierarchy; + } public List getPublishedCollections(String graphId, int offset, int limit) { @@ -792,7 +794,7 @@ public void hierarchyCleanUp(Map map) { } } - public Map getCSPMigrationObjectCount(String graphId, List objectTypes, List mimeTypeList, List statusList, double migrationVersion) { + public Map getCSPMigrationObjectCount(String graphId, List objectTypes, List mimeTypeList, List statusList, List contentIdsList, double migrationVersion) { Map counts = new HashMap(); Request request = getRequest(graphId, GraphEngineManagers.SEARCH_MANAGER, "executeQueryForProps"); StringBuilder queryString = new StringBuilder(); @@ -804,14 +806,17 @@ public Map getCSPMigrationObjectCount(String graphId, List if(statusList!=null && !statusList.isEmpty()) queryString.append(" AND n.status IN {3} "); + if(contentIdsList!=null && !contentIdsList.isEmpty()) + queryString.append(" AND n.IL_UNIQUE_ID IN {5} "); + if(migrationVersion == 0) queryString.append(" AND NOT EXISTS(n.migrationVersion) "); else queryString.append(" AND n.migrationVersion={4} "); queryString.append("RETURN n.IL_FUNC_OBJECT_TYPE AS objectType, COUNT(n) AS count;"); - System.out.println("Count queryString:: " + MessageFormat.format(queryString.toString(), graphId, new JSONArray(objectTypes), new JSONArray(mimeTypeList), new JSONArray(statusList), migrationVersion)); + System.out.println("Count queryString:: " + MessageFormat.format(queryString.toString(), graphId, new JSONArray(objectTypes), new JSONArray(mimeTypeList), new JSONArray(statusList), migrationVersion, new JSONArray(contentIdsList))); - request.put(GraphDACParams.query.name(), MessageFormat.format(queryString.toString(), graphId, new JSONArray(objectTypes), new JSONArray(mimeTypeList), new JSONArray(statusList), migrationVersion)); + request.put(GraphDACParams.query.name(), MessageFormat.format(queryString.toString(), graphId, new JSONArray(objectTypes), new JSONArray(mimeTypeList), new JSONArray(statusList), migrationVersion, new JSONArray(contentIdsList))); List props = new ArrayList(); props.add("objectType"); @@ -832,4 +837,4 @@ public Map getCSPMigrationObjectCount(String graphId, List return counts; } -} +} \ No newline at end of file diff --git a/platform-tools/spikes/sync-tool/src/main/java/org/sunbird/sync/tool/mgr/CSPMigrationMessageGenerator.java b/platform-tools/spikes/sync-tool/src/main/java/org/sunbird/sync/tool/mgr/CSPMigrationMessageGenerator.java index 1815f6226f..a433d11a81 100644 --- a/platform-tools/spikes/sync-tool/src/main/java/org/sunbird/sync/tool/mgr/CSPMigrationMessageGenerator.java +++ b/platform-tools/spikes/sync-tool/src/main/java/org/sunbird/sync/tool/mgr/CSPMigrationMessageGenerator.java @@ -42,23 +42,26 @@ private void init() throws Exception { batchSize = batch; } - public void generateMgrMsg(String graphId, String[] objectTypes, String[] mimeTypes, String[] status, double migrationVersion, Integer limit, Integer delay) throws Exception { + public void generateMgrMsg(String graphId, String[] objectTypes, String[] mimeTypes, String[] status, String[] contentIds, double migrationVersion, Integer limit, Integer delay) throws Exception { if (StringUtils.isBlank(graphId)) throw new ClientException("ERR_INVALID_GRAPH_ID", "Graph Id is blank."); if (null == objectTypes || objectTypes.length == 0) throw new ClientException("ERR_EMPTY_OBJECT_TYPE", "Object Type is blank."); List mimeTypeList = new ArrayList(); List statusList = new ArrayList(); + List contentIdsList = new ArrayList(); if (null != mimeTypes && mimeTypes.length > 0) mimeTypeList = Arrays.asList(mimeTypes); if (null != status && status.length > 0) statusList = Arrays.asList(status); + if (null != contentIds && contentIds.length > 0) + contentIdsList = Arrays.asList(contentIds); Map errors = new HashMap<>(); long startTime = System.currentTimeMillis(); System.out.println("-----------------------------------------"); System.out.println("\nMigration Event Generation starting at " + startTime); - Map counts = util.getCSPMigrationObjectCount(graphId, Arrays.asList(objectTypes), mimeTypeList, statusList, migrationVersion); + Map counts = util.getCSPMigrationObjectCount(graphId, Arrays.asList(objectTypes), mimeTypeList, statusList, contentIdsList, migrationVersion); if (counts.isEmpty()) { System.out.println("No objects found in this graph."); } else { @@ -90,7 +93,7 @@ public void generateMgrMsg(String graphId, String[] objectTypes, String[] mimeTy while (found && start < stopLimit) { List nodes = null; try { - nodes = util.getNodes(graphId, objectType.trim(), mimeTypeList, statusList, migrationVersion, start, batchSize); + nodes = util.getNodes(graphId, objectType.trim(), mimeTypeList, statusList, contentIdsList, migrationVersion, start, batchSize); } catch (ResourceNotFoundException e) { System.out.println("Error while fetching neo4j records for objectType=" + objectType + ", start=" + start + ",batchSize=" + batchSize); start += batchSize; @@ -186,25 +189,25 @@ private String getEvent(Node node, Map errors) { private static void printProgress(long startTime, long total, long current) { long eta = current == 0 ? 0 : - (total - current) * (System.currentTimeMillis() - startTime) / current; + (total - current) * (System.currentTimeMillis() - startTime) / current; String etaHms = current == 0 ? "N/A" : - String.format("%02d:%02d:%02d", TimeUnit.MILLISECONDS.toHours(eta), - TimeUnit.MILLISECONDS.toMinutes(eta) % TimeUnit.HOURS.toMinutes(1), - TimeUnit.MILLISECONDS.toSeconds(eta) % TimeUnit.MINUTES.toSeconds(1)); + String.format("%02d:%02d:%02d", TimeUnit.MILLISECONDS.toHours(eta), + TimeUnit.MILLISECONDS.toMinutes(eta) % TimeUnit.HOURS.toMinutes(1), + TimeUnit.MILLISECONDS.toSeconds(eta) % TimeUnit.MINUTES.toSeconds(1)); StringBuilder string = new StringBuilder(140); int percent = (int) (current * 100 / total); string - .append('\r') - .append(String.join("", Collections.nCopies(percent == 0 ? 2 : 2 - (int) (Math.log10(percent)), " "))) - .append(String.format(" %d%% [", percent)) - .append(String.join("", Collections.nCopies(percent, "="))) - .append('>') - .append(String.join("", Collections.nCopies(100 - percent, " "))) - .append(']') - .append(String.join("", Collections.nCopies((int) (Math.log10(total)) - (int) (Math.log10(current)), " "))) - .append(String.format(" %d/%d, ETA: %s", current, total, etaHms)); + .append('\r') + .append(String.join("", Collections.nCopies(percent == 0 ? 2 : 2 - (int) (Math.log10(percent)), " "))) + .append(String.format(" %d%% [", percent)) + .append(String.join("", Collections.nCopies(percent, "="))) + .append('>') + .append(String.join("", Collections.nCopies(100 - percent, " "))) + .append(']') + .append(String.join("", Collections.nCopies((int) (Math.log10(total)) - (int) (Math.log10(current)), " "))) + .append(String.format(" %d/%d, ETA: %s", current, total, etaHms)); System.out.print(string); } @@ -212,4 +215,4 @@ private static void printProgress(long startTime, long total, long current) { public static void filterMigrationNodes(List nodes, Integer limit) { nodes.removeIf(n -> SystemNodeTypes.DEFINITION_NODE.name().equals(n.getNodeType())); } -} +} \ No newline at end of file diff --git a/platform-tools/spikes/sync-tool/src/main/java/org/sunbird/sync/tool/shell/MigrateCSPDataCommand.java b/platform-tools/spikes/sync-tool/src/main/java/org/sunbird/sync/tool/shell/MigrateCSPDataCommand.java index fe990fed8a..b1d1c576d0 100644 --- a/platform-tools/spikes/sync-tool/src/main/java/org/sunbird/sync/tool/shell/MigrateCSPDataCommand.java +++ b/platform-tools/spikes/sync-tool/src/main/java/org/sunbird/sync/tool/shell/MigrateCSPDataCommand.java @@ -18,19 +18,20 @@ public class MigrateCSPDataCommand implements CommandMarker { @CliCommand(value = "migratecspdata", help = "Generate CSP Data Migration Event") public void migrateCSPData( - @CliOption(key = {"graphId"}, mandatory = false, unspecifiedDefaultValue = "domain", help = "graphId of the object") final String graphId, - @CliOption(key = {"objectType"}, mandatory = true, help = "Object Type is Required") final String[] objectType, - @CliOption(key = {"mimeType"}, mandatory = false, help = "mimeTypes can be provided") final String[] mimeType, - @CliOption(key = {"status"}, mandatory = false, help = "Specific Status can be passed") final String[] status, - @CliOption(key = {"migrationVersion"}, mandatory = false, unspecifiedDefaultValue = "0", help = "Specific migration version can be passed") final double migrationVersion, - @CliOption(key = {"limit"}, mandatory = false, unspecifiedDefaultValue = "0", help = "Specific Limit can be passed") final Integer limit, - @CliOption(key = {"delay"}, mandatory = false, unspecifiedDefaultValue = "10", help = "time gap between each batch") final Integer delay) - throws Exception { + @CliOption(key = {"graphId"}, mandatory = false, unspecifiedDefaultValue = "domain", help = "graphId of the object") final String graphId, + @CliOption(key = {"objectType"}, mandatory = true, help = "Object Type is Required") final String[] objectType, + @CliOption(key = {"mimeType"}, mandatory = false, help = "mimeTypes can be provided") final String[] mimeType, + @CliOption(key = {"status"}, mandatory = false, help = "Specific Status can be passed") final String[] status, + @CliOption(key = {"ids"}, mandatory = false, help = "Specific content Ids can be passed") final String[] contentIds, + @CliOption(key = {"migrationVersion"}, mandatory = false, unspecifiedDefaultValue = "0", help = "Specific migration version can be passed") final double migrationVersion, + @CliOption(key = {"limit"}, mandatory = false, unspecifiedDefaultValue = "0", help = "Specific Limit can be passed") final Integer limit, + @CliOption(key = {"delay"}, mandatory = false, unspecifiedDefaultValue = "10", help = "time gap between each batch") final Integer delay) + throws Exception { long startTime = System.currentTimeMillis(); DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm:ss"); LocalDateTime start = LocalDateTime.now(); - cspMsgGenerator.generateMgrMsg(graphId, objectType, mimeType, status, migrationVersion, limit, delay); + cspMsgGenerator.generateMgrMsg(graphId, objectType, mimeType, status, contentIds, migrationVersion, limit, delay); long endTime = System.currentTimeMillis(); long exeTime = endTime - startTime; System.out.println("Total time of execution: " + exeTime + "ms"); @@ -39,4 +40,4 @@ public void migrateCSPData( } -} +} \ No newline at end of file From 27fba4b9e6cd27b30f7eccbb9183f8ce9cab0034 Mon Sep 17 00:00:00 2001 From: Jayaprakash8887 Date: Wed, 11 Jan 2023 14:16:25 +0530 Subject: [PATCH 168/222] Issue #KN-445 fix: MigrateCSPDataCommand enhancement with content Ids --- .../org/sunbird/learning/util/ControllerUtil.java | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/platform-modules/actors/src/main/java/org/sunbird/learning/util/ControllerUtil.java b/platform-modules/actors/src/main/java/org/sunbird/learning/util/ControllerUtil.java index f0a09239a6..4acdae33be 100644 --- a/platform-modules/actors/src/main/java/org/sunbird/learning/util/ControllerUtil.java +++ b/platform-modules/actors/src/main/java/org/sunbird/learning/util/ControllerUtil.java @@ -418,10 +418,12 @@ public List getNodes(String graphId, String objectType, List mimeT filters.add(new Filter("mimeType", SearchConditions.OP_IN, mimeTypes)); if(!status.isEmpty()) filters.add(new Filter("status", SearchConditions.OP_IN, status)); - if(!contentIdsList.isEmpty()) + if(!contentIdsList.isEmpty()) { filters.add(new Filter("IL_UNIQUE_ID", SearchConditions.OP_IN, contentIdsList)); - if(migrationVersion == 0) filters.add(new Filter("migrationVersion", SearchConditions.OP_IS, Values.NULL)); - else filters.add(new Filter("migrationVersion", SearchConditions.OP_EQUAL, migrationVersion)); + } else { + if (migrationVersion == 0) filters.add(new Filter("migrationVersion", SearchConditions.OP_IS, Values.NULL)); + else filters.add(new Filter("migrationVersion", SearchConditions.OP_EQUAL, migrationVersion)); + } SearchCriteria sc = new SearchCriteria(); sc.setNodeType(SystemNodeTypes.DATA_NODE.name()); @@ -808,9 +810,10 @@ public Map getCSPMigrationObjectCount(String graphId, List if(contentIdsList!=null && !contentIdsList.isEmpty()) queryString.append(" AND n.IL_UNIQUE_ID IN {5} "); - - if(migrationVersion == 0) queryString.append(" AND NOT EXISTS(n.migrationVersion) "); - else queryString.append(" AND n.migrationVersion={4} "); + else { + if (migrationVersion == 0) queryString.append(" AND NOT EXISTS(n.migrationVersion) "); + else queryString.append(" AND n.migrationVersion={4} "); + } queryString.append("RETURN n.IL_FUNC_OBJECT_TYPE AS objectType, COUNT(n) AS count;"); From 4446dd9a8b37186d0af058d58c8e42201c8fc18b Mon Sep 17 00:00:00 2001 From: Jayaprakash8887 Date: Wed, 11 Jan 2023 14:59:33 +0530 Subject: [PATCH 169/222] Issue #KN-445 fix: MigrateCSPDataCommand enhancement with content Ids --- kubernetes/helm_charts/datapipeline_jobs/values.j2 | 4 ++-- .../sunbird/sync/tool/mgr/CSPMigrationMessageGenerator.java | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/kubernetes/helm_charts/datapipeline_jobs/values.j2 b/kubernetes/helm_charts/datapipeline_jobs/values.j2 index 46c6e33903..9262657308 100644 --- a/kubernetes/helm_charts/datapipeline_jobs/values.j2 +++ b/kubernetes/helm_charts/datapipeline_jobs/values.j2 @@ -1158,8 +1158,8 @@ csp-migrator: neo4j_fields_to_migrate = { "asset": ["artifactUrl", "thumbnail", "downloadUrl","variants"], - "content": ["appIcon", "artifactUrl", "posterImage", "previewUrl", "thumbnail", "assetsMap", "certTemplate", "itemSetPreviewUrl", "grayScaleAppIcon", "sourceURL", "variants", "downloadUrl","streamingUrl"], - "contentimage": ["appIcon", "artifactUrl", "posterImage", "previewUrl", "thumbnail", "assetsMap", "certTemplate", "itemSetPreviewUrl", "grayScaleAppIcon", "sourceURL", "variants", "downloadUrl","streamingUrl"], + "content": ["appIcon", "artifactUrl", "posterImage", "previewUrl", "thumbnail", "assetsMap", "certTemplate", "itemSetPreviewUrl", "grayScaleAppIcon", "sourceURL", "variants", "downloadUrl","streamingUrl","transcripts"], + "contentimage": ["appIcon", "artifactUrl", "posterImage", "previewUrl", "thumbnail", "assetsMap", "certTemplate", "itemSetPreviewUrl", "grayScaleAppIcon", "sourceURL", "variants", "downloadUrl","streamingUrl","transcripts"], "collection": ["appIcon", "artifactUrl", "posterImage", "previewUrl", "thumbnail", "toc_url", "grayScaleAppIcon", "variants", "downloadUrl"], "collectionimage": ["appIcon", "artifactUrl", "posterImage", "previewUrl", "thumbnail", "toc_url", "grayScaleAppIcon", "variants", "downloadUrl"], "plugins": ["artifactUrl"], diff --git a/platform-tools/spikes/sync-tool/src/main/java/org/sunbird/sync/tool/mgr/CSPMigrationMessageGenerator.java b/platform-tools/spikes/sync-tool/src/main/java/org/sunbird/sync/tool/mgr/CSPMigrationMessageGenerator.java index a433d11a81..c285937397 100644 --- a/platform-tools/spikes/sync-tool/src/main/java/org/sunbird/sync/tool/mgr/CSPMigrationMessageGenerator.java +++ b/platform-tools/spikes/sync-tool/src/main/java/org/sunbird/sync/tool/mgr/CSPMigrationMessageGenerator.java @@ -100,8 +100,10 @@ public void generateMgrMsg(String graphId, String[] objectTypes, String[] mimeTy continue; } if (CollectionUtils.isNotEmpty(nodes)) { + System.out.println("CSPMigrationMessageGenerator:: generateMgrMsg:: nodes: " + nodes.size()); start += batchSize; Map events = generateMigrationEvent(nodes, errors); + System.out.println("CSPMigrationMessageGenerator:: generateMgrMsg:: events: " + events.size()); sendEvent(events, errors); current += events.size(); printProgress(startTime, total, current); From e4de2c3b7258c1efa8f758b8f4a23da24461bc0c Mon Sep 17 00:00:00 2001 From: Amit Priyadarshi Date: Tue, 17 Jan 2023 12:16:08 +0530 Subject: [PATCH 170/222] Handling google drive link --- kubernetes/helm_charts/datapipeline_jobs/values.j2 | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/kubernetes/helm_charts/datapipeline_jobs/values.j2 b/kubernetes/helm_charts/datapipeline_jobs/values.j2 index 9262657308..8acc5a39ea 100644 --- a/kubernetes/helm_charts/datapipeline_jobs/values.j2 +++ b/kubernetes/helm_charts/datapipeline_jobs/values.j2 @@ -1137,6 +1137,16 @@ csp-migrator: assessment_table = "question_data" } + cloud_storage { + folder { + content = "content" + artifact = "artifact" + } + } + + gdrive.application_name=drive-download + g_service_acct_cred="{{ auto_creator_g_service_acct_cred }}" + questionset.hierarchy.keyspace="{{ questionset_hierarchy_keyspace_name }}" questionset.hierarchy.table="questionset_hierarchy" From 4ea3f878d45a95f62a58afcd78f52e7cc19d48a5 Mon Sep 17 00:00:00 2001 From: Amit Priyadarshi Date: Tue, 17 Jan 2023 22:33:39 +0530 Subject: [PATCH 171/222] Update values.j2 --- kubernetes/helm_charts/datapipeline_jobs/values.j2 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kubernetes/helm_charts/datapipeline_jobs/values.j2 b/kubernetes/helm_charts/datapipeline_jobs/values.j2 index 8acc5a39ea..342bcdb7e3 100644 --- a/kubernetes/helm_charts/datapipeline_jobs/values.j2 +++ b/kubernetes/helm_charts/datapipeline_jobs/values.j2 @@ -73,7 +73,7 @@ base_config: | } job { env = "{{ env_name }}" - enable.distributed.checkpointing = true + enable.distributed.checkpointing = false statebackend { blob { storage { From 8f7b5b3946d73ef8e9fb60adee7fd792f4e84fc2 Mon Sep 17 00:00:00 2001 From: Amit Priyadarshi Date: Thu, 19 Jan 2023 01:23:53 +0530 Subject: [PATCH 172/222] configuring the application name --- kubernetes/ansible/roles/flink-jobs-deploy/defaults/main.yml | 1 + kubernetes/helm_charts/datapipeline_jobs/values.j2 | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/kubernetes/ansible/roles/flink-jobs-deploy/defaults/main.yml b/kubernetes/ansible/roles/flink-jobs-deploy/defaults/main.yml index 0bf436db09..220e2f9fa7 100644 --- a/kubernetes/ansible/roles/flink-jobs-deploy/defaults/main.yml +++ b/kubernetes/ansible/roles/flink-jobs-deploy/defaults/main.yml @@ -396,3 +396,4 @@ content_republish_topic_name: "{{ env_name }}.republish.job.request" video_stream_topic_name: "{{ env_name }}.live.video.stream.request" content_hierarchy_keyspace_name: "{{ hierarchy_keyspace_name }}" questionset_hierarchy_keyspace_name: "{{ hierarchy_keyspace_name }}" +gdrive_application_name: "drive-download" diff --git a/kubernetes/helm_charts/datapipeline_jobs/values.j2 b/kubernetes/helm_charts/datapipeline_jobs/values.j2 index 342bcdb7e3..301e0f411d 100644 --- a/kubernetes/helm_charts/datapipeline_jobs/values.j2 +++ b/kubernetes/helm_charts/datapipeline_jobs/values.j2 @@ -73,7 +73,7 @@ base_config: | } job { env = "{{ env_name }}" - enable.distributed.checkpointing = false + enable.distributed.checkpointing = true statebackend { blob { storage { @@ -1144,7 +1144,7 @@ csp-migrator: } } - gdrive.application_name=drive-download + gdrive.application_name="{{ gdrive_application_name }}" g_service_acct_cred="{{ auto_creator_g_service_acct_cred }}" questionset.hierarchy.keyspace="{{ questionset_hierarchy_keyspace_name }}" From 6c7bf591d0c53cc06726842676a0e22fc0ea608e Mon Sep 17 00:00:00 2001 From: anilgupta Date: Mon, 30 Jan 2023 11:38:26 +0530 Subject: [PATCH 173/222] Issue #KN-439 chore: Moved out the query out from condition. --- ansible/roles/cassandra-db-update/templates/data.cql.j2 | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/ansible/roles/cassandra-db-update/templates/data.cql.j2 b/ansible/roles/cassandra-db-update/templates/data.cql.j2 index 6c1b623494..de964cfcbf 100644 --- a/ansible/roles/cassandra-db-update/templates/data.cql.j2 +++ b/ansible/roles/cassandra-db-update/templates/data.cql.j2 @@ -51,11 +51,9 @@ ALTER KEYSPACE {{ hierarchy_keyspace_name }} WITH replication = { 'class': 'NetworkTopologyStrategy', 'datacenter1' : 2 }; - -ALTER TABLE {{ hierarchy_keyspace_name }}.content_hierarchy ADD relational_metadata text; - {% endif %} +ALTER TABLE {{ hierarchy_keyspace_name }}.content_hierarchy ADD relational_metadata text; CREATE TRIGGER IF NOT EXISTS content_data_trigger ON {{ content_keyspace_name }}.content_data USING 'org.sunbird.cassandra.triggers.TransactionEventTrigger'; CREATE TRIGGER IF NOT EXISTS question_data_trigger ON {{ content_keyspace_name }}.question_data USING 'org.sunbird.cassandra.triggers.TransactionEventTrigger'; From f9458be51f510d6e13b868d3cffcf3ce065507af Mon Sep 17 00:00:00 2001 From: Kartheek Palla Date: Thu, 2 Feb 2023 17:33:46 +0530 Subject: [PATCH 174/222] Issue #KN-801 fix: removing samza jobs from knowlg --- ansible/lp_samza_deploy.yml | 37 - ansible/lp_samza_telemetry_schemas.yml | 28 - ansible/lp_yarn_provision.yml | 47 -- .../samza-job-monitor/files/samza_alerts.zip | Bin 5034 -> 0 bytes .../roles/samza-job-monitor/tasks/main.yml | 62 -- .../samza-job-monitor/templates/samza-monitor | 109 --- .../templates/samza-monitor-systemd | 14 - ansible/roles/samza-job-server/tasks/main.yml | 14 - .../templates/samza-job-server.sh | 104 --- .../defaults/main.yml | 5 - .../tasks/main.yml | 12 - .../es-router-additional-config.json | 140 ---- ...es-router-additional-secondary-config.json | 29 - ...ect-denormalization-additional-config.json | 84 -- .../tasks/main.yml | 17 - ansible/roles/samza-jobs/defaults/main.yml | 51 -- .../roles/samza-jobs/files/find_job_name.sh | 1 - .../samza-jobs/files/get_all_job_name.sh | 7 - .../files/get_all_running_app_id.sh | 2 - .../files/get_all_running_app_name.sh | 11 - .../roles/samza-jobs/files/kill_all_app.sh | 9 - ansible/roles/samza-jobs/files/kill_jobs.sh | 11 - .../roles/samza-jobs/files/remove_old_tar.sh | 12 - ansible/roles/samza-jobs/files/start_jobs.sh | 15 - .../samza-jobs/files/update_new_job_name.sh | 14 - ansible/roles/samza-jobs/tasks/deploy.yml | 104 --- ansible/roles/samza-jobs/tasks/main.yml | 9 - ansible/roles/samza-jobs/tasks/start_jobs.yml | 21 - ansible/roles/samza-jobs/tasks/stop_jobs.yml | 16 - ansible/roles/yarn/defaults/main.yml | 16 - ansible/roles/yarn/files/truncate_logs.sh | 21 - ansible/roles/yarn/tasks/common.yml | 78 -- ansible/roles/yarn/tasks/main.yml | 55 -- ansible/roles/yarn/tasks/truncate-logs.yml | 8 - .../yarn/templates/capacity-scheduler.xml | 111 --- ansible/roles/yarn/templates/config.j2 | 3 - ansible/roles/yarn/templates/core-site.xml | 7 - ansible/roles/yarn/templates/hadoop-env.sh | 2 - ansible/roles/yarn/templates/log4j.properties | 267 ------- ansible/roles/yarn/templates/yarn-site.xml | 64 -- ansible/samza_jobs_alert.yml | 9 - ansible/samza_logs_provision.yml | 8 - pipelines/build/yarn/Jenkinsfile | 45 -- pipelines/build/yarn/auto_build_deploy | 63 -- pipelines/deploy/yarn/Jenkinsfile | 60 -- platform-jobs/.gitignore | 1 - platform-jobs/pom.xml | 83 -- platform-jobs/samza/auto-creator/pom.xml | 87 --- .../auto-creator/src/main/assembly/src.xml | 69 -- .../src/main/config/auto-creator.properties | 89 --- .../local.auto-creator.properties.properties | 74 -- .../samza/service/AutoCreatorService.java | 111 --- .../jobs/samza/task/AutoCreatorTask.java | 59 -- .../jobs/samza/util/AutoCreatorParams.java | 8 - .../sunbird/jobs/samza/util/ContentUtil.java | 717 ------------------ .../jobs/samza/util/GoogleDriveUtil.java | 156 ---- .../sunbird/jobs/samza/util/UnirestUtil.java | 148 ---- .../src/main/resources/actor-config.xml | 24 - .../src/main/resources/application.conf | 13 - .../auto-creator/src/main/resources/log4j.xml | 20 - platform-jobs/samza/common/.gitignore | 1 - platform-jobs/samza/common/pom.xml | 116 --- .../samza/exception/PlatformErrorCodes.java | 6 - .../samza/exception/PlatformException.java | 24 - .../samza/serializers/EkstepJsonSerde.java | 109 --- .../serializers/EkstepJsonSerdeFactory.java | 16 - .../jobs/samza/service/ISamzaService.java | 26 - .../jobs/samza/service/task/JobMetrics.java | 129 ---- .../samza/service/util/AbstractESIndexer.java | 20 - .../sunbird/jobs/samza/task/AbstractTask.java | 204 ----- .../org/sunbird/jobs/samza/task/BaseTask.java | 14 - .../jobs/samza/util/FailedEventsUtil.java | 41 - .../sunbird/jobs/samza/util/JSONUtils.java | 32 - .../sunbird/jobs/samza/util/JobLogger.java | 90 --- .../jobs/samza/util/SamzaCommonParams.java | 6 - .../jobs/samza/util/TrackableENUM.java | 5 - .../sunbird/samza/jobs/test/JSONUtilTest.java | 33 - .../samza/jobs/test/JobMetricsTest.java | 120 --- .../samza/jobs/test/MetricsStreamStub.java | 138 ---- platform-jobs/samza/course-common/pom.xml | 30 - .../org/sunbird/jobs/samza/task/BaseTask.java | 189 ----- .../jobs/samza/util/CassandraConnector.java | 37 - .../sunbird/jobs/samza/util/RedisConnect.java | 37 - .../util/SunbirdCassandraColumnMapper.java | 56 -- .../jobs/samza/util/SunbirdCassandraUtil.java | 102 --- platform-jobs/samza/distribution/.gitignore | 1 - platform-jobs/samza/distribution/pom.xml | 48 -- .../distribution/src/main/assembly/src.xml | 14 - .../samza/merge-user-courses/pom.xml | 139 ---- .../src/main/assembly/src.xml | 69 -- .../local.merge-user-courses.properties | 81 -- .../main/config/merge-user-courses.properties | 78 -- .../samza/model/BatchEnrollmentSyncModel.java | 32 - .../service/MergeUserCoursesService.java | 460 ----------- .../jobs/samza/task/MergeUserCoursesTask.java | 40 - .../samza/util/MergeUserCoursesParams.java | 9 - .../src/main/resources/log4j.xml | 20 - .../samza/mvc-processor-indexer/pom.xml | 114 --- .../src/main/assembly/src.xml | 70 -- .../local.mvc-processor-indexer.properties | 78 -- .../config/mvc-processor-indexer.properties | 99 --- .../samza/service/MVCProcessorService.java | 96 --- .../service/util/CassandraConnector.java | 95 --- .../samza/service/util/ContentUtil.java | 47 -- .../util/MVCProcessorCassandraIndexer.java | 149 ---- .../service/util/MVCProcessorESIndexer.java | 130 ---- .../samza/task/MVCSearchIndexerTask.java | 100 --- .../src/main/resources/application.conf | 0 .../src/main/resources/log4j.xml | 20 - .../mvcjobs/samza/test/ContentUtilTest.java | 48 -- .../samza/test/MVCProcessorCassandraTest.java | 77 -- .../samza/test/MVCProcessorESIndexerTest.java | 89 --- .../src/test/resources/application.conf | 40 - platform-jobs/samza/pom.xml | 45 -- .../samza/publish-pipeline/.gitignore | 1 - platform-jobs/samza/publish-pipeline/pom.xml | 87 --- .../src/main/assembly/src.xml | 69 -- .../config/local.publish-pipeline.properties | 167 ---- .../main/config/publish-pipeline.properties | 192 ----- .../samza/service/PublishPipelineService.java | 366 --------- .../jobs/samza/task/PublishPipelineTask.java | 42 - .../samza/util/PublishPipelineParams.java | 11 - .../src/main/resources/actor-config.xml | 24 - .../src/main/resources/application.conf | 13 - .../src/main/resources/log4j.xml | 20 - .../src/main/resources/questionSetTemplate.vm | 76 -- .../samza/qr-image-generator/pom.xml | 45 -- .../qrimage/generator/QRImageGenerator.java | 281 ------- .../qrimage/request/QRImageConfig.java | 104 --- .../qrimage/request/QRImageRequest.java | 54 -- .../src/main/resources/Verdana.ttf | Bin 129364 -> 0 bytes .../samza/qrcode-image-generator/.gitignore | 2 - .../samza/qrcode-image-generator/pom.xml | 63 -- .../src/main/assembly/src.xml | 69 -- .../local.qrcode-image-generator.properties | 73 -- .../config/qrcode-image-generator.properties | 73 -- .../samza/model/QRCodeGenerationRequest.java | 134 ---- .../service/QRCodeImageGeneratorService.java | 154 ---- .../samza/task/QRCodeImageGeneratorTask.java | 64 -- .../jobs/samza/util/CloudStorageUtil.java | 62 -- .../samza/util/QRCodeCassandraConnector.java | 27 - .../util/QRCodeImageGeneratorParams.java | 10 - .../samza/util/QRCodeImageGeneratorUtil.java | 288 ------- .../jobs/samza/util/ZipEditorUtil.java | 40 - .../src/main/resources/Verdana.ttf | Bin 129364 -> 0 bytes .../src/main/resources/log4j.xml | 20 - pom.xml | 9 - 147 files changed, 9970 deletions(-) delete mode 100644 ansible/lp_samza_deploy.yml delete mode 100644 ansible/lp_samza_telemetry_schemas.yml delete mode 100644 ansible/lp_yarn_provision.yml delete mode 100644 ansible/roles/samza-job-monitor/files/samza_alerts.zip delete mode 100644 ansible/roles/samza-job-monitor/tasks/main.yml delete mode 100644 ansible/roles/samza-job-monitor/templates/samza-monitor delete mode 100644 ansible/roles/samza-job-monitor/templates/samza-monitor-systemd delete mode 100644 ansible/roles/samza-job-server/tasks/main.yml delete mode 100644 ansible/roles/samza-job-server/templates/samza-job-server.sh delete mode 100644 ansible/roles/samza-jobs-additional-config/defaults/main.yml delete mode 100644 ansible/roles/samza-jobs-additional-config/tasks/main.yml delete mode 100644 ansible/roles/samza-jobs-additional-config/templates/es-router-additional-config.json delete mode 100644 ansible/roles/samza-jobs-additional-config/templates/es-router-additional-secondary-config.json delete mode 100644 ansible/roles/samza-jobs-additional-config/templates/object-denormalization-additional-config.json delete mode 100644 ansible/roles/samza-jobs-telemetry-schemas/tasks/main.yml delete mode 100644 ansible/roles/samza-jobs/defaults/main.yml delete mode 100644 ansible/roles/samza-jobs/files/find_job_name.sh delete mode 100644 ansible/roles/samza-jobs/files/get_all_job_name.sh delete mode 100644 ansible/roles/samza-jobs/files/get_all_running_app_id.sh delete mode 100644 ansible/roles/samza-jobs/files/get_all_running_app_name.sh delete mode 100644 ansible/roles/samza-jobs/files/kill_all_app.sh delete mode 100644 ansible/roles/samza-jobs/files/kill_jobs.sh delete mode 100644 ansible/roles/samza-jobs/files/remove_old_tar.sh delete mode 100644 ansible/roles/samza-jobs/files/start_jobs.sh delete mode 100644 ansible/roles/samza-jobs/files/update_new_job_name.sh delete mode 100644 ansible/roles/samza-jobs/tasks/deploy.yml delete mode 100644 ansible/roles/samza-jobs/tasks/main.yml delete mode 100644 ansible/roles/samza-jobs/tasks/start_jobs.yml delete mode 100644 ansible/roles/samza-jobs/tasks/stop_jobs.yml delete mode 100644 ansible/roles/yarn/defaults/main.yml delete mode 100644 ansible/roles/yarn/files/truncate_logs.sh delete mode 100644 ansible/roles/yarn/tasks/common.yml delete mode 100644 ansible/roles/yarn/tasks/main.yml delete mode 100644 ansible/roles/yarn/tasks/truncate-logs.yml delete mode 100644 ansible/roles/yarn/templates/capacity-scheduler.xml delete mode 100644 ansible/roles/yarn/templates/config.j2 delete mode 100644 ansible/roles/yarn/templates/core-site.xml delete mode 100644 ansible/roles/yarn/templates/hadoop-env.sh delete mode 100644 ansible/roles/yarn/templates/log4j.properties delete mode 100644 ansible/roles/yarn/templates/yarn-site.xml delete mode 100644 ansible/samza_jobs_alert.yml delete mode 100644 ansible/samza_logs_provision.yml delete mode 100644 pipelines/build/yarn/Jenkinsfile delete mode 100644 pipelines/build/yarn/auto_build_deploy delete mode 100644 pipelines/deploy/yarn/Jenkinsfile delete mode 100644 platform-jobs/.gitignore delete mode 100644 platform-jobs/pom.xml delete mode 100644 platform-jobs/samza/auto-creator/pom.xml delete mode 100644 platform-jobs/samza/auto-creator/src/main/assembly/src.xml delete mode 100644 platform-jobs/samza/auto-creator/src/main/config/auto-creator.properties delete mode 100644 platform-jobs/samza/auto-creator/src/main/config/local.auto-creator.properties.properties delete mode 100644 platform-jobs/samza/auto-creator/src/main/java/org/sunbird/jobs/samza/service/AutoCreatorService.java delete mode 100644 platform-jobs/samza/auto-creator/src/main/java/org/sunbird/jobs/samza/task/AutoCreatorTask.java delete mode 100644 platform-jobs/samza/auto-creator/src/main/java/org/sunbird/jobs/samza/util/AutoCreatorParams.java delete mode 100644 platform-jobs/samza/auto-creator/src/main/java/org/sunbird/jobs/samza/util/ContentUtil.java delete mode 100644 platform-jobs/samza/auto-creator/src/main/java/org/sunbird/jobs/samza/util/GoogleDriveUtil.java delete mode 100644 platform-jobs/samza/auto-creator/src/main/java/org/sunbird/jobs/samza/util/UnirestUtil.java delete mode 100644 platform-jobs/samza/auto-creator/src/main/resources/actor-config.xml delete mode 100644 platform-jobs/samza/auto-creator/src/main/resources/application.conf delete mode 100644 platform-jobs/samza/auto-creator/src/main/resources/log4j.xml delete mode 100644 platform-jobs/samza/common/.gitignore delete mode 100644 platform-jobs/samza/common/pom.xml delete mode 100644 platform-jobs/samza/common/src/main/java/org/sunbird/jobs/samza/exception/PlatformErrorCodes.java delete mode 100644 platform-jobs/samza/common/src/main/java/org/sunbird/jobs/samza/exception/PlatformException.java delete mode 100644 platform-jobs/samza/common/src/main/java/org/sunbird/jobs/samza/serializers/EkstepJsonSerde.java delete mode 100644 platform-jobs/samza/common/src/main/java/org/sunbird/jobs/samza/serializers/EkstepJsonSerdeFactory.java delete mode 100644 platform-jobs/samza/common/src/main/java/org/sunbird/jobs/samza/service/ISamzaService.java delete mode 100644 platform-jobs/samza/common/src/main/java/org/sunbird/jobs/samza/service/task/JobMetrics.java delete mode 100644 platform-jobs/samza/common/src/main/java/org/sunbird/jobs/samza/service/util/AbstractESIndexer.java delete mode 100644 platform-jobs/samza/common/src/main/java/org/sunbird/jobs/samza/task/AbstractTask.java delete mode 100644 platform-jobs/samza/common/src/main/java/org/sunbird/jobs/samza/task/BaseTask.java delete mode 100644 platform-jobs/samza/common/src/main/java/org/sunbird/jobs/samza/util/FailedEventsUtil.java delete mode 100644 platform-jobs/samza/common/src/main/java/org/sunbird/jobs/samza/util/JSONUtils.java delete mode 100644 platform-jobs/samza/common/src/main/java/org/sunbird/jobs/samza/util/JobLogger.java delete mode 100644 platform-jobs/samza/common/src/main/java/org/sunbird/jobs/samza/util/SamzaCommonParams.java delete mode 100644 platform-jobs/samza/common/src/main/java/org/sunbird/jobs/samza/util/TrackableENUM.java delete mode 100644 platform-jobs/samza/common/src/test/java/org/sunbird/samza/jobs/test/JSONUtilTest.java delete mode 100644 platform-jobs/samza/common/src/test/java/org/sunbird/samza/jobs/test/JobMetricsTest.java delete mode 100644 platform-jobs/samza/common/src/test/java/org/sunbird/samza/jobs/test/MetricsStreamStub.java delete mode 100644 platform-jobs/samza/course-common/pom.xml delete mode 100644 platform-jobs/samza/course-common/src/main/java/org/sunbird/jobs/samza/task/BaseTask.java delete mode 100644 platform-jobs/samza/course-common/src/main/java/org/sunbird/jobs/samza/util/CassandraConnector.java delete mode 100644 platform-jobs/samza/course-common/src/main/java/org/sunbird/jobs/samza/util/RedisConnect.java delete mode 100644 platform-jobs/samza/course-common/src/main/java/org/sunbird/jobs/samza/util/SunbirdCassandraColumnMapper.java delete mode 100644 platform-jobs/samza/course-common/src/main/java/org/sunbird/jobs/samza/util/SunbirdCassandraUtil.java delete mode 100644 platform-jobs/samza/distribution/.gitignore delete mode 100644 platform-jobs/samza/distribution/pom.xml delete mode 100644 platform-jobs/samza/distribution/src/main/assembly/src.xml delete mode 100644 platform-jobs/samza/merge-user-courses/pom.xml delete mode 100644 platform-jobs/samza/merge-user-courses/src/main/assembly/src.xml delete mode 100644 platform-jobs/samza/merge-user-courses/src/main/config/local.merge-user-courses.properties delete mode 100644 platform-jobs/samza/merge-user-courses/src/main/config/merge-user-courses.properties delete mode 100644 platform-jobs/samza/merge-user-courses/src/main/java/org/sunbird/jobs/samza/model/BatchEnrollmentSyncModel.java delete mode 100644 platform-jobs/samza/merge-user-courses/src/main/java/org/sunbird/jobs/samza/service/MergeUserCoursesService.java delete mode 100644 platform-jobs/samza/merge-user-courses/src/main/java/org/sunbird/jobs/samza/task/MergeUserCoursesTask.java delete mode 100644 platform-jobs/samza/merge-user-courses/src/main/java/org/sunbird/jobs/samza/util/MergeUserCoursesParams.java delete mode 100644 platform-jobs/samza/merge-user-courses/src/main/resources/log4j.xml delete mode 100644 platform-jobs/samza/mvc-processor-indexer/pom.xml delete mode 100644 platform-jobs/samza/mvc-processor-indexer/src/main/assembly/src.xml delete mode 100644 platform-jobs/samza/mvc-processor-indexer/src/main/config/local.mvc-processor-indexer.properties delete mode 100644 platform-jobs/samza/mvc-processor-indexer/src/main/config/mvc-processor-indexer.properties delete mode 100644 platform-jobs/samza/mvc-processor-indexer/src/main/java/org/sunbird/mvcjobs/samza/service/MVCProcessorService.java delete mode 100644 platform-jobs/samza/mvc-processor-indexer/src/main/java/org/sunbird/mvcjobs/samza/service/util/CassandraConnector.java delete mode 100644 platform-jobs/samza/mvc-processor-indexer/src/main/java/org/sunbird/mvcjobs/samza/service/util/ContentUtil.java delete mode 100644 platform-jobs/samza/mvc-processor-indexer/src/main/java/org/sunbird/mvcjobs/samza/service/util/MVCProcessorCassandraIndexer.java delete mode 100644 platform-jobs/samza/mvc-processor-indexer/src/main/java/org/sunbird/mvcjobs/samza/service/util/MVCProcessorESIndexer.java delete mode 100644 platform-jobs/samza/mvc-processor-indexer/src/main/java/org/sunbird/mvcjobs/samza/task/MVCSearchIndexerTask.java delete mode 100644 platform-jobs/samza/mvc-processor-indexer/src/main/resources/application.conf delete mode 100644 platform-jobs/samza/mvc-processor-indexer/src/main/resources/log4j.xml delete mode 100644 platform-jobs/samza/mvc-processor-indexer/src/test/java/org/sunbird/mvcjobs/samza/test/ContentUtilTest.java delete mode 100644 platform-jobs/samza/mvc-processor-indexer/src/test/java/org/sunbird/mvcjobs/samza/test/MVCProcessorCassandraTest.java delete mode 100644 platform-jobs/samza/mvc-processor-indexer/src/test/java/org/sunbird/mvcjobs/samza/test/MVCProcessorESIndexerTest.java delete mode 100644 platform-jobs/samza/mvc-processor-indexer/src/test/resources/application.conf delete mode 100644 platform-jobs/samza/pom.xml delete mode 100644 platform-jobs/samza/publish-pipeline/.gitignore delete mode 100644 platform-jobs/samza/publish-pipeline/pom.xml delete mode 100644 platform-jobs/samza/publish-pipeline/src/main/assembly/src.xml delete mode 100644 platform-jobs/samza/publish-pipeline/src/main/config/local.publish-pipeline.properties delete mode 100644 platform-jobs/samza/publish-pipeline/src/main/config/publish-pipeline.properties delete mode 100644 platform-jobs/samza/publish-pipeline/src/main/java/org/sunbird/jobs/samza/service/PublishPipelineService.java delete mode 100644 platform-jobs/samza/publish-pipeline/src/main/java/org/sunbird/jobs/samza/task/PublishPipelineTask.java delete mode 100644 platform-jobs/samza/publish-pipeline/src/main/java/org/sunbird/jobs/samza/util/PublishPipelineParams.java delete mode 100644 platform-jobs/samza/publish-pipeline/src/main/resources/actor-config.xml delete mode 100644 platform-jobs/samza/publish-pipeline/src/main/resources/application.conf delete mode 100644 platform-jobs/samza/publish-pipeline/src/main/resources/log4j.xml delete mode 100644 platform-jobs/samza/publish-pipeline/src/main/resources/questionSetTemplate.vm delete mode 100644 platform-jobs/samza/qr-image-generator/pom.xml delete mode 100644 platform-jobs/samza/qr-image-generator/src/main/java/org/sunbird/qrimage/generator/QRImageGenerator.java delete mode 100644 platform-jobs/samza/qr-image-generator/src/main/java/org/sunbird/qrimage/request/QRImageConfig.java delete mode 100644 platform-jobs/samza/qr-image-generator/src/main/java/org/sunbird/qrimage/request/QRImageRequest.java delete mode 100755 platform-jobs/samza/qr-image-generator/src/main/resources/Verdana.ttf delete mode 100644 platform-jobs/samza/qrcode-image-generator/.gitignore delete mode 100644 platform-jobs/samza/qrcode-image-generator/pom.xml delete mode 100644 platform-jobs/samza/qrcode-image-generator/src/main/assembly/src.xml delete mode 100644 platform-jobs/samza/qrcode-image-generator/src/main/config/local.qrcode-image-generator.properties delete mode 100644 platform-jobs/samza/qrcode-image-generator/src/main/config/qrcode-image-generator.properties delete mode 100644 platform-jobs/samza/qrcode-image-generator/src/main/java/org/sunbird/jobs/samza/model/QRCodeGenerationRequest.java delete mode 100644 platform-jobs/samza/qrcode-image-generator/src/main/java/org/sunbird/jobs/samza/service/QRCodeImageGeneratorService.java delete mode 100644 platform-jobs/samza/qrcode-image-generator/src/main/java/org/sunbird/jobs/samza/task/QRCodeImageGeneratorTask.java delete mode 100644 platform-jobs/samza/qrcode-image-generator/src/main/java/org/sunbird/jobs/samza/util/CloudStorageUtil.java delete mode 100644 platform-jobs/samza/qrcode-image-generator/src/main/java/org/sunbird/jobs/samza/util/QRCodeCassandraConnector.java delete mode 100644 platform-jobs/samza/qrcode-image-generator/src/main/java/org/sunbird/jobs/samza/util/QRCodeImageGeneratorParams.java delete mode 100644 platform-jobs/samza/qrcode-image-generator/src/main/java/org/sunbird/jobs/samza/util/QRCodeImageGeneratorUtil.java delete mode 100644 platform-jobs/samza/qrcode-image-generator/src/main/java/org/sunbird/jobs/samza/util/ZipEditorUtil.java delete mode 100755 platform-jobs/samza/qrcode-image-generator/src/main/resources/Verdana.ttf delete mode 100644 platform-jobs/samza/qrcode-image-generator/src/main/resources/log4j.xml diff --git a/ansible/lp_samza_deploy.yml b/ansible/lp_samza_deploy.yml deleted file mode 100644 index f66377a98d..0000000000 --- a/ansible/lp_samza_deploy.yml +++ /dev/null @@ -1,37 +0,0 @@ ---- -- name: "Start Nodemanager on Slaves" - hosts: "yarn-slave" - vars: - hadoop_version: 2.7.2 - vars_files: - - "{{inventory_dir}}/secrets.yml" - become: yes - tasks: - - name: Ensure yarn nodemanager is running - become_user: hduser - shell: | - (ps aux | grep yarn-hduser-nodemanager | grep -v grep) || /usr/local/hadoop/sbin/yarn-daemon.sh --config /usr/local/hadoop-{{hadoop_version}}/conf/ start nodemanager || sleep 10 - - - name: Install mysql client - apt: name=mysql-client state=present - - - name: install imagemagick - apt: name=imagemagick state=present update_cache=yes - -- name: "Copy Samza jobs additional configuration to slaves" - become: yes - hosts: "yarn-slave" - vars_files: - - "{{inventory_dir}}/secrets.yml" - roles: - - samza-jobs-additional-config - -- name: "Deploy Samza jobs" - hosts: "yarn-master" - become: yes - vars_files: - - "{{inventory_dir}}/secrets.yml" - vars: - deploy_jobs: true - roles: - - samza-jobs diff --git a/ansible/lp_samza_telemetry_schemas.yml b/ansible/lp_samza_telemetry_schemas.yml deleted file mode 100644 index c3b56eae4d..0000000000 --- a/ansible/lp_samza_telemetry_schemas.yml +++ /dev/null @@ -1,28 +0,0 @@ ---- -- name: "Copy validation schema files to Yarn Slaves" - hosts: "yarn-slave" - vars_files: - - "{{inventory_dir}}/secrets.yml" - become: yes - tasks: - - name: cloning the telemetry schema repo - git: - repo: "{{schema_repo_url}}" - dest: "{{telemetry_schema_directory}}" - version: "{{version}}" - force: yes - - name: Create schema directory - file: path={{telemetry_schema_directory}} owner=hduser group=hadoop recurse=yes state=directory - - - name: get schema dir names - raw: find {{telemetry_schema_path}} -type f -name "*.*" - register: schemas - - - name: change internal schema file reference - replace: - dest: "{{item}}" - regexp: "http://localhost:7070/schemas/" - replace: "file://{{telemetry_schema_path}}/" - owner: hduser - group: hadoop - with_items: "{{ schemas.stdout_lines }}" diff --git a/ansible/lp_yarn_provision.yml b/ansible/lp_yarn_provision.yml deleted file mode 100644 index b7866e0fa3..0000000000 --- a/ansible/lp_yarn_provision.yml +++ /dev/null @@ -1,47 +0,0 @@ ---- -- hosts: yarn - become: yes - vars_files: - - "{{inventory_dir}}/secrets.yml" - tasks: - - name: Create group - group: - name: hadoop - state: present - - name: Create user - user: - name: hduser - comment: "hduser" - group: hadoop - groups: sudo - shell: /bin/bash - -- name: Install samza job server - hosts: "yarn-master" - vars_files: - - "{{inventory_dir}}/secrets.yml" - become: yes - roles: - - jdk-1.8.0_121 - - yarn - - samza-job-server - -# # As of ansible2.5 delegation and inlcude tasks file won't work -# # with looping. So this is a possible workaround -# - name: Running common tasks in yarn slaves -# vars_files: -# - ./roles/yarn/defaults/main.yml -# hosts: "lp-yarn-slave" -# become: yes -# tasks: -# - include: ./roles/yarn/tasks/common.yml -# - include: ./roles/yarn/templates - -- name: Install java on all yarn slaves - hosts: "yarn-slave" - vars_files: - - "{{inventory_dir}}/secrets.yml" - become: yes - remote_user: hduser - roles: - - jdk-1.8.0_121 diff --git a/ansible/roles/samza-job-monitor/files/samza_alerts.zip b/ansible/roles/samza-job-monitor/files/samza_alerts.zip deleted file mode 100644 index 8a690455dfc16c9b65b3d195827dad30fd88e387..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5034 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 XZU= 3.0-6) to ensure that this file is present. -. /lib/lsb/init-functions - -# -# Function that starts the daemon/service -# -do_start() -{ - - start-stop-daemon --start --pidfile $PID --quiet --exec $NAME --test > /dev/null \ - || return 1 - - start-stop-daemon −−chdir=/opt/samza_alerts --start --make-pidfile --pidfile $PID --quiet --background --exec /usr/bin/env JOBS_COUNT=$JOBS_COUNT YARN_URL=$YARN_URL SAMZA_ENV=$SAMZA_ENV MONITORINGING_ENV_NAME=$MONITORINGING_ENV_NAME SLACK_CHANNEL=$SLACK_CHANNEL SLACK_URL=$SLACK_URL CHECK_DELAY=$CHECK_DELAY $NAME -- $ARGS \ - || return 2 -} - -# -# Function that stops the daemon/service -# -do_stop() -{ - # Return - # 0 if daemon has been stopped - # 1 if daemon was already stopped - # 2 if daemon could not be stopped - # other if a failure occurred - start-stop-daemon --stop --pidfile $PID --quiet --oknodo - RETVAL="$?" - rm -f $PID - return "$RETVAL" -} - -case "$1" in - start) - log_daemon_msg "Starting $DESC" - do_start - case "$?" in - 0|1) log_end_msg 0 ;; - 2) log_end_msg 1 ;; - esac - ;; - stop) - log_daemon_msg "Stopping $DESC" - do_stop - case "$?" in - 0|1) log_end_msg 0 ;; - 2) log_end_msg 1 ;; - esac - ;; - status) - status_of_proc -p $PID $NAME $DESC && exit 0 || exit $? - ;; - restart) - log_daemon_msg "Restarting $DESC" - do_stop - case "$?" in - 0|1) - do_start - case "$?" in - 0) log_end_msg 0 ;; - 1) log_end_msg 1 ;; # Old process is still running - *) log_end_msg 1 ;; # Failed to start - esac - ;; - *) - # Failed to stop - log_end_msg 1 - ;; - esac - ;; - *) - echo "Usage: $SCRIPTNAME {start|stop|restart}" >&2 - exit 3 - ;; -esac - -exit 0 diff --git a/ansible/roles/samza-job-monitor/templates/samza-monitor-systemd b/ansible/roles/samza-job-monitor/templates/samza-monitor-systemd deleted file mode 100644 index bd8dce7efd..0000000000 --- a/ansible/roles/samza-job-monitor/templates/samza-monitor-systemd +++ /dev/null @@ -1,14 +0,0 @@ -[Unit] -Description=samza-monitor deamon - -[Service] -Type=forking -User=root -Group=root -LimitNOFILE=32768 -Restart=on-failure -ExecStart=/etc/init.d/samza-monitor start -ExecStop=/etc/init.d/samza-monitor stop - -[Install] -WantedBy=multi-user.target diff --git a/ansible/roles/samza-job-server/tasks/main.yml b/ansible/roles/samza-job-server/tasks/main.yml deleted file mode 100644 index ce685a8d5d..0000000000 --- a/ansible/roles/samza-job-server/tasks/main.yml +++ /dev/null @@ -1,14 +0,0 @@ -- name: Create Directory for Jobs - file: path={{item}} owner=hduser group=hadoop recurse=yes state=directory - with_items: - - /home/hduser/samza-jobs - become: yes -- name: Install python - apt: name=python state=present - become: yes -- name: Copy init file - template: src=samza-job-server.sh dest=/etc/init.d/samza-job-server mode=755 - become: yes -- name: Start samza job server - service: name=samza-job-server state=restarted enabled=yes - become: yes \ No newline at end of file diff --git a/ansible/roles/samza-job-server/templates/samza-job-server.sh b/ansible/roles/samza-job-server/templates/samza-job-server.sh deleted file mode 100644 index c0535ef699..0000000000 --- a/ansible/roles/samza-job-server/templates/samza-job-server.sh +++ /dev/null @@ -1,104 +0,0 @@ -#! /bin/sh -### BEGIN INIT INFO -# Provides: Ecosystem-Platform-API -# Default-Start: 2 3 4 5 -# Default-Stop: S 0 1 6 -# Short-Description: Ecosystem-Platform-API -# Description: Starts samza-job-server as a daemon. -### END INIT INFO - -DESC="Samza-Job-Server Daemon" -NAME=/usr/bin/python -LOGFILE="/var/log/samza-job-server/" -SCRIPTNAME=/etc/init.d/samza-job-server -PID="/var/run/samza-job-server.pid" - -ARGS="-m SimpleHTTPServer" - -SERVER_PATH="/home/hduser/samza-jobs" -# Exit if the package is not installed -if [ ! -x "$NAME" ]; then -{ - echo "Couldn't find $NAME" - exit 99 -} -fi - -# Define LSB log_* functions. -# Depend on lsb-base (>= 3.0-6) to ensure that this file is present. -. /lib/lsb/init-functions - -# -# Function that starts the daemon/service -# -do_start() -{ - - start-stop-daemon --start --pidfile $PID --quiet --exec $NAME --test > /dev/null \ - || return 1 - - start-stop-daemon --start --make-pidfile --pidfile $PID --quiet --background -d $SERVER_PATH --exec $NAME -- $ARGS \ - || return 2 -} - -# -# Function that stops the daemon/service -# -do_stop() -{ - # Return - # 0 if daemon has been stopped - # 1 if daemon was already stopped - # 2 if daemon could not be stopped - # other if a failure occurred - start-stop-daemon --stop --pidfile $PID --quiet --oknodo - RETVAL="$?" - rm -f $PID - return "$RETVAL" -} - -case "$1" in - start) - log_daemon_msg "Starting $DESC" - do_start - case "$?" in - 0|1) log_end_msg 0 ;; - 2) log_end_msg 1 ;; - esac - ;; - stop) - log_daemon_msg "Stopping $DESC" - do_stop - case "$?" in - 0|1) log_end_msg 0 ;; - 2) log_end_msg 1 ;; - esac - ;; - status) - status_of_proc -p $PID $NAME $DESC && exit 0 || exit $? - ;; - restart) - log_daemon_msg "Restarting $DESC" - do_stop - case "$?" in - 0|1) - do_start - case "$?" in - 0) log_end_msg 0 ;; - 1) log_end_msg 1 ;; # Old process is still running - *) log_end_msg 1 ;; # Failed to start - esac - ;; - *) - # Failed to stop - log_end_msg 1 - ;; - esac - ;; - *) - echo "Usage: $SCRIPTNAME {start|stop|restart}" >&2 - exit 3 - ;; -esac - -exit 0 diff --git a/ansible/roles/samza-jobs-additional-config/defaults/main.yml b/ansible/roles/samza-jobs-additional-config/defaults/main.yml deleted file mode 100644 index 626b7f0b2d..0000000000 --- a/ansible/roles/samza-jobs-additional-config/defaults/main.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -object_denormalization_additional_config_dir: /etc/samza-jobs/{{env}} -object_denormalization_additional_config: "{{object_denormalization_additional_config_dir}}/object-denormalization-additional-config.json" -es_router_additional_config: "{{object_denormalization_additional_config_dir}}/es-router-additional-config.json" -es_router_additional_secondary_config: "{{object_denormalization_additional_config_dir}}/es-router-additional-secondary-config.json" diff --git a/ansible/roles/samza-jobs-additional-config/tasks/main.yml b/ansible/roles/samza-jobs-additional-config/tasks/main.yml deleted file mode 100644 index 11e1535f12..0000000000 --- a/ansible/roles/samza-jobs-additional-config/tasks/main.yml +++ /dev/null @@ -1,12 +0,0 @@ ---- -- name: Create directory for additional config - file: path={{object_denormalization_additional_config_dir}} owner=hduser group=hadoop recurse=yes state=directory - -- name: Copy Object denormalization additional config - template: src=object-denormalization-additional-config.json dest={{object_denormalization_additional_config}} owner=hduser group=hadoop - -- name: Copy Primary es router additional config - template: src=es-router-additional-config.json dest={{es_router_additional_config}} owner=hduser group=hadoop - -- name: Copy Secondary es router additional config - template: src=es-router-additional-secondary-config.json dest={{es_router_additional_secondary_config}} owner=hduser group=hadoop diff --git a/ansible/roles/samza-jobs-additional-config/templates/es-router-additional-config.json b/ansible/roles/samza-jobs-additional-config/templates/es-router-additional-config.json deleted file mode 100644 index 2c47f45f49..0000000000 --- a/ansible/roles/samza-jobs-additional-config/templates/es-router-additional-config.json +++ /dev/null @@ -1,140 +0,0 @@ -{ - "topicConfigs":[ - { - "names":["{{env}}.telemetry.objects.de_normalized"], - "eventConfigs":[ - { - "rules":[ - { - "idPath":"eid", - "idValue":"OE.*" - } - ], - "esIndexValue":"telemetry", - "esIndexType":"events_v1", - "weight":3, - "cumulative":false, - "esIndexDate": { - "primary" : "ts", - "primaryFormat": "string", - "secondary" : "ets", - "secondaryFormat": "epoch", - "updatePrimary": true - } - - }, - { - "rules":[ - { - "idPath":"eid", - "idValue":"GE.*" - } - ], - "esIndexValue":"telemetry", - "esIndexType":"events_v1", - "weight":3, - "cumulative":false, - "esIndexDate": { - "primary" : "ts", - "primaryFormat": "string", - "secondary" : "ets", - "secondaryFormat": "epoch", - "updatePrimary": true - } - }, - { - "rules":[ - { - "idPath":"eid", - "idValue":"BE_ACCESS|BE_JOB_START|BE_JOB_LOG|BE_JOB_END|BE_SERVICE_LOG|BE_SERVICE_LIFECYCLE|BE_SERVICE_METRIC" - } - ], - "esIndexValue":"infra", - "esIndexType":"infra", - "weight":4, - "cumulative":false, - "esIndexDate": { - "primary" : "ts", - "primaryFormat": "string", - "secondary" : "ets", - "secondaryFormat": "epoch", - "updatePrimary": true - } - }, - { - "rules":[ - { - "idPath":"eid", - "idValue":"CP_.*|CE_.*|BE_.*" - } - ], - "esIndexValue":"backend", - "esIndexType":"backend", - "weight":3, - "cumulative":false, - "esIndexDate": { - "primary" : "ts", - "primaryFormat": "string", - "secondary" : "ets", - "secondaryFormat": "epoch", - "updatePrimary": true - } - }, - { - "rules":[ - { - "idPath":"context.granularity", - "idValue":"CUMULATIVE" - }, - { - "idPath":"learning", - "idValue": "true" - } - ], - - "esIndexValue":"learning-cumulative", - "esIndexType":"events_v1", - "weight":3, - "cumulative":true - }, - { - "rules":[ - { - "idPath":"learning", - "idValue": "true" - } - ], - "esIndexValue":"learning", - "esIndexType":"events_v1", - "weight":2, - "cumulative":false, - "esIndexDate": { - "primary" : "context.date_range.to", - "primaryFormat": "epoch", - "updatePrimary": false - } - }, - { - "rules":[ - { - "idPath":"eid", - "idValue":".*" - } - ], - "esIndexValue":"telemetry", - "esIndexType":"events_v1", - "weight":1, - "cumulative":false, - "esIndexDate": { - "primary" : "ts", - "primaryFormat": "string", - "secondary" : "ets", - "secondaryFormat": "epoch", - "updatePrimary": true - } - } - ] - - } - ] -} diff --git a/ansible/roles/samza-jobs-additional-config/templates/es-router-additional-secondary-config.json b/ansible/roles/samza-jobs-additional-config/templates/es-router-additional-secondary-config.json deleted file mode 100644 index f5c15c46ed..0000000000 --- a/ansible/roles/samza-jobs-additional-config/templates/es-router-additional-secondary-config.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "topicConfigs":[ - { - "names":["{{env}}.telemetry.valid.fail","{{env}}.telemetry.duplicate","{{env}}.telemetry.with_location.fail","{{env}}.telemetry.objects.de_normalized.fail","{{env}}.telemetry.es_router_primary.fail","{{env}}.telemetry.es_indexer_primary.fail"], - "eventConfigs":[ - { - "rules":[ - { - "idPath":"eid", - "idValue":".*" - } - ], - "esIndexValue":"failed-telemetry", - "esIndexType":"events", - "weight":3, - "cumulative":false, - "esIndexDate": { - "primary" : "ts", - "primaryFormat": "string", - "secondary" : "ets", - "secondaryFormat": "epoch", - "updatePrimary": false - } - - } - ] - } - ] -} diff --git a/ansible/roles/samza-jobs-additional-config/templates/object-denormalization-additional-config.json b/ansible/roles/samza-jobs-additional-config/templates/object-denormalization-additional-config.json deleted file mode 100644 index 874a18a337..0000000000 --- a/ansible/roles/samza-jobs-additional-config/templates/object-denormalization-additional-config.json +++ /dev/null @@ -1,84 +0,0 @@ -{ - "eventConfigs": [ - { - "name": "Portal User for all CP & CE events", - "eidPattern": "C[PE]\\_.*", - "denormalizationConfigs": [ - { - "idFieldPath": "uid", - "denormalizedFieldPath": "portaluserdata" - } - ] - }, - { - "name": "Partner data for ME_CONTENT_SNAPSHOT_SUMMARY", - "eidPattern": "ME_CONTENT_SNAPSHOT_SUMMARY", - "denormalizationConfigs": [ - { - "idFieldPath": "dimensions.partner_id", - "denormalizedFieldPath": "partnerdata" - } - ] - }, - { - "name": "Partner data for ME_ASSET_SNAPSHOT_SUMMARY", - "eidPattern": "ME_ASSET_SNAPSHOT_SUMMARY", - "denormalizationConfigs": [ - { - "idFieldPath": "dimensions.partner_id", - "denormalizedFieldPath": "partnerdata" - } - ] - }, - { - "name": "Portal user data for ME_APP_SESSION_SUMMARY", - "eidPattern": "ME_APP_SESSION_SUMMARY", - "denormalizationConfigs": [ - { - "idFieldPath": "uid", - "denormalizedFieldPath": "portaluserdata" - } - ] - }, - { - "name": "Portal user data for ME_APP_USAGE_SUMMARY", - "eidPattern": "ME_APP_USAGE_SUMMARY", - "denormalizationConfigs": [ - { - "idFieldPath": "dimensions.author_id", - "denormalizedFieldPath": "portaluserdata" - } - ] - }, - { - "name": "Portal user data for ME_CE_SESSION_SUMMARY", - "eidPattern": "ME_CE_SESSION_SUMMARY", - "denormalizationConfigs": [ - { - "idFieldPath": "uid", - "denormalizedFieldPath": "portaluserdata" - } - ] - }, - { - "name": "Portal user data for ME_AUTHOR_USAGE_SUMMARY", - "eidPattern": "ME_AUTHOR_USAGE_SUMMARY", - "denormalizationConfigs": [ - { - "idFieldPath": "uid", - "denormalizedFieldPath": "portaluserdata" - } - ] - }, - { - "name": "Portal user data for ME_TEXTBOOK_SESSION_SUMMARY", - "eidPattern": "ME_TEXTBOOK_SESSION_SUMMARY", - "denormalizationConfigs": [ - { - "idFieldPath": "uid", - "denormalizedFieldPath": "portaluserdata" - } - ] - } - ] -} diff --git a/ansible/roles/samza-jobs-telemetry-schemas/tasks/main.yml b/ansible/roles/samza-jobs-telemetry-schemas/tasks/main.yml deleted file mode 100644 index e7cee03b36..0000000000 --- a/ansible/roles/samza-jobs-telemetry-schemas/tasks/main.yml +++ /dev/null @@ -1,17 +0,0 @@ ---- -- name: Create schema directory - file: path={{telemetry_schema_directory}} owner=hduser group=hadoop recurse=yes state=directory - become: yes - -- name: Copy schemas folder - copy: src=schemas dest={{telemetry_schema_directory}} owner=hduser group=hadoop - become: yes - -- name: get schema dir names - raw: find {{telemetry_schema_path}} -type f -name "*.*" - register: schemas - -- name: change internal schema file reference - replace: dest={{item}} regexp="http://localhost:7070/schemas/" replace="file://{{telemetry_schema_path}}/" owner=hduser group=hadoop - with_items: "{{ schemas.stdout_lines }}" - become: yes \ No newline at end of file diff --git a/ansible/roles/samza-jobs/defaults/main.yml b/ansible/roles/samza-jobs/defaults/main.yml deleted file mode 100644 index 63f806a721..0000000000 --- a/ansible/roles/samza-jobs/defaults/main.yml +++ /dev/null @@ -1,51 +0,0 @@ ---- -samza_jobs_dir: /home/hduser/samza-jobs/{{env}} -job_status_file: /home/hduser/samza-jobs/{{env}}/extract/job_status -yarn_path: /usr/local/hadoop/bin -object_denormalization_additional_config_dir: /etc/samza-jobs/{{env}} -object_denormalization_additional_config: "{{object_denormalization_additional_config_dir}}/object-denormalization-additional-config.json" -es_router_additional_config: "{{object_denormalization_additional_config_dir}}/es-router-additional-config.json" -es_router_additional_secondary_config: "{{object_denormalization_additional_config_dir}}/es-router-additional-secondary-config.json" -# lpdeploy: no -# dpdeploy: no -hierarchy_keyspace_name: "{{env}}_hierarchy_store" -cloud_upload_retry_count: 3 -streaming_mime_type: "video/mp4,video/webm" -__yarn_port__: 8000 -delayInMilliSeconds: 60000 -retryTimeInMilliSeconds: 10000 -retry_backoff_base_in_seconds: 10 -bypass_reverse_search: true -retry_limit: 4 -retry_limit_enable: true -publish_pipeline_container_count: 1 -publish_yarn_container_memory_mb: 1024 -publish_pipeline_task_opts: "-Dfile.encoding=UTF8 -XX:-UseG1GC -Xmx800m" -mw_shard_id: 1 -google_vision_tagging: false -es_port: 9200 -cassandra_port: 9042 -content_keyspace_table: content_data -collection_fullecar_disable: true -max_iteration_count_for_samza_job: 2 -composite_search_indexer_container_count: 1 -compositesearch_index_name: "compositesearch" -cloud_store: azure -hadoop_version: 2.7.2 -redis_port: 6379 -google_api_key: "123" -sunbird_installation: "{{env}}" -dial_base_url: "https://{{domain_name}}/dial/" -samza_coordinator_replication_factor: 1 -samza_checkpoint_replication_factor: 1 -course_batch_updater_container_count: 1 -course_certificate_generator_container_count: 1 -course_progress_batch_size: 100 -itemset_generate_pdf: true -auto_creator_container_count: 1 -content_streaming_enabled: false -mvc_search_indexer_container_count: 1 -auto_creator_artifact_allowed_sources: "" -auto_creator_gservice_acct_cred: "" -certificate_pre_processor_container_count: 1 -master_category_validation_enabled: "Yes" \ No newline at end of file diff --git a/ansible/roles/samza-jobs/files/find_job_name.sh b/ansible/roles/samza-jobs/files/find_job_name.sh deleted file mode 100644 index 05f0605223..0000000000 --- a/ansible/roles/samza-jobs/files/find_job_name.sh +++ /dev/null @@ -1 +0,0 @@ -sed -n "/job\.name.*$/ p" $1 | sed -n "s/=/\\t/g p" | cut -f 2 \ No newline at end of file diff --git a/ansible/roles/samza-jobs/files/get_all_job_name.sh b/ansible/roles/samza-jobs/files/get_all_job_name.sh deleted file mode 100644 index 7975c8a34a..0000000000 --- a/ansible/roles/samza-jobs/files/get_all_job_name.sh +++ /dev/null @@ -1,7 +0,0 @@ -#!/usr/bin/env bash -find . -name "*.properties" | while read fname; do - job_name=`sed -n "/^job\.name.*$/ p" $fname | sed -n "s/=/\\t/g p" | cut -f 2` - folder_path=$(dirname `dirname "$fname"`) - folder_name=`basename $folder_path` - echo "$folder_name:$job_name:---:stopped" -done > $1 diff --git a/ansible/roles/samza-jobs/files/get_all_running_app_id.sh b/ansible/roles/samza-jobs/files/get_all_running_app_id.sh deleted file mode 100644 index 74aa7c0491..0000000000 --- a/ansible/roles/samza-jobs/files/get_all_running_app_id.sh +++ /dev/null @@ -1,2 +0,0 @@ -#!/usr/bin/env bash -./yarn application -list | cut -f 2 | sed 1,'/Application-Name/'d \ No newline at end of file diff --git a/ansible/roles/samza-jobs/files/get_all_running_app_name.sh b/ansible/roles/samza-jobs/files/get_all_running_app_name.sh deleted file mode 100644 index b3b1b9dff2..0000000000 --- a/ansible/roles/samza-jobs/files/get_all_running_app_name.sh +++ /dev/null @@ -1,11 +0,0 @@ -#!/usr/bin/env bash -job_names=(`./yarn application -list | cut -f 2 | sed 1,'/Application-Name/'d | sed 's/_1$//'`) -job_ids=(`./yarn application -list | cut -f 1 | sed 1,'/Application-Id/'d`) -count=${#job_names[@]} -for (( i=0; i<${count}; i++ )); -do - job_name=${job_names[i]} - job_id=${job_ids[i]} - `sed -i /$job_name/s/stopped/started/g $1` - `sed -i /$job_name/s/---/$job_id/g $1` -done diff --git a/ansible/roles/samza-jobs/files/kill_all_app.sh b/ansible/roles/samza-jobs/files/kill_all_app.sh deleted file mode 100644 index 55f7341e25..0000000000 --- a/ansible/roles/samza-jobs/files/kill_all_app.sh +++ /dev/null @@ -1,9 +0,0 @@ -#!/usr/bin/env bash -./yarn application -list > applist.txt -sed -n "/$1.*$/ p" applist.txt | cut -f 1 > temp.txt -while read in; -do -./yarn application -kill "$in"; -done < temp.txt -rm temp.txt -rm applist.txt \ No newline at end of file diff --git a/ansible/roles/samza-jobs/files/kill_jobs.sh b/ansible/roles/samza-jobs/files/kill_jobs.sh deleted file mode 100644 index 267515cdea..0000000000 --- a/ansible/roles/samza-jobs/files/kill_jobs.sh +++ /dev/null @@ -1,11 +0,0 @@ -#!/usr/bin/env bash -cat $1 | while read LINE -do - application_id=`echo $LINE | awk -F':' '{print $3}'`; - status=`echo $LINE | awk -F':' '{print $4}'`; - - if [ "$status" == "restart" ] - then - ./yarn application -kill $application_id - fi -done \ No newline at end of file diff --git a/ansible/roles/samza-jobs/files/remove_old_tar.sh b/ansible/roles/samza-jobs/files/remove_old_tar.sh deleted file mode 100644 index 13d0547b89..0000000000 --- a/ansible/roles/samza-jobs/files/remove_old_tar.sh +++ /dev/null @@ -1,12 +0,0 @@ -#!/usr/bin/env bash -cat $1 | awk -F':' '{print $1}' > tmp.txt -DIRS=`ls -l $2/extract/ | egrep '^d'| awk '{print $9}'` -for dir in $DIRS -do - if ! grep -Fxq $dir tmp.txt - then - rm -rf $dir - rm $2/$dir - fi -done -rm tmp.txt \ No newline at end of file diff --git a/ansible/roles/samza-jobs/files/start_jobs.sh b/ansible/roles/samza-jobs/files/start_jobs.sh deleted file mode 100644 index 4d048a58a8..0000000000 --- a/ansible/roles/samza-jobs/files/start_jobs.sh +++ /dev/null @@ -1,15 +0,0 @@ -#!/usr/bin/env bash -folder_path=$2 -cat $1 | while read LINE -do - dir_name=`echo $LINE | awk -F':' '{print $1}'`; - job_name=`echo $LINE | awk -F':' '{print $2}'`; - application_id=`echo $LINE | awk -F':' '{print $3}'`; - status=`echo $LINE | awk -F':' '{print $4}'`; - properties_path="$folder_path/$dir_name/config/*.properties" - config_file_path=`ls -d $properties_path` - if [ "$status" == "stopped" ] || [ "$status" == "restart" ] - then - ./$dir_name/bin/run-job.sh --config-factory=org.apache.samza.config.factories.PropertiesConfigFactory --config-path=file:///$config_file_path - fi -done \ No newline at end of file diff --git a/ansible/roles/samza-jobs/files/update_new_job_name.sh b/ansible/roles/samza-jobs/files/update_new_job_name.sh deleted file mode 100644 index 24e174ce54..0000000000 --- a/ansible/roles/samza-jobs/files/update_new_job_name.sh +++ /dev/null @@ -1,14 +0,0 @@ -#!/usr/bin/env bash -find $2 -name "*.properties" | while read fname; do - job_name=`sed -n "/^job\.name.*$/ p" $fname | sed -n "s/=/\\t/g p" | cut -f 2` - folder_path=$(dirname `dirname "$fname"`) - folder_name=`basename $folder_path` - if grep -Fwq $job_name $1 - then - `sed -i /$job_name/s/^.*\.gz/$folder_name/ $1`; - `sed -i /$job_name/s/started/restart/ $1`; - else - echo "adding" - echo "$folder_name:$job_name:---:stopped" >> $1 - fi -done \ No newline at end of file diff --git a/ansible/roles/samza-jobs/tasks/deploy.yml b/ansible/roles/samza-jobs/tasks/deploy.yml deleted file mode 100644 index 2b9598eb02..0000000000 --- a/ansible/roles/samza-jobs/tasks/deploy.yml +++ /dev/null @@ -1,104 +0,0 @@ ---- -- name: Create Directory for Jobs - file: path={{item}} owner=hduser group=hadoop recurse=yes state=directory - with_items: - - "{{samza_jobs_dir}}" - - "{{samza_jobs_dir}}/extract" - -- name: Copy script to get all running jobs - copy: src=get_all_running_app_name.sh dest=/usr/local/hadoop/bin owner=hduser group=hadoop mode="u=rwx,g=rx,o=r" - -- name: Copy script to get all job names - copy: src=get_all_job_name.sh dest="{{samza_jobs_dir}}/extract" owner=hduser group=hadoop mode="u=rwx,g=rx,o=r" - -- name: Copy script to get updated job names from extracted tar - copy: src=update_new_job_name.sh dest="{{samza_jobs_dir}}/extract" owner=hduser group=hadoop mode="u=rwx,g=rx,o=r" - -- name: Copy script to start jobs based on the status - copy: src=start_jobs.sh dest="{{samza_jobs_dir}}/extract" owner=hduser group=hadoop mode="u=rwx,g=rx,o=r" - -- name: Copy script to remove old job tar - copy: src=remove_old_tar.sh dest="{{samza_jobs_dir}}/extract" owner=hduser group=hadoop mode="u=rwx,g=rx,o=r" - -- name: Copy script to kill jobs based on the status - copy: src=kill_jobs.sh dest=/usr/local/hadoop/bin owner=hduser group=hadoop mode="u=rwx,g=rx,o=r" - -- name: Remove file of job status - file: path="{{job_status_file}}" state=absent - -- name: Get job names from folder - command: bash -lc "./get_all_job_name.sh {{job_status_file}}" - args: - chdir: "{{samza_jobs_dir}}/extract" - -- name: Ensure yarn resource manager is running - command: bash -lc "(ps aux | grep yarn-hduser-resourcemanager | grep -v grep) || /usr/local/hadoop/sbin/yarn-daemon.sh --config /usr/local/hadoop-{{hadoop_version}}/conf/ start resourcemanager" - become: yes - become_user: hduser - -- name: Update status of running job in file - command: bash -lc "./get_all_running_app_name.sh {{job_status_file}}" - args: - chdir: /usr/local/hadoop/bin - -- name: copy new jobs tar ball - copy: src={{ item }} dest={{samza_jobs_dir}}/ force=no owner=hduser group=hadoop - with_fileglob: - - ./jobs/* - register: new_jobs - -- name: Create Directory to extract new jobs - file: path={{samza_jobs_dir}}/extract/{{item.item | basename }} owner=hduser group=hadoop recurse=yes state=directory - register: extract_dir - when: "{{item|changed}}" - with_items: "{{ (new_jobs|default({})).results|default([]) }}" - -- name: extract new jobs - command: tar -xvf "{{samza_jobs_dir}}/{{item.item | basename}}" -C "{{samza_jobs_dir}}/extract/{{item.item | basename }}" - when: "{{item|changed}}" - with_items: "{{ (new_jobs|default({})).results|default([]) }}" - -- name: Create Directory to extract new jobs - file: path={{samza_jobs_dir}}/extract/ owner=hduser group=hadoop recurse=yes - -- name: Get all new job configs - shell: "ls -d -1 {{item.path}}/config/*.properties" - register: config_files - when: "{{item|changed}}" - with_items: "{{ (extract_dir|default({})).results|default([]) }}" - -- name: update environment specific details in new job configs - replace: dest="{{item[1].stdout}}" regexp="{{item[0].key}}" replace="{{item[0].value}}" - when: "{{item[1]|changed}}" - with_nested: - - [{key: "__yarn_host__", value: "{{__yarn_host__}}"}, {key: "__yarn_port__", value: "{{__yarn_port__}}"}, {key: "__env__", value: "{{env}}" }, {key: "__env_name__", value: "{{env_name}}" }, {key: "__zookeepers__", value: "{{zookeepers}}"}, {key: "__kafka_brokers__", value: "{{kafka_brokers}}"}, {key: "__delayInMilliSeconds__", value: "{{delayInMilliSeconds}}" }, {key: "__retryTimeInMilliSeconds__", value: "{{retryTimeInMilliSeconds}}" }, {key: "__bypass_reverse_search__", value: "{{bypass_reverse_search}}" }, {key: "__retryBackoffBaseInSeconds__", value: "{{retry_backoff_base_in_seconds}}" }, {key: "__retryLimit__", value: "{{retry_limit}}" }, {key: "__retryLimitEnable__", value: "{{retry_limit_enable}}" }, {key: "__google_api_key__", value: "{{google_api_key}}" }, {key: "__searchServiceEndpoint__", value: "{{search_service_endpoint}}" }, {key: "__objectDenormalizationAdditionalConfig__", value: "{{object_denormalization_additional_config}}" },{key: "__audit_es_host__", value: "{{audit_es_host}}"}, {key: "__search_es_host__", value: "{{search_es_host}}"}, {key: "__redis_host__", value: "{{redis_host}}"}, {key: "__dp_redis_host__", value: "{{dp_redis_host}}"}, {key: "__redis_port__", value: "{{redis_port}}"}, {key: "__environment_id__", value: "{{environment_id}}"}, {key: "__graph_passport_key__", value: "{{graph_passport_key}}"}, {key: "__lp_bolt_url__", value: "{{lp_bolt_url}}"}, {key: "__lp_bolt_read_url__", value: "{{lp_bolt_read_url}}"}, {key: "__lp_bolt_write_url__", value: "{{lp_bolt_write_url}}"}, {key: "__other_bolt_url__", value: "{{other_bolt_url}}"}, {key: "__other_bolt_read_url__", value: "{{other_bolt_read_url}}"}, {key: "__other_bolt_write_url__", value: "{{other_bolt_write_url}}"}, {key: "__mw_shard_id__", value: "{{mw_shard_id}}"}, {key: "__lp_url__", value: "{{lp_url}}"}, {key: "__cloud_storage_config_environment__", value: "{{cloud_storage_config_environment}}"}, {key: "__google_vision_tagging__", value: "{{google_vision_tagging}}"}, {key: "__lp_tmpfile_location__", value: "{{lp_tmpfile_location}}"}, {key: "__esRouterAdditionalConfig__", value: "{{es_router_additional_config}}"},{key: "__esRouterSecondaryAdditionalConfig__", value: "{{es_router_additional_secondary_config}}"},{key: "__es_port__", value: "{{es_port}}"}, {key: "__keyspace_name__", value: "{{content_keyspace_name}}"}, {key: "__collection_fullecar_disable__", value: "{{collection_fullecar_disable}}"},{key: "__max_iteration_count_for_samza_job__", value: "{{max_iteration_count_for_samza_job}}"},{key: "__cloud_storage_type__", value: "{{cloud_store}}"},{key: "__azure_storage_key__", value: "{{sunbird_public_storage_account_name}}"},{key: "__azure_storage_secret__", value: "{{sunbird_public_storage_account_key}}"},{key: "__azure_storage_container__", value: "{{azure_public_container}}"},{key: "__content_media_base_url__", value: "{{content_media_base_url}}"}, {key: "__plugin_media_base_url__", value: "{{plugin_media_base_url}}"}, {key: "__installation_id__", value: "{{instance_name}}"}, {key: "__content_media_base_url__", value: "{{content_media_base_url}}"}, {key: "__hierarchy_keyspace_name__", value: "{{hierarchy_keyspace_name}}"}, {key: "__composite_search_indexer_container_count__", value: "{{composite_search_indexer_container_count}}"},{key: "__cassandra_lp_connection__", value: "{{lp_cassandra_connection}}"}, {key: "__cassandra_lpa_connection__", value: "{{dp_cassandra_connection}}"}, {key: "__streaming_mime_type__", value: "{{streaming_mime_type}}"}, {key: "__cassandra_sunbird_connection__", value: "{{core_cassandra_connection}}"}, {key: "__cloud_upload_retry_count__", value: "{{cloud_upload_retry_count}}"}, {key: "__compositesearch_index_name__", value: "{{compositesearch_index_name}}"},{key: "__publish_pipeline_container_count__", value: "{{publish_pipeline_container_count}}"},{key: "__yarn_container_memory_mb__", value: "{{publish_yarn_container_memory_mb}}"},{key: "__youtube_api_key__", value: "{{youtube_api_key}}"},{key: "__kp_learning_service_base_url__", value: "{{kp_learning_service_base_url}}"},{key: "__sunbird_installation__", value: "{{sunbird_platform_installation}}"}, {key: "__search_lms_es_host__", value: "{{search_lms_es_host}}"},{key: "__dial_image_storage_container__", value: "{{dial_image_storage_container}}"},{key: "__dial_base_url__", value: "{{dial_base_url}}"},{key: "__learner_service_base_url__", value: "{{learner_service_base_url}}"},{key: "__cert_service_base_url__", value: "{{cert_service_base_url}}"},{key: "__certificate_base_path__", value: "{{certificate_base_path}}"},{key: "__kp_content_service_base_url__", value: "{{kp_content_service_base_url}}"},{key: "__kp_print_service_base_url__", value: "{{kp_print_service_base_url}}"},{key: "__cert_reg_service_base_url__", value: "{{cert_reg_service_base_url}}"},{key: "__kp_search_service_base_url__", value: "{{kp_search_service_base_url}}"},{key: "__samza_coordinator_replication_factor__", value: "{{samza_coordinator_replication_factor}}"},{key: "__samza_checkpoint_replication_factor__", value: "{{samza_checkpoint_replication_factor}}"},{key: "__course_batch_updater_container_count__", value: "{{course_batch_updater_container_count}}"},{key: "__course_certificate_generator_container_count__", value: "{{course_certificate_generator_container_count}}"},{key: "__course_progress_batch_size__", value: "{{course_progress_batch_size}}"},{key: "__itemset_generate_pdf__", value: "{{itemset_generate_pdf}}"},{key: "__auto_creator_container_count__", value: "{{auto_creator_container_count}}"},{key: "__content_streaming_enabled__", value: "{{content_streaming_enabled}}"},{key: "__lms_service_base_url__", value: "{{lms_service_base_url}}"},{key: "__mvc_search_indexer_container_count__", value: "{{mvc_search_indexer_container_count}}"}, {key: "__search_es7_host__", value: "{{search_es7_host}}"} , {key: "__ml-keywordapi__", value: "{{mlworkbench}}"},{key: "__auto_creator_artifact_allowed_sources__", value: "{{auto_creator_artifact_allowed_sources}}"},{key: "__publish_pipeline_task_opts__", value: "{{publish_pipeline_task_opts}}"},{key: "__auto_creator_g_service_acct_cred__", value: "{{auto_creator_gservice_acct_cred}}"},{key: "__certificate_pre_processor_container_count__", value: "{{certificate_pre_processor_container_count}}"},{key: "__master_category_validation_enabled__", value: "{{master_category_validation_enabled}}"}] - - "{{ (config_files|default({})).results|default([]) }}" - - -- name: Create directory for additional config - file: path={{object_denormalization_additional_config_dir}} owner=hduser group=hadoop recurse=yes state=directory - -- name: Update status of new jobs in file - command: bash -lc "./update_new_job_name.sh {{job_status_file}} {{samza_jobs_dir}}/extract/{{item.item | basename}}" - args: - chdir: "{{samza_jobs_dir}}/extract/" - when: "{{item|changed}}" - with_items: "{{ (new_jobs|default({})).results|default([]) }}" - -- name: Kill jobs - command: bash -lc "./kill_jobs.sh {{job_status_file}}" - args: - chdir: /usr/local/hadoop/bin - -- name: Start jobs - command: bash -lc "./start_jobs.sh {{job_status_file}} {{samza_jobs_dir}}/extract" - args: - chdir: "{{samza_jobs_dir}}/extract/" - become_user: hduser - -- name: Remove all old tar - command: bash -lc "./remove_old_tar.sh {{job_status_file}} {{samza_jobs_dir}}" - args: - chdir: "{{samza_jobs_dir}}/extract/" - -- file: path={{samza_jobs_dir}} owner=hduser group=hadoop state=directory recurse=yes diff --git a/ansible/roles/samza-jobs/tasks/main.yml b/ansible/roles/samza-jobs/tasks/main.yml deleted file mode 100644 index 0feb5dcd99..0000000000 --- a/ansible/roles/samza-jobs/tasks/main.yml +++ /dev/null @@ -1,9 +0,0 @@ ---- -- include: deploy.yml - when: deploy_jobs | default(false) - -- include: stop_jobs.yml - when: stop_jobs | default(false) - -- include: start_jobs.yml - when: start_jobs | default(false) diff --git a/ansible/roles/samza-jobs/tasks/start_jobs.yml b/ansible/roles/samza-jobs/tasks/start_jobs.yml deleted file mode 100644 index 4bb0c65c9c..0000000000 --- a/ansible/roles/samza-jobs/tasks/start_jobs.yml +++ /dev/null @@ -1,21 +0,0 @@ ---- -- name: Remove file of job status - file: path="{{job_status_file}}" state=absent - become: yes - -- name: Get job names from folder - command: bash -lc "./get_all_job_name.sh {{job_status_file}}" - args: - chdir: "{{samza_jobs_dir}}/extract" - become: yes - -- name: Ensure yarn resource manager is running - command: bash -lc "(ps aux | grep yarn-hduser-resourcemanager | grep -v grep) || /usr/local/hadoop/sbin/yarn-daemon.sh --config /usr/local/hadoop-{{hadoop_version}}/conf/ start resourcemanager" - become: yes - become_user: hduser - -- name: Start jobs - command: bash -lc "./start_jobs.sh {{job_status_file}} {{samza_jobs_dir}}/extract" - args: - chdir: "{{samza_jobs_dir}}/extract/" - become: yes diff --git a/ansible/roles/samza-jobs/tasks/stop_jobs.yml b/ansible/roles/samza-jobs/tasks/stop_jobs.yml deleted file mode 100644 index 1ef2f7b748..0000000000 --- a/ansible/roles/samza-jobs/tasks/stop_jobs.yml +++ /dev/null @@ -1,16 +0,0 @@ ---- -- name: Remove file of job status - file: path="{{job_status_file}}" state=absent - become: yes - -- name: Get job names from folder - command: bash -lc "./get_all_job_name.sh {{job_status_file}}" - args: - chdir: "{{samza_jobs_dir}}/extract" - become: yes - -- name: Kill jobs - command: bash -lc "./kill_jobs.sh {{job_status_file}}" - args: - chdir: /usr/local/hadoop/bin - become: yes diff --git a/ansible/roles/yarn/defaults/main.yml b/ansible/roles/yarn/defaults/main.yml deleted file mode 100644 index 4060075e8e..0000000000 --- a/ansible/roles/yarn/defaults/main.yml +++ /dev/null @@ -1,16 +0,0 @@ ---- -yarn_deploy_dir: /home/ecosystem/.deploy -repo_folder: /home/hduser/Ecosystem-Platform -hadoop_tarball: hadoop-{{hadoop_version}}.tar.gz -hadoop_download_url: https://archive.apache.org/dist/hadoop/common/hadoop-{{hadoop_version}}/{{hadoop_tarball}} -scala_tarball: scala-{{scala_version}}.tgz -scala_download_url: http://www.scala-lang.org/files/archive/{{scala_tarball}} -hadoop_yarn_home: /usr/local/hadoop-{{hadoop_version}} -hadoop_version: 2.7.2 -scala_version: 2.10.4 - -yarn_config_override: true -yarn_vmem_check_enabled: false -yarn_vmem_pmem_ratio: 2.1 -yarn_vcores: 16 -yarn_resource_memory: 20000 diff --git a/ansible/roles/yarn/files/truncate_logs.sh b/ansible/roles/yarn/files/truncate_logs.sh deleted file mode 100644 index 7dddd9d702..0000000000 --- a/ansible/roles/yarn/files/truncate_logs.sh +++ /dev/null @@ -1,21 +0,0 @@ -#!/bin/bash - -# Truncate hadoop/yarn userlogs and keep the last 100 lines - -HADOOP_LOGS_HOME=/usr/local/hadoop/logs/userlogs - -for d in $HADOOP_LOGS_HOME/*/*/ ; do (cd $d && tail -n 100 stdout > stdout.tmp && cat stdout.tmp > stdout && rm stdout.tmp); done - -LOGSTASH_LOGS=/var/log/logstash -tail -n 100 $LOGSTASH_LOGS/logstash.stdout > $LOGSTASH_LOGS/logstash.stdout.tmp -cat $LOGSTASH_LOGS/logstash.stdout.tmp > $LOGSTASH_LOGS/logstash.stdout -rm $LOGSTASH_LOGS/logstash.stdout.tmp - -HADOOP_TMP_USERLOGS=/usr/local/hadoop/logs/userlogs - -for g in $HADOOP_TMP_USERLOGS/*/*/ do - cd $g - tail -n 100 stdout > stdout.tmp - cat stdout.tmp > stdout - rm stdout.tmp -done diff --git a/ansible/roles/yarn/tasks/common.yml b/ansible/roles/yarn/tasks/common.yml deleted file mode 100644 index 75ceb4b69a..0000000000 --- a/ansible/roles/yarn/tasks/common.yml +++ /dev/null @@ -1,78 +0,0 @@ -- name: Common tasks for yarn master and slave - block: - - name: Download and extract hadoop tarball - unarchive: - src: "{{hadoop_download_url}}" - dest: "/usr/local/" - owner: hduser - group: hadoop - creates: "{{hadoop_yarn_home}}" - remote_src: yes - - - name: Creates symlink - file: - src: /usr/local/hadoop-{{hadoop_version}} - dest: /usr/local/hadoop - owner: hduser - group: hadoop - state: link - - - name: creating conf dir - file: - path: "{{hadoop_yarn_home}}/conf" - owner: hduser - group: hadoop - recurse: yes - state: directory - - - name: Templating configs - template: - src: "{{item}}" - dest: "{{hadoop_yarn_home}}/conf/{{item}}" - owner: hduser - group: hadoop - with_items: - - yarn-site.xml - - capacity-scheduler.xml - - core-site.xml - - log4j.properties - - hadoop-env.sh - - - name: Downloading artifacts - get_url: - url: "http://search.maven.org/remotecontent?filepath=org/{{item}}" - dest: "{{hadoop_yarn_home}}/share/hadoop/hdfs/lib/" - owner: hduser - group: hadoop - with_items: - - clapper/grizzled-slf4j_2.10/1.0.1/grizzled-slf4j_2.10-1.0.1.jar - - apache/samza/samza-yarn_2.10/0.8.0/samza-yarn_2.10-0.8.0.jar - - apache/samza/samza-core_2.10/0.8.0/samza-core_2.10-0.8.0.jar - - - name: Download and extract scala - unarchive: - src: "{{scala_download_url}}" - dest: "/usr/local/" - owner: hduser - group: hadoop - remote_src: yes - - - name: Creates symlink - file: - src: "/usr/local/scala-{{scala_version}}" - dest: /usr/local/scala - owner: hduser - group: hadoop - state: link - - - name: copying scala files - copy: - src: "/usr/local/scala-{{scala_version}}/lib/{{item}}" - dest: "{{hadoop_yarn_home}}/share/hadoop/hdfs/lib/" - owner: hduser - group: hadoop - remote_src: true - with_items: - - scala-compiler.jar - - scala-library.jar - delegate_to: "{{slave|default(inventory_hostname)}}" diff --git a/ansible/roles/yarn/tasks/main.yml b/ansible/roles/yarn/tasks/main.yml deleted file mode 100644 index a297e9a951..0000000000 --- a/ansible/roles/yarn/tasks/main.yml +++ /dev/null @@ -1,55 +0,0 @@ -- name: Debian | Install Maven - apt: - pkg: "{{item}}" - update_cache: yes - state: latest - install_recommends: yes - with_items: - - maven - - git - -# Running common tasks in master -- include: common.yml - -- lineinfile: - dest: /home/hduser/.bashrc - state: present - regexp: '^HADOOP_YARN_HOME' - line: 'HADOOP_YARN_HOME={{hadoop_yarn_home}}' - -- lineinfile: - dest: /home/hduser/.bashrc - state: present - regexp: '^HADOOP_CONF_DIR' - line: 'HADOOP_CONF_DIR=$HADOOP_YARN_HOME/conf' - -- file: - path: "/{{hadoop_yarn_home}}/conf/slaves" - state: touch - -- lineinfile: - dest: "/{{hadoop_yarn_home}}/conf/slaves" - state: present - regexp: "^{{item}}" - line: "{{item}}" - with_items: "{{yarn_slaves}}" - -# Running common tasks in slaves -- name: Running common tasks in slaves - include: common.yml - with_items: "{{yarn_slaves}}" - loop_control: - loop_var: slave - -- name: Copy truncate_files.sh - copy: - src: truncate_logs.sh - dest: /usr/local/bin - mode: 755 - -- name: Add truncate logs to cron - cron: - name: "Truncate yarn logs" - minute: "0" - job: "/usr/local/bin/truncate_logs.sh" - backup: yes diff --git a/ansible/roles/yarn/tasks/truncate-logs.yml b/ansible/roles/yarn/tasks/truncate-logs.yml deleted file mode 100644 index e0d550a8ec..0000000000 --- a/ansible/roles/yarn/tasks/truncate-logs.yml +++ /dev/null @@ -1,8 +0,0 @@ ---- -- name: Copy truncate_files.sh - copy: src=truncate_logs.sh dest=/ mode=755 - become: yes - -- name: Add truncate logs to cron - cron: name="Truncate yarn logs" minute="0" job="/truncate_logs.sh" backup=yes - become: yes \ No newline at end of file diff --git a/ansible/roles/yarn/templates/capacity-scheduler.xml b/ansible/roles/yarn/templates/capacity-scheduler.xml deleted file mode 100644 index 4161b7ac03..0000000000 --- a/ansible/roles/yarn/templates/capacity-scheduler.xml +++ /dev/null @@ -1,111 +0,0 @@ - - - - - yarn.scheduler.capacity.maximum-applications - 10000 - - Maximum number of applications that can be pending and running. - - - - - yarn.scheduler.capacity.maximum-am-resource-percent - 0.5 - - Maximum percent of resources in the cluster which can be used to run - application masters i.e. controls number of concurrent running - applications. - - - - - yarn.scheduler.capacity.resource-calculator - org.apache.hadoop.yarn.util.resource.DefaultResourceCalculator - - The ResourceCalculator implementation to be used to compare - Resources in the scheduler. - The default i.e. DefaultResourceCalculator only uses Memory while - DominantResourceCalculator uses dominant-resource to compare - multi-dimensional resources such as Memory, CPU etc. - - - - - yarn.scheduler.capacity.root.queues - default - - The queues at the this level (root is the root queue). - - - - - yarn.scheduler.capacity.root.default.capacity - 100 - Default queue target capacity. - - - - yarn.scheduler.capacity.root.default.user-limit-factor - 1 - - Default queue user limit a percentage from 0.0 to 1.0. - - - - - yarn.scheduler.capacity.root.default.maximum-capacity - 100 - - The maximum capacity of the default queue. - - - - - yarn.scheduler.capacity.root.default.state - RUNNING - - The state of the default queue. State can be one of RUNNING or STOPPED. - - - - - yarn.scheduler.capacity.root.default.acl_submit_applications - * - - The ACL of who can submit jobs to the default queue. - - - - - yarn.scheduler.capacity.root.default.acl_administer_queue - * - - The ACL of who can administer jobs on the default queue. - - - - - yarn.scheduler.capacity.node-locality-delay - -1 - - Number of missed scheduling opportunities after which the CapacityScheduler - attempts to schedule rack-local containers. - Typically this should be set to number of racks in the cluster, this - feature is disabled by default, set to -1. - - - - diff --git a/ansible/roles/yarn/templates/config.j2 b/ansible/roles/yarn/templates/config.j2 deleted file mode 100644 index f1b411ed5f..0000000000 --- a/ansible/roles/yarn/templates/config.j2 +++ /dev/null @@ -1,3 +0,0 @@ -Host {{item}} - IdentityFile ~/.ssh/hadoop_rsa - StrictHostKeyChecking no diff --git a/ansible/roles/yarn/templates/core-site.xml b/ansible/roles/yarn/templates/core-site.xml deleted file mode 100644 index d5a0752d52..0000000000 --- a/ansible/roles/yarn/templates/core-site.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - fs.http.impl - org.apache.samza.util.hadoop.HttpFileSystem - - diff --git a/ansible/roles/yarn/templates/hadoop-env.sh b/ansible/roles/yarn/templates/hadoop-env.sh deleted file mode 100644 index fd200c373c..0000000000 --- a/ansible/roles/yarn/templates/hadoop-env.sh +++ /dev/null @@ -1,2 +0,0 @@ -#export JAVA_HOME=/usr/lib/jvm/java-8-oracle -export JAVA_HOME=/opt/jdk1.8.0_121 diff --git a/ansible/roles/yarn/templates/log4j.properties b/ansible/roles/yarn/templates/log4j.properties deleted file mode 100644 index 4181560fcc..0000000000 --- a/ansible/roles/yarn/templates/log4j.properties +++ /dev/null @@ -1,267 +0,0 @@ -# Copyright 2011 The Apache Software Foundation -# -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# Define some default values that can be overridden by system properties -hadoop.root.logger=INFO,console -hadoop.log.dir=. -hadoop.log.file=hadoop.log - -# Define the root logger to the system property "hadoop.root.logger". -log4j.rootLogger=${hadoop.root.logger}, EventCounter - -# Logging Threshold -log4j.threshold=ALL - -# Null Appender -log4j.appender.NullAppender=org.apache.log4j.varia.NullAppender - -# -# Rolling File Appender - cap space usage at 5gb. -# -hadoop.log.maxfilesize=25MB -hadoop.log.maxbackupindex=1 -log4j.appender.RFA=org.apache.log4j.RollingFileAppender -log4j.appender.RFA.File=${hadoop.log.dir}/${hadoop.log.file} - -log4j.appender.RFA.MaxFileSize=${hadoop.log.maxfilesize} -log4j.appender.RFA.MaxBackupIndex=${hadoop.log.maxbackupindex} - -log4j.appender.RFA.layout=org.apache.log4j.PatternLayout - -# Pattern format: Date LogLevel LoggerName LogMessage -log4j.appender.RFA.layout.ConversionPattern=%d{ISO8601} %p %c: %m%n -# Debugging Pattern format -#log4j.appender.RFA.layout.ConversionPattern=%d{ISO8601} %-5p %c{2} (%F:%M(%L)) - %m%n - - -# -# Daily Rolling File Appender -# - -log4j.appender.DRFA=org.apache.log4j.DailyRollingFileAppender -log4j.appender.DRFA.File=${hadoop.log.dir}/${hadoop.log.file} - -# Rollver at midnight -log4j.appender.DRFA.DatePattern=.yyyy-MM-dd - -# 30-day backup -log4j.appender.DRFA.MaxBackupIndex=1 -log4j.appender.DRFA.layout=org.apache.log4j.PatternLayout - -# Pattern format: Date LogLevel LoggerName LogMessage -log4j.appender.DRFA.layout.ConversionPattern=%d{ISO8601} %p %c: %m%n -# Debugging Pattern format -#log4j.appender.DRFA.layout.ConversionPattern=%d{ISO8601} %-5p %c{2} (%F:%M(%L)) - %m%n - - -# -# console -# Add "console" to rootlogger above if you want to use this -# - -log4j.appender.console=org.apache.log4j.ConsoleAppender -log4j.appender.console.target=System.err -log4j.appender.console.layout=org.apache.log4j.PatternLayout -log4j.appender.console.layout.ConversionPattern=%d{yy/MM/dd HH:mm:ss} %p %c{2}: %m%n - -# -# TaskLog Appender -# - -#Default values -hadoop.tasklog.taskid=null -hadoop.tasklog.iscleanup=false -hadoop.tasklog.noKeepSplits=4 -hadoop.tasklog.totalLogFileSize=100 -hadoop.tasklog.purgeLogSplits=true -hadoop.tasklog.logsRetainHours=1 - -log4j.appender.TLA=org.apache.hadoop.mapred.TaskLogAppender -log4j.appender.TLA.taskId=${hadoop.tasklog.taskid} -log4j.appender.TLA.isCleanup=${hadoop.tasklog.iscleanup} -log4j.appender.TLA.totalLogFileSize=${hadoop.tasklog.totalLogFileSize} - -log4j.appender.TLA.layout=org.apache.log4j.PatternLayout -log4j.appender.TLA.layout.ConversionPattern=%d{ISO8601} %p %c: %m%n - -# -# HDFS block state change log from block manager -# -# Uncomment the following to suppress normal block state change -# messages from BlockManager in NameNode. -#log4j.logger.BlockStateChange=WARN - -# -#Security appender -# -hadoop.security.logger=INFO,NullAppender -hadoop.security.log.maxfilesize=256MB -hadoop.security.log.maxbackupindex=20 -log4j.category.SecurityLogger=${hadoop.security.logger} -hadoop.security.log.file=SecurityAuth-${user.name}.audit -log4j.appender.RFAS=org.apache.log4j.RollingFileAppender -log4j.appender.RFAS.File=${hadoop.log.dir}/${hadoop.security.log.file} -log4j.appender.RFAS.layout=org.apache.log4j.PatternLayout -log4j.appender.RFAS.layout.ConversionPattern=%d{ISO8601} %p %c: %m%n -log4j.appender.RFAS.MaxFileSize=${hadoop.security.log.maxfilesize} -log4j.appender.RFAS.MaxBackupIndex=${hadoop.security.log.maxbackupindex} - -# -# Daily Rolling Security appender -# -log4j.appender.DRFAS=org.apache.log4j.DailyRollingFileAppender -log4j.appender.DRFAS.File=${hadoop.log.dir}/${hadoop.security.log.file} -log4j.appender.DRFAS.layout=org.apache.log4j.PatternLayout -log4j.appender.DRFAS.layout.ConversionPattern=%d{ISO8601} %p %c: %m%n -log4j.appender.DRFAS.DatePattern=.yyyy-MM-dd - -# -# hadoop configuration logging -# - -# Uncomment the following line to turn off configuration deprecation warnings. -# log4j.logger.org.apache.hadoop.conf.Configuration.deprecation=WARN - -# -# hdfs audit logging -# -hdfs.audit.logger=INFO,NullAppender -hdfs.audit.log.maxfilesize=25MB -hdfs.audit.log.maxbackupindex=1 -log4j.logger.org.apache.hadoop.hdfs.server.namenode.FSNamesystem.audit=${hdfs.audit.logger} -log4j.additivity.org.apache.hadoop.hdfs.server.namenode.FSNamesystem.audit=false -log4j.appender.RFAAUDIT=org.apache.log4j.RollingFileAppender -log4j.appender.RFAAUDIT.File=${hadoop.log.dir}/hdfs-audit.log -log4j.appender.RFAAUDIT.layout=org.apache.log4j.PatternLayout -log4j.appender.RFAAUDIT.layout.ConversionPattern=%d{ISO8601} %p %c{2}: %m%n -log4j.appender.RFAAUDIT.MaxFileSize=${hdfs.audit.log.maxfilesize} -log4j.appender.RFAAUDIT.MaxBackupIndex=${hdfs.audit.log.maxbackupindex} - -# -# mapred audit logging -# -mapred.audit.logger=INFO,NullAppender -mapred.audit.log.maxfilesize=256MB -mapred.audit.log.maxbackupindex=20 -log4j.logger.org.apache.hadoop.mapred.AuditLogger=${mapred.audit.logger} -log4j.additivity.org.apache.hadoop.mapred.AuditLogger=false -log4j.appender.MRAUDIT=org.apache.log4j.RollingFileAppender -log4j.appender.MRAUDIT.File=${hadoop.log.dir}/mapred-audit.log -log4j.appender.MRAUDIT.layout=org.apache.log4j.PatternLayout -log4j.appender.MRAUDIT.layout.ConversionPattern=%d{ISO8601} %p %c{2}: %m%n -log4j.appender.MRAUDIT.MaxFileSize=${mapred.audit.log.maxfilesize} -log4j.appender.MRAUDIT.MaxBackupIndex=${mapred.audit.log.maxbackupindex} - -# Custom Logging levels - -#log4j.logger.org.apache.hadoop.mapred.JobTracker=DEBUG -#log4j.logger.org.apache.hadoop.mapred.TaskTracker=DEBUG -#log4j.logger.org.apache.hadoop.hdfs.server.namenode.FSNamesystem.audit=DEBUG - -# Jets3t library -log4j.logger.org.jets3t.service.impl.rest.httpclient.RestS3Service=ERROR - -# -# Event Counter Appender -# Sends counts of logging messages at different severity levels to Hadoop Metrics. -# -log4j.appender.EventCounter=org.apache.hadoop.log.metrics.EventCounter - -# -# Job Summary Appender -# -# Use following logger to send summary to separate file defined by -# hadoop.mapreduce.jobsummary.log.file : -# hadoop.mapreduce.jobsummary.logger=INFO,JSA -# -hadoop.mapreduce.jobsummary.logger=${hadoop.root.logger} -hadoop.mapreduce.jobsummary.log.file=hadoop-mapreduce.jobsummary.log -hadoop.mapreduce.jobsummary.log.maxfilesize=256MB -hadoop.mapreduce.jobsummary.log.maxbackupindex=20 -log4j.appender.JSA=org.apache.log4j.RollingFileAppender -log4j.appender.JSA.File=${hadoop.log.dir}/${hadoop.mapreduce.jobsummary.log.file} -log4j.appender.JSA.MaxFileSize=${hadoop.mapreduce.jobsummary.log.maxfilesize} -log4j.appender.JSA.MaxBackupIndex=${hadoop.mapreduce.jobsummary.log.maxbackupindex} -log4j.appender.JSA.layout=org.apache.log4j.PatternLayout -log4j.appender.JSA.layout.ConversionPattern=%d{yy/MM/dd HH:mm:ss} %p %c{2}: %m%n -log4j.logger.org.apache.hadoop.mapred.JobInProgress$JobSummary=${hadoop.mapreduce.jobsummary.logger} -log4j.additivity.org.apache.hadoop.mapred.JobInProgress$JobSummary=false - -# -# Yarn ResourceManager Application Summary Log -# -# Set the ResourceManager summary log filename -yarn.server.resourcemanager.appsummary.log.file=rm-appsummary.log -# Set the ResourceManager summary log level and appender -yarn.server.resourcemanager.appsummary.logger=${hadoop.root.logger} -#yarn.server.resourcemanager.appsummary.logger=INFO,RMSUMMARY - -# To enable AppSummaryLogging for the RM, -# set yarn.server.resourcemanager.appsummary.logger to -# ,RMSUMMARY in hadoop-env.sh - -# Appender for ResourceManager Application Summary Log -# Requires the following properties to be set -# - hadoop.log.dir (Hadoop Log directory) -# - yarn.server.resourcemanager.appsummary.log.file (resource manager app summary log filename) -# - yarn.server.resourcemanager.appsummary.logger (resource manager app summary log level and appender) - -log4j.logger.org.apache.hadoop.yarn.server.resourcemanager.RMAppManager$ApplicationSummary=${yarn.server.resourcemanager.appsummary.logger} -log4j.additivity.org.apache.hadoop.yarn.server.resourcemanager.RMAppManager$ApplicationSummary=false -log4j.appender.RMSUMMARY=org.apache.log4j.RollingFileAppender -log4j.appender.RMSUMMARY.File=${hadoop.log.dir}/${yarn.server.resourcemanager.appsummary.log.file} -log4j.appender.RMSUMMARY.MaxFileSize=25MB -log4j.appender.RMSUMMARY.MaxBackupIndex=1 -log4j.appender.RMSUMMARY.layout=org.apache.log4j.PatternLayout -log4j.appender.RMSUMMARY.layout.ConversionPattern=%d{ISO8601} %p %c{2}: %m%n - -# HS audit log configs -#mapreduce.hs.audit.logger=INFO,HSAUDIT -#log4j.logger.org.apache.hadoop.mapreduce.v2.hs.HSAuditLogger=${mapreduce.hs.audit.logger} -#log4j.additivity.org.apache.hadoop.mapreduce.v2.hs.HSAuditLogger=false -#log4j.appender.HSAUDIT=org.apache.log4j.DailyRollingFileAppender -#log4j.appender.HSAUDIT.File=${hadoop.log.dir}/hs-audit.log -#log4j.appender.HSAUDIT.layout=org.apache.log4j.PatternLayout -#log4j.appender.HSAUDIT.layout.ConversionPattern=%d{ISO8601} %p %c{2}: %m%n -#log4j.appender.HSAUDIT.DatePattern=.yyyy-MM-dd - -# Http Server Request Logs -#log4j.logger.http.requests.namenode=INFO,namenoderequestlog -#log4j.appender.namenoderequestlog=org.apache.hadoop.http.HttpRequestLogAppender -#log4j.appender.namenoderequestlog.Filename=${hadoop.log.dir}/jetty-namenode-yyyy_mm_dd.log -#log4j.appender.namenoderequestlog.RetainDays=1 - -#log4j.logger.http.requests.datanode=INFO,datanoderequestlog -#log4j.appender.datanoderequestlog=org.apache.hadoop.http.HttpRequestLogAppender -#log4j.appender.datanoderequestlog.Filename=${hadoop.log.dir}/jetty-datanode-yyyy_mm_dd.log -#log4j.appender.datanoderequestlog.RetainDays=3 - -#log4j.logger.http.requests.resourcemanager=INFO,resourcemanagerrequestlog -#log4j.appender.resourcemanagerrequestlog=org.apache.hadoop.http.HttpRequestLogAppender -#log4j.appender.resourcemanagerrequestlog.Filename=${hadoop.log.dir}/jetty-resourcemanager-yyyy_mm_dd.log -#log4j.appender.resourcemanagerrequestlog.RetainDays=3 - -#log4j.logger.http.requests.jobhistory=INFO,jobhistoryrequestlog -#log4j.appender.jobhistoryrequestlog=org.apache.hadoop.http.HttpRequestLogAppender -#log4j.appender.jobhistoryrequestlog.Filename=${hadoop.log.dir}/jetty-jobhistory-yyyy_mm_dd.log -#log4j.appender.jobhistoryrequestlog.RetainDays=3 - -#log4j.logger.http.requests.nodemanager=INFO,nodemanagerrequestlog -#log4j.appender.nodemanagerrequestlog=org.apache.hadoop.http.HttpRequestLogAppender -#log4j.appender.nodemanagerrequestlog.Filename=${hadoop.log.dir}/jetty-nodemanager-yyyy_mm_dd.log -#log4j.appender.nodemanagerrequestlog.RetainDays=3 diff --git a/ansible/roles/yarn/templates/yarn-site.xml b/ansible/roles/yarn/templates/yarn-site.xml deleted file mode 100644 index 796f6450c3..0000000000 --- a/ansible/roles/yarn/templates/yarn-site.xml +++ /dev/null @@ -1,64 +0,0 @@ - - - - - yarn.resourcemanager.hostname - - {{resourcemanager}} - - - yarn.nodemanager.resource.memory-mb - {{yarn_resource_memory}} - - - yarn.scheduler.minimum-allocation-mb - 128 - - - mapreduce.job.userlog.retain.hours - 240 - - - yarn.log-aggregation-enable - false - - - yarn.nodemanager.log.retain-seconds - 3600 - - - yarn.nodemanager.recovery.enabled - true - - - yarn.nodemanager.address - 0.0.0.0:45454 - - - yarn.nodemanager.resource.cpu-vcores - {{yarn_vcores}} - - {% if yarn_config_override is defined %} - - yarn.nodemanager.vmem-check-enabled - {{yarn_vmem_check_enabled}} - - - yarn.nodemanager.vmem-pmem-ratio - {{yarn_vmem_pmem_ratio}} - - {% endif %} - - diff --git a/ansible/samza_jobs_alert.yml b/ansible/samza_jobs_alert.yml deleted file mode 100644 index f225d1d5bc..0000000000 --- a/ansible/samza_jobs_alert.yml +++ /dev/null @@ -1,9 +0,0 @@ ---- -- hosts: lp-yarn-master - vars_files: - - "{{inventory_dir}}/secrets.yml" - tasks: - - command: ./samza_alerts.sh - args: - chdir: /home/hduser - become: yes diff --git a/ansible/samza_logs_provision.yml b/ansible/samza_logs_provision.yml deleted file mode 100644 index 9f2a952bc2..0000000000 --- a/ansible/samza_logs_provision.yml +++ /dev/null @@ -1,8 +0,0 @@ ---- -- hosts: dp-yarn-slave - vars_files: - - "{{inventory_dir}}/secrets.yml" - become: yes - tasks: - - name: copy the backup script to yarn slaves - copy: src=resources/upload_samza_logs dest="{{script_path}}/upload_samza_logs.sh" mode=755 diff --git a/pipelines/build/yarn/Jenkinsfile b/pipelines/build/yarn/Jenkinsfile deleted file mode 100644 index 6116b4e291..0000000000 --- a/pipelines/build/yarn/Jenkinsfile +++ /dev/null @@ -1,45 +0,0 @@ -@Library('deploy-conf') _ -node() { - try { - String ANSI_GREEN = "\u001B[32m" - String ANSI_NORMAL = "\u001B[0m" - String ANSI_BOLD = "\u001B[1m" - String ANSI_RED = "\u001B[31m" - String ANSI_YELLOW = "\u001B[33m" - - ansiColor('xterm') { - stage('Checkout') { - cleanWs() - checkout scm - commit_hash = sh(script: 'git rev-parse --short HEAD', returnStdout: true).trim() - artifact_version = sh(script: "echo " + params.github_release_tag.split('/')[-1] + "_" + commit_hash + "_" + env.BUILD_NUMBER, returnStdout: true).trim() - echo "artifact_version: "+ artifact_version - } - } - - stage('Build') { - sh 'mvn clean package -DskipTests -P samza-jobs' - } - - stage('Archive artifacts'){ - sh """ - mkdir lp_yarn_artifacts - cp platform-jobs/samza/distribution/target/distribution-*.tar.gz lp_yarn_artifacts - zip -j lp_yarn_artifacts.zip:${artifact_version} lp_yarn_artifacts/* - """ - archiveArtifacts artifacts: "lp_yarn_artifacts.zip:${artifact_version}", fingerprint: true, onlyIfSuccessful: true - sh """echo {\\"artifact_name\\" : \\"lp_yarn_artifacts.zip\\", \\"artifact_version\\" : \\"${artifact_version}\\", \\"node_name\\" : \\"${env.NODE_NAME}\\"} > metadata.json""" - archiveArtifacts artifacts: 'metadata.json', onlyIfSuccessful: true - currentBuild.result = "SUCCESS" - currentBuild.description = "Artifact: ${artifact_version}, Public: ${params.github_release_tag}" - } - } - catch (err) { - currentBuild.result = "FAILURE" - throw err - } - finally { - slack_notify(currentBuild.result) - email_notify() - } -} diff --git a/pipelines/build/yarn/auto_build_deploy b/pipelines/build/yarn/auto_build_deploy deleted file mode 100644 index 17b4654463..0000000000 --- a/pipelines/build/yarn/auto_build_deploy +++ /dev/null @@ -1,63 +0,0 @@ -@Library('deploy-conf') _ -node() { - try { - String ANSI_GREEN = "\u001B[32m" - String ANSI_NORMAL = "\u001B[0m" - String ANSI_BOLD = "\u001B[1m" - String ANSI_RED = "\u001B[31m" - String ANSI_YELLOW = "\u001B[33m" - - ansiColor('xterm') { - stage('Checkout') { - tag_name = env.JOB_NAME.split("/")[-1] - module = env.JOB_NAME.split("/")[-3] - envDir = env.JOB_NAME.split("/")[-4] - pre_checks() - cleanWs() - def scmVars = checkout scm - checkout scm: [$class: 'GitSCM', branches: [[name: "refs/tags/$tag_name"]], userRemoteConfigs: [[url: scmVars.GIT_URL]]] - commit_hash = sh(script: 'git rev-parse --short HEAD', returnStdout: true).trim() - artifact_version = tag_name + "_" + commit_hash - echo "artifact_version: "+ artifact_version - } - } - -// stage Build learning. -// println ANSI_BOLD + ANSI_GREEN + "Triggering KnowledgePlatform build.." + ANSI_NORMAL -// lpbuild = build job: "AutoBuild/$envDir/$module/Learning", parameters: [string(name: 'github_release_tag', value: "$tag_name")] - -// if (lpbuild.currentResult == "SUCCESS") { -// stage Build - sh 'mvn clean package -DskipTests -P samza-jobs' - - -// stage Archive artifacts - sh """ - mkdir lp_yarn_artifacts - cp platform-jobs/samza/distribution/target/distribution-*.tar.gz lp_yarn_artifacts - zip -j lp_yarn_artifacts.zip:${artifact_version} lp_yarn_artifacts/* - """ - archiveArtifacts artifacts: "lp_yarn_artifacts.zip:${artifact_version}", fingerprint: true, onlyIfSuccessful: true - sh """echo {\\"artifact_name\\" : \\"lp_yarn_artifacts.zip\\", \\"artifact_version\\" : \\"${artifact_version}\\", \\"node_name\\" : \\"${env.NODE_NAME}\\"} > metadata.json""" - archiveArtifacts artifacts: 'metadata.json', onlyIfSuccessful: true - currentBuild.result = "SUCCESS" - currentBuild.description = "Artifact: ${artifact_version}, Public: ${params.github_release_tag}" - - currentBuild.result = "SUCCESS" - slack_notify(currentBuild.result, tag_name) - email_notify() - auto_build_deploy() -// } -// else { -// println (ANSI_BOLD + ANSI_RED + "knowledge platform build failed. Skipping build" + ANSI_NORMAL) -// error "knowledge platform build failed" -// } - - } - catch (err) { - currentBuild.result = "FAILURE" - slack_notify(currentBuild.result, tag_name) - email_notify() - throw err - } -} diff --git a/pipelines/deploy/yarn/Jenkinsfile b/pipelines/deploy/yarn/Jenkinsfile deleted file mode 100644 index cf551df104..0000000000 --- a/pipelines/deploy/yarn/Jenkinsfile +++ /dev/null @@ -1,60 +0,0 @@ -@Library('deploy-conf') _ -node() { - try { - String ANSI_GREEN = "\u001B[32m" - String ANSI_NORMAL = "\u001B[0m" - String ANSI_BOLD = "\u001B[1m" - String ANSI_RED = "\u001B[31m" - String ANSI_YELLOW = "\u001B[33m" - - stage('checkout public repo') { - folder = new File("$WORKSPACE/.git") - if (folder.exists()) - { - println "Found .git folder. Clearing it.." - sh'git clean -fxd' - } - checkout scm - } - - ansiColor('xterm') { - values = lp_dp_params() - stage('get artifact') { - currentWs = sh(returnStdout: true, script: 'pwd').trim() - artifact = values.artifact_name + ":" + values.artifact_version - values.put('currentWs', currentWs) - values.put('artifact', artifact) - artifact_download(values) - } - stage('deploy artifact') { - sh """ - unzip ${artifact} - mv distribution-*.tar.gz ansible - rm -rf ansible/roles/samza-jobs/files/jobs - mkdir ansible/roles/samza-jobs/files/jobs - tar -xvf ansible/distribution-*.tar.gz -C ansible/roles/samza-jobs/files/jobs/ - - """ - ansiblePlaybook = "${currentWs}/ansible/lp_samza_deploy.yml" - ansibleExtraArgs = "--vault-password-file /var/lib/jenkins/secrets/vault-pass" - values.put('ansiblePlaybook', ansiblePlaybook) - values.put('ansibleExtraArgs', ansibleExtraArgs) - println values - ansible_playbook_run(values) - currentBuild.result = "SUCCESS" - currentBuild.description = "Artifact: ${values.artifact_version}, Private: ${params.private_branch}, Public: ${params.branch_or_tag}" - archiveArtifacts artifacts: "${artifact}", fingerprint: true, onlyIfSuccessful: true - archiveArtifacts artifacts: 'metadata.json', onlyIfSuccessful: true - } - } - summary() - } - catch (err) { - currentBuild.result = "FAILURE" - throw err - } - finally { - slack_notify(currentBuild.result) - email_notify() - } -} \ No newline at end of file diff --git a/platform-jobs/.gitignore b/platform-jobs/.gitignore deleted file mode 100644 index c195647c3a..0000000000 --- a/platform-jobs/.gitignore +++ /dev/null @@ -1 +0,0 @@ -**/deploy/samza diff --git a/platform-jobs/pom.xml b/platform-jobs/pom.xml deleted file mode 100644 index c562efae5b..0000000000 --- a/platform-jobs/pom.xml +++ /dev/null @@ -1,83 +0,0 @@ - - - 4.0.0 - - org.sunbird - sunbird-platform - 1.1-SNAPSHOT - ../pom.xml - - platform-jobs - pom - Base for all platform jobs - - - UTF-8 - 4.2.4.RELEASE - 2.3.1 - 1.8 - 1.8 - - - - samza - - - - - - - - - - org.apache.maven.plugins - maven-compiler-plugin - 2.3.2 - - 1.8 - 1.8 - - - - - - - org.apache.maven.plugins - maven-surefire-plugin - 2.20 - - - - org.jacoco - jacoco-maven-plugin - 0.7.9 - - - **/common/** - **/dto/** - **/enums/** - **/pipeline/** - **/servlet/** - **/interceptor/** - - - - - default-prepare-agent - - prepare-agent - - - - default-report - prepare-package - - report - - - - - - - \ No newline at end of file diff --git a/platform-jobs/samza/auto-creator/pom.xml b/platform-jobs/samza/auto-creator/pom.xml deleted file mode 100644 index 9ecf52adb4..0000000000 --- a/platform-jobs/samza/auto-creator/pom.xml +++ /dev/null @@ -1,87 +0,0 @@ - - - - samza - org.sunbird - 1.1-SNAPSHOT - - 4.0.0 - auto-creator - 0.0.39 - - - - com.konghq - unirest-java - 3.7.02 - - - org.sunbird - course-common - 1.1-SNAPSHOT - - - unirest-java - com.mashape.unirest - - - - - org.sunbird - unit-tests - 1.1-SNAPSHOT - test - - - org.mockito - mockito-all - 1.10.19 - test - - - org.powermock - powermock-api-mockito - 1.7.4 - test - - - org.powermock - powermock-module-junit4 - 1.7.4 - test - - - - - - - org.apache.maven.plugins - maven-compiler-plugin - - ${java.version} - ${java.version} - - - - - maven-assembly-plugin - - - src/main/assembly/src.xml - - - - - make-assembly - package - - single - - - - - - - \ No newline at end of file diff --git a/platform-jobs/samza/auto-creator/src/main/assembly/src.xml b/platform-jobs/samza/auto-creator/src/main/assembly/src.xml deleted file mode 100644 index aa00816eba..0000000000 --- a/platform-jobs/samza/auto-creator/src/main/assembly/src.xml +++ /dev/null @@ -1,69 +0,0 @@ - - - - - distribution - - tar.gz - - false - - - ${basedir} - - README* - LICENSE* - NOTICE* - - - - - - ${basedir}/src/main/resources/log4j.xml - lib - - - - ${basedir}/src/main/config/auto-creator.properties - config - true - - - - - bin - - org.apache.samza:samza-shell:tgz:dist:* - - 0744 - true - - - lib - - org.apache.samza:samza-api - org.sunbird:auto-creator - org.apache.samza:samza-core_2.11 - org.apache.samza:samza-kafka_2.11 - org.apache.samza:samza-yarn_2.11 - org.apache.samza:samza-log4j - org.apache.kafka:kafka_2.11 - org.apache.hadoop:hadoop-hdfs - - true - - - \ No newline at end of file diff --git a/platform-jobs/samza/auto-creator/src/main/config/auto-creator.properties b/platform-jobs/samza/auto-creator/src/main/config/auto-creator.properties deleted file mode 100644 index b3f049495b..0000000000 --- a/platform-jobs/samza/auto-creator/src/main/config/auto-creator.properties +++ /dev/null @@ -1,89 +0,0 @@ -# Job -job.factory.class=org.apache.samza.job.yarn.YarnJobFactory -job.name=__env__.auto-creator -job.container.count=__auto_creator_container_count__ - -# YARN -yarn.package.path=http://__yarn_host__:__yarn_port__/__env__/${project.artifactId}-${pom.version}-distribution.tar.gz - -# Metrics -metrics.reporters=snapshot,jmx -metrics.reporter.snapshot.class=org.apache.samza.metrics.reporter.MetricsSnapshotReporterFactory -metrics.reporter.snapshot.stream=kafka.__env__.metrics -metrics.reporter.jmx.class=org.apache.samza.metrics.reporter.JmxReporterFactory -output.metrics.job.name=auto-creator -output.metrics.topic.name=__env__.pipeline_metrics - -# Task -task.class=org.sunbird.jobs.samza.task.AutoCreatorTask -task.inputs=kafka.__env__.auto.creation.job.request -task.checkpoint.factory=org.apache.samza.checkpoint.kafka.KafkaCheckpointManagerFactory -task.checkpoint.system=kafka -task.checkpoint.replication.factor=__samza_checkpoint_replication_factor__ -task.commit.ms=60000 -task.window.ms=300000 -task.opts=-Dfile.encoding=UTF8 - -# Serializers -serializers.registry.json.class=org.sunbird.jobs.samza.serializers.EkstepJsonSerdeFactory -serializers.registry.metrics.class=org.apache.samza.serializers.MetricsSnapshotSerdeFactory - -# Systems -systems.kafka.samza.factory=org.apache.samza.system.kafka.KafkaSystemFactory -systems.kafka.samza.msg.serde=json -systems.kafka.streams.metrics.samza.msg.serde=metrics -systems.kafka.consumer.zookeeper.connect=__zookeepers__ -systems.kafka.consumer.auto.offset.reset=smallest -systems.kafka.samza.offset.default=oldest -systems.kafka.producer.bootstrap.servers=__kafka_brokers__ - -# Job Coordinator -job.coordinator.system=kafka - -# Normally, this would be 3, but we have only one broker. -job.coordinator.replication.factor=__samza_coordinator_replication_factor__ - -#Job Specific Config -graph.passport.key.base=__graph_passport_key__ -output.failed.events.topic.name=__env__.auto.creation.job.request.failed -lp.tempfile.location=__lp_tmpfile_location__ -max.iteration.count.samza.job=__max_iteration_count_for_samza_job__ - -kp.content_service.base_url=__kp_content_service_base_url__ -kp.learning_service.base_url=__kp_learning_service_base_url__ -kp.search_service_base_url=__kp_search_service_base_url__ - -auto_creator.actions=auto-create -auto_creator.allowed_object_types=Content -auto_creator.content_mandatory_fields=name,code,mimeType,primaryCategory,artifactUrl,lastPublishedBy -#TODO: Need to test, if collectionId will be overridden by publish, is there any impact -auto_creator.content_props_to_removed=identifier,downloadUrl,artifactUrl,variants,createdOn,collections,children,lastUpdatedOn,SYS_INTERNAL_LAST_UPDATED_ON,versionKey,s3Key,status,pkgVersion,toc_url,mimeTypesCount,contentTypesCount,leafNodesCount,childNodes,prevState,lastPublishedOn,flagReasons,compatibilityLevel,size,publishChecklist,publishComment,lastPublishedBy,rejectReasons,rejectComment,badgeAssertions,leafNodes,sYS_INTERNAL_LAST_UPDATED_ON,previewUrl,channel,objectType,visibility,version,pragma,prevStatus,streamingUrl,idealScreenSize,contentDisposition,lastStatusChangedOn,idealScreenDensity,lastSubmittedOn,publishError,flaggedBy,flags,lastFlaggedOn,publisher,lastUpdatedBy,lastSubmittedBy,uploadError,lockKey,publish_type,reviewError,totalCompressedSize,origin,originData,importError,questions -auto_creator.bulk_upload.mime_types=video/mp4 -auto_creator.artifact_upload.max_size=157286400 -auto_creator.content_create_props=name,code,mimeType,contentType,framework,processId,primaryCategory -auto_creator.artifact_upload.allowed_source=__auto_creator_artifact_allowed_sources__ -# Delay between each api call in seconds -auto_creator.api_call_delay=1 - -auto_creator_g_service_acct_cred=__auto_creator_g_service_acct_cred__ -auto_creator.gdrive.application_name=drive-download -auto_creator.initial_backoff_delay=120000 -auto_creator.maximum_backoff_delay=1200000 -auto_creator.increment_backoff_delay=2 - - -# Folder Config -cloud_storage.content.folder=content -cloud_storage.artefact.folder=artifact - -# Cloud store details -cloud_storage_type=__cloud_storage_type__ -azure_storage_key=__azure_storage_key__ -azure_storage_secret=__azure_storage_secret__ -azure_storage_container=__azure_storage_container__ -aws_storage_key=__aws_access_key_id__ -aws_storage_secret=__aws_secret_access_key__ -aws_storage_container=__aws_storage_container__ - - - diff --git a/platform-jobs/samza/auto-creator/src/main/config/local.auto-creator.properties.properties b/platform-jobs/samza/auto-creator/src/main/config/local.auto-creator.properties.properties deleted file mode 100644 index 45783a38b0..0000000000 --- a/platform-jobs/samza/auto-creator/src/main/config/local.auto-creator.properties.properties +++ /dev/null @@ -1,74 +0,0 @@ -# Job -job.factory.class=org.apache.samza.job.yarn.YarnJobFactory -job.name=local.auto-creator -job.container.count=1 - -# YARN -yarn.package.path=file://${basedir}/target/${project.artifactId}-${pom.version}-distribution.tar.gz - -# Metrics -metrics.reporters=snapshot,jmx -metrics.reporter.snapshot.class=org.apache.samza.metrics.reporter.MetricsSnapshotReporterFactory -metrics.reporter.snapshot.stream=kafka.local.lp.metrics -metrics.reporter.jmx.class=org.apache.samza.metrics.reporter.JmxReporterFactory -output.metrics.job.name=course-batch-updater -output.metrics.topic.name=local.lp.metrics - -# Task -task.class=org.sunbird.job.samza.task.AutoCreatorTask -task.inputs=kafka.local.auto.creation.job.request -task.checkpoint.factory=org.apache.samza.checkpoint.kafka.KafkaCheckpointManagerFactory -task.checkpoint.system=kafka -task.checkpoint.replication.factor=1 -task.commit.ms=60000 -task.window.ms=300000 - -# Serializers -serializers.registry.json.class=org.sunbird.jobs.samza.serializers.EkstepJsonSerdeFactory -serializers.registry.metrics.class=org.apache.samza.serializers.MetricsSnapshotSerdeFactory - -# Systems -systems.kafka.samza.factory=org.apache.samza.system.kafka.KafkaSystemFactory -systems.kafka.samza.msg.serde=json -systems.kafka.streams.metrics.samza.msg.serde=metrics -systems.kafka.consumer.zookeeper.connect=localhost:2181 -systems.kafka.consumer.auto.offset.reset=smallest -systems.kafka.producer.bootstrap.servers=localhost:9092 - -# Job Coordinator -job.coordinator.system=kafka -# Normally, this would be 3, but we have only one broker. -job.coordinator.replication.factor=1 - -#Remote Debug Configuration -# task.opts=-agentlib:jdwp=transport=dt_socket,address=localhost:9009,server=y,suspend=y - -#Job Specific Config -output.failed.events.topic.name=__env__.learning.events.failed -lp.tempfile.location=__lp_tmpfile_location__ -max.iteration.count.samza.job=__max_iteration_count_for_samza_job__ - -kp.content_service.base_url=__kp_content_service_base_url__ -kp.learning_service.base_url=__kp_learning_service_base_url__ -kp.search_service_base_url=__kp_search_service_base_url__ - -auto_creator.actions=auto-create -auto_creator.allowed_object_types=Content -auto_creator.content_mandatory_fields=identifier,name,description,code,mimeType,contentType,artifactUrl,lastPublishedBy -auto_creator.content_props_to_removed=identifier,downloadUrl,artifactUrl,variants,createdOn,collections,children,lastUpdatedOn,SYS_INTERNAL_LAST_UPDATED_ON,versionKey,s3Key,status,pkgVersion,toc_url,mimeTypesCount,contentTypesCount,leafNodesCount,childNodes,prevState,lastPublishedOn,flagReasons,compatibilityLevel,size,publishChecklist,publishComment,lastPublishedBy,rejectReasons,rejectComment,badgeAssertions,leafNodes -auto_creator.bulk_upload.mime_types=video/mp4 -auto_creator.artifact_upload.max_size=62914560 - -# Folder Config -cloud_storage.content.folder=content -cloud_storage.artefact.folder=artifact - -# Cloud store details -cloud_storage_type=__cloud_storage_type__ -azure_storage_key=__azure_storage_key__ -azure_storage_secret=__azure_storage_secret__ -azure_storage_container=__azure_storage_container__ -aws_storage_key=__aws_access_key_id__ -aws_storage_secret=__aws_secret_access_key__ -aws_storage_container=__aws_storage_container__ - diff --git a/platform-jobs/samza/auto-creator/src/main/java/org/sunbird/jobs/samza/service/AutoCreatorService.java b/platform-jobs/samza/auto-creator/src/main/java/org/sunbird/jobs/samza/service/AutoCreatorService.java deleted file mode 100644 index ca60887eed..0000000000 --- a/platform-jobs/samza/auto-creator/src/main/java/org/sunbird/jobs/samza/service/AutoCreatorService.java +++ /dev/null @@ -1,111 +0,0 @@ -package org.sunbird.jobs.samza.service; - -import org.apache.commons.collections.MapUtils; -import org.apache.commons.lang3.StringUtils; -import org.apache.samza.config.Config; -import org.apache.samza.system.SystemStream; -import org.apache.samza.task.MessageCollector; -import org.sunbird.common.Platform; -import org.sunbird.common.exception.ServerException; -import org.sunbird.jobs.samza.exception.PlatformErrorCodes; -import org.sunbird.jobs.samza.service.task.JobMetrics; -import org.sunbird.jobs.samza.util.AutoCreatorParams; -import org.sunbird.jobs.samza.util.ContentUtil; -import org.sunbird.jobs.samza.util.FailedEventsUtil; -import org.sunbird.jobs.samza.util.JSONUtils; -import org.sunbird.jobs.samza.util.JobLogger; -import org.sunbird.jobs.samza.util.SamzaCommonParams; - -import java.util.*; - -public class AutoCreatorService implements ISamzaService { - private static JobLogger LOGGER = new JobLogger(AutoCreatorService.class); - private Config config = null; - private SystemStream failedEventStream; - private static Integer MAX_ITERATION_COUNT = null; - private List ALLOWED_OBJECT_TYPES = null; - private ContentUtil contentUtil = null; - - @Override - public void initialize(Config config) throws Exception { - this.config = config; - JSONUtils.loadProperties(config); - LOGGER.info("Service config initialized"); - failedEventStream = new SystemStream("kafka", config.get("output.failed.events.topic.name")); - LOGGER.info("Stream initialized for Failed Events"); - MAX_ITERATION_COUNT = (Platform.config.hasPath("max.iteration.count.samza.job")) ? - Platform.config.getInt("max.iteration.count.samza.job") : 2; - ALLOWED_OBJECT_TYPES = Arrays.asList(Platform.config.getString("auto_creator.allowed_object_types").split(",")); - contentUtil = new ContentUtil(); - LOGGER.info("ContentUtil initialized."); - } - - @Override - public void processMessage(Map message, JobMetrics metrics, MessageCollector collector) throws Exception { - if (null == message) { - LOGGER.info("Null Event Received. So Skipped Processing."); - return; - } - Map edata = (Map) message.get(SamzaCommonParams.edata.name()); - Map object = (Map) message.get(SamzaCommonParams.object.name()); - Map context = (Map) message.get(AutoCreatorParams.context.name()); - try { - Integer currentIteration = (Integer) edata.getOrDefault(SamzaCommonParams.iteration.name(),1); - String channel = (String) context.getOrDefault(AutoCreatorParams.channel.name(), ""); - String identifier = (String) object.getOrDefault(AutoCreatorParams.id.name(), ""); - String objectType = (String) edata.getOrDefault(AutoCreatorParams.objectType.name(), ""); - String repository = (String) edata.getOrDefault(AutoCreatorParams.repository.name(), ""); - Map metadata = (Map) edata.getOrDefault(AutoCreatorParams.metadata.name(), new HashMap()); - List> collection = (List>) edata.getOrDefault(AutoCreatorParams.collection.name(), new ArrayList>()); - String stage = (String) edata.getOrDefault(AutoCreatorParams.stage.name(), ""); - - if (!validateEvent(currentIteration, channel, identifier, objectType, metadata)) { - LOGGER.info("Event Ignored. Event Validation Failed for auto-creator operation : " + edata.get("action") + " | Event : " + message); - return; - } - - switch (objectType.toLowerCase()) { - case "content": { - if(!contentUtil.validateStage(stage)) { - LOGGER.info("Event Ignored. Content Stage Validation Failed for :" + identifier + " | Stage : " + stage + " Allowed Stages are : " + contentUtil.ALLOWED_CONTENT_STAGE); - return; - } - if (!(contentUtil.validateMetadata(metadata))) { - LOGGER.info("Event Ignored. Event Metadata Validation Failed for :" + identifier + " | Metadata : " + metadata + " Required fields are : " + contentUtil.REQUIRED_METADATA_FIELDS); - return; - } - contentUtil.process(channel, identifier, edata); - break; - } - default: { - LOGGER.info("Event Ignored. Event objectType doesn't match with allowed objectType."); - } - } - } catch (Exception e) { - LOGGER.error("AutoCreatorService :: Message processing failed for mid : " + message.get("mid"), message, e); - metrics.incErrorCounter(); - Integer currentIteration = (Integer) edata.getOrDefault(SamzaCommonParams.iteration.name(), 1); - if (currentIteration < MAX_ITERATION_COUNT) { - ((Map) message.get(SamzaCommonParams.edata.name())).put(SamzaCommonParams.iteration.name(), currentIteration + 1); - FailedEventsUtil.pushEventForRetry(failedEventStream, message, metrics, collector, - PlatformErrorCodes.PROCESSING_ERROR.name(), e); - LOGGER.info("Failed Event Sent To Kafka Topic : " + config.get("output.failed.events.topic.name") + " | for mid : " + message.get("mid"), message); - }else{ - LOGGER.info("Event Reached Maximum Retry Limit having mid : " + message.get("mid"), message); - } - if ((e instanceof ServerException) && StringUtils.equalsIgnoreCase(((ServerException) e).getErrCode(), "ERR_API_CALL")) { - LOGGER.error("Error While making api calls. ", e); - throw e; - } - } - } - - private Boolean validateEvent(Integer currentIteration, String channel, String identifier, String objectType, Map metadata) { - if ((currentIteration <= MAX_ITERATION_COUNT) && (StringUtils.isNotBlank(channel) && StringUtils.isNotBlank(identifier) && MapUtils.isNotEmpty(metadata)) && - (StringUtils.isNotBlank(objectType) && ALLOWED_OBJECT_TYPES.contains(objectType))) { - return true; - } - return false; - } - -} diff --git a/platform-jobs/samza/auto-creator/src/main/java/org/sunbird/jobs/samza/task/AutoCreatorTask.java b/platform-jobs/samza/auto-creator/src/main/java/org/sunbird/jobs/samza/task/AutoCreatorTask.java deleted file mode 100644 index 4c78084dc6..0000000000 --- a/platform-jobs/samza/auto-creator/src/main/java/org/sunbird/jobs/samza/task/AutoCreatorTask.java +++ /dev/null @@ -1,59 +0,0 @@ -package org.sunbird.jobs.samza.task; - -import org.apache.commons.lang3.StringUtils; -import org.apache.samza.system.OutgoingMessageEnvelope; -import org.apache.samza.system.SystemStream; -import org.apache.samza.task.MessageCollector; -import org.apache.samza.task.TaskCoordinator; -import org.sunbird.common.Platform; -import org.sunbird.common.exception.ServerException; -import org.sunbird.jobs.samza.service.AutoCreatorService; -import org.sunbird.jobs.samza.service.ISamzaService; -import org.sunbird.jobs.samza.util.JobLogger; -import org.sunbird.jobs.samza.task.BaseTask; - - -import java.util.Arrays; -import java.util.Map; - -public class AutoCreatorTask extends BaseTask { - - private ISamzaService service = new AutoCreatorService(); - - private static JobLogger LOGGER = new JobLogger(AutoCreatorTask.class); - - public ISamzaService initialize() throws Exception { - LOGGER.info("Task initialized"); - this.action = Platform.config.hasPath("auto_creator.actions") ? - Arrays.asList(Platform.config.getString("auto_creator.actions").split(",")) : Arrays.asList("auto-create"); - LOGGER.info("Available Actions : " + this.action); - this.jobStartMessage = "Started processing of auto-creator samza job"; - this.jobEndMessage = "Completed processing of auto-creator job"; - this.jobClass = "org.sunbird.jobs.samza.task.AutoCreatorTask"; - return service; - } - - @Override - public void process(Map message, MessageCollector collector, TaskCoordinator coordinator) throws Exception { - try { - LOGGER.info("Starting Task Process for auto-creator operation for mid : " + message.get("mid") + " at :: " + System.currentTimeMillis()); - long startTime = System.currentTimeMillis(); - service.processMessage(message, metrics, collector); - long endTime = System.currentTimeMillis(); - LOGGER.info("Successfully completed processing for auto-creator operation for mid : " + message.get("mid") + " at :: " + System.currentTimeMillis()); - } catch (Exception e) { - LOGGER.error("AutoCreatorTask ::: process ::: Message processing failed.", message, e); - if ((e instanceof ServerException) && StringUtils.equalsIgnoreCase(((ServerException) e).getErrCode(), "ERR_API_CALL")) { - LOGGER.error("Error While making api calls. ", e); - throw e; - } - } - } - - @Override - public void window(MessageCollector collector, TaskCoordinator coordinator) throws Exception { - Map event = metrics.collect(); - collector.send(new OutgoingMessageEnvelope(new SystemStream("kafka", metrics.getTopic()), event)); - metrics.clear(); - } -} \ No newline at end of file diff --git a/platform-jobs/samza/auto-creator/src/main/java/org/sunbird/jobs/samza/util/AutoCreatorParams.java b/platform-jobs/samza/auto-creator/src/main/java/org/sunbird/jobs/samza/util/AutoCreatorParams.java deleted file mode 100644 index 45f94007d2..0000000000 --- a/platform-jobs/samza/auto-creator/src/main/java/org/sunbird/jobs/samza/util/AutoCreatorParams.java +++ /dev/null @@ -1,8 +0,0 @@ -package org.sunbird.jobs.samza.util; - -public enum AutoCreatorParams { - channel, id, objectType, metadata, artifactUrl, status, request, filters, origin, originData, count, content, identifier, - repository, pkgVersion, lastPublishedBy, children, childNodes, rootId,unitId, context, collection, - processId, versionKey, importError, textbookInfo, unitIdentifiers, mimeType, stage - -} diff --git a/platform-jobs/samza/auto-creator/src/main/java/org/sunbird/jobs/samza/util/ContentUtil.java b/platform-jobs/samza/auto-creator/src/main/java/org/sunbird/jobs/samza/util/ContentUtil.java deleted file mode 100644 index 0dc3555d39..0000000000 --- a/platform-jobs/samza/auto-creator/src/main/java/org/sunbird/jobs/samza/util/ContentUtil.java +++ /dev/null @@ -1,717 +0,0 @@ -package org.sunbird.jobs.samza.util; - -import com.fasterxml.jackson.databind.ObjectMapper; -import kong.unirest.HttpResponse; -import kong.unirest.Unirest; -import org.apache.commons.collections4.CollectionUtils; -import org.apache.commons.collections4.MapUtils; -import org.apache.commons.io.FileUtils; -import org.apache.commons.io.FilenameUtils; -import org.apache.commons.lang.StringUtils; -import org.apache.tika.Tika; -import org.sunbird.common.Platform; -import org.sunbird.common.Slug; -import org.sunbird.common.dto.Response; -import org.sunbird.common.enums.TaxonomyErrorCodes; -import org.sunbird.common.exception.ResponseCode; -import org.sunbird.common.exception.ServerException; -import org.sunbird.common.util.HttpDownloadUtility; -import org.sunbird.common.util.S3PropertyReader; -import org.sunbird.learning.common.enums.ContentErrorCodes; -import org.sunbird.learning.util.CloudStore; - -import java.io.File; -import java.io.IOException; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.stream.Collectors; - -public class ContentUtil { - - private static final String KP_CS_BASE_URL = Platform.config.getString("kp.content_service.base_url"); - private static final String KP_LEARNING_BASE_URL = Platform.config.getString("kp.learning_service.base_url"); - private static final String KP_SEARCH_URL = Platform.config.getString("kp.search_service_base_url") + "/v3/search"; - private static final String PASSPORT_KEY = Platform.config.getString("graph.passport.key.base"); - private static final String TEMP_FILE_LOCATION = Platform.config.hasPath("lp.tempfile.location") ? - Platform.config.getString("lp.tempfile.location") : "/tmp/content"; - public static final List REQUIRED_METADATA_FIELDS = Arrays.asList(Platform.config.getString("auto_creator.content_mandatory_fields").split(",")); - public static final List METADATA_FIELDS_TO_BE_REMOVED = Arrays.asList(Platform.config.getString("auto_creator.content_props_to_removed").split(",")); - private static final List SEARCH_FIELDS = Arrays.asList("identifier", "mimeType", "pkgVersion", "channel", "status", "origin", "originData","artifactUrl"); - private static final List SEARCH_EXISTS_FIELDS = Arrays.asList("originData"); - private static final List FINAL_STATUS = Arrays.asList("Live", "Unlisted", "Processing"); - private static final String DEFAULT_CONTENT_TYPE = "application/json"; - private static final int IDX_CLOUD_KEY = 0; - private static final int IDX_CLOUD_URL = 1; - private static final String CONTENT_FOLDER = "cloud_storage.content.folder"; - private static final String ARTEFACT_FOLDER = "cloud_storage.artefact.folder"; - private static final Long CONTENT_UPLOAD_ARTIFACT_MAX_SIZE = Platform.config.hasPath("auto_creator.artifact_upload.max_size") ? Platform.config.getLong("auto_creator.artifact_upload.max_size") : 62914560; - private static final List BULK_UPLOAD_MIMETYPES = Platform.config.hasPath("auto_creator.bulk_upload.mime_types") ? Arrays.asList(Platform.config.getString("auto_creator.bulk_upload.mime_types").split(",")) : new ArrayList(); - private static final List CONTENT_CREATE_PROPS = Platform.config.hasPath("auto_creator.content_create_props") ? Arrays.asList(Platform.config.getString("auto_creator.content_create_props").split(",")) : new ArrayList(); - private static final List ALLOWED_ARTIFACT_SOURCE = Platform.config.hasPath("auto_creator.artifact_upload.allowed_source") ? Arrays.asList(Platform.config.getString("auto_creator.artifact_upload.allowed_source").split(",")) : new ArrayList(); - private static final Integer API_CALL_DELAY = Platform.config.hasPath("auto_creator.api_call_delay") ? Platform.config.getInt("auto_creator.api_call_delay") : 2; - public static final List ALLOWED_CONTENT_STAGE = Platform.config.hasPath("auto_creator.allowed_content_stages") ? Arrays.asList(Platform.config.getString("auto_creator.allowed_content_stages").split(",")) : Arrays.asList("create", "upload", "review", "publish"); - private static ObjectMapper mapper = new ObjectMapper(); - private static Tika tika = new Tika(); - private static JobLogger LOGGER = new JobLogger(ContentUtil.class); - - - public Boolean validateMetadata(Map metadata) { - List reqFields = REQUIRED_METADATA_FIELDS.stream().filter(x -> null == metadata.get(x)).collect(Collectors.toList()); - return CollectionUtils.isEmpty(reqFields) ? true : false; - } - - public Boolean validateStage(String stage) { - return StringUtils.isNotBlank(stage) ? ALLOWED_CONTENT_STAGE.contains(stage) : true; - } - - public void process(String channelId, String identifier, Map edata) throws Exception { - String stage = (String) edata.getOrDefault(AutoCreatorParams.stage.name(), ""); - String repository = (String) edata.getOrDefault(AutoCreatorParams.repository.name(), ""); - Map metadata = (Map) edata.getOrDefault(AutoCreatorParams.metadata.name(), new HashMap()); - Map filteredMetadata = metadata.entrySet().stream().filter(x -> !METADATA_FIELDS_TO_BE_REMOVED.contains(x.getKey())).collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); - String mimeType = (String) metadata.getOrDefault(AutoCreatorParams.mimeType.name(), ""); - Integer delayUpload = StringUtils.equalsIgnoreCase(mimeType, "application/vnd.ekstep.h5p-archive") ? 6 * API_CALL_DELAY : API_CALL_DELAY; - List> collection = (List>) edata.getOrDefault(AutoCreatorParams.collection.name(), new ArrayList>()); - Map textbookInfo = (Map) edata.getOrDefault(AutoCreatorParams.textbookInfo.name(), new HashMap()); - String newIdentifier = (String) edata.get(AutoCreatorParams.identifier.name()); - LOGGER.info("ContentUtil :: process :: started processing for: " + identifier + " | Channel : " + channelId + " | Metadata : " + metadata+ " | collection :"+collection +" | textbookInfo : "+textbookInfo); - String contentStage = ""; - String internalId = ""; - Boolean isCreated = false; - Boolean isUploaded = false; - Boolean isReviewed = false; - Boolean isPublished = false; - Double pkgVersion = Double.parseDouble(String.valueOf(metadata.getOrDefault(AutoCreatorParams.pkgVersion.name(), "0.0"))); - Map createMetadata = filteredMetadata.entrySet().stream().filter(x -> CONTENT_CREATE_PROPS.contains(x.getKey())).collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); - Map updateMetadata = filteredMetadata.entrySet().stream().filter(x->!CONTENT_CREATE_PROPS.contains(x.getKey())).collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); - Map reqOriginData = (Map) edata.getOrDefault(AutoCreatorParams.originData.name(), new HashMap()); - String originId = (String) reqOriginData.getOrDefault(AutoCreatorParams.identifier.name(), ""); - if (MapUtils.isNotEmpty(reqOriginData) && StringUtils.isNotBlank(originId)) { - Map contentMetadata = getOriginContent(channelId, identifier); - if (MapUtils.isNotEmpty(contentMetadata)) { - internalId = originId; - contentStage = "na"; - } - } - - if (StringUtils.isBlank(contentStage)) { - Map contentMetadata = searchContent(identifier); - if (MapUtils.isEmpty(contentMetadata)) { - contentStage = "create"; - } else { - contentStage = getContentStage(identifier, pkgVersion, contentMetadata); - internalId = (String) contentMetadata.get("contentId"); - } - } - - try { - switch (contentStage) { - case "create": { - Map result = create(channelId, identifier, newIdentifier, repository, createMetadata); - internalId = (String) result.get(AutoCreatorParams.identifier.name()); - if (StringUtils.isNotBlank(internalId)) { - isCreated = true; - updateMetadata.put(AutoCreatorParams.versionKey.name(), (String) result.get(AutoCreatorParams.versionKey.name())); - } - } - case "update": { - if (!isCreated) { - Map readMetadata = read(channelId, internalId); - updateMetadata.put(AutoCreatorParams.versionKey.name(), (String) readMetadata.get(AutoCreatorParams.versionKey.name())); - } - update(channelId, internalId, updateMetadata); - if (StringUtils.equalsIgnoreCase("create", stage)) - break; - } - case "upload": { - isUploaded = upload(channelId, internalId, metadata); - if(StringUtils.equalsIgnoreCase("upload", stage)) - break; - delay(delayUpload); - } - case "review": { - isReviewed = review(channelId, internalId); - if(StringUtils.equalsIgnoreCase("review", stage)) - break; - delay(API_CALL_DELAY); - } - case "publish": { - isPublished = publish(channelId, internalId, (String) metadata.get(AutoCreatorParams.lastPublishedBy.name())); - break; - } - default: { - LOGGER.info("ContentUtil :: process :: Event Skipped for operations (create, upload, publish) for: " + identifier + " | Content Stage : " + contentStage); - } - } - }catch (Exception e) { - if(StringUtils.isNotBlank(internalId)) - updateStatus(channelId, internalId, e.getMessage()); - throw e; - } - - if(CollectionUtils.isNotEmpty(collection) && (isUploaded || isReviewed || isPublished || StringUtils.equalsIgnoreCase("na", contentStage))) { - linkCollection(channelId, identifier, collection, internalId); - } else if(MapUtils.isNotEmpty(textbookInfo) && (isUploaded || isReviewed || isPublished || StringUtils.equalsIgnoreCase("na", contentStage))) { - linkTextbook(channelId, identifier, textbookInfo, internalId); - }else { - LOGGER.info("ContentUtil :: process :: Textbook Linking Skipped because received empty collection/textbookInfo for : " + identifier); - } - LOGGER.info("ContentUtil :: process :: finished processing for: " + identifier); - } - - private void updateStatus(String channelId, String identifier, String message) throws Exception { - String errorMsg = StringUtils.isNotBlank(message) ? message : "Processing Error"; - String url = KP_LEARNING_BASE_URL + "/system/v3/content/update/" + identifier; - Map request = new HashMap() {{ - put("request", new HashMap() {{ - put("content", new HashMap() {{ - put(AutoCreatorParams.importError.name(), errorMsg); - put(AutoCreatorParams.status.name(), "Failed"); - }}); - }}); - }}; - Map header = new HashMap() {{ - put("X-Channel-Id", channelId); - put("Content-Type", DEFAULT_CONTENT_TYPE); - }}; - Response resp = UnirestUtil.patch(url, request, header); - if ((null != resp && resp.getResponseCode() == ResponseCode.OK) && MapUtils.isNotEmpty(resp.getResult())) { - String node_id = (String) resp.getResult().get("node_id"); - if (StringUtils.isNotBlank(node_id)) { - LOGGER.info("ContentUtil :: updateStatus :: Content failed status successfully updated for : " + identifier); - } - else - throw new ServerException(TaxonomyErrorCodes.SYSTEM_ERROR.name(), "Content update status Call Failed For : " + identifier); - } else { - LOGGER.info("ContentUtil :: updateStatus :: Invalid Response received while updating failed status for : " + identifier + getErrorDetails(resp)); - throw new ServerException("ERR_API_CALL", "Invalid Response received while updating content status for : " + identifier + getErrorDetails(resp)); - } - } - - private Map searchContent(String identifier) throws Exception { - Map result = new HashMap(); - Map header = new HashMap() {{ - put("Content-Type", DEFAULT_CONTENT_TYPE); - }}; - - Map request = new HashMap() {{ - put(AutoCreatorParams.request.name(), new HashMap() {{ - put(AutoCreatorParams.filters.name(), new HashMap() {{ - put(AutoCreatorParams.objectType.name(), "Content"); - put(AutoCreatorParams.status.name(), Arrays.asList()); - put(AutoCreatorParams.origin.name(), identifier); - }}); - put("exists", SEARCH_EXISTS_FIELDS); - put("fields", SEARCH_FIELDS); - }}); - }}; - Response resp = UnirestUtil.post(KP_SEARCH_URL, request, header); - if ((null != resp && resp.getResponseCode() == ResponseCode.OK)) { - if (MapUtils.isNotEmpty(resp.getResult()) && (Integer) resp.getResult().get(AutoCreatorParams.count.name()) > 0) { - List contents = (List) resp.getResult().get(AutoCreatorParams.content.name()); - contents.stream().map(obj -> (Map) obj).forEach(map -> { - String contentId = (String) map.get(AutoCreatorParams.identifier.name()); - Map originData = null; - try { - originData = mapper.readValue((String) map.get(AutoCreatorParams.originData.name()), Map.class); - } catch (IOException e) { - e.printStackTrace(); - } - if (MapUtils.isNotEmpty(originData)) { - String originId = (String) originData.get(AutoCreatorParams.identifier.name()); - String repository = (String) originData.get(AutoCreatorParams.repository.name()); - if (StringUtils.equalsIgnoreCase(identifier, originId) && StringUtils.isNotBlank(repository)) { - result.put("contentId", contentId); - result.put(AutoCreatorParams.status.name(), map.get(AutoCreatorParams.status.name())); - result.put(AutoCreatorParams.artifactUrl.name(), map.get(AutoCreatorParams.artifactUrl.name())); - result.put(AutoCreatorParams.pkgVersion.name(), map.get(AutoCreatorParams.pkgVersion.name())); - LOGGER.info("ContentUtil :: searchContent :: Internal Content Found with Identifier : " + contentId + " for :" + identifier + " | Result : " + result); - } - } else - LOGGER.info("Received empty originData for " + identifier); - }); - } else - LOGGER.info("ContentUtil :: searchContent :: Received 0 count while searching content for : " + identifier); - - } else { - LOGGER.info("ContentUtil :: searchContent :: Invalid Response received while searching content for : " + identifier + getErrorDetails(resp)); - throw new ServerException("ERR_API_CALL", "Invalid Response received while searching content for : " + identifier + getErrorDetails(resp)); - } - return result; - } - - private String getContentStage(String identifier, Double pkgVersion, Map metadata) { - String result = "na"; - String status = (String) metadata.get(AutoCreatorParams.status.name()); - String artifactUrl = (String) metadata.get(AutoCreatorParams.artifactUrl.name()); - Double pkgVer = 0.0; - try { - pkgVer = (Double) metadata.getOrDefault(AutoCreatorParams.pkgVersion.name(), 0.0); - } catch (ClassCastException ccex) { - pkgVer = Double.valueOf((Integer) metadata.getOrDefault(AutoCreatorParams.pkgVersion.name(), 0)); - } - if (!FINAL_STATUS.contains(status)) - result = StringUtils.isNotBlank(artifactUrl) ? "review" : "update"; - else if (pkgVersion > pkgVer) - result = "update"; - else - LOGGER.info("ContentUtil :: getContentStage :: Skipped Processing for : " + identifier + " | Internal Identifier : " + metadata.get("contentId") + " ,Status : " + status + " , artifactUrl : " + artifactUrl); - return result; - } - - - private Map create(String channelId, String identifier, String newIdentifier, String repository, Map metadata) throws Exception { - String contentId = ""; - String url = KP_CS_BASE_URL + "/content/v3/create"; - Map metaFields = new HashMap(); - metaFields.putAll(metadata); - if(StringUtils.isNotBlank(newIdentifier)) - metaFields.put(AutoCreatorParams.identifier.name(), newIdentifier); - else { - metaFields.put(AutoCreatorParams.identifier.name(), identifier); - metaFields.put(AutoCreatorParams.origin.name(), identifier); - metaFields.put(AutoCreatorParams.originData.name(), new HashMap(){{ - put(AutoCreatorParams.identifier.name(), identifier); - put(AutoCreatorParams.repository.name(), repository); - }}); - } - Map request = new HashMap() {{ - put("request", new HashMap() {{ - put("content", metaFields); - }}); - }}; - LOGGER.info("ContentUtil :: create :: create request : "+request); - Map header = new HashMap() {{ - put("X-Channel-Id", channelId); - put("Content-Type", DEFAULT_CONTENT_TYPE); - }}; - Response resp = UnirestUtil.post(url, request, header); - if ((null != resp && resp.getResponseCode() == ResponseCode.OK) && MapUtils.isNotEmpty(resp.getResult())) { - contentId = (String) resp.getResult().get("identifier"); - LOGGER.info("ContentUtil :: create :: Content Created Successfully with identifier : " + contentId); - } else { - LOGGER.info("ContentUtil :: create :: Invalid Response received while creating content for : " + identifier + getErrorDetails(resp)); - throw new ServerException(TaxonomyErrorCodes.SYSTEM_ERROR.name(), "Invalid Response received while creating content for : " + identifier+ getErrorDetails(resp)); - } - return resp.getResult(); - } - - private Map read(String channelId, String identifier) throws Exception { - String contentId = ""; - String url = KP_CS_BASE_URL + "/content/v3/read/" + identifier; - LOGGER.info("ContentUtil :: read :: Reading content having identifier : "+identifier); - Map header = new HashMap() {{ - put("X-Channel-Id", channelId); - put("Content-Type", DEFAULT_CONTENT_TYPE); - }}; - Response resp = UnirestUtil.get(url, "mode=edit", header); - if ((null != resp && resp.getResponseCode() == ResponseCode.OK) && MapUtils.isNotEmpty(resp.getResult())) { - contentId = ((String) ((Map)resp.getResult().getOrDefault("content", new HashMap())).getOrDefault("identifier", "")).replace(".img", ""); - if(StringUtils.equalsIgnoreCase(identifier, contentId)) - LOGGER.info("ContentUtil :: read :: Content Fetched Successfully with identifier : " + contentId); - else throw new ServerException(TaxonomyErrorCodes.SYSTEM_ERROR.name(), "Invalid Response received while reading content for : " + identifier); - } else { - LOGGER.info("ContentUtil :: read :: Invalid Response received while reading content for : " + identifier + getErrorDetails(resp)); - throw new ServerException(TaxonomyErrorCodes.SYSTEM_ERROR.name(), "Invalid Response received while reading content for : " + identifier + getErrorDetails(resp)); - } - return ((Map) resp.getResult().getOrDefault("content", new HashMap())); - } - - private void update(String channelId, String internalId, Map updateMetadata) throws Exception { - String url = KP_CS_BASE_URL + "/content/v3/update/" + internalId; - String appIconUrl = (String) updateMetadata.getOrDefault("appIcon", ""); - if(appIconUrl != null && !appIconUrl.trim().isEmpty()) { - LOGGER.info("ContentUtil :: update :: Initiating Icon download for : " + internalId + " | appIconUrl : " + appIconUrl); - File file = getFile(internalId, appIconUrl, "image"); - LOGGER.info("ContentUtil :: update :: Icon downloaded for : " + internalId + " | appIconUrl : " + appIconUrl); - if (null == file || !file.exists()) { - throw new ServerException(TaxonomyErrorCodes.SYSTEM_ERROR.name(), "Error Occurred while downloading appIcon file for " + internalId + " | File Url : " + appIconUrl); - } - String[] urls = uploadArtifact(file, internalId); - if (null != urls && StringUtils.isNotBlank(urls[1])) { - String appIconBlobUrl = urls[IDX_CLOUD_URL]; - LOGGER.info("ContentUtil :: update :: Icon Uploaded Successfully to cloud for : " + internalId + " | appIconUrl : " + appIconUrl + " | appIconBlobUrl : " + appIconBlobUrl); - updateMetadata.put("appIcon", appIconBlobUrl); - } - } - Map request = new HashMap() {{ - put("request", new HashMap() {{ - put("content", updateMetadata); - }}); - }}; - LOGGER.info("ContentUtil :: update :: update request : "+request); - Map header = new HashMap() {{ - put("X-Channel-Id", channelId); - put("Content-Type", DEFAULT_CONTENT_TYPE); - }}; - Response resp = UnirestUtil.patch(url, request, header); - if ((null != resp && resp.getResponseCode() == ResponseCode.OK) && MapUtils.isNotEmpty(resp.getResult())) { - String contentId = (String) resp.getResult().get("identifier"); - LOGGER.info("ContentUtil :: update :: Content Update Successfully having identifier : " + contentId); - } else { - LOGGER.info("ContentUtil :: update :: Invalid Response received while updating content for : " + internalId + getErrorDetails(resp)); - throw new ServerException(TaxonomyErrorCodes.SYSTEM_ERROR.name(), "Invalid Response received while updating content for : " + internalId + getErrorDetails(resp)); - } - } - - /*private void upload(String channelId, String identifier, File file) throws Exception { - if (null != file && !file.exists()) - LOGGER.info("ContentUtil :: upload :: File Path for " + identifier + "is : " + file.getAbsolutePath() + " | File Size : " + file.length()); - String preSignedUrl = getPreSignedUrl(identifier, file.getName()); - String fileUrl = preSignedUrl.split("\\?")[0]; - Boolean isUploaded = uploadBlob(identifier, preSignedUrl, file); - if (isUploaded) { - String url = KP_CS_BASE_URL + "/content/v3/upload/" + identifier; - Map header = new HashMap() {{ - put("X-Channel-Id", channelId); - }}; - Response resp = UnirestUtil.post(url, "fileUrl", fileUrl, header); - if ((null != resp && resp.getResponseCode() == ResponseCode.OK) && MapUtils.isNotEmpty(resp.getResult())) { - String artifactUrl = (String) resp.getResult().get(AutoCreatorParams.artifactUrl.name()); - if (StringUtils.isNotBlank(artifactUrl) && StringUtils.equalsIgnoreCase(fileUrl, artifactUrl)) - LOGGER.info("ContentUtil :: upload :: Content Uploaded Successfully for : " + identifier + " | artifactUrl : " + artifactUrl); - } else { - LOGGER.info("ContentUtil :: upload :: Invalid Response received while uploading for: " + identifier + getErrorDetails(resp)); - throw new ServerException(TaxonomyErrorCodes.SYSTEM_ERROR.name(), "Invalid Response received while uploading : " + identifier); - } - } else { - LOGGER.info("ContentUtil :: upload :: Blob upload failed for: " + identifier); - throw new ServerException(TaxonomyErrorCodes.SYSTEM_ERROR.name(), "Upload failed for: " + identifier); - } - }*/ - - private Boolean upload(String channelId, String identifier, Map metadata) throws Exception { - Response resp = null; - Long downloadStartTime = System.currentTimeMillis(); - String sourceUrl = (String) metadata.get(AutoCreatorParams.artifactUrl.name()); - String mimeType = (String) metadata.getOrDefault("mimeType", ""); - if (CollectionUtils.isNotEmpty(ALLOWED_ARTIFACT_SOURCE) && CollectionUtils.isEmpty(ALLOWED_ARTIFACT_SOURCE.stream().filter(x -> sourceUrl.contains(x)).collect(Collectors.toList()))) { - LOGGER.info("Artifact Source is not from allowed one for : " + identifier + " | artifactUrl: " + sourceUrl + " | Allowed Sources : " + ALLOWED_ARTIFACT_SOURCE); - throw new ServerException(TaxonomyErrorCodes.SYSTEM_ERROR.name(), "Artifact Source is not from allowed one for : " + identifier + " | artifactUrl: " + sourceUrl + " | Allowed Sources : " + ALLOWED_ARTIFACT_SOURCE); - } - File file = getFile(identifier, sourceUrl, mimeType); - Long downloadEndTime = System.currentTimeMillis(); - LOGGER.info("ContentUtil :: upload :: Total time taken for download: " + (downloadEndTime - downloadStartTime)); - if (null == file || !file.exists()) { - throw new ServerException(TaxonomyErrorCodes.SYSTEM_ERROR.name(), "Error Occurred while downloading file for " + identifier + " | File Url : "+sourceUrl); - } - LOGGER.info("ContentUtil :: upload :: File Path for " + identifier + "is : " + file.getAbsolutePath() + " | File Size : " + file.length()); - Long size = FileUtils.sizeOf(file); - LOGGER.info("ContentUtil :: upload :: file size (MB): " + (size / 1048576)); - String url = KP_CS_BASE_URL + "/content/v3/upload/" + identifier + "?validation=false"; - if (StringUtils.isNotBlank(mimeType) && (StringUtils.equalsIgnoreCase("application/vnd.ekstep.h5p-archive", mimeType) && !StringUtils.equalsIgnoreCase("h5p", FilenameUtils.getExtension(file.getAbsolutePath())))) - url = url + "&fileFormat=composed-h5p-zip"; - LOGGER.info("Upload API URL : " + url); - Map header = new HashMap() {{ - put("X-Channel-Id", channelId); - }}; - - if (size > CONTENT_UPLOAD_ARTIFACT_MAX_SIZE && !BULK_UPLOAD_MIMETYPES.contains(mimeType)) { - LOGGER.info("ContentUtil :: upload :: File Size is larger than allowed file size allowed in upload api for : " + identifier + " | File Size (MB): " + (size / 1048576) + " | mimeType : " + mimeType); - throw new ServerException(TaxonomyErrorCodes.SYSTEM_ERROR.name(), "File Size is larger than allowed file size allowed in upload api for : " + identifier + " | File Size (MB): " + (size / 1048576) + " | mimeType : " + mimeType); - } - - Long uploadStartTime = System.currentTimeMillis(); - String[] urls = uploadArtifact(file, identifier); - Long uploadEndTime = System.currentTimeMillis(); - LOGGER.info("ContentUtil :: upload :: Total time taken for upload: " + (uploadEndTime - uploadStartTime)); - if (null != urls && StringUtils.isNotBlank(urls[1])) { - String uploadUrl = urls[IDX_CLOUD_URL]; - LOGGER.info("ContentUtil :: upload :: Artifact Uploaded Successfully to cloud for : " + identifier + " | uploadUrl : " + uploadUrl); - resp = UnirestUtil.post(url, "fileUrl", uploadUrl, header); - } - - if ((null != resp && resp.getResponseCode() == ResponseCode.OK) && MapUtils.isNotEmpty(resp.getResult())) { - String artifactUrl = (String) resp.getResult().get(AutoCreatorParams.artifactUrl.name()); - if (StringUtils.isNotBlank(artifactUrl)) { - LOGGER.info("ContentUtil :: upload :: Content Uploaded Successfully for : " + identifier + " | artifactUrl : " + artifactUrl); - return true; - } - } else { - LOGGER.info("ContentUtil :: upload :: Invalid Response received while uploading for: " + identifier + getErrorDetails(resp)); - throw new ServerException(TaxonomyErrorCodes.SYSTEM_ERROR.name(), "Invalid Response received while uploading : " + identifier + getErrorDetails(resp)); - } - return false; - } - - private Boolean review(String channelId, String identifier) throws Exception { - String url = KP_LEARNING_BASE_URL + "/content/v3/review/" + identifier; - Map request = new HashMap() {{ - put("request", new HashMap() {{ - put("content", new HashMap()); - }}); - }}; - - Map header = new HashMap() {{ - put("X-Channel-Id", channelId); - put("Content-Type", DEFAULT_CONTENT_TYPE); - }}; - Response resp = UnirestUtil.post(url, request, header); - if ((null != resp && resp.getResponseCode() == ResponseCode.OK) && MapUtils.isNotEmpty(resp.getResult())) { - String contentId = (String) resp.getResult().get("node_id"); - if(StringUtils.isNotBlank(contentId)) { - LOGGER.info("ContentUtil :: review :: Content Sent For Review Successfully having identifier : " + contentId); - return true; - } - } else { - LOGGER.info("ContentUtil :: review :: Invalid Response received while sending content to review for : " + identifier + getErrorDetails(resp)); - throw new ServerException(TaxonomyErrorCodes.SYSTEM_ERROR.name(), "Invalid Response received while sending content to review for : " + identifier + getErrorDetails(resp)); - } - return false; - } - - private Boolean publish(String channelId, String identifier, String lastPublishedBy) throws Exception { - String url = KP_LEARNING_BASE_URL + "/content/v3/publish/" + identifier; - Map request = new HashMap() {{ - put("request", new HashMap() {{ - put("content", new HashMap() {{ - put(AutoCreatorParams.lastPublishedBy.name(), lastPublishedBy); - }}); - }}); - }}; - Map header = new HashMap() {{ - put("X-Channel-Id", channelId); - put("Content-Type", DEFAULT_CONTENT_TYPE); - }}; - Response resp = UnirestUtil.post(url, request, header); - if ((null != resp && resp.getResponseCode() == ResponseCode.OK) && MapUtils.isNotEmpty(resp.getResult())) { - String publishStatus = (String) resp.getResult().get("publishStatus"); - if (StringUtils.isNotBlank(publishStatus)) { - LOGGER.info("ContentUtil :: publish :: Content sent for publish successfully for : " + identifier); - return true; - } - else - throw new ServerException(TaxonomyErrorCodes.SYSTEM_ERROR.name(), "Content Publish Call Failed For : " + identifier); - } else { - LOGGER.info("ContentUtil :: publish :: Invalid Response received while publishing content for : " + identifier + getErrorDetails(resp)); - throw new ServerException(TaxonomyErrorCodes.SYSTEM_ERROR.name(), "Invalid Response received while publishing content for : " + identifier + getErrorDetails(resp)); - } - - } - - private String getPreSignedUrl(String identifier, String fileName) throws Exception { - String preSignedUrl = ""; - Map request = new HashMap(){{ - put("request", new HashMap(){{ - put("content", new HashMap(){{ - put("fileName", fileName); - }}); - }}); - }}; - Map header = new HashMap(){{ - put("Content-Type","application/json"); - }}; - String url = KP_CS_BASE_URL + "/content/v3/upload/url/" + identifier; - Response resp = UnirestUtil.post(url, request, header); - if ((null != resp && resp.getResponseCode() == ResponseCode.OK) && MapUtils.isNotEmpty(resp.getResult())) { - preSignedUrl = (String) resp.getResult().get("pre_signed_url"); - return preSignedUrl; - } else { - LOGGER.info("ContentUtil :: getPreSignedUrl :: Invalid Response received while generating pre-signed url for : " + identifier + getErrorDetails(resp)); - throw new ServerException(TaxonomyErrorCodes.SYSTEM_ERROR.name(), "Invalid Response received while generating pre-signed url for : " + identifier + getErrorDetails(resp)); - } - } - - private Boolean uploadBlob(String identifier, String url, File file) throws Exception { - Boolean result = false; - String contentType = tika.detect(file); - LOGGER.info("contentType of file : "+contentType); - Map header = new HashMap(){{ - put("x-ms-blob-type", "BlockBlob"); - put("Content-Type", contentType); - }}; - HttpResponse response = Unirest.put(url).headers(header).field("file", new File(file.getAbsolutePath())).asString(); - if (null != response && response.getStatus()==201) { - result = true; - } else { - LOGGER.info("ContentUtil :: uploadBlob :: Invalid Response received while uploading file to blob store for : " + identifier + " | Response Code : " + response.getStatus()); - throw new ServerException(TaxonomyErrorCodes.SYSTEM_ERROR.name(), "Invalid Response received while uploading file to blob store for : " + identifier); - } - return result; - } - - private void linkCollection(String channel, String eventObjectId, List> collection, String resourceId) throws Exception { - for (Map textbookInfo : collection) { - String textbookId = (String) textbookInfo.getOrDefault(AutoCreatorParams.identifier.name(), ""); - String unitId = (String) textbookInfo.getOrDefault(AutoCreatorParams.unitId.name(), ""); - if (StringUtils.isNotBlank(textbookId) && StringUtils.isNotEmpty(unitId)) { - Map rootHierarchy = null; - rootHierarchy = getHierarchy(textbookId); - if (validateHierarchy(textbookId, rootHierarchy, unitId)) { - Map hierarchyReq = new HashMap() {{ - put(AutoCreatorParams.request.name(), new HashMap() {{ - put(AutoCreatorParams.rootId.name(), textbookId); - put(AutoCreatorParams.unitId.name(), unitId); - put(AutoCreatorParams.children.name(), Arrays.asList(resourceId)); - }}); - }}; - addToHierarchy(channel, textbookId, hierarchyReq); - } else { - LOGGER.info("ContentUtil :: linkCollection :: Hierarchy Validation Failed For : " + textbookId); - } - } else { - LOGGER.info("ContentUtil :: linkCollection :: Collection Linking Skipped because required data is not available for : " + eventObjectId); - } - } - } - - //TODO: Remove this method in release-3.3.0, Added only for backward compatibility - private void linkTextbook(String channel, String eventObjectId, Map textbookInfo, String resourceId) throws Exception { - String textbookId = (String) textbookInfo.getOrDefault(AutoCreatorParams.identifier.name(), ""); - List unitIdentifiers = (List) textbookInfo.getOrDefault(AutoCreatorParams.unitIdentifiers.name(), new ArrayList()); - if (StringUtils.isNotBlank(textbookId) && CollectionUtils.isNotEmpty(unitIdentifiers)) { - Map rootHierarchy = getHierarchy(textbookId); - List childNodes = (List) rootHierarchy.getOrDefault(AutoCreatorParams.childNodes.name(), new ArrayList()); - if (CollectionUtils.isNotEmpty(childNodes) && childNodes.containsAll(unitIdentifiers)) { - Map hierarchyReq = new HashMap() {{ - put(AutoCreatorParams.request.name(), new HashMap() {{ - put(AutoCreatorParams.rootId.name(), textbookId); - put(AutoCreatorParams.unitId.name(), unitIdentifiers.get(0)); - put(AutoCreatorParams.children.name(), Arrays.asList(resourceId)); - }}); - }}; - addToHierarchy(channel, textbookId, hierarchyReq); - } else { - LOGGER.info("ContentUtil :: linkTextbook :: Hierarchy Validation Failed For : " + textbookId); - } - } else { - LOGGER.info("ContentUtil :: linkTextbook :: Textbook Linking Skipped because required data is not available for : " + eventObjectId); - } - } - - private boolean validateHierarchy(String textbookId, Map rootHierarchy, String unitId) { - List childNodes = (List) rootHierarchy.getOrDefault(AutoCreatorParams.childNodes.name(), new ArrayList()); - if (CollectionUtils.isNotEmpty(childNodes) && childNodes.contains(unitId)) { - return true; - } else { - LOGGER.info("ContentUtil :: validateHierarchy :: Unit Identifier is not found under : " + textbookId); - throw new ServerException(TaxonomyErrorCodes.SYSTEM_ERROR.name(), "Unit Identifier is not found under : " + textbookId); - } - } - - private Boolean addToHierarchy(String channel, String textbookId, Map hierarchyReq) throws Exception { - Boolean result = false; - String url = KP_CS_BASE_URL + "/content/v3/hierarchy/add"; - Map header = new HashMap() {{ - put("X-Channel-Id", channel); - put("Content-Type", DEFAULT_CONTENT_TYPE); - }}; - Response resp = UnirestUtil.patch(url, hierarchyReq, header); - if ((null != resp && resp.getResponseCode() == ResponseCode.OK) && MapUtils.isNotEmpty(resp.getResult())) { - String contentId = (String) resp.getResult().get("rootId"); - if (StringUtils.equalsIgnoreCase(contentId, textbookId)) { - LOGGER.info("ContentUtil :: addToHierarchy :: Content Hierarchy Updated Successfully for: " + textbookId); - result = true; - } - } else { - LOGGER.info("ContentUtil :: updateHierarchy :: Invalid Response received while adding resource to hierarchy for : " + textbookId + getErrorDetails(resp)); - throw new ServerException(TaxonomyErrorCodes.SYSTEM_ERROR.name(), "Invalid Response received while adding resource to hierarchy for : " + textbookId + getErrorDetails(resp)); - } - return result; - } - - private Map getHierarchy(String identifier) throws Exception { - Map result = new HashMap(); - String url = KP_CS_BASE_URL + "/content/v3/hierarchy/" + identifier; - Map header = new HashMap(){{ - put("Content-Type", DEFAULT_CONTENT_TYPE); - }}; - Response resp = UnirestUtil.get(url, "mode=edit", header); - if ((null != resp && resp.getResponseCode() == ResponseCode.OK) && MapUtils.isNotEmpty(resp.getResult())) { - result = (Map) resp.getResult().getOrDefault("content", new HashMap()); - return result; - } else { - LOGGER.info("ContentUtil :: getHierarchy :: Invalid Response received while fetching hierarchy for : " + identifier + getErrorDetails(resp)); - throw new ServerException(TaxonomyErrorCodes.SYSTEM_ERROR.name(), "Invalid Response received while fetching hierarchy for : " + identifier + getErrorDetails(resp)); - } - } - - private Map getOriginContent(String channelId, String identifier) throws Exception { - String contentId = ""; - String url = KP_CS_BASE_URL + "/content/v3/read/" + identifier; - LOGGER.info("ContentUtil :: getOriginContent :: Reading origin content having identifier : " + identifier); - Map header = new HashMap() {{ - put("X-Channel-Id", channelId); - put("Content-Type", DEFAULT_CONTENT_TYPE); - }}; - Response resp = UnirestUtil.get(url, "mode=edit", header); - if ((null != resp && resp.getResponseCode() == ResponseCode.OK) && MapUtils.isNotEmpty(resp.getResult())) { - contentId = ((String) ((Map) resp.getResult().getOrDefault("content", new HashMap())).getOrDefault("identifier", "")).replace(".img", ""); - if (StringUtils.equalsIgnoreCase(identifier, contentId)) { - LOGGER.info("ContentUtil :: getOriginContent :: Origin Content Fetched Successfully with identifier : " + contentId); - return ((Map) resp.getResult().getOrDefault("content", new HashMap())); - } else - throw new ServerException("ERR_API_CALL", "Identifier Mismatched while reading content for : " + identifier); - } else if (null != resp && resp.getResponseCode() == ResponseCode.RESOURCE_NOT_FOUND) { - LOGGER.info("ContentUtil :: getOriginContent :: Origin Content Not Found With Identifier : " + identifier + getErrorDetails(resp)); - } else - throw new ServerException("ERR_API_CALL", "ContentUtil :: getOriginContent :: Invalid Response Received While Reading Origin Content With Identifier : " + identifier + getErrorDetails(resp)); - return new HashMap(); - } - - private String getBasePath(String objectId) { - return StringUtils.isNotBlank(objectId) ? TEMP_FILE_LOCATION + File.separator + objectId + File.separator + "_temp_" + System.currentTimeMillis(): TEMP_FILE_LOCATION + File.separator + "_temp_" + System.currentTimeMillis(); - } - - private String getFileNameFromURL(String fileUrl) { - String fileName = FilenameUtils.getBaseName(fileUrl) + "_" + System.currentTimeMillis(); - if (!FilenameUtils.getExtension(fileUrl).isEmpty()) - fileName += "." + FilenameUtils.getExtension(fileUrl); - return fileName; - } - - private File getFile(String identifier, String fileUrl, String mimeType) throws Exception { - File file = null; - try { - if (StringUtils.isNotBlank(fileUrl) && fileUrl.contains("drive.google.com")) { - String fileId = fileUrl.split("download&id=")[1]; - if(StringUtils.isBlank(fileId)) - throw new ServerException(TaxonomyErrorCodes.ERR_INVALID_UPLOAD_FILE_URL.name(), "Invalid fileUrl received for : " + identifier + " | fileUrl : " + fileUrl); - while (null == file && GoogleDriveUtil.BACKOFF_DELAY <= GoogleDriveUtil.MAXIMUM_BACKOFF_DELAY) { - file = GoogleDriveUtil.downloadFile(fileId, getBasePath(identifier), mimeType); - } - } else { - file = HttpDownloadUtility.downloadFile(fileUrl, getBasePath(identifier)); - } - return file; - } catch (Exception e) { - if(e instanceof ServerException) - throw e; - else { - LOGGER.info("Invalid fileUrl received for : " + identifier + " | fileUrl : " + fileUrl + "Exception is : " + e.getMessage()); - throw new ServerException(TaxonomyErrorCodes.ERR_INVALID_UPLOAD_FILE_URL.name(), "Invalid fileUrl received for : " + identifier + " | fileUrl : " + fileUrl); - } - } - } - - private String[] uploadArtifact(File uploadedFile, String identifier) { - String[] urlArray = new String[] {}; - try { - String folder = S3PropertyReader.getProperty(CONTENT_FOLDER); - folder = folder + "/" + Slug.makeSlug(identifier, true) + "/" + S3PropertyReader.getProperty(ARTEFACT_FOLDER); - urlArray = CloudStore.uploadFile(folder, uploadedFile, true); - } catch (Exception e) { - LOGGER.info("ContentUtil :: uploadArtifact :: Exception occurred while uploading artifact for : " + identifier + "Exception is : " + e.getMessage()); - e.printStackTrace(); - throw new ServerException(ContentErrorCodes.ERR_CONTENT_UPLOAD_FILE.name(), - "Error while uploading the File.", e); - } - return urlArray; - } - - private void delay(long time) { - try { - Thread.sleep(time * 1000); - } catch (Exception e) { - - } - } - - private static String getErrorDetails(Response resp) { - return (null != resp) ? (" | Response Code :" + resp.getResponseCode().toString() + " | Result : " + resp.getResult() + " | Error Message : " + resp.getParams().getErrmsg()) : " | Null Response Received."; - } - -} diff --git a/platform-jobs/samza/auto-creator/src/main/java/org/sunbird/jobs/samza/util/GoogleDriveUtil.java b/platform-jobs/samza/auto-creator/src/main/java/org/sunbird/jobs/samza/util/GoogleDriveUtil.java deleted file mode 100644 index 9e9a5fc3aa..0000000000 --- a/platform-jobs/samza/auto-creator/src/main/java/org/sunbird/jobs/samza/util/GoogleDriveUtil.java +++ /dev/null @@ -1,156 +0,0 @@ -package org.sunbird.jobs.samza.util; - -import com.google.api.client.auth.oauth2.Credential; -import com.google.api.client.googleapis.auth.oauth2.GoogleCredential; -import com.google.api.client.googleapis.javanet.GoogleNetHttpTransport; -import com.google.api.client.googleapis.json.GoogleJsonResponseException; -import com.google.api.client.googleapis.services.GoogleClientRequestInitializer; -import com.google.api.client.http.HttpResponseException; -import com.google.api.client.http.HttpTransport; -import com.google.api.client.json.JsonFactory; -import com.google.api.client.json.jackson2.JacksonFactory; -import com.google.api.services.drive.Drive; -import com.google.api.services.drive.DriveRequestInitializer; -import com.google.api.services.drive.DriveScopes; -import org.apache.commons.lang.StringUtils; -import org.sunbird.common.Platform; -import org.sunbird.common.Slug; -import org.sunbird.common.enums.TaxonomyErrorCodes; -import org.sunbird.common.exception.ServerException; - -import java.io.ByteArrayInputStream; -import java.io.File; -import java.io.FileOutputStream; -import java.io.InputStream; -import java.io.OutputStream; -import java.nio.charset.Charset; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; - -public class GoogleDriveUtil { - - private static final JsonFactory JSON_FACTORY = new JacksonFactory(); - private static final String ERR_MSG = "Please Provide Valid Google Drive URL!"; - private static final String SERVICE_ERROR = "Unable to Connect to Google Service. Please Try Again After Sometime!"; - private static final List errorCodes = Arrays.asList("dailyLimitExceeded402", "limitExceeded", - "dailyLimitExceeded", "quotaExceeded", "userRateLimitExceeded", "quotaExceeded402", "keyExpired", - "keyInvalid"); - private static final List SCOPES = Arrays.asList(DriveScopes.DRIVE_READONLY); - private static final String APP_NAME = Platform.config.hasPath("auto_creator.gdrive.application_name") ? Platform.config.getString("auto_creator.gdrive.application_name") : "drive-download-sunbird"; - private static final String SERVICE_ACC_CRED = Platform.config.getString("auto_creator_g_service_acct_cred"); - public static final Integer INITIAL_BACKOFF_DELAY = Platform.config.hasPath("auto_creator.initial_backoff_delay") ? Platform.config.getInt("auto_creator.initial_backoff_delay") : 1200000; // 20 min - public static final Integer MAXIMUM_BACKOFF_DELAY = Platform.config.hasPath("auto_creator.maximum_backoff_delay") ? Platform.config.getInt("auto_creator.maximum_backoff_delay") : 3900000; // 65 min - public static final Integer INCREMENT_BACKOFF_DELAY = Platform.config.hasPath("auto_creator.increment_backoff_delay") ? Platform.config.getInt("auto_creator.increment_backoff_delay") : 300000; // 5 min - public static Integer BACKOFF_DELAY = INITIAL_BACKOFF_DELAY; - private static boolean limitExceeded = false; - private static Drive drive = null; - private static JobLogger LOGGER = new JobLogger(GoogleDriveUtil.class); - - static { - try { - HttpTransport HTTP_TRANSPORT = GoogleNetHttpTransport.newTrustedTransport(); - drive = new Drive.Builder(HTTP_TRANSPORT, JSON_FACTORY, getCredentials()).setApplicationName(APP_NAME).build(); - } catch (Exception e) { - LOGGER.error("Error occurred while creating google drive client ::: " + e.getMessage(), e); - e.printStackTrace(); - throw new ServerException(TaxonomyErrorCodes.SYSTEM_ERROR.name(), "Error occurred while creating google drive client ::: "+ e.getMessage()); - } - } - - private static Credential getCredentials() throws Exception { - InputStream credentialsStream = new ByteArrayInputStream(SERVICE_ACC_CRED.getBytes(Charset.forName("UTF-8"))); - GoogleCredential credential = GoogleCredential.fromStream(credentialsStream).createScoped(SCOPES); - return credential; - } - - public static File downloadFile(String fileId, String saveDir, String mimeType) throws Exception { - try { - Drive.Files.Get getFile = drive.files().get(fileId); - getFile.setFields("id,name,size,owners,mimeType,properties,permissionIds,webContentLink"); - com.google.api.services.drive.model.File googleDriveFile = getFile.execute(); - LOGGER.info("GoogleDriveUtil :: downloadFile ::: Drive File Details:: " + googleDriveFile); - String fileName = googleDriveFile.getName(); - String fileMimeType = googleDriveFile.getMimeType(); - LOGGER.info("GoogleDriveUtil :: downloadFile ::: Node mimeType :: "+mimeType + " | File mimeType :: "+fileMimeType); - if(!StringUtils.equalsIgnoreCase(mimeType,"image")) - validateMimeType(fileId, mimeType, fileMimeType); - File saveFile = new File(saveDir); - if (!saveFile.exists()) { - saveFile.mkdirs(); - } - String saveFilePath = saveDir + File.separator + fileName; - LOGGER.info("GoogleDriveUtil :: downloadFile :: File Id :" + fileId + " | Save File Path: " + saveFilePath); - - OutputStream outputStream = new FileOutputStream(saveFilePath); - getFile.executeMediaAndDownloadTo(outputStream); - outputStream.close(); - File file = new File(saveFilePath); - file = Slug.createSlugFile(file); - LOGGER.info("GoogleDriveUtil :: downloadFile :: File Downloaded Successfully. Sluggified File Name: " + file.getAbsolutePath()); - if (null != file && BACKOFF_DELAY != INITIAL_BACKOFF_DELAY) - BACKOFF_DELAY = INITIAL_BACKOFF_DELAY; - return file; - } catch(GoogleJsonResponseException ge) { - LOGGER.error("GoogleDriveUtil :: downloadFile :: GoogleJsonResponseException :: Error Occurred while downloading file having id "+fileId + " | Error is ::"+ge.getDetails().toString(), ge); - throw new ServerException(TaxonomyErrorCodes.ERR_INVALID_UPLOAD_FILE_URL.name(), "Invalid Response Received From Google API for file Id : " + fileId + " | Error is : " + ge.getDetails().toString()); - } catch(HttpResponseException he) { - LOGGER.error("GoogleDriveUtil :: downloadFile :: HttpResponseException :: Error Occurred while downloading file having id "+fileId + " | Error is ::"+he.getContent(), he); - he.printStackTrace(); - if(he.getStatusCode() == 403) { - if (BACKOFF_DELAY <= MAXIMUM_BACKOFF_DELAY) - delay(BACKOFF_DELAY); - if (BACKOFF_DELAY == 2400000) - BACKOFF_DELAY += 1500000; - else - BACKOFF_DELAY = BACKOFF_DELAY * INCREMENT_BACKOFF_DELAY; - } else throw new ServerException(TaxonomyErrorCodes.ERR_INVALID_UPLOAD_FILE_URL.name(), "Invalid Response Received From Google API for file Id : " + fileId + " | Error is : " + he.getContent()); - } catch (Exception e) { - LOGGER.error("GoogleDriveUtil :: downloadFile :: Exception :: Error Occurred While Downloading Google Drive File having Id " + fileId + " : " + e.getMessage(), e); - e.printStackTrace(); - if(e instanceof ServerException) - throw e; - else throw new ServerException(TaxonomyErrorCodes.ERR_INVALID_UPLOAD_FILE_URL.name(), "Invalid Response Received From Google API for file Id : " + fileId + " | Error is : " + e.getMessage()); - } - return null; - } - - private static void validateMimeType(String fileId, String mimeType, String fileMimeType) { - String errMsg = "Invalid File Url! File MimeType Is Not Same As Object MimeType for File Id : " + fileId + " | File MimeType is : " +fileMimeType + " | Node MimeType is : "+mimeType; - switch (mimeType){ - case "application/vnd.ekstep.h5p-archive" : { - if(!(StringUtils.equalsIgnoreCase("application/x-zip", fileMimeType) || StringUtils.equalsIgnoreCase("application/zip", fileMimeType))) - throw new ServerException(TaxonomyErrorCodes.ERR_INVALID_UPLOAD_FILE_URL.name(), errMsg); - break; - } - case "application/epub" : { - if(!StringUtils.equalsIgnoreCase("application/epub+zip", fileMimeType)) - throw new ServerException(TaxonomyErrorCodes.ERR_INVALID_UPLOAD_FILE_URL.name(), errMsg); - break; - } - case "audio/mp3" : { - if(!StringUtils.equalsIgnoreCase("audio/mpeg", fileMimeType)) - throw new ServerException(TaxonomyErrorCodes.ERR_INVALID_UPLOAD_FILE_URL.name(), errMsg); - break; - } - case "application/vnd.ekstep.html-archive" : { - if(!StringUtils.equalsIgnoreCase("application/x-zip-compressed", fileMimeType)) - throw new ServerException(TaxonomyErrorCodes.ERR_INVALID_UPLOAD_FILE_URL.name(), errMsg); - break; - } - default: { - if(!StringUtils.equalsIgnoreCase(mimeType, fileMimeType)) - throw new ServerException(TaxonomyErrorCodes.ERR_INVALID_UPLOAD_FILE_URL.name(), errMsg); - } - } - } - - public static void delay(int time) { - LOGGER.info("delay is called with : " + time); - try { - Thread.sleep(time); - } catch (Exception e) { - - } - } -} diff --git a/platform-jobs/samza/auto-creator/src/main/java/org/sunbird/jobs/samza/util/UnirestUtil.java b/platform-jobs/samza/auto-creator/src/main/java/org/sunbird/jobs/samza/util/UnirestUtil.java deleted file mode 100644 index 6219df79b5..0000000000 --- a/platform-jobs/samza/auto-creator/src/main/java/org/sunbird/jobs/samza/util/UnirestUtil.java +++ /dev/null @@ -1,148 +0,0 @@ -package org.sunbird.jobs.samza.util; - -import com.fasterxml.jackson.databind.ObjectMapper; -import kong.unirest.HttpResponse; -import kong.unirest.Unirest; -import org.apache.commons.collections4.MapUtils; -import org.apache.commons.lang3.StringUtils; -import org.sunbird.common.Platform; -import org.sunbird.common.dto.Response; -import org.sunbird.common.enums.TaxonomyErrorCodes; -import org.sunbird.common.exception.ServerException; - -import java.io.File; -import java.util.Map; - -public class UnirestUtil { - - private static ObjectMapper mapper = new ObjectMapper(); - private static JobLogger LOGGER = new JobLogger(UnirestUtil.class); - public static final Long INITIAL_BACKOFF_DELAY = Platform.config.hasPath("auto_creator.internal_api.initial_backoff_delay") ? Platform.config.getLong("auto_creator.internal_api.initial_backoff_delay") : 10000; // 10 seconds - public static final Long MAXIMUM_BACKOFF_DELAY = Platform.config.hasPath("auto_creator.internal_api.maximum_backoff_delay") ? Platform.config.getLong("auto_creator.internal_api.initial_backoff_delay") : 300000; // 5 min - public static final Integer INCREMENT_BACKOFF_DELAY = Platform.config.hasPath("auto_creator.increment_backoff_delay") ? Platform.config.getInt("auto_creator.increment_backoff_delay") : 2; - public static Long BACKOFF_DELAY = INITIAL_BACKOFF_DELAY; - - public static Response post(String url, Map requestMap, Map headerParam) - throws Exception { - Response resp = null; - validateRequest(url, headerParam); - if (MapUtils.isEmpty(requestMap)) - throw new ServerException("ERR_INVALID_REQUEST_BODY", "Request Body is Missing!"); - try { - while (null == resp) { - HttpResponse response = Unirest.post(url).headers(headerParam).body(mapper.writeValueAsString(requestMap)).asString(); - resp = getResponse(url, response); - } - return resp; - } catch (Exception e) { - throw new ServerException("ERR_API_CALL", "Something Went Wrong While Making API Call | Error is: " + e.getMessage()); - } - } - - public static Response patch(String url, Map requestMap, Map headerParam) - throws Exception { - Response resp = null; - validateRequest(url, headerParam); - if (MapUtils.isEmpty(requestMap)) - throw new ServerException("ERR_INVALID_REQUEST_BODY", "Request Body is Missing!"); - try { - while (null == resp) { - HttpResponse response = Unirest.patch(url).headers(headerParam).body(mapper.writeValueAsString(requestMap)).asString(); - resp = getResponse(url, response); - } - return resp; - } catch (Exception e) { - throw new ServerException("ERR_API_CALL", "Something Went Wrong While Making API Call | Error is: " + e.getMessage()); - } - } - - public static Response post(String url, String paramName, File value, Map headerParam) - throws Exception { - Response resp = null; - validateRequest(url, headerParam); - if (null == value || null == value) - throw new ServerException("ERR_INVALID_REQUEST_PARAM", "Invalid Request Param!"); - try { - while (null == resp) { - HttpResponse response = Unirest.post(url).headers(headerParam).multiPartContent().field(paramName, new File(value.getAbsolutePath())).asString(); - resp = getResponse(url, response); - } - return resp; - } catch (Exception e) { - throw new ServerException("ERR_API_CALL", "Something Went Wrong While Making API Call | Error is: " + e.getMessage()); - } - } - - public static Response post(String url, String paramName, String value, Map headerParam) - throws Exception { - Response resp = null; - validateRequest(url, headerParam); - if (null == value || null == value) - throw new ServerException("ERR_INVALID_REQUEST_PARAM", "Invalid Request Param!"); - try { - while (null == resp) { - HttpResponse response = Unirest.post(url).headers(headerParam).multiPartContent().field(paramName, value).asString(); - resp = getResponse(url, response); - } - return resp; - } catch (Exception e) { - throw new ServerException("ERR_API_CALL", "Something Went Wrong While Making API Call | Error is: " + e.getMessage()); - } - } - - public static Response get(String url, String queryParam, Map headerParam) - throws Exception { - Response resp = null; - validateRequest(url, headerParam); - String reqUrl = StringUtils.isNotBlank(queryParam) ? url + "?" + queryParam : url; - try { - while (null == resp) { - HttpResponse response = Unirest.get(reqUrl).headers(headerParam).asString(); - resp = getResponse(reqUrl, response); - } - return resp; - } catch (Exception e) { - throw new ServerException("ERR_API_CALL", "Something Went Wrong While Making API Call | Error is: " + e.getMessage()); - } - } - - private static void validateRequest(String url, Map headerParam) { - if (StringUtils.isBlank(url)) - throw new ServerException("ERR_INVALID_URL", "Url Parameter is Missing!"); - if (null == headerParam) - throw new ServerException("ERR_INVALID_HEADER_PARAM", "Header Parameter is Missing!"); - } - - private static Response getResponse(String url, HttpResponse response) { - Response resp = null; - if (null != response && StringUtils.isNotBlank(response.getBody())) { - try { - resp = mapper.readValue(response.getBody(), Response.class); - BACKOFF_DELAY = INITIAL_BACKOFF_DELAY; - } catch (Exception e) { - LOGGER.error("UnirestUtil ::: getResponse ::: Error occurred while parsing api response for url ::: " + url + ". | Error is: " + e.getMessage(), e); - LOGGER.info("UnirestUtil :::: BACKOFF_DELAY ::: " + BACKOFF_DELAY); - if (BACKOFF_DELAY <= MAXIMUM_BACKOFF_DELAY) { - long delay = BACKOFF_DELAY; - BACKOFF_DELAY = BACKOFF_DELAY * INCREMENT_BACKOFF_DELAY; - LOGGER.info("UnirestUtil :::: BACKOFF_DELAY after increment::: " + BACKOFF_DELAY); - delay(delay); - } else throw new ServerException("ERR_API_CALL", "Unable to parse response data for url: "+ url +" | Error is: " + e.getMessage()); - } - } else { - LOGGER.info("Null Response Received While Making Api Call!"); - throw new ServerException("ERR_API_CALL", "Null Response Received While Making Api Call!"); - } - return resp; - } - - private static void delay(long time) { - LOGGER.info("UnirestUtil :::: backoff delay is called with : " + time); - try { - Thread.sleep(time); - } catch (Exception e) { - - } - } - -} diff --git a/platform-jobs/samza/auto-creator/src/main/resources/actor-config.xml b/platform-jobs/samza/auto-creator/src/main/resources/actor-config.xml deleted file mode 100644 index f349225f43..0000000000 --- a/platform-jobs/samza/auto-creator/src/main/resources/actor-config.xml +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/platform-jobs/samza/auto-creator/src/main/resources/application.conf b/platform-jobs/samza/auto-creator/src/main/resources/application.conf deleted file mode 100644 index 55482aeae5..0000000000 --- a/platform-jobs/samza/auto-creator/src/main/resources/application.conf +++ /dev/null @@ -1,13 +0,0 @@ -LearningActorSystem{ - default-dispatcher { - type = "Dispatcher" - executor = "fork-join-executor" - fork-join-executor { - parallelism-min = 1 - parallelism-factor = 2.0 - parallelism-max = 4 - } - # Throughput for default Dispatcher, set to 1 for as fair as possible - throughput = 1 - } -} diff --git a/platform-jobs/samza/auto-creator/src/main/resources/log4j.xml b/platform-jobs/samza/auto-creator/src/main/resources/log4j.xml deleted file mode 100644 index d2db3940cc..0000000000 --- a/platform-jobs/samza/auto-creator/src/main/resources/log4j.xml +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/platform-jobs/samza/common/.gitignore b/platform-jobs/samza/common/.gitignore deleted file mode 100644 index b83d22266a..0000000000 --- a/platform-jobs/samza/common/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/target/ diff --git a/platform-jobs/samza/common/pom.xml b/platform-jobs/samza/common/pom.xml deleted file mode 100644 index 4eedd8abfa..0000000000 --- a/platform-jobs/samza/common/pom.xml +++ /dev/null @@ -1,116 +0,0 @@ - - 4.0.0 - - org.sunbird - samza - 1.1-SNAPSHOT - - samza-common - - - org.sunbird - unit-tests - 1.1-SNAPSHOT - test - - - org.apache.samza - samza-api - ${samza.version} - - - org.apache.samza - samza-core_${scala.version} - ${samza.version} - - - org.apache.samza - samza-yarn_${scala.version} - ${samza.version} - - - org.apache.samza - samza-kafka_${scala.version} - ${samza.version} - - - org.apache.samza - samza-log4j - ${samza.version} - - - org.apache.hadoop - hadoop-yarn-client - ${hadoop.version} - - - org.apache.hadoop - hadoop-yarn-common - ${hadoop.version} - - - org.apache.hadoop - hadoop-hdfs - ${hadoop.version} - - - org.apache.httpcomponents - httpclient - 4.5.2 - - - org.apache.samza - samza-shell - dist - tgz - ${samza.version} - - - com.typesafe - config - 1.3.1 - - - org.sunbird - platform-telemetry - 1.1-SNAPSHOT - - - com.google.guava - guava - 19.0 - - - org.sunbird - learning-actors - 1.1-SNAPSHOT - - - log4j-1.2-api - org.apache.logging.log4j - - - - - org.mockito - mockito-core - 2.0.31-beta - test - - - - - - - - org.apache.maven.plugins - maven-compiler-plugin - - 1.8 - 1.8 - - - - - diff --git a/platform-jobs/samza/common/src/main/java/org/sunbird/jobs/samza/exception/PlatformErrorCodes.java b/platform-jobs/samza/common/src/main/java/org/sunbird/jobs/samza/exception/PlatformErrorCodes.java deleted file mode 100644 index a6fe50749a..0000000000 --- a/platform-jobs/samza/common/src/main/java/org/sunbird/jobs/samza/exception/PlatformErrorCodes.java +++ /dev/null @@ -1,6 +0,0 @@ -package org.sunbird.jobs.samza.exception; - -public enum PlatformErrorCodes { - - ERR_DEFINITION_NOT_FOUND, ERR_HOST_UNAVAILABLE, SYSTEM_ERROR, DATA_ERROR, PROCESSING_ERROR, PUBLISH_FAILED -} diff --git a/platform-jobs/samza/common/src/main/java/org/sunbird/jobs/samza/exception/PlatformException.java b/platform-jobs/samza/common/src/main/java/org/sunbird/jobs/samza/exception/PlatformException.java deleted file mode 100644 index 206fee7ae5..0000000000 --- a/platform-jobs/samza/common/src/main/java/org/sunbird/jobs/samza/exception/PlatformException.java +++ /dev/null @@ -1,24 +0,0 @@ -package org.sunbird.jobs.samza.exception; - -import org.sunbird.common.exception.MiddlewareException; - -public class PlatformException extends MiddlewareException { - - private static final long serialVersionUID = -8708641286413033915L; - - public PlatformException(String errCode, String message) { - super(errCode, message); - } - - public PlatformException(String errCode, String message, Object... params) { - super(errCode, message, params); - } - - public PlatformException(String errCode, String message, Throwable root) { - super(errCode, message, root); - } - - public PlatformException(String errCode, String message, Throwable root, Object... params) { - super(errCode, message, root, params); - } -} diff --git a/platform-jobs/samza/common/src/main/java/org/sunbird/jobs/samza/serializers/EkstepJsonSerde.java b/platform-jobs/samza/common/src/main/java/org/sunbird/jobs/samza/serializers/EkstepJsonSerde.java deleted file mode 100644 index 5574b1dba8..0000000000 --- a/platform-jobs/samza/common/src/main/java/org/sunbird/jobs/samza/serializers/EkstepJsonSerde.java +++ /dev/null @@ -1,109 +0,0 @@ -package org.sunbird.jobs.samza.serializers; - -import java.io.UnsupportedEncodingException; -import java.util.HashMap; -import java.util.Map; - -import org.apache.samza.SamzaException; -import org.apache.samza.serializers.Serde; -import org.codehaus.jackson.map.ObjectMapper; -import org.codehaus.jackson.type.TypeReference; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/* - * A serializer for JSON strings that - *
    - *
  1. - * returns a LinkedHashMap upon deserialization. - *
  2. - * enforces the 'dash-separated' property naming convention. - *
- * - * @author Mahesh Kumar Gangula - */ - -public class EkstepJsonSerde implements Serde { - - private static final Logger LOG = LoggerFactory.getLogger(EkstepJsonSerde.class); - private final Class clazz; - private transient ObjectMapper mapper = new ObjectMapper(); - - /** - * Constructs a EkstepJsonSerde that returns a LinkedHashMap<String, - * Object< upon deserialization. - */ - public EkstepJsonSerde() { - this(null); - } - - /** - * Constructs a EkstepJsonSerde that (de)serializes POJOs of class - * {@code clazz}. - * - * @param clazz - * the class of the POJO being (de)serialized. - */ - public EkstepJsonSerde(Class clazz) { - this.clazz = clazz; - } - - public static EkstepJsonSerde of(Class clazz) { - return new EkstepJsonSerde<>(clazz); - } - - @Override - public byte[] toBytes(T obj) { - if (obj != null) { - try { - String str = mapper.writeValueAsString(obj); - return str.getBytes("UTF-8"); - } catch (Exception e) { - throw new SamzaException("Error serializing data.", e); - } - } else { - return null; - } - } - - @SuppressWarnings("unchecked") - @Override - public T fromBytes(byte[] bytes) { - if (bytes != null) { - String str = null; - try { - str = new String(bytes, "UTF-8"); - if (clazz != null) { - return mapper.readValue(str, clazz); - } else { - return mapper.readValue(str, new TypeReference() { - }); - } - } catch (UnsupportedEncodingException e) { - LOG.error("Error deserializing data. Unsupported encoding: " + bytes, e); - Map map = exceptionMap(bytes, "Error deserializing data. Unsupported encoding", e); - return (T) map; - } catch (Exception e) { - LOG.error("Error deserializing data: " + str, e); - Map map = exceptionMap(str, "Error deserializing data", e); - return (T) map; - } - } else { - LOG.error("Bytes data is null"); - Map map = exceptionMap(bytes, "Bytes data is null", null); - return (T) map; - } - } - - public Map exceptionMap(Object data, String message, Exception e) { - Map map = new HashMap(); - if (data instanceof Byte) - map.put("bytes", data); - if (data instanceof String) - map.put("str", data); - map.put("message", message); - map.put("exception", e); - map.put("serde", "error"); - return map; - } -} diff --git a/platform-jobs/samza/common/src/main/java/org/sunbird/jobs/samza/serializers/EkstepJsonSerdeFactory.java b/platform-jobs/samza/common/src/main/java/org/sunbird/jobs/samza/serializers/EkstepJsonSerdeFactory.java deleted file mode 100644 index 92034a4feb..0000000000 --- a/platform-jobs/samza/common/src/main/java/org/sunbird/jobs/samza/serializers/EkstepJsonSerdeFactory.java +++ /dev/null @@ -1,16 +0,0 @@ -package org.sunbird.jobs.samza.serializers; - -import org.apache.samza.config.Config; -import org.apache.samza.serializers.SerdeFactory; - -/** - * - * @author Mahesh Kumar Gangula - * - */ - -public class EkstepJsonSerdeFactory implements SerdeFactory { - public EkstepJsonSerde getSerde(String name, Config config) { - return new EkstepJsonSerde<>(); - } -} diff --git a/platform-jobs/samza/common/src/main/java/org/sunbird/jobs/samza/service/ISamzaService.java b/platform-jobs/samza/common/src/main/java/org/sunbird/jobs/samza/service/ISamzaService.java deleted file mode 100644 index cc0a1010aa..0000000000 --- a/platform-jobs/samza/common/src/main/java/org/sunbird/jobs/samza/service/ISamzaService.java +++ /dev/null @@ -1,26 +0,0 @@ -package org.sunbird.jobs.samza.service; - -import java.util.Map; - -import org.apache.samza.config.Config; -import org.apache.samza.task.MessageCollector; -import org.sunbird.jobs.samza.service.task.JobMetrics; - -public interface ISamzaService { - - /** - * - * @param config - * @throws Exception - */ - public void initialize(Config config) throws Exception; - - /** - * The class processMessage is mainly responsible for processing the messages sent from consumers based on required - * specifications - * - * @param MessageData The messageData - */ - public void processMessage(Map message, JobMetrics metrics, MessageCollector collector) throws Exception; - -} diff --git a/platform-jobs/samza/common/src/main/java/org/sunbird/jobs/samza/service/task/JobMetrics.java b/platform-jobs/samza/common/src/main/java/org/sunbird/jobs/samza/service/task/JobMetrics.java deleted file mode 100644 index b50dbb3a60..0000000000 --- a/platform-jobs/samza/common/src/main/java/org/sunbird/jobs/samza/service/task/JobMetrics.java +++ /dev/null @@ -1,129 +0,0 @@ -package org.sunbird.jobs.samza.service.task; - -import org.apache.samza.metrics.Counter; -import org.apache.samza.metrics.Metric; -import org.apache.samza.metrics.MetricsRegistry; -import org.apache.samza.metrics.MetricsRegistryMap; -import org.apache.samza.system.SystemStreamPartition; -import org.apache.samza.task.TaskContext; -import org.sunbird.jobs.samza.util.JobLogger; - -import java.util.HashMap; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; - -public class JobMetrics { - - private static JobLogger LOGGER = new JobLogger(JobMetrics.class); - private String jobName; - private String topic; - private TaskContext context; - private final Counter successMessageCount; - private final Counter failedMessageCount; - private final Counter skippedMessageCount; - private final Counter errorMessageCount; - private int partition; - - public JobMetrics(TaskContext context) { - this(context, null, null); - } - - public JobMetrics(TaskContext context, String jName, String topic) { - MetricsRegistry metricsRegistry = context.getMetricsRegistry(); - successMessageCount = metricsRegistry.newCounter(getClass().getName(), "success-message-count"); - failedMessageCount = metricsRegistry.newCounter(getClass().getName(), "failed-message-count"); - skippedMessageCount = metricsRegistry.newCounter(getClass().getName(), "skipped-message-count"); - errorMessageCount = metricsRegistry.newCounter(getClass().getName(), "error-message-count"); - jobName = jName; - this.topic = topic; - this.context=context; - } - - public void clear() { - successMessageCount.clear(); - failedMessageCount.clear(); - skippedMessageCount.clear(); - errorMessageCount.clear(); - } - - public void incSuccessCounter() { - successMessageCount.inc(); - } - - public void incFailedCounter() { - failedMessageCount.inc(); - } - - public void incSkippedCounter() { - skippedMessageCount.inc(); - } - - public void incErrorCounter() { - errorMessageCount.inc(); - } - - public String getJobName() { - return jobName; - } - - public void setJobName(String jobName) { - this.jobName = jobName; - } - - public String getTopic() { - return topic; - } - - public void setTopic(String topic) { - this.topic = topic; - } - - /** - * - * @param containerMetricsRegistry - * @return - */ - public long computeConsumerLag(Map> containerMetricsRegistry) { - long consumerLag = 0; - try { - for (SystemStreamPartition sysPartition : context.getSystemStreamPartitions()) { - if (!sysPartition.getStream().endsWith("system.command")) { - long highWatermarkOffset = - Long.valueOf(containerMetricsRegistry.get("org.apache.samza.system.kafka.KafkaSystemConsumerMetrics") - .get(getSamzaMetricKey(sysPartition, "high-watermark")).toString()); - long checkPointOffset = Long.valueOf(containerMetricsRegistry.get("org.apache.samza.checkpoint.OffsetManagerMetrics") - .get(getSamzaMetricKey(sysPartition, "checkpointed-offset")).toString()); - String lagMessage = "Job Name : " + getJobName() + " , partition : " + sysPartition.getPartition().getPartitionId() + " , Stream : " + sysPartition.toString() + " , Current High Water Mark Offset : " + highWatermarkOffset + " , Current Checkpoint Offset : " + checkPointOffset + " , consumer lag : " + (highWatermarkOffset - checkPointOffset) + " , timestamp :" + System.currentTimeMillis(); - System.out.println(lagMessage); - LOGGER.info(lagMessage); - consumerLag += highWatermarkOffset - checkPointOffset; - this.partition = sysPartition.getPartition().getPartitionId(); - } - } - - } catch (Exception e) { - LOGGER.error("Exception Occurred While Computing Consumer Lag. Exception is : ", "", e); - } - return consumerLag; - } - - private String getSamzaMetricKey(SystemStreamPartition partition, String samzaMetricName) { - return String.format("%s-%s-%s-%s", - partition.getSystem(), partition.getStream(), partition.getPartition().getPartitionId(), samzaMetricName); - } - - public Map collect() { - LOGGER.info("collect is called for Job : "+getJobName()+" , partition : "+partition); - Map metricsEvent = new HashMap<>(); - metricsEvent.put("job-name", jobName); - metricsEvent.put("success-message-count", successMessageCount.getCount()); - metricsEvent.put("failed-message-count", failedMessageCount.getCount()); - metricsEvent.put("error-message-count", errorMessageCount.getCount()); - metricsEvent.put("skipped-message-count", skippedMessageCount.getCount()); - metricsEvent.put("partition",partition); - metricsEvent.put("consumer-lag", - computeConsumerLag(((MetricsRegistryMap) context.getSamzaContainerContext().metricsRegistry).metrics())); - return metricsEvent; - } - -} \ No newline at end of file diff --git a/platform-jobs/samza/common/src/main/java/org/sunbird/jobs/samza/service/util/AbstractESIndexer.java b/platform-jobs/samza/common/src/main/java/org/sunbird/jobs/samza/service/util/AbstractESIndexer.java deleted file mode 100644 index e3aa023b71..0000000000 --- a/platform-jobs/samza/common/src/main/java/org/sunbird/jobs/samza/service/util/AbstractESIndexer.java +++ /dev/null @@ -1,20 +0,0 @@ -/** - * - */ -package org.sunbird.jobs.samza.service.util; - -/** - * @author pradyumna - * - */ -public abstract class AbstractESIndexer { - - /** - * - */ - public AbstractESIndexer() { - init(); - } - - protected abstract void init(); -} diff --git a/platform-jobs/samza/common/src/main/java/org/sunbird/jobs/samza/task/AbstractTask.java b/platform-jobs/samza/common/src/main/java/org/sunbird/jobs/samza/task/AbstractTask.java deleted file mode 100644 index 4656d9a472..0000000000 --- a/platform-jobs/samza/common/src/main/java/org/sunbird/jobs/samza/task/AbstractTask.java +++ /dev/null @@ -1,204 +0,0 @@ -package org.sunbird.jobs.samza.task; - -import java.util.HashMap; -import java.util.Map; -import java.util.UUID; - -import org.apache.commons.lang3.StringUtils; -import org.apache.samza.config.Config; -import org.apache.samza.system.IncomingMessageEnvelope; -import org.apache.samza.system.OutgoingMessageEnvelope; -import org.apache.samza.system.SystemStream; -import org.apache.samza.task.InitableTask; -import org.apache.samza.task.MessageCollector; -import org.apache.samza.task.StreamTask; -import org.apache.samza.task.TaskContext; -import org.apache.samza.task.TaskCoordinator; -import org.apache.samza.task.WindowableTask; -import org.sunbird.common.Platform; -import org.sunbird.jobs.samza.service.ISamzaService; -import org.sunbird.jobs.samza.service.task.JobMetrics; -import org.sunbird.jobs.samza.util.SamzaCommonParams; -import org.sunbird.learning.util.ControllerUtil; -import org.sunbird.telemetry.TelemetryGenerator; -import org.sunbird.telemetry.TelemetryParams; -import org.sunbird.telemetry.handler.Level; - -public abstract class AbstractTask extends BaseTask { - - protected JobMetrics metrics; - private Config config = null; - private String eventId = ""; - protected String jobType = ""; - protected String jobStartMessage = ""; - protected String jobEndMessage = ""; - protected String jobClass = ""; - - private static String mid = "LP."+UUID.randomUUID(); - private static String startJobEventId = "JOB_START"; - private static String endJobEventId = "JOB_END"; - private static int MAXITERTIONCOUNT= 2; - private static ControllerUtil controllerUtil = new ControllerUtil(); - - @Override - public void init(Config config, TaskContext context) throws Exception { - metrics = new JobMetrics(context, config.get("output.metrics.job.name"), config.get("output.metrics.topic.name")); - ISamzaService service = initialize(); - service.initialize(config); - this.config = config; - this.eventId = "BE_JOB_REQUEST"; - } - - public abstract ISamzaService initialize() throws Exception; - - protected int getMaxIterations() { - if(Platform.config.hasPath("max.iteration.count.samza.job")) - return Platform.config.getInt("max.iteration.count.samza.job"); - else - return MAXITERTIONCOUNT; - } - - @SuppressWarnings("unchecked") - @Override - public void process(IncomingMessageEnvelope envelope, MessageCollector collector, TaskCoordinator coordinator) throws Exception { - Map message = (Map) envelope.getMessage(); - Map execution = new HashMap<>(); - int maxIterations = getMaxIterations(); - String eid = (String) message.get(SamzaCommonParams.eid.name()); - Map edata = (Map) message.getOrDefault(SamzaCommonParams.edata.name(), new HashMap()); - if(StringUtils.equalsIgnoreCase(this.eventId, eid)) { - String requestedJobType = (String) edata.get(SamzaCommonParams.action.name()); - if(StringUtils.equalsIgnoreCase(this.jobType, requestedJobType)) { - int currentIteration = ((Number) edata.get(SamzaCommonParams.iteration.name())).intValue(); - preProcess(message, collector, execution, maxIterations, currentIteration); - process(message, collector, coordinator); - postProcess(message, collector, execution, maxIterations, currentIteration); - } else if(StringUtils.equalsIgnoreCase("definition_update", requestedJobType)){ - String graphId = edata.getOrDefault("graphId","").toString(); - String objectType = edata.getOrDefault("objectType","").toString(); - controllerUtil.updateDefinitionCache(graphId, objectType); - }else{ - //Throw exception has to be added. - } - } else { - //Throw exception has to be added. - } - } - - public abstract void process(Map message, MessageCollector collector, TaskCoordinator coordinator) throws Exception; - - public void preProcess(Map message, MessageCollector collector, Map execution, int maxIterationCount, int iterationCount) { - if (isInvalidMessage(message)) { - String event = generateEvent(Level.ERROR.name(), "Samza job de-serialization error", message); - collector.send(new OutgoingMessageEnvelope(new SystemStream(SamzaCommonParams.kafka.name(), this.config.get("kafka.topics.backend.telemetry")), event)); - } - try { - if(iterationCount <= maxIterationCount) { - Map jobStartEvent = getJobEvent("JOBSTARTEVENT", message); - - execution.put(SamzaCommonParams.submitted_date.name(), (long)message.get(SamzaCommonParams.ets.name())); - execution.put(SamzaCommonParams.processing_date.name(), (long)jobStartEvent.get(SamzaCommonParams.ets.name())); - execution.put(SamzaCommonParams.latency.name(), (long)jobStartEvent.get(SamzaCommonParams.ets.name()) - (long)message.get(SamzaCommonParams.ets.name())); - - pushEvent(jobStartEvent, collector, this.config.get("kafka.topics.backend.telemetry")); - } - }catch (Exception e) { - e.printStackTrace(); - } - } - - - @SuppressWarnings("unchecked") - public void postProcess(Map message, MessageCollector collector, Map execution, int maxIterationCount, int iterationCount) throws Exception { - try { - if(iterationCount <= maxIterationCount) { - Map jobEndEvent = getJobEvent("JOBENDEVENT", message); - - execution.put(SamzaCommonParams.completed_date.name(), (long)jobEndEvent.get(SamzaCommonParams.ets.name())); - execution.put(SamzaCommonParams.execution_time.name(), (long)jobEndEvent.get(SamzaCommonParams.ets.name()) - (long)execution.get(SamzaCommonParams.processing_date.name())); - Map eks = (Map)((Map)jobEndEvent.get(SamzaCommonParams.edata.name())).get(SamzaCommonParams.eks.name()); - eks.put(SamzaCommonParams.execution.name(), execution); - //addExecutionTime(jobEndEvent, execution); //Call to add execution time - - pushEvent(jobEndEvent, collector, this.config.get("kafka.topics.backend.telemetry")); - } - String eventExecutionStatus = (String)((Map) message.get(SamzaCommonParams.edata.name())).get(SamzaCommonParams.status.name()); - if(StringUtils.equalsIgnoreCase(eventExecutionStatus, SamzaCommonParams.FAILED.name()) && iterationCount < maxIterationCount) { - ((Map) message.get(SamzaCommonParams.edata.name())).put(SamzaCommonParams.iteration.name(), iterationCount+1); - collector.send(new OutgoingMessageEnvelope(new SystemStream(SamzaCommonParams.kafka.name(), this.config.get("kafka.topics.failed")), message)); - } - }catch(Exception e) { - e.printStackTrace(); - } - } - - /*@SuppressWarnings("unchecked") - private void addExecutionTime(Map jobEndEvent, Map execution) { - Map eks = (Map)((Map)jobEndEvent.get(SamzaCommonParams.edata.name())).get(SamzaCommonParams.eks.name()); - eks.put(SamzaCommonParams.execution.name(), execution); - }*/ - - private void pushEvent(Map message, MessageCollector collector, String topicId) throws Exception { - try { - //TODO: Fix Event Template for "START" & "END" Event and enable below line for backend telemetry. - //collector.send(new OutgoingMessageEnvelope(new SystemStream(SamzaCommonParams.kafka.name(), topicId), message)); - } catch (Exception e) { - e.printStackTrace(); - } - } - - @SuppressWarnings("unchecked") - public Map getJobEvent(String jobEvendID, Map message){ - - long unixTime = System.currentTimeMillis(); - Map jobEvent = new HashMap<>(); - - jobEvent.put(SamzaCommonParams.ets.name(), unixTime); - jobEvent.put(SamzaCommonParams.mid.name(), mid); - - Map edata = new HashMap<>(); - Map eks = new HashMap<>(); - eks.put(SamzaCommonParams.ets.name(), message.get(SamzaCommonParams.ets.name())); - eks.put(SamzaCommonParams.action.name(), ((Map) message.get(SamzaCommonParams.edata.name())).get(SamzaCommonParams.action.name())); - eks.put(SamzaCommonParams.iteration.name(), ((Map) message.get(SamzaCommonParams.edata.name())).get(SamzaCommonParams.iteration.name())); - eks.put(SamzaCommonParams.status.name(), ((Map) message.get(SamzaCommonParams.edata.name())).get(SamzaCommonParams.status.name())); - eks.put(SamzaCommonParams.reqid.name(), message.get(SamzaCommonParams.mid.name())); - edata.put(SamzaCommonParams.eks.name(), eks); - edata.put(SamzaCommonParams.level.name(), SamzaCommonParams.INFO.name()); - edata.put(SamzaCommonParams.jobclass.name(), this.jobClass); - edata.put(SamzaCommonParams.object.name(), message.get("object")); - - - if(StringUtils.equalsIgnoreCase(jobEvendID, "JOBSTARTEVENT")) { - jobEvent.put(SamzaCommonParams.eid.name(), startJobEventId); - edata.put(SamzaCommonParams.message.name(), this.jobStartMessage); - } - else if(StringUtils.equalsIgnoreCase(jobEvendID, "JOBENDEVENT")) { - jobEvent.put(SamzaCommonParams.eid.name(), endJobEventId); - edata.put(SamzaCommonParams.message.name(), this.jobEndMessage); - } - - jobEvent.put(SamzaCommonParams.edata.name(), edata); - return jobEvent; - } - - private String generateEvent(String logLevel, String message, Map data) { - Map context = new HashMap(); - context.put(TelemetryParams.ACTOR.name(), "org.sunbird.learning.platform"); - context.put(TelemetryParams.ENV.name(), "content"); - context.put(TelemetryParams.CHANNEL.name(), Platform.config.getString("channel.default")); - return TelemetryGenerator.log(context, "system", logLevel, message); - } - - protected boolean isInvalidMessage(Map message) { - return (message == null || (null != message && message.containsKey("serde") - && "error".equalsIgnoreCase((String) message.get("serde")))); - } - - @Override - public void window(MessageCollector collector, TaskCoordinator coordinator) throws Exception { - Map event = metrics.collect(); - collector.send(new OutgoingMessageEnvelope(new SystemStream("kafka", metrics.getTopic()), event)); - metrics.clear(); - } -} diff --git a/platform-jobs/samza/common/src/main/java/org/sunbird/jobs/samza/task/BaseTask.java b/platform-jobs/samza/common/src/main/java/org/sunbird/jobs/samza/task/BaseTask.java deleted file mode 100644 index a3a6d2f0b9..0000000000 --- a/platform-jobs/samza/common/src/main/java/org/sunbird/jobs/samza/task/BaseTask.java +++ /dev/null @@ -1,14 +0,0 @@ -package org.sunbird.jobs.samza.task; - -import org.apache.samza.task.InitableTask; -import org.apache.samza.task.StreamTask; -import org.apache.samza.task.WindowableTask; - -/** - * Base Class for Samza Task - * - * @author Kumar Gauraw - */ -public abstract class BaseTask implements StreamTask, InitableTask, WindowableTask { - //TODO: Provide Common Method Implementation Here. -} diff --git a/platform-jobs/samza/common/src/main/java/org/sunbird/jobs/samza/util/FailedEventsUtil.java b/platform-jobs/samza/common/src/main/java/org/sunbird/jobs/samza/util/FailedEventsUtil.java deleted file mode 100644 index d0203fcd13..0000000000 --- a/platform-jobs/samza/common/src/main/java/org/sunbird/jobs/samza/util/FailedEventsUtil.java +++ /dev/null @@ -1,41 +0,0 @@ -package org.sunbird.jobs.samza.util; - -import java.util.Arrays; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -import org.apache.commons.lang3.exception.ExceptionUtils; -import org.apache.samza.system.OutgoingMessageEnvelope; -import org.apache.samza.system.SystemStream; -import org.apache.samza.task.MessageCollector; -import org.sunbird.jobs.samza.service.task.JobMetrics; - -/** - * @author gauraw - * - */ -public class FailedEventsUtil { - - private static JobLogger LOGGER = new JobLogger(FailedEventsUtil.class); - - public static void pushEventForRetry(SystemStream sysStream, Map eventMessage, - JobMetrics metrics, MessageCollector collector, String errorCode, Throwable error) { - Map failedEventMap = new HashMap(); - String errorString[] = ExceptionUtils.getStackTrace(error).split("\\n\\t"); - - List stackTrace; - if(errorString.length > 21) { - stackTrace = Arrays.asList(errorString).subList((errorString.length - 21), errorString.length - 1); - }else{ - stackTrace = Arrays.asList(errorString); - } - - failedEventMap.put("errorCode", errorCode); - failedEventMap.put("error", error.getMessage() + " : : " + stackTrace); - eventMessage.put("jobName", metrics.getJobName()); - eventMessage.put("failInfo", failedEventMap); - collector.send(new OutgoingMessageEnvelope(sysStream, eventMessage)); - LOGGER.debug("Event sent to fail topic for job : " + metrics.getJobName()); - } -} diff --git a/platform-jobs/samza/common/src/main/java/org/sunbird/jobs/samza/util/JSONUtils.java b/platform-jobs/samza/common/src/main/java/org/sunbird/jobs/samza/util/JSONUtils.java deleted file mode 100644 index a441b9d78c..0000000000 --- a/platform-jobs/samza/common/src/main/java/org/sunbird/jobs/samza/util/JSONUtils.java +++ /dev/null @@ -1,32 +0,0 @@ -package org.sunbird.jobs.samza.util; - -import com.typesafe.config.ConfigFactory; -import org.apache.commons.lang.StringUtils; -import org.apache.samza.config.Config; -import org.codehaus.jackson.map.ObjectMapper; -import org.sunbird.common.Platform; - -import java.util.HashMap; -import java.util.Map; -import java.util.Map.Entry; - -public class JSONUtils { - - private static ObjectMapper mapper = new ObjectMapper();; - - public static String serialize(Object object) throws Exception { - return mapper.writeValueAsString(object); - } - - public static void loadProperties(Config config){ - Map props = new HashMap(); - for (Entry entry : config.entrySet()) { - if (StringUtils.equalsIgnoreCase("True", entry.getValue()) || StringUtils.equalsIgnoreCase("False", entry.getValue())) - props.put(entry.getKey(), entry.getValue().toLowerCase()); - else - props.put(entry.getKey(), entry.getValue()); - } - com.typesafe.config.Config conf = ConfigFactory.parseMap(props); - Platform.loadProperties(conf); - } -} \ No newline at end of file diff --git a/platform-jobs/samza/common/src/main/java/org/sunbird/jobs/samza/util/JobLogger.java b/platform-jobs/samza/common/src/main/java/org/sunbird/jobs/samza/util/JobLogger.java deleted file mode 100644 index 653eebfe83..0000000000 --- a/platform-jobs/samza/common/src/main/java/org/sunbird/jobs/samza/util/JobLogger.java +++ /dev/null @@ -1,90 +0,0 @@ -package org.sunbird.jobs.samza.util; - -import java.text.MessageFormat; -import java.util.Map; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -public class JobLogger { - - private final Logger logger; - - @SuppressWarnings("rawtypes") - public JobLogger(Class clazz) { - logger = LoggerFactory.getLogger(clazz); - } - - public void debug(String msg, Map event) { - if (logger.isDebugEnabled()) - try { - debug(msg, JSONUtils.serialize(event)); - } catch (Exception e) { - e.printStackTrace(); - } - } - - public void info(String msg, Map event) { - if (logger.isInfoEnabled()) - try { - info(msg, JSONUtils.serialize(event)); - } catch (Exception e) { - e.printStackTrace(); - } - } - - public void error(String msg, Map event, Throwable t) { - if (logger.isErrorEnabled()) - try { - error(msg, JSONUtils.serialize(event), t); - } catch (Exception e) { - e.printStackTrace(); - } - } - - public void debug(String msg) { - logger.debug(getLogMessage(msg, null)); - } - - public void debug(String msg, String event) { - logger.debug(getLogMessage(msg, event)); - } - - public void info(String msg) { - logger.info(getLogMessage(msg, null)); - } - - public void warn(String msg) { - logger.warn(getLogMessage(msg, null)); - } - - public void warn(String msg, Map event) { - if (logger.isWarnEnabled()) { - try { - warn(msg, JSONUtils.serialize(event)); - } catch (Exception e) { - e.printStackTrace(); - } - } - } - - private void warn(String msg, String event) { - logger.warn(getLogMessage(msg, event)); - } - - public void info(String msg, String event) { - logger.info(getLogMessage(msg, event)); - } - - public void error(String msg, Throwable t) { - logger.error(getLogMessage(msg, null), t); - } - - public void error(String msg, String event, Throwable t) { - logger.error(getLogMessage(msg, event), t); - } - - private String getLogMessage(String msg, String event) { - return event == null ? MessageFormat.format("Message: {0}", msg) : MessageFormat.format("Message: {0} | event:{1}", msg, event); - } -} diff --git a/platform-jobs/samza/common/src/main/java/org/sunbird/jobs/samza/util/SamzaCommonParams.java b/platform-jobs/samza/common/src/main/java/org/sunbird/jobs/samza/util/SamzaCommonParams.java deleted file mode 100644 index c5a2fc7b0c..0000000000 --- a/platform-jobs/samza/common/src/main/java/org/sunbird/jobs/samza/util/SamzaCommonParams.java +++ /dev/null @@ -1,6 +0,0 @@ -package org.sunbird.jobs.samza.util; - -public enum SamzaCommonParams { - kafka, eid, edata, action, iteration, status, SUCCESS, FAILED, ets, submitted_date, processing_date, completed_date, latency, execution_time, execution, mid, reqid, eks, - level, INFO, message, object, jobclass, domain, context -} diff --git a/platform-jobs/samza/common/src/main/java/org/sunbird/jobs/samza/util/TrackableENUM.java b/platform-jobs/samza/common/src/main/java/org/sunbird/jobs/samza/util/TrackableENUM.java deleted file mode 100644 index e252797d1f..0000000000 --- a/platform-jobs/samza/common/src/main/java/org/sunbird/jobs/samza/util/TrackableENUM.java +++ /dev/null @@ -1,5 +0,0 @@ -package org.sunbird.jobs.samza.util; - -public enum TrackableENUM { - Yes, No -} diff --git a/platform-jobs/samza/common/src/test/java/org/sunbird/samza/jobs/test/JSONUtilTest.java b/platform-jobs/samza/common/src/test/java/org/sunbird/samza/jobs/test/JSONUtilTest.java deleted file mode 100644 index 93d40374cb..0000000000 --- a/platform-jobs/samza/common/src/test/java/org/sunbird/samza/jobs/test/JSONUtilTest.java +++ /dev/null @@ -1,33 +0,0 @@ -package org.eksep.samza.jobs.test; - -import java.util.HashMap; -import java.util.Map; - -import org.sunbird.common.Platform; -import org.junit.Assert; -import org.junit.Test; - -import com.typesafe.config.ConfigFactory; - -public class JSONUtilTest { - - static Map configMap = new HashMap(); - - static { - configMap.put("elastic-search-host", "http://localhost"); - configMap.put("elastic-search-port", "9200"); - configMap.put("graph.dir", "/data/graphDB/"); - configMap.put("route.bolt.write.domain", "bolt://localhost:7687"); - configMap.put("graph.bolt.enable", "true"); - } - - @Test - public void loadConfigProps_1() { - com.typesafe.config.Config conf = ConfigFactory.parseMap(configMap); - Platform.loadProperties(conf); - String route = Platform.config.getString("route.bolt.write.domain"); - Assert.assertEquals("bolt://localhost:7687", route); - Assert.assertEquals("/data/graphDB/", Platform.config.getString("graph.dir")); - Assert.assertTrue(Platform.config.getBoolean("graph.bolt.enable")); - } -} diff --git a/platform-jobs/samza/common/src/test/java/org/sunbird/samza/jobs/test/JobMetricsTest.java b/platform-jobs/samza/common/src/test/java/org/sunbird/samza/jobs/test/JobMetricsTest.java deleted file mode 100644 index 9fa4472090..0000000000 --- a/platform-jobs/samza/common/src/test/java/org/sunbird/samza/jobs/test/JobMetricsTest.java +++ /dev/null @@ -1,120 +0,0 @@ -package org.eksep.samza.jobs.test; - -import org.apache.samza.Partition; -import org.apache.samza.metrics.Counter; -import org.apache.samza.metrics.Metric; -import org.apache.samza.metrics.MetricsRegistry; -import org.apache.samza.system.SystemStreamPartition; -import org.apache.samza.task.TaskContext; -import org.sunbird.jobs.samza.service.task.JobMetrics; -import org.junit.Before; -import org.junit.Test; - -import java.util.HashSet; -import java.util.Map; -import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; - -import static org.mockito.Mockito.anyString; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.stub; -import static org.mockito.Mockito.when; -import static org.junit.Assert.assertEquals; - -/** - * JobMetrics Test for Consumer Lag Computation - * - * @author Kumar Gauraw - */ -public class JobMetricsTest { - - private TaskContext contextMock; - private JobMetrics jobMetricsMock; - - @Before - public void setUp() { - contextMock = mock(TaskContext.class); - MetricsRegistry metricsRegistry = mock(MetricsRegistry.class); - Counter counter = mock(Counter.class); - stub(metricsRegistry.newCounter(anyString(), anyString())).toReturn(counter); - stub(contextMock.getMetricsRegistry()).toReturn(metricsRegistry); - } - - @Test - public void testConsumerLagWithMultipleTopicEventProcessed() { - - jobMetricsMock = new JobMetrics(contextMock); - - Set systemStreamPartitions = new HashSet<>(); - SystemStreamPartition systemStreamTopic1Partition0 = new SystemStreamPartition("kafka", "topic1", new Partition(0)); - SystemStreamPartition systemStreamTopic2Partition0 = new SystemStreamPartition("kafka", "topic2", new Partition(0)); - systemStreamPartitions.add(systemStreamTopic1Partition0); - systemStreamPartitions.add(systemStreamTopic2Partition0); - - Map> concurrentHashMap = MetricsStreamStub.getMetricMap(MetricsStreamStub.METRIC_STREAM_SOME_EVENT_MULTI_PARTITION); - - when(contextMock.getSystemStreamPartitions()).thenReturn(systemStreamPartitions); - long consumer_lag = jobMetricsMock.computeConsumerLag(concurrentHashMap); - assertEquals(55, consumer_lag); - - } - - @Test - public void testConsumerLagWithMultiplePartitionEventProcessed() { - - jobMetricsMock = new JobMetrics(contextMock); - - Set systemStreamPartitions = new HashSet<>(); - SystemStreamPartition systemStreamTopic1Partition0 = new SystemStreamPartition("kafka", "topic1", new Partition(0)); - SystemStreamPartition systemStreamTopic1Partition1 = new SystemStreamPartition("kafka", "topic1", new Partition(1)); - systemStreamPartitions.add(systemStreamTopic1Partition0); - systemStreamPartitions.add(systemStreamTopic1Partition1); - - Map> concurrentHashMap = MetricsStreamStub.getMetricMap(MetricsStreamStub.METRIC_STREAM_SOME_EVENT_SINGLE_TOPIC_MULTI_PARTITION); - - when(contextMock.getSystemStreamPartitions()).thenReturn(systemStreamPartitions); - long consumer_lag = jobMetricsMock.computeConsumerLag(concurrentHashMap); - assertEquals(55, consumer_lag); - - } - - @Test - public void testConsumerLagWithNoEventProcessed() { - - jobMetricsMock = new JobMetrics(contextMock); - - Set systemStreamPartitions = new HashSet<>(); - SystemStreamPartition systemStreamPartition = new SystemStreamPartition("kafka", "test.topic", new Partition(0)); - systemStreamPartitions.add(systemStreamPartition); - - Map> concurrentHashMap = MetricsStreamStub.getMetricMap(MetricsStreamStub.METRIC_STREAM_NO_EVENT); - - when(contextMock.getSystemStreamPartitions()).thenReturn(systemStreamPartitions); - long consumer_lag = jobMetricsMock.computeConsumerLag(concurrentHashMap); - assertEquals(0, consumer_lag); - - } - - @Test - public void testConsumerLagWithSystemCommandStream() { - jobMetricsMock = new JobMetrics(contextMock); - Set systemStreamPartitions = new HashSet<>(); - - SystemStreamPartition streamSysCommand = new SystemStreamPartition("kafka", "system.command", new Partition(0)); - SystemStreamPartition streamJobReq = new SystemStreamPartition("kafka", "learning.job.request", new Partition(0)); - systemStreamPartitions.add(streamSysCommand); - systemStreamPartitions.add(streamJobReq); - - Map> concurrentHashMap = MetricsStreamStub.getMetricMap(MetricsStreamStub.SAMZA_EVENT_STREAM_WITH_SYSTEM_COMMAND); - - when(contextMock.getSystemStreamPartitions()).thenReturn(systemStreamPartitions); - long consumer_lag = jobMetricsMock.computeConsumerLag(concurrentHashMap); - System.out.println("consumer_lag :"+consumer_lag); - //Before Ignoring system.command stream - //assertEquals(110, consumer_lag); - - //After Ignoring system.command stream - assertEquals(100, consumer_lag); - - } -} diff --git a/platform-jobs/samza/common/src/test/java/org/sunbird/samza/jobs/test/MetricsStreamStub.java b/platform-jobs/samza/common/src/test/java/org/sunbird/samza/jobs/test/MetricsStreamStub.java deleted file mode 100644 index 31cf1fa233..0000000000 --- a/platform-jobs/samza/common/src/test/java/org/sunbird/samza/jobs/test/MetricsStreamStub.java +++ /dev/null @@ -1,138 +0,0 @@ -package org.eksep.samza.jobs.test; - -import com.google.gson.Gson; -import com.google.gson.reflect.TypeToken; -import org.apache.samza.metrics.Counter; -import org.apache.samza.metrics.Metric; - -import java.lang.reflect.Type; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; - -/** - * Stub for Consumer Metric Stream - * - * @author Kumar Gauraw - */ -public class MetricsStreamStub { - - public static final String METRIC_STREAM_NO_EVENT = "{\n" + - " \"org.apache.samza.system.kafka.KafkaSystemConsumerMetrics\": {\n" + - " \"kafka-test.topic-0-high-watermark\": {\n" + - " \"name\": \"kafka-test.topic-0-high-watermark\",\n" + - " \"count\": {\n" + - " \"value\": 0\n" + - " }\n" + - " }\n" + - " },\n" + - " \"org.apache.samza.checkpoint.OffsetManagerMetrics\": {\n" + - " \"kafka-test.topic-0-checkpointed-offset\": {\n" + - " \"name\": \"kafka-test.topic-0-checkpointed-offset\",\n" + - " \"count\": {\n" + - " \"value\": 0\n" + - " }\n" + - " }\n" + - " }\n" + - "}"; - - public static final String METRIC_STREAM_SOME_EVENT_MULTI_PARTITION = "{\n" + - " \"org.apache.samza.system.kafka.KafkaSystemConsumerMetrics\": {\n" + - " \"kafka-topic1-0-high-watermark\": {\n" + - " \"name\": \"kafka-topic1-0-high-watermark\",\n" + - " \"count\": {\n" + - " \"value\": 10\n" + - " }\n" + - " },\n" + - " \"kafka-topic2-0-high-watermark\": {\n" + - " \"name\": \"kafka-topic2-0-high-watermark\",\n" + - " \"count\": {\n" + - " \"value\": 100\n" + - " }\n" + - " }\n" + - " },\n" + - " \"org.apache.samza.checkpoint.OffsetManagerMetrics\": {\n" + - " \"kafka-topic1-0-checkpointed-offset\": {\n" + - " \"name\": \"kafka-topic1-0-checkpointed-offset\",\n" + - " \"count\": {\n" + - " \"value\": 5\n" + - " }\n" + - " },\n" + - " \"kafka-topic2-0-checkpointed-offset\": {\n" + - " \"name\": \"kafka-topic2-0-checkpointed-offset\",\n" + - " \"count\": {\n" + - " \"value\": 50\n" + - " }\n" + - " }\n" + - " }\n" + - "}"; - - public static final String SAMZA_EVENT_STREAM_WITH_SYSTEM_COMMAND = "{\n" + - " \"org.apache.samza.system.kafka.KafkaSystemConsumerMetrics\": {\n" + - " \"kafka-system.command-0-high-watermark\": {\n" + - " \"name\": \"kafka-system.command-0-high-watermark\",\n" + - " \"count\": {\n" + - " \"value\": 100\n" + - " }\n" + - " },\n" + - " \"kafka-learning.job.request-0-high-watermark\": {\n" + - " \"name\": \"kafka-learning.job.request-0-high-watermark\",\n" + - " \"count\": {\n" + - " \"value\": 200\n" + - " }\n" + - " }\n" + - " },\n" + - " \"org.apache.samza.checkpoint.OffsetManagerMetrics\": {\n" + - " \"kafka-system.command-0-checkpointed-offset\": {\n" + - " \"name\": \"kafka-system.command-0-checkpointed-offset\",\n" + - " \"count\": {\n" + - " \"value\": 90\n" + - " }\n" + - " },\n" + - " \"kafka-learning.job.request-0-checkpointed-offset\": {\n" + - " \"name\": \"kafka-learning.job.request-0-checkpointed-offset\",\n" + - " \"count\": {\n" + - " \"value\": 100\n" + - " }\n" + - " }\n" + - " }\n" + - "}"; - - public static final String METRIC_STREAM_SOME_EVENT_SINGLE_TOPIC_MULTI_PARTITION = "{\n" + - " \"org.apache.samza.system.kafka.KafkaSystemConsumerMetrics\": {\n" + - " \"kafka-topic1-0-high-watermark\": {\n" + - " \"name\": \"kafka-topic1-0-high-watermark\",\n" + - " \"count\": {\n" + - " \"value\": 10\n" + - " }\n" + - " },\n" + - " \"kafka-topic1-1-high-watermark\": {\n" + - " \"name\": \"kafka-topic1-1-high-watermark\",\n" + - " \"count\": {\n" + - " \"value\": 100\n" + - " }\n" + - " }\n" + - " },\n" + - " \"org.apache.samza.checkpoint.OffsetManagerMetrics\": {\n" + - " \"kafka-topic1-0-checkpointed-offset\": {\n" + - " \"name\": \"kafka-topic1-0-checkpointed-offset\",\n" + - " \"count\": {\n" + - " \"value\": 5\n" + - " }\n" + - " },\n" + - " \"kafka-topic1-1-checkpointed-offset\": {\n" + - " \"name\": \"kafka-topic1-1-checkpointed-offset\",\n" + - " \"count\": {\n" + - " \"value\": 50\n" + - " }\n" + - " }\n" + - " }\n" + - "}"; - - - public static Map> getMetricMap(String message) { - Type type = new TypeToken>>() { - }.getType(); - return (Map>) new Gson().fromJson(message, type); - } - -} diff --git a/platform-jobs/samza/course-common/pom.xml b/platform-jobs/samza/course-common/pom.xml deleted file mode 100644 index f5eb486a11..0000000000 --- a/platform-jobs/samza/course-common/pom.xml +++ /dev/null @@ -1,30 +0,0 @@ - - 4.0.0 - - org.sunbird - samza - 1.1-SNAPSHOT - - course-common - - - org.sunbird - samza-common - 1.1-SNAPSHOT - - - - - - - org.apache.maven.plugins - maven-compiler-plugin - - 1.8 - 1.8 - - - - - diff --git a/platform-jobs/samza/course-common/src/main/java/org/sunbird/jobs/samza/task/BaseTask.java b/platform-jobs/samza/course-common/src/main/java/org/sunbird/jobs/samza/task/BaseTask.java deleted file mode 100644 index 44b985bbd0..0000000000 --- a/platform-jobs/samza/course-common/src/main/java/org/sunbird/jobs/samza/task/BaseTask.java +++ /dev/null @@ -1,189 +0,0 @@ -package org.sunbird.jobs.samza.task; - -import org.apache.commons.lang3.StringUtils; -import org.apache.samza.config.Config; -import org.apache.samza.system.IncomingMessageEnvelope; -import org.apache.samza.system.OutgoingMessageEnvelope; -import org.apache.samza.system.SystemStream; -import org.apache.samza.task.InitableTask; -import org.apache.samza.task.MessageCollector; -import org.apache.samza.task.StreamTask; -import org.apache.samza.task.TaskContext; -import org.apache.samza.task.TaskCoordinator; -import org.apache.samza.task.WindowableTask; -import org.sunbird.common.Platform; -import org.sunbird.jobs.samza.service.ISamzaService; -import org.sunbird.jobs.samza.service.task.JobMetrics; -import org.sunbird.jobs.samza.util.SamzaCommonParams; -import org.sunbird.telemetry.TelemetryGenerator; -import org.sunbird.telemetry.TelemetryParams; -import org.sunbird.telemetry.handler.Level; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.UUID; - -public abstract class BaseTask implements StreamTask, InitableTask, WindowableTask { - protected JobMetrics metrics; - protected Config config = null; - protected String eventId = ""; - protected List action = new ArrayList<>(); - protected String jobStartMessage = ""; - protected String jobEndMessage = ""; - protected String jobClass = ""; - - protected static String mid = "LP."+ UUID.randomUUID(); - protected static String startJobEventId = "JOB_START"; - protected static String endJobEventId = "JOB_END"; - protected static int MAXITERTIONCOUNT= 2; - - @Override - public void init(Config config, TaskContext context) throws Exception { - metrics = new JobMetrics(context, config.get("output.metrics.job.name"), config.get("output.metrics.topic.name")); - this.config = config; - this.eventId = "BE_JOB_REQUEST"; - ISamzaService service = initialize(); - service.initialize(config); - } - - public abstract ISamzaService initialize() throws Exception; - - protected int getMaxIterations() { - if(Platform.config.hasPath("max.iteration.count.samza.job")) - return Platform.config.getInt("max.iteration.count.samza.job"); - else - return MAXITERTIONCOUNT; - } - - @SuppressWarnings("unchecked") - @Override - public void process(IncomingMessageEnvelope envelope, MessageCollector collector, TaskCoordinator coordinator) throws Exception { - Map message = (Map) envelope.getMessage(); - Map execution = new HashMap<>(); - int maxIterations = getMaxIterations(); - String eid = (String) message.get(SamzaCommonParams.eid.name()); - Map edata = (Map) message.getOrDefault(SamzaCommonParams.edata.name(), new HashMap()); - if(StringUtils.equalsIgnoreCase(this.eventId, eid)) { - String action = (String) edata.get(SamzaCommonParams.action.name()); - if(this.action.contains(action)) { - int currentIteration = ((Number) edata.get(SamzaCommonParams.iteration.name())).intValue(); - - preProcess(message, collector, execution, maxIterations, currentIteration); - process(message, collector, coordinator); - postProcess(message, collector, execution, maxIterations, currentIteration); - } else{ - //Throw exception has to be added. - } - } else { - //Throw exception has to be added. - } - } - - public abstract void process(Map message, MessageCollector collector, TaskCoordinator coordinator) throws Exception; - - public void preProcess(Map message, MessageCollector collector, Map execution, int maxIterationCount, int iterationCount) { - if (isInvalidMessage(message)) { - String event = generateEvent(Level.ERROR.name(), "Samza job de-serialization error", message); - collector.send(new OutgoingMessageEnvelope(new SystemStream(SamzaCommonParams.kafka.name(), this.config.get("kafka.topics.backend.telemetry")), event)); - } - try { - if(iterationCount <= maxIterationCount) { - Map jobStartEvent = getJobEvent("JOBSTARTEVENT", message); - - execution.put(SamzaCommonParams.submitted_date.name(), (long)message.get(SamzaCommonParams.ets.name())); - execution.put(SamzaCommonParams.processing_date.name(), (long)jobStartEvent.get(SamzaCommonParams.ets.name())); - execution.put(SamzaCommonParams.latency.name(), (long)jobStartEvent.get(SamzaCommonParams.ets.name()) - (long)message.get(SamzaCommonParams.ets.name())); - - pushEvent(jobStartEvent, collector, this.config.get("kafka.topics.backend.telemetry")); - } - }catch (Exception e) { - e.printStackTrace(); - } - } - - - @SuppressWarnings("unchecked") - public void postProcess(Map message, MessageCollector collector, Map execution, int maxIterationCount, int iterationCount) throws Exception { - try { - if(iterationCount <= maxIterationCount) { - Map jobEndEvent = getJobEvent("JOBENDEVENT", message); - - execution.put(SamzaCommonParams.completed_date.name(), (long)jobEndEvent.get(SamzaCommonParams.ets.name())); - execution.put(SamzaCommonParams.execution_time.name(), (long)jobEndEvent.get(SamzaCommonParams.ets.name()) - (long)execution.get(SamzaCommonParams.processing_date.name())); - Map eks = (Map)((Map)jobEndEvent.get(SamzaCommonParams.edata.name())).get(SamzaCommonParams.eks.name()); - eks.put(SamzaCommonParams.execution.name(), execution); - //addExecutionTime(jobEndEvent, execution); //Call to add execution time - - pushEvent(jobEndEvent, collector, this.config.get("kafka.topics.backend.telemetry")); - } - }catch(Exception e) { - e.printStackTrace(); - } - } - - private void pushEvent(Map message, MessageCollector collector, String topicId) throws Exception { - try { - //TODO: Fix Event Template for "START" & "END" Event and enable below line for backend telemetry. - //collector.send(new OutgoingMessageEnvelope(new SystemStream(SamzaCommonParams.kafka.name(), topicId), message)); - } catch (Exception e) { - e.printStackTrace(); - } - } - - @SuppressWarnings("unchecked") - public Map getJobEvent(String jobEvendID, Map message){ - - long unixTime = System.currentTimeMillis(); - Map jobEvent = new HashMap<>(); - - jobEvent.put(SamzaCommonParams.ets.name(), unixTime); - jobEvent.put(SamzaCommonParams.mid.name(), mid); - - Map edata = new HashMap<>(); - Map eks = new HashMap<>(); - eks.put(SamzaCommonParams.ets.name(), message.get(SamzaCommonParams.ets.name())); - eks.put(SamzaCommonParams.action.name(), ((Map) message.get(SamzaCommonParams.edata.name())).get(SamzaCommonParams.action.name())); - eks.put(SamzaCommonParams.iteration.name(), ((Map) message.get(SamzaCommonParams.edata.name())).get(SamzaCommonParams.iteration.name())); - eks.put(SamzaCommonParams.status.name(), ((Map) message.get(SamzaCommonParams.edata.name())).get(SamzaCommonParams.status.name())); - eks.put(SamzaCommonParams.reqid.name(), message.get(SamzaCommonParams.mid.name())); - edata.put(SamzaCommonParams.eks.name(), eks); - edata.put(SamzaCommonParams.level.name(), SamzaCommonParams.INFO.name()); - edata.put(SamzaCommonParams.jobclass.name(), this.jobClass); - edata.put(SamzaCommonParams.object.name(), message.get("object")); - - - if(StringUtils.equalsIgnoreCase(jobEvendID, "JOBSTARTEVENT")) { - jobEvent.put(SamzaCommonParams.eid.name(), startJobEventId); - edata.put(SamzaCommonParams.message.name(), this.jobStartMessage); - } - else if(StringUtils.equalsIgnoreCase(jobEvendID, "JOBENDEVENT")) { - jobEvent.put(SamzaCommonParams.eid.name(), endJobEventId); - edata.put(SamzaCommonParams.message.name(), this.jobEndMessage); - } - - jobEvent.put(SamzaCommonParams.edata.name(), edata); - return jobEvent; - } - - protected boolean isInvalidMessage(Map message) { - return (message == null || (null != message && message.containsKey("serde") - && "error".equalsIgnoreCase((String) message.get("serde")))); - } - - @Override - public void window(MessageCollector collector, TaskCoordinator coordinator) throws Exception { - Map event = metrics.collect(); - collector.send(new OutgoingMessageEnvelope(new SystemStream("kafka", metrics.getTopic()), event)); - metrics.clear(); - } - - private String generateEvent(String logLevel, String message, Map data) { - Map context = new HashMap(); - context.put(TelemetryParams.ACTOR.name(), "org.sunbird.learning.platform"); - context.put(TelemetryParams.ENV.name(), "content"); - context.put(TelemetryParams.CHANNEL.name(), Platform.config.getString("channel.default")); - return TelemetryGenerator.log(context, "system", logLevel, message); - } -} diff --git a/platform-jobs/samza/course-common/src/main/java/org/sunbird/jobs/samza/util/CassandraConnector.java b/platform-jobs/samza/course-common/src/main/java/org/sunbird/jobs/samza/util/CassandraConnector.java deleted file mode 100644 index 91d3fad667..0000000000 --- a/platform-jobs/samza/course-common/src/main/java/org/sunbird/jobs/samza/util/CassandraConnector.java +++ /dev/null @@ -1,37 +0,0 @@ -package org.sunbird.jobs.samza.util; - -import com.datastax.driver.core.Cluster; -import com.datastax.driver.core.ConsistencyLevel; -import com.datastax.driver.core.QueryOptions; -import com.datastax.driver.core.Session; -import org.apache.samza.config.Config; - -import java.net.InetSocketAddress; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; - -public class CassandraConnector { - private Session session = null; - - public CassandraConnector(Config config) { - List connectionInfo = Arrays.asList(config.get("cassandra.connection.platform_courses", "localhost:9042").split(",")); - List addresses = getSocketAddress(connectionInfo); - session = Cluster.builder().addContactPointsWithPorts(addresses).withQueryOptions(new QueryOptions().setConsistencyLevel(ConsistencyLevel.QUORUM)).build().connect(); - } - - private static List getSocketAddress(List hosts) { - List connectionList = new ArrayList<>(); - for (String connection : hosts) { - String[] conn = connection.split(":"); - String host = conn[0]; - int port = Integer.valueOf(conn[1]); - connectionList.add(new InetSocketAddress(host, port)); - } - return connectionList; - } - - public Session getSession() { - return session; - } -} diff --git a/platform-jobs/samza/course-common/src/main/java/org/sunbird/jobs/samza/util/RedisConnect.java b/platform-jobs/samza/course-common/src/main/java/org/sunbird/jobs/samza/util/RedisConnect.java deleted file mode 100644 index b7120e6e5e..0000000000 --- a/platform-jobs/samza/course-common/src/main/java/org/sunbird/jobs/samza/util/RedisConnect.java +++ /dev/null @@ -1,37 +0,0 @@ -package org.sunbird.jobs.samza.util; - -import org.apache.samza.config.Config; -import redis.clients.jedis.Jedis; - -public class RedisConnect { - - private Config config; - - public RedisConnect(Config config) { - this.config = config; - } - - private Jedis getConnection(long backoffTimeInMillis) { - String redisHost = config.get("redis.host", "localhost"); - Integer redisPort = config.getInt("redis.port", 6379); - if(backoffTimeInMillis > 0) { - try { - Thread.sleep(backoffTimeInMillis); - } catch (InterruptedException e) { - e.printStackTrace(); - } - } - return new Jedis(redisHost, redisPort, 30000); - } - - public Jedis getConnection(int db, long backoffTimeInMillis) { - - Jedis jedis = getConnection(backoffTimeInMillis); - jedis.select(db); - return jedis; - } - - public Jedis getConnection() { - return getConnection(0); - } -} diff --git a/platform-jobs/samza/course-common/src/main/java/org/sunbird/jobs/samza/util/SunbirdCassandraColumnMapper.java b/platform-jobs/samza/course-common/src/main/java/org/sunbird/jobs/samza/util/SunbirdCassandraColumnMapper.java deleted file mode 100644 index 4c3131630b..0000000000 --- a/platform-jobs/samza/course-common/src/main/java/org/sunbird/jobs/samza/util/SunbirdCassandraColumnMapper.java +++ /dev/null @@ -1,56 +0,0 @@ -package org.sunbird.jobs.samza.util; - -import java.util.Map; -import java.util.HashMap; - -public class SunbirdCassandraColumnMapper { - - private static Map COLUMN_MAPPING = new HashMap<>(); - - public static Map getColumnMapping() { - if(COLUMN_MAPPING.isEmpty()) { - //sunbird_courses.user_courses table - COLUMN_MAPPING.put("batchid", "batchId"); - COLUMN_MAPPING.put("userid", "userId"); - COLUMN_MAPPING.put("active", "active"); - COLUMN_MAPPING.put("addedby", "addedBy"); - COLUMN_MAPPING.put("completedon","completedOn"); - COLUMN_MAPPING.put("completionpercentage","completionPercentage"); - COLUMN_MAPPING.put("contentstatus", "contentStatus"); - COLUMN_MAPPING.put("courseid", "courseId"); - COLUMN_MAPPING.put("datetime", "dateTime"); - COLUMN_MAPPING.put("delta", "delta"); - COLUMN_MAPPING.put("enrolleddate","enrolledDate"); - COLUMN_MAPPING.put("grade","grade"); - COLUMN_MAPPING.put("lastreadcontentid", "lastReadContentId"); - COLUMN_MAPPING.put("lastreadcontentstatus", "lastReadContentStatus"); - COLUMN_MAPPING.put("progress","progress"); - COLUMN_MAPPING.put("status","status"); - - //sunbird_courses.content_consumption table - COLUMN_MAPPING.put("contentid", "contentId"); - COLUMN_MAPPING.put("completedcount", "completedCount"); - COLUMN_MAPPING.put("contentversion", "contentVersion"); - COLUMN_MAPPING.put("lastaccesstime", "lastAccessTime"); - COLUMN_MAPPING.put("lastcompletedtime","lastCompletedTime"); - COLUMN_MAPPING.put("lastupdatedtime","lastUpdatedTime"); - COLUMN_MAPPING.put("result", "result"); - COLUMN_MAPPING.put("score", "score"); - COLUMN_MAPPING.put("viewcount", "viewCount"); - - //sunbird_courses.course_batch table - COLUMN_MAPPING.put("createdby", "createdBy"); - COLUMN_MAPPING.put("createddate","createdDate"); - COLUMN_MAPPING.put("createdfor","createdFor"); - COLUMN_MAPPING.put("description", "description"); - COLUMN_MAPPING.put("enddate", "endDate"); - COLUMN_MAPPING.put("enrollmentenddate","enrollmentEndDate"); - COLUMN_MAPPING.put("enrollmenttype","enrollmentType"); - COLUMN_MAPPING.put("mentors", "mentors"); - COLUMN_MAPPING.put("name", "name"); - COLUMN_MAPPING.put("startdate","startDate"); - COLUMN_MAPPING.put("updateddate","updatedDate"); - } - return COLUMN_MAPPING; - } -} diff --git a/platform-jobs/samza/course-common/src/main/java/org/sunbird/jobs/samza/util/SunbirdCassandraUtil.java b/platform-jobs/samza/course-common/src/main/java/org/sunbird/jobs/samza/util/SunbirdCassandraUtil.java deleted file mode 100644 index 29cf94461b..0000000000 --- a/platform-jobs/samza/course-common/src/main/java/org/sunbird/jobs/samza/util/SunbirdCassandraUtil.java +++ /dev/null @@ -1,102 +0,0 @@ -package org.sunbird.jobs.samza.util; - -import com.datastax.driver.core.ResultSet; -import com.datastax.driver.core.Row; -import com.datastax.driver.core.Session; -import com.datastax.driver.core.querybuilder.QueryBuilder; -import com.datastax.driver.core.querybuilder.Select; -import com.datastax.driver.core.querybuilder.Update; -import com.datastax.driver.core.querybuilder.Insert; -import com.datastax.driver.core.querybuilder.Delete; -import org.apache.commons.collections4.CollectionUtils; -import org.apache.commons.collections4.MapUtils; -import org.sunbird.cassandra.connector.util.CassandraConnector; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -public class SunbirdCassandraUtil { - - private static Map COLUMN_MAPPING = SunbirdCassandraColumnMapper.getColumnMapping(); - - public static void update(Session session, String keyspace, String table, Map propertiesToUpdate, Map propertiesToSelect) { - Update.Where updateQuery = QueryBuilder.update(keyspace, table).where(); - propertiesToUpdate.entrySet().forEach(entry -> updateQuery.with(QueryBuilder.set(entry.getKey(), entry.getValue()))); - propertiesToSelect.entrySet().forEach(entry -> { - if (entry.getValue() instanceof List) - updateQuery.and(QueryBuilder.in(entry.getKey(), (List) entry.getValue())); - else - updateQuery.and(QueryBuilder.eq(entry.getKey(), entry.getValue())); - }); - - - session.execute(updateQuery); - } - - public static ResultSet read(Session session, String keyspace, String table, Map propertiesToSelect) { - Select.Where selectQuery = QueryBuilder.select().all().from(keyspace, table).where(); - propertiesToSelect.entrySet().forEach(entry -> { - if (entry.getValue() instanceof List) - selectQuery.and(QueryBuilder.in(entry.getKey(), (List) entry.getValue())); - else - selectQuery.and(QueryBuilder.eq(entry.getKey(), entry.getValue())); - }); - ResultSet results = session.execute(selectQuery); - return results; - } - - public static List> readAsListOfMap(Session session, String keyspace, String table, Map propertiesToSelect) { - ResultSet resultSet = read(session, keyspace, table, convertKeyCase(propertiesToSelect)); - List rows = resultSet.all(); - List> response = new ArrayList>(); - if (CollectionUtils.isNotEmpty(rows)) { - for (Row row : rows) { - Map rowMap = new HashMap(); - row.getColumnDefinitions().forEach(column -> rowMap.put(COLUMN_MAPPING.get(column.getName()), row.getObject(column.getName()))); - response.add(rowMap); - } - } - return response; - } - - public static List> readAsListOfMap(String keyspace, String table, Map propertiesToSelect) { - Session session = CassandraConnector.getSession("platform-courses"); - return readAsListOfMap(session, keyspace, table, propertiesToSelect); - } - - public static void upsert(String keyspace, String table, Map properties) { - Session session = CassandraConnector.getSession("platform-courses"); - Insert insertQuery = QueryBuilder.insertInto(keyspace, table); - convertKeyCase(properties).entrySet().forEach(entry -> insertQuery.value(entry.getKey(), entry.getValue())); - session.execute(insertQuery); - } - - public static void delete(String keyspace, String table, Map properties) { - Session session = CassandraConnector.getSession("platform-courses"); - Delete.Where deleteQuery = QueryBuilder.delete().from(keyspace, table).where(); - convertKeyCase(properties).entrySet().forEach(entry -> { - deleteQuery.and(QueryBuilder.eq(entry.getKey(), entry.getValue())); - }); - session.execute(deleteQuery); - } - - private static Map convertKeyCase(Map properties) { - Map keyLowerCaseMap = new HashMap<>(); - if (MapUtils.isNotEmpty(properties)) { - properties.entrySet().forEach(entry -> { - if (null != entry && null != entry.getKey()) { - keyLowerCaseMap.put(entry.getKey().toLowerCase(), entry.getValue()); - } - }); - } - return keyLowerCaseMap; - } - - public static ResultSet execute(Session cassandraSession, String query) { - ResultSet results = cassandraSession.execute(query); - return results; - } - -} \ No newline at end of file diff --git a/platform-jobs/samza/distribution/.gitignore b/platform-jobs/samza/distribution/.gitignore deleted file mode 100644 index b83d22266a..0000000000 --- a/platform-jobs/samza/distribution/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/target/ diff --git a/platform-jobs/samza/distribution/pom.xml b/platform-jobs/samza/distribution/pom.xml deleted file mode 100644 index e1b8976930..0000000000 --- a/platform-jobs/samza/distribution/pom.xml +++ /dev/null @@ -1,48 +0,0 @@ - - 4.0.0 - - org.sunbird - samza - 1.1-SNAPSHOT - - distribution - pom - Distribution - - - - - - - - - - org.sunbird - mvc-processor-indexer - 1.3.7 - tar.gz - distribution - - - - - - maven-assembly-plugin - - - distro-assembly - package - - single - - - - src/main/assembly/src.xml - - - - - - - - diff --git a/platform-jobs/samza/distribution/src/main/assembly/src.xml b/platform-jobs/samza/distribution/src/main/assembly/src.xml deleted file mode 100644 index 92246c444b..0000000000 --- a/platform-jobs/samza/distribution/src/main/assembly/src.xml +++ /dev/null @@ -1,14 +0,0 @@ - - distribution - false - - tar.gz - - - - - false - false - - - \ No newline at end of file diff --git a/platform-jobs/samza/merge-user-courses/pom.xml b/platform-jobs/samza/merge-user-courses/pom.xml deleted file mode 100644 index e7cb6dc116..0000000000 --- a/platform-jobs/samza/merge-user-courses/pom.xml +++ /dev/null @@ -1,139 +0,0 @@ - - - - samza - org.sunbird - 1.1-SNAPSHOT - - 4.0.0 - - merge-user-courses - - - UTF-8 - 0.12.0 - 2.11 - 2.6.2 - - 0.0.19 - - - org.sunbird - course-common - 1.1-SNAPSHOT - - - org.sunbird - unit-tests - 1.1-SNAPSHOT - test - - - org.mockito - mockito-all - 1.10.19 - test - - - com.fasterxml.jackson.core - jackson-databind - 2.7.8 - - - com.fasterxml.jackson.core - jackson-core - 2.6.0 - - - com.fasterxml.jackson.core - jackson-annotations - 2.7.8 - - - org.powermock - powermock-api-mockito - 1.7.4 - test - - - org.powermock - powermock-module-junit4 - 1.7.4 - test - - - - - - org.apache.maven.plugins - maven-compiler-plugin - - 1.8 - 1.8 - - - - - maven-assembly-plugin - - - src/main/assembly/src.xml - - - - - make-assembly - package - - single - - - - - - org.apache.maven.plugins - maven-surefire-plugin - 2.20 - - - - org.jacoco - jacoco-maven-plugin - 0.7.9 - - - **/common/** - **/dto/** - **/enums/** - **/pipeline/** - **/servlet/** - **/interceptor/** - - - - - default-prepare-agent - - prepare-agent - - - - default-report - prepare-package - - report - - - - report-aggregate - verify - - report-aggregate - - - - - - - \ No newline at end of file diff --git a/platform-jobs/samza/merge-user-courses/src/main/assembly/src.xml b/platform-jobs/samza/merge-user-courses/src/main/assembly/src.xml deleted file mode 100644 index b8c4bf8a85..0000000000 --- a/platform-jobs/samza/merge-user-courses/src/main/assembly/src.xml +++ /dev/null @@ -1,69 +0,0 @@ - - - - - distribution - - tar.gz - - false - - - ${basedir} - - README* - LICENSE* - NOTICE* - - - - - - ${basedir}/src/main/resources/log4j.xml - lib - - - - ${basedir}/src/main/config/merge-user-courses.properties - config - true - - - - - bin - - org.apache.samza:samza-shell:tgz:dist:* - - 0744 - true - - - lib - - org.apache.samza:samza-api - org.sunbird:merge-user-courses - org.apache.samza:samza-core_2.11 - org.apache.samza:samza-kafka_2.11 - org.apache.samza:samza-yarn_2.11 - org.apache.samza:samza-log4j - org.apache.kafka:kafka_2.11 - org.apache.hadoop:hadoop-hdfs - - true - - - \ No newline at end of file diff --git a/platform-jobs/samza/merge-user-courses/src/main/config/local.merge-user-courses.properties b/platform-jobs/samza/merge-user-courses/src/main/config/local.merge-user-courses.properties deleted file mode 100644 index d162cc8e60..0000000000 --- a/platform-jobs/samza/merge-user-courses/src/main/config/local.merge-user-courses.properties +++ /dev/null @@ -1,81 +0,0 @@ -# Job -job.factory.class=org.apache.samza.job.yarn.YarnJobFactory -job.name=local.merge-user-courses - -# YARN -yarn.package.path=file://${basedir}/target/${project.artifactId}-${pom.version}-distribution.tar.gz - -# Metrics -metrics.reporters=snapshot,jmx -metrics.reporter.snapshot.class=org.apache.samza.metrics.reporter.MetricsSnapshotReporterFactory -metrics.reporter.snapshot.stream=kafka.local.metrics -metrics.reporter.jmx.class=org.apache.samza.metrics.reporter.JmxReporterFactory - -# Task -task.class=org.sunbird.jobs.samza.task.MergeUserCoursesTask -task.inputs=kafka.local.lms.user.account.merge -task.checkpoint.factory=org.apache.samza.checkpoint.kafka.KafkaCheckpointManagerFactory -task.checkpoint.system=kafka -task.checkpoint.replication.factor=1 -task.commit.ms=60000 -task.window.ms=300000 - -# Serializers -serializers.registry.json.class=org.sunbird.jobs.samza.serializers.EkstepJsonSerdeFactory -serializers.registry.metrics.class=org.apache.samza.serializers.MetricsSnapshotSerdeFactory - -# Systems -systems.kafka.samza.factory=org.apache.samza.system.kafka.KafkaSystemFactory -systems.kafka.samza.msg.serde=json -systems.kafka.streams.metrics.samza.msg.serde=metrics -systems.kafka.consumer.zookeeper.connect=localhost:2181 -systems.kafka.consumer.auto.offset.reset=smallest -systems.kafka.samza.offset.default=oldest -systems.kafka.producer.bootstrap.servers=localhost:9092 - -# Job Coordinator -job.coordinator.system=kafka - -# Normally, this would be 3, but we have only one broker. -job.coordinator.replication.factor=1 - -# Job specific configuration - -# Metrics -output.metrics.job.name=merge-user-courses -output.metrics.topic.name=local.pipeline_metrics -kafka.topics.backend.telemetry=local.telemetry.raw - -#Failed Topic Config -output.failed.events.topic.name=local.learning.events.failed - -# Retry Topic -kafka.topics.failed=local.lms.user.account.merge - -#Remote Debug Configuration -# task.opts=-agentlib:jdwp=transport=dt_socket,address=localhost:9009,server=y,suspend=y - -# Configuration for default channel ID -channel.default=in.ekstep - -#elastic-search -sunbird_es_cluster=local.lms.es.cluster -sunbird_es_host=127.0.0.1 -sunbird_es_port=9200 - -cassandra.lp.connection=localhost:9042 -cassandra.lpa.connection=localhost:9042 - -cassandra.connection.platform_courses=localhost:9042 -kp.learning_service.base_url=https://dev.sunbirded.org/action -courses.keyspace.name=sunbird_courses -search.es_conn_info=localhost:9200 -job.time_zone=IST -sunbird.installation=local -user.courses.table=user_enrolments -content.consumption.table=user_content_consumption -user.courses.es.index=user-courses -user.courses.es.type=_doc -course.batch.updater.kafka.topic=local.coursebatch.job.request -max.iteration.count.samza.job=2 -course.date.format=yyyy-MM-dd HH:mm:ss:SSSZ \ No newline at end of file diff --git a/platform-jobs/samza/merge-user-courses/src/main/config/merge-user-courses.properties b/platform-jobs/samza/merge-user-courses/src/main/config/merge-user-courses.properties deleted file mode 100644 index f386bc21c3..0000000000 --- a/platform-jobs/samza/merge-user-courses/src/main/config/merge-user-courses.properties +++ /dev/null @@ -1,78 +0,0 @@ -# Job -job.factory.class=org.apache.samza.job.yarn.YarnJobFactory -job.name=__env__.merge-user-courses - -# YARN -yarn.package.path=http://__yarn_host__:__yarn_port__/__env__/${project.artifactId}-${pom.version}-distribution.tar.gz - -# Metrics -metrics.reporters=snapshot,jmx -metrics.reporter.snapshot.class=org.apache.samza.metrics.reporter.MetricsSnapshotReporterFactory -metrics.reporter.snapshot.stream=kafka.__env__.metrics -metrics.reporter.jmx.class=org.apache.samza.metrics.reporter.JmxReporterFactory - -# Task -task.class=org.sunbird.jobs.samza.task.MergeUserCoursesTask -task.inputs=kafka.__env__.lms.user.account.merge -task.checkpoint.factory=org.apache.samza.checkpoint.kafka.KafkaCheckpointManagerFactory -task.checkpoint.system=kafka -task.checkpoint.replication.factor=__samza_checkpoint_replication_factor__ -task.commit.ms=60000 -task.window.ms=300000 - -# Serializers -serializers.registry.json.class=org.sunbird.jobs.samza.serializers.EkstepJsonSerdeFactory -serializers.registry.metrics.class=org.apache.samza.serializers.MetricsSnapshotSerdeFactory - -# Systems -systems.kafka.samza.factory=org.apache.samza.system.kafka.KafkaSystemFactory -systems.kafka.samza.msg.serde=json -systems.kafka.streams.metrics.samza.msg.serde=metrics -systems.kafka.consumer.zookeeper.connect=__zookeepers__ -systems.kafka.consumer.auto.offset.reset=smallest -systems.kafka.samza.offset.default=oldest -systems.kafka.producer.bootstrap.servers=__kafka_brokers__ - -# Job Coordinator -job.coordinator.system=kafka - -# Normally, this would be 3, but we have only one broker. -job.coordinator.replication.factor=__samza_coordinator_replication_factor__ - -# Job specific configuration - -# Metrics -output.metrics.job.name=merge-user-courses -output.metrics.topic.name=__env__.pipeline_metrics -kafka.topics.backend.telemetry=__env__.telemetry.raw - -#Failed Topic Config -output.failed.events.topic.name=__env__.learning.events.failed - -# Retry Topic -kafka.topics.failed=__env__.lms.user.account.merge - -# Configuration for default channel ID -channel.default=in.ekstep - -#elastic-search -sunbird_es_cluster=__lms_es_cluster__ -sunbird_es_host=__lms_es_host__ -sunbird_es_port=__lms_es_port__ - -cassandra.lp.connection=__cassandra_lp_connection__ -cassandra.lpa.connection=__cassandra_lpa_connection__ - -cassandra.connection.platform_courses=__cassandra_sunbird_connection__ -kp.learning_service.base_url=__kp_learning_service_base_url__ -courses.keyspace.name=sunbird_courses -search.es_conn_info=__search_lms_es_host__ -job.time_zone=IST -sunbird.installation=__sunbird_installation__ -user.courses.table=user_enrolments -content.consumption.table=user_content_consumption -user.courses.es.index=user-courses -user.courses.es.type=_doc -course.batch.updater.kafka.topic=__env__.coursebatch.job.request -max.iteration.count.samza.job=__max_iteration_count_for_samza_job__ -course.date.format=yyyy-MM-dd HH:mm:ss:SSSZ \ No newline at end of file diff --git a/platform-jobs/samza/merge-user-courses/src/main/java/org/sunbird/jobs/samza/model/BatchEnrollmentSyncModel.java b/platform-jobs/samza/merge-user-courses/src/main/java/org/sunbird/jobs/samza/model/BatchEnrollmentSyncModel.java deleted file mode 100644 index 7e29db4a19..0000000000 --- a/platform-jobs/samza/merge-user-courses/src/main/java/org/sunbird/jobs/samza/model/BatchEnrollmentSyncModel.java +++ /dev/null @@ -1,32 +0,0 @@ -package org.sunbird.jobs.samza.model; - -public class BatchEnrollmentSyncModel { - - private String batchId; - private String userId; - private String courseId; - - public String getBatchId() { - return batchId; - } - - public void setBatchId(String batchId) { - this.batchId = batchId; - } - - public String getUserId() { - return userId; - } - - public void setUserId(String userId) { - this.userId = userId; - } - - public String getCourseId() { - return courseId; - } - - public void setCourseId(String courseId) { - this.courseId = courseId; - } -} diff --git a/platform-jobs/samza/merge-user-courses/src/main/java/org/sunbird/jobs/samza/service/MergeUserCoursesService.java b/platform-jobs/samza/merge-user-courses/src/main/java/org/sunbird/jobs/samza/service/MergeUserCoursesService.java deleted file mode 100644 index 91bf959f85..0000000000 --- a/platform-jobs/samza/merge-user-courses/src/main/java/org/sunbird/jobs/samza/service/MergeUserCoursesService.java +++ /dev/null @@ -1,460 +0,0 @@ -package org.sunbird.jobs.samza.service; - -import com.datastax.driver.core.RegularStatement; -import com.datastax.driver.core.Session; -import com.datastax.driver.core.querybuilder.Batch; -import com.datastax.driver.core.querybuilder.QueryBuilder; -import com.datastax.driver.core.querybuilder.Update; -import com.fasterxml.jackson.databind.ObjectMapper; -import org.apache.commons.collections4.CollectionUtils; -import org.apache.commons.collections4.MapUtils; -import org.apache.commons.lang3.StringUtils; -import org.apache.samza.config.Config; -import org.apache.samza.system.OutgoingMessageEnvelope; -import org.apache.samza.system.SystemStream; -import org.apache.samza.task.MessageCollector; -import org.sunbird.common.Platform; -import org.sunbird.common.exception.ClientException; -import org.sunbird.jobs.samza.exception.PlatformErrorCodes; -import org.sunbird.jobs.samza.service.ISamzaService; -import org.sunbird.jobs.samza.service.task.JobMetrics; -import org.sunbird.jobs.samza.util.FailedEventsUtil; -import org.sunbird.jobs.samza.util.JSONUtils; -import org.sunbird.jobs.samza.util.JobLogger; -import org.sunbird.searchindex.elasticsearch.ElasticSearchUtil; -import org.sunbird.jobs.samza.model.BatchEnrollmentSyncModel; -import org.sunbird.jobs.samza.util.CassandraConnector; -import org.sunbird.jobs.samza.util.MergeUserCoursesParams; -import org.sunbird.jobs.samza.util.SunbirdCassandraUtil; - -import java.text.ParseException; -import java.text.SimpleDateFormat; -import java.util.*; -import java.util.stream.Collectors; - -public class MergeUserCoursesService implements ISamzaService { - private static JobLogger LOGGER = new JobLogger(MergeUserCoursesService.class); - private SystemStream systemStream; - private Config config = null; - private static final String UNDERSCORE = "_"; - private ObjectMapper mapper = new ObjectMapper(); - private static final String ACTION = "merge-user-courses-and-cert"; - private static int MAXITERTIONCOUNT = 2; - - private static String KEYSPACE; - private static String CONTENT_CONSUMPTION_TABLE; - private static String USER_COURSES_TABLE; - private static String USER_COURSE_ES_INDEX; - private static String USER_COURSE_ES_TYPE; - private static String COURSE_BATCH_UPDATER_KAFKA_TOPIC; - private static String COURSE_DATE_FORMAT; - private static SimpleDateFormat DateFormatter; - private static String USER_ACTIVITY_AGG; - private Session cassandraSession = null; - - protected int getMaxIterations() { - if (Platform.config.hasPath("max.iteration.count.samza.job")) - return Platform.config.getInt("max.iteration.count.samza.job"); - else - return MAXITERTIONCOUNT; - } - - private boolean validateObject(Map edata) { - String action = (String) edata.get(MergeUserCoursesParams.action.name()); - Integer iteration = (Integer) edata.get(MergeUserCoursesParams.iteration.name()); - if (StringUtils.equalsIgnoreCase(ACTION, action) && (iteration <= getMaxIterations())) { - return true; - } - return false; - } - - private static void initializeConfigurations() { - KEYSPACE = Platform.config.hasPath("courses.keyspace.name") ? - Platform.config.getString("courses.keyspace.name") : "sunbird_courses"; - - CONTENT_CONSUMPTION_TABLE = Platform.config.hasPath("content.consumption.table") ? - Platform.config.getString("content.consumption.table") : "user_content_consumption"; - - USER_COURSES_TABLE = Platform.config.hasPath("user.courses.table") ? - Platform.config.getString("user.courses.table") : "user_enrolments"; - - USER_COURSE_ES_INDEX = Platform.config.hasPath("user.courses.es.index") ? - Platform.config.getString("user.courses.es.index") : "user-courses"; - - USER_COURSE_ES_TYPE = Platform.config.hasPath("user.courses.es.type") ? - Platform.config.getString("user.courses.es.type") : "_doc"; - - COURSE_BATCH_UPDATER_KAFKA_TOPIC = Platform.config.getString("course.batch.updater.kafka.topic"); - - COURSE_DATE_FORMAT = Platform.config.hasPath("course.date.format") ? - Platform.config.getString("course.date.format") : "yyyy-MM-dd HH:mm:ss:SSSZ"; - - USER_ACTIVITY_AGG = "user_activity_agg"; - - DateFormatter = new SimpleDateFormat(COURSE_DATE_FORMAT); - } - - @Override - public void initialize(Config config) throws Exception { - this.config = config; - JSONUtils.loadProperties(config); - initializeConfigurations(); - this.cassandraSession = new CassandraConnector(config).getSession(); - LOGGER.info("MergeUserCoursesService:initialize: Service config initialized"); - ElasticSearchUtil.initialiseESClient(USER_COURSE_ES_INDEX, Platform.config.getString("search.es_conn_info")); - LOGGER.info("MergeUserCoursesService:initialize: ESClient initialized for index:" + USER_COURSE_ES_INDEX); - systemStream = new SystemStream("kafka", config.get("output.failed.events.topic.name")); - LOGGER.info("MergeUserCoursesService:initialize: Stream initialized for Failed Events"); - } - - @Override - public void processMessage(Map message, JobMetrics metrics, MessageCollector collector) throws Exception { - if (MapUtils.isEmpty(message)) { - LOGGER.info("MergeUserCoursesService:processMessage: Ignoring the event since message is empty."); - FailedEventsUtil.pushEventForRetry(systemStream, message, metrics, collector, - PlatformErrorCodes.DATA_ERROR.name(), new ClientException("ERR_MERGE_USER_COURSES_SAMZA", "message is empty")); - metrics.incSkippedCounter(); - return; - } - - Map edata = (Map) message.get(MergeUserCoursesParams.edata.name()); - if (MapUtils.isEmpty(edata)) { - LOGGER.info("MergeUserCoursesService:processMessage: Ignoring the event since edata is empty."); - FailedEventsUtil.pushEventForRetry(systemStream, message, metrics, collector, - PlatformErrorCodes.DATA_ERROR.name(), new ClientException("ERR_MERGE_USER_COURSES_SAMZA", "message.edata is empty")); - metrics.incSkippedCounter(); - return; - } - - String fromUserId = (String) edata.get(MergeUserCoursesParams.fromAccountId.name()); - String toUserId = (String) edata.get(MergeUserCoursesParams.toAccountId.name()); - - if (StringUtils.isBlank(fromUserId) || StringUtils.isBlank(toUserId) || !validateObject(edata)) { - LOGGER.info("MergeUserCoursesService:processMessage: Ignoring the event due to invalid edata:" + edata); - FailedEventsUtil.pushEventForRetry(systemStream, message, metrics, collector, - PlatformErrorCodes.DATA_ERROR.name(), new ClientException("ERR_MERGE_USER_COURSES_SAMZA", "message.edata values are not valid")); - metrics.incSkippedCounter(); - return; - } - - try { - mergeContentConsumption(fromUserId, toUserId); - mergeUserBatches(fromUserId, toUserId); - generateBatchEnrollmentSyncEvents(toUserId, collector); - mergeUserActivityAggregates(fromUserId, toUserId); - metrics.incSuccessCounter(); - LOGGER.info("MergeUserCoursesService:processMessage: Event processed successfully", message); - } catch (Exception e) { - edata.put(MergeUserCoursesParams.status.name(), MergeUserCoursesParams.FAILED.name()); - FailedEventsUtil.pushEventForRetry(systemStream, message, metrics, collector, - PlatformErrorCodes.PROCESSING_ERROR.name(), e); - throw e; - } - - } - - private void generateBatchEnrollmentSyncEvents(String userId, MessageCollector collector) throws Exception { - List objects = getBatchDetailsOfUser(userId); - if (CollectionUtils.isNotEmpty(objects)) { - for (BatchEnrollmentSyncModel model : objects) { - Map event = getBatchEnrollmentSyncEvent(model); - collector.send(new OutgoingMessageEnvelope(new SystemStream("kafka", COURSE_BATCH_UPDATER_KAFKA_TOPIC), event)); - } - } - } - - private void mergeUserBatches(String fromUserId, String toUserId) throws Exception { - List fromBatches = getBatchDetailsOfUser(fromUserId); - List toBatches = getBatchDetailsOfUser(toUserId); - - Map fromBatchIds = new HashMap<>(); - Map toBatchIds = new HashMap<>(); - if (CollectionUtils.isNotEmpty(fromBatches)) { - for (BatchEnrollmentSyncModel fromBatch : fromBatches) { - if (StringUtils.isNotBlank(fromBatch.getBatchId())) - fromBatchIds.put(fromBatch.getBatchId(), fromBatch); - } - } - if (CollectionUtils.isNotEmpty(toBatches)) { - for (BatchEnrollmentSyncModel toBatch : toBatches) { - if (StringUtils.isNotBlank(toBatch.getBatchId())) - toBatchIds.put(toBatch.getBatchId(), toBatch); - } - } - - List batchIdsToBeMigrated = (List) CollectionUtils.subtract(fromBatchIds.keySet(), toBatchIds.keySet()); - - //Migrate batch records in Cassandra and ES - if (CollectionUtils.isNotEmpty(batchIdsToBeMigrated)) { - for (String batchId : batchIdsToBeMigrated) { - String courseId = fromBatchIds.get(batchId).getCourseId(); - Map userCourse = getUserCourse(batchId, fromUserId, courseId); - if (MapUtils.isNotEmpty(userCourse)) { - userCourse.put(MergeUserCoursesParams.userId.name(), toUserId); - LOGGER.info("MergeUserCoursesService:mergeUserBatches: Merging batch:" + batchId + " updated record:" + userCourse); - SunbirdCassandraUtil.upsert(KEYSPACE, USER_COURSES_TABLE, userCourse); - - /*String documentJson = ElasticSearchUtil.getDocumentAsStringById(USER_COURSE_ES_INDEX, USER_COURSE_ES_TYPE, - batchId + UNDERSCORE + fromUserId); - Map userCourseDoc = mapper.readValue(documentJson, Map.class); - userCourseDoc.put(MergeUserCoursesParams.userId.name(), toUserId); - userCourseDoc.put(MergeUserCoursesParams.id.name(), batchId + UNDERSCORE + toUserId); - userCourseDoc.put(MergeUserCoursesParams.identifier.name(), batchId + UNDERSCORE + toUserId); - ElasticSearchUtil.addDocumentWithId(USER_COURSE_ES_INDEX, USER_COURSE_ES_TYPE, - batchId + UNDERSCORE + toUserId, mapper.writeValueAsString(userCourseDoc));*/ - } else { - LOGGER.info("MergeUserCoursesService:mergeUserBatches: user_courses record with batchId:" + batchId + " userId:" + fromUserId + " found in ES but not in Cassandra"); - } - } - } - } - - private void mergeContentConsumption(String fromUserId, String toUserId) { - //Get content consumption data - List> fromContentConsumptionList = getContentConsumption(fromUserId); - List> toContentConsumptionList = getContentConsumption(toUserId); - - if (CollectionUtils.isNotEmpty(fromContentConsumptionList)) { - for (Map contentConsumption : fromContentConsumptionList) { - Map matchingRecord = getMatchingRecord(contentConsumption, toContentConsumptionList); - if (MapUtils.isEmpty(matchingRecord)) { - matchingRecord = contentConsumption; - matchingRecord.put(MergeUserCoursesParams.userId.name(), toUserId); - } else { - mergeContentConsumptionRecord(contentConsumption, matchingRecord); - } - SunbirdCassandraUtil.upsert(KEYSPACE, CONTENT_CONSUMPTION_TABLE, matchingRecord); - } - } - } - - private void mergeContentConsumptionRecord(Map oldRecord, Map newRecord) { - /* - * for status, progress, datetime, lastaccesstime, lastcompletedtime, lastupdatedtime fields, - * max value should be considered - * for completedcount, viewcount fields, sum of both records should be considered - * */ - newRecord.put(MergeUserCoursesParams.status.name(), getUpdatedValue("Integer", "Max", - MergeUserCoursesParams.status.name(), oldRecord, newRecord)); - newRecord.put(MergeUserCoursesParams.progress.name(), getUpdatedValue("Integer", "Max", - MergeUserCoursesParams.progress.name(), oldRecord, newRecord)); - newRecord.put(MergeUserCoursesParams.viewCount.name(), getUpdatedValue("Integer", "Sum", - MergeUserCoursesParams.viewCount.name(), oldRecord, newRecord)); - newRecord.put(MergeUserCoursesParams.completedCount.name(), getUpdatedValue("Integer", "Sum", - MergeUserCoursesParams.completedCount.name(), oldRecord, newRecord)); - - newRecord.put(MergeUserCoursesParams.dateTime.name(), getUpdatedValue("Date", "Max", - MergeUserCoursesParams.dateTime.name(), oldRecord, newRecord)); - newRecord.put(MergeUserCoursesParams.lastAccessTime.name(), getUpdatedValue("DateString", "Max", - MergeUserCoursesParams.lastAccessTime.name(), oldRecord, newRecord)); - newRecord.put(MergeUserCoursesParams.lastCompletedTime.name(), getUpdatedValue("DateString", "Max", - MergeUserCoursesParams.lastCompletedTime.name(), oldRecord, newRecord)); - newRecord.put(MergeUserCoursesParams.lastUpdatedTime.name(), getUpdatedValue("DateString", "Max", - MergeUserCoursesParams.lastUpdatedTime.name(), oldRecord, newRecord)); - } - - private Object getUpdatedValue(String dataType, String operation, String fieldName, Map oldRecord, Map newRecord) { - if (null == oldRecord.get(fieldName)) { - return newRecord.get(fieldName); - } - if (null == newRecord.get(fieldName)) { - return oldRecord.get(fieldName); - } - switch (dataType) { - case "Integer": - if (oldRecord.get(fieldName) instanceof Integer && - newRecord.get(fieldName) instanceof Integer) { - int val1 = (int) oldRecord.get(fieldName); - int val2 = (int) newRecord.get(fieldName); - if (StringUtils.equalsIgnoreCase("Sum", operation)) { - return val1 + val2; - } else if (StringUtils.equalsIgnoreCase("Max", operation)) { - return val1 > val2 ? val1 : val2; - } - } - break; - case "DateString": - if (oldRecord.get(fieldName) instanceof String && - newRecord.get(fieldName) instanceof String) { - String dateStr1 = (String) oldRecord.get(fieldName); - String dateStr2 = (String) newRecord.get(fieldName); - Date date1; - Date date2; - try { - date1 = DateFormatter.parse(dateStr1); - } catch (ParseException pe) { - LOGGER.info("MergeUserCoursesService:getUpdatedValue: Date Parsing failed for field:" + fieldName + " value:" + dateStr1); - return dateStr2; - } - try { - date2 = DateFormatter.parse(dateStr2); - } catch (ParseException pe) { - LOGGER.info("MergeUserCoursesService:getUpdatedValue: Date Parsing failed for field:" + fieldName + " value:" + dateStr2); - return dateStr1; - } - if (StringUtils.equalsIgnoreCase("Max", operation)) { - if (date1.after(date2)) { - return dateStr1; - } else { - return dateStr2; - } - } - } - break; - case "Date": - if (oldRecord.get(fieldName) instanceof Date && - newRecord.get(fieldName) instanceof Date) { - Date date1 = (Date) oldRecord.get(fieldName); - Date date2 = (Date) newRecord.get(fieldName); - if (StringUtils.equalsIgnoreCase("Max", operation)) { - if (date1.after(date2)) { - return date1; - } else { - return date2; - } - } - } - break; - } - return newRecord.get(fieldName); - } - - private Map getMatchingRecord(Map contentConsumption, List> toContentConsumptionList) { - Map matchingRecord = new HashMap(); - if (CollectionUtils.isNotEmpty(toContentConsumptionList)) { - for (Map toContentConsumption : toContentConsumptionList) { - if (StringUtils.equalsIgnoreCase((String) contentConsumption.get(MergeUserCoursesParams.contentId.name()), (String) toContentConsumption.get(MergeUserCoursesParams.contentId.name())) && - StringUtils.equalsIgnoreCase((String) contentConsumption.get(MergeUserCoursesParams.batchId.name()), (String) toContentConsumption.get(MergeUserCoursesParams.batchId.name())) && - StringUtils.equalsIgnoreCase((String) contentConsumption.get(MergeUserCoursesParams.courseId.name()), (String) toContentConsumption.get(MergeUserCoursesParams.courseId.name()))) { - matchingRecord = toContentConsumption; - break; - } - } - } - return matchingRecord; - } - - private List> getContentConsumption(String userId) { - Map key = new HashMap<>(); - key.put(MergeUserCoursesParams.userId.name(), userId); - return SunbirdCassandraUtil.readAsListOfMap(KEYSPACE, CONTENT_CONSUMPTION_TABLE, key); - } - - private Map getUserCourse(String batchId, String userId, String courseId) { - Map key = new HashMap<>(); - key.put(MergeUserCoursesParams.batchId.name(), batchId); - key.put(MergeUserCoursesParams.userId.name(), userId); - key.put(MergeUserCoursesParams.courseId.name(), courseId); - List> data = SunbirdCassandraUtil.readAsListOfMap(KEYSPACE, USER_COURSES_TABLE, key); - return CollectionUtils.isEmpty(data) ? new HashMap() : data.get(0); - } - - private List getBatchDetailsOfUser(String userId) throws Exception { - List objects = new ArrayList<>(); - Map searchQuery = new HashMap<>(); - List userIdList = new ArrayList<>(); - userIdList.add(userId); - searchQuery.put(MergeUserCoursesParams.userId.name(), userIdList); - Map key = new HashMap<>(); - key.put(MergeUserCoursesParams.userId.name(), userIdList); - List> documents = SunbirdCassandraUtil.readAsListOfMap(KEYSPACE, USER_COURSES_TABLE, key); - //List documents = ElasticSearchUtil.textSearchReturningId(searchQuery, USER_COURSE_ES_INDEX, USER_COURSE_ES_TYPE); - if (CollectionUtils.isNotEmpty(documents)) { - documents.forEach(doc -> { - BatchEnrollmentSyncModel model = new BatchEnrollmentSyncModel(); - model.setBatchId((String) doc.get(MergeUserCoursesParams.batchId.name())); - model.setUserId((String) doc.get(MergeUserCoursesParams.userId.name())); - model.setCourseId((String) doc.get(MergeUserCoursesParams.courseId.name())); - objects.add(model); - }); - } - return objects; - } - - private Map getBatchEnrollmentSyncEvent(BatchEnrollmentSyncModel model) { - return new HashMap() {{ - put("actor", new HashMap() {{ - put("id", "Course Batch Updater"); - put("type", "System"); - }}); - put("eid", "BE_JOB_REQUEST"); - put("edata", new HashMap() {{ - put("action", "batch-enrolment-sync"); - put("iteration", 1); - put("batchId", model.getBatchId()); - put("userId", model.getUserId()); - put("courseId", model.getCourseId()); - put("reset", Arrays.asList("completionPercentage", "status", "progress")); - }}); - put("ets", System.currentTimeMillis()); - put("context", new HashMap() {{ - put("pdata", new HashMap() {{ - put("ver", "1.0"); - put("id", "org.sunbird.platform"); - }}); - }}); - put("mid", "LP." + System.currentTimeMillis() + "." + UUID.randomUUID()); - put("object", new HashMap() {{ - put("id", model.getBatchId() + UNDERSCORE + model.getUserId()); - put("type", "CourseBatchEnrolment"); - }}); - }}; - } - - - private void mergeUserActivityAggregates(String fromUserId, String toUserId) throws Exception { - List fromBatches = getBatchDetailsOfUser(fromUserId); - if(CollectionUtils.isNotEmpty(fromBatches)) { - List fromCourseIds = fromBatches.stream().map(enrol -> enrol.getCourseId()).collect(Collectors.toList()); - List toCourseIds = fromBatches.stream().map(enrol -> enrol.getCourseId()).collect(Collectors.toList()); - Map key = new HashMap<>(); - key.put(MergeUserCoursesParams.activity_type.name(), "Course"); - key.put(MergeUserCoursesParams.user_id.name(), fromUserId); - key.put(MergeUserCoursesParams.activity_id.name(), fromCourseIds); - List> fromData = SunbirdCassandraUtil.readAsListOfMap(KEYSPACE, USER_ACTIVITY_AGG, key); - key.put(MergeUserCoursesParams.activity_id.name(), toCourseIds); - List> toData = SunbirdCassandraUtil.readAsListOfMap(KEYSPACE, USER_ACTIVITY_AGG, key); - Map toDataMap = toData.stream().collect(Collectors.toMap(m -> (String)m.get("context_id"), m -> m)); - List updateQueryList = new ArrayList<>(); - if(CollectionUtils.isNotEmpty(fromData)) { - fromData.stream().filter(data -> MapUtils.isNotEmpty(data)).collect(Collectors.toList()).forEach(data -> { - data.put(MergeUserCoursesParams.user_id.name(), toUserId); - Map fromAgg = (Map) data.get("agg"); - Map toAgg = (Map) ((Map)toDataMap.getOrDefault(data.get("context_id"), new HashMap())).getOrDefault("agg", new HashMap()); - data.put("agg", new HashMap(){{ - put("completedCount", Math.max(fromAgg.getOrDefault("completedCount", 0), toAgg.getOrDefault("completedCount", 0))); - }}); - data.put("agg_last_updated", new HashMap(){{ - put("completedCount", new Date()); - }}); - Map dataToSelect = new HashMap() {{ - put(MergeUserCoursesParams.activity_type.name(), "Course"); - put(MergeUserCoursesParams.activity_id.name(), data.get("activity_id")); - put(MergeUserCoursesParams.user_id.name(), toUserId); - put("context_id", data.get("context_id")); - }}; - updateQueryList.add(updateQuery(KEYSPACE, USER_ACTIVITY_AGG, data, dataToSelect)); - }); - } - if(CollectionUtils.isNotEmpty(updateQueryList)){ - Batch batch = QueryBuilder.batch(updateQueryList.toArray(new RegularStatement[updateQueryList.size()])); - cassandraSession.execute(batch); - } - } - - } - - - public Update.Where updateQuery(String keyspace, String table, Map propertiesToUpdate, Map propertiesToSelect) { - Update.Where updateQuery = QueryBuilder.update(keyspace, table).where(); - propertiesToUpdate.entrySet().forEach(entry -> updateQuery.with(QueryBuilder.set(entry.getKey(), entry.getValue()))); - propertiesToSelect.entrySet().forEach(entry -> { - if (entry.getValue() instanceof List) - updateQuery.and(QueryBuilder.in(entry.getKey(), (List) entry.getValue())); - else - updateQuery.and(QueryBuilder.eq(entry.getKey(), entry.getValue())); - }); - return updateQuery; - } - -} diff --git a/platform-jobs/samza/merge-user-courses/src/main/java/org/sunbird/jobs/samza/task/MergeUserCoursesTask.java b/platform-jobs/samza/merge-user-courses/src/main/java/org/sunbird/jobs/samza/task/MergeUserCoursesTask.java deleted file mode 100644 index 3c360c690b..0000000000 --- a/platform-jobs/samza/merge-user-courses/src/main/java/org/sunbird/jobs/samza/task/MergeUserCoursesTask.java +++ /dev/null @@ -1,40 +0,0 @@ -package org.sunbird.jobs.samza.task; - - -import org.apache.samza.task.MessageCollector; -import org.apache.samza.task.TaskCoordinator; -import org.sunbird.jobs.samza.service.ISamzaService; -import org.sunbird.jobs.samza.util.JobLogger; -import org.sunbird.jobs.samza.service.MergeUserCoursesService; - -import java.util.Arrays; -import java.util.Map; - -public class MergeUserCoursesTask extends BaseTask { - - private ISamzaService service = new MergeUserCoursesService(); - private static JobLogger LOGGER = new JobLogger(MergeUserCoursesTask.class); - - @Override - public ISamzaService initialize() throws Exception { - LOGGER.info("MergeUserCoursesTask:initialize: Task initialized"); - this.action = Arrays.asList("merge-user-courses-and-cert"); - this.jobStartMessage = "Started processing of merge-user-courses samza job"; - this.jobEndMessage = "merge-user-courses job processing complete"; - this.jobClass = "org.sunbird.jobs.samza.task.MergeUserCoursesTask"; - return service; - } - - @Override - public void process(Map message, MessageCollector collector, TaskCoordinator coordinator) throws Exception { - try { - LOGGER.info("MergeUserCoursesTask:process: Starting to process for mid : " + message.get("mid") + " at :: " + System.currentTimeMillis()); - service.processMessage(message, metrics, collector); - LOGGER.info("MergeUserCoursesTask:process: Successfully completed processing for mid : " + message.get("mid") + " at :: " + System.currentTimeMillis()); - } catch (Exception e) { - metrics.incErrorCounter(); - LOGGER.error("MergeUserCoursesTask:process: Message processing failed", message, e); - } - } - -} diff --git a/platform-jobs/samza/merge-user-courses/src/main/java/org/sunbird/jobs/samza/util/MergeUserCoursesParams.java b/platform-jobs/samza/merge-user-courses/src/main/java/org/sunbird/jobs/samza/util/MergeUserCoursesParams.java deleted file mode 100644 index 8645c89bcd..0000000000 --- a/platform-jobs/samza/merge-user-courses/src/main/java/org/sunbird/jobs/samza/util/MergeUserCoursesParams.java +++ /dev/null @@ -1,9 +0,0 @@ -package org.sunbird.jobs.samza.util; - -import org.sunbird.graph.dac.util.RelationType; - -public enum MergeUserCoursesParams { - userId, batchId, contentId, courseId, status, edata, id, identifier, action, fromAccountId, - toAccountId, FAILED, iteration, progress, dateTime, lastAccessTime, lastCompletedTime, - lastUpdatedTime, completedCount, viewCount, activity_type, activity_id, user_id; -} diff --git a/platform-jobs/samza/merge-user-courses/src/main/resources/log4j.xml b/platform-jobs/samza/merge-user-courses/src/main/resources/log4j.xml deleted file mode 100644 index 0f37824c0c..0000000000 --- a/platform-jobs/samza/merge-user-courses/src/main/resources/log4j.xml +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/platform-jobs/samza/mvc-processor-indexer/pom.xml b/platform-jobs/samza/mvc-processor-indexer/pom.xml deleted file mode 100644 index 38fb46df62..0000000000 --- a/platform-jobs/samza/mvc-processor-indexer/pom.xml +++ /dev/null @@ -1,114 +0,0 @@ - - - 4.0.0 - - org.sunbird - samza - 1.1-SNAPSHOT - - 1.3.7 - mvc-processor-indexer - - - - org.mockito - mockito-all - 1.10.19 - test - - - org.sunbird - unit-tests - 1.1-SNAPSHOT - test - - - org.sunbird - samza-common - 1.1-SNAPSHOT - - - io.netty - netty-transport - - - io.netty - netty - - - io.netty - netty-handler - - - org.sunbird - searchindex-elasticsearch - - - - - org.sunbird - mvcsearchindex-elasticsearch - 1.1-SNAPSHOT - jar - - - org.apache.logging.log4j - log4j-api - - - org.apache.logging.log4j - log4j-core - - - - - io.netty - netty-all - 4.1.16.Final - - - org.powermock - powermock-module-junit4 - 1.7.4 - test - - - org.powermock - powermock-api-mockito - 1.7.4 - test - - - - - - org.apache.maven.plugins - maven-compiler-plugin - - 1.8 - 1.8 - - - - - maven-assembly-plugin - - - src/main/assembly/src.xml - - - - - make-assembly - package - - single - - - - - - - \ No newline at end of file diff --git a/platform-jobs/samza/mvc-processor-indexer/src/main/assembly/src.xml b/platform-jobs/samza/mvc-processor-indexer/src/main/assembly/src.xml deleted file mode 100644 index c6a072bd63..0000000000 --- a/platform-jobs/samza/mvc-processor-indexer/src/main/assembly/src.xml +++ /dev/null @@ -1,70 +0,0 @@ - - - - - distribution - - tar.gz - - false - - - ${basedir} - - README* - LICENSE* - NOTICE* - - - - - - ${basedir}/src/main/resources/log4j.xml - lib - - - - - ${basedir}/src/main/config/mvc-processor-indexer.properties - config - true - - - - - bin - - org.apache.samza:samza-shell:tgz:dist:* - - 0744 - true - - - lib - - org.apache.samza:samza-api - org.sunbird:mvc-processor-indexer - org.apache.samza:samza-core_2.11 - org.apache.samza:samza-kafka_2.11 - org.apache.samza:samza-yarn_2.11 - org.apache.samza:samza-log4j - org.apache.kafka:kafka_2.11 - org.apache.hadoop:hadoop-hdfs - - true - - - diff --git a/platform-jobs/samza/mvc-processor-indexer/src/main/config/local.mvc-processor-indexer.properties b/platform-jobs/samza/mvc-processor-indexer/src/main/config/local.mvc-processor-indexer.properties deleted file mode 100644 index 8525e9692a..0000000000 --- a/platform-jobs/samza/mvc-processor-indexer/src/main/config/local.mvc-processor-indexer.properties +++ /dev/null @@ -1,78 +0,0 @@ -# Job -job.factory.class=org.apache.samza.job.yarn.YarnJobFactory -job.name=local.mvc-processor-indexer - -# YARN -yarn.package.path=file://${basedir}/target/${project.artifactId}-${pom.version}-distribution.tar.gz - -# Metrics -metrics.reporters=snapshot,jmx -metrics.reporter.snapshot.class=org.apache.samza.metrics.reporter.MetricsSnapshotReporterFactory -metrics.reporter.snapshot.stream=kafka.metrics -metrics.reporter.jmx.class=org.apache.samza.metrics.reporter.JmxReporterFactory - -# Task -task.class=org.sunbird.mvcjobs.samza.task.MVCSearchIndexerTask -task.inputs=kafka.local.mvc.processor.job.request -task.checkpoint.factory=org.apache.samza.checkpoint.kafka.KafkaCheckpointManagerFactory -task.checkpoint.system=kafka -task.checkpoint.replication.factor=1 -task.commit.ms=60000 -task.window.ms=300000 -task.broadcast.inputs=kafka.dev.system.command#0 - -# Serializers -serializers.registry.json.class=org.sunbird.jobs.samza.serializers.EkstepJsonSerdeFactory -serializers.registry.metrics.class=org.apache.samza.serializers.MetricsSnapshotSerdeFactory - -# Systems -systems.kafka.samza.factory=org.apache.samza.system.kafka.KafkaSystemFactory -systems.kafka.samza.msg.serde=json -systems.kafka.streams.metrics.samza.msg.serde=metrics -systems.kafka.consumer.zookeeper.connect=localhost:2181 -systems.kafka.consumer.auto.offset.reset=smallest -systems.kafka.producer.bootstrap.servers=localhost:9092 - -# Job Coordinator -job.coordinator.system=kafka -# Normally, this would be 3, but we have only one broker. -job.coordinator.replication.factor=1 - -# Job specific config properties -search.es_conn_info=localhost:9200 -platform-api-url= -ekstepPlatformApiUserId=system - -content.keyspace.name=content_store -cassandra.lp.connection=127.0.0.1:9042 - -# Consistency Level for Multi Node Cassandra cluster -cassandra.lp.consistency.level=QUORUM - -# Metrics -output.metrics.job.name=mvc-processor-indexer -output.metrics.topic.name=local.pipeline_metrics - -# Nested Fields -nested.fields=badgeAssertions,targets,badgeAssociations,plugins,batches - -#Failed Topic Config -output.failed.events.topic.name=local.mvc.events.failed - -#Remote Debug Configuration -task.opts=-agentlib:jdwp=transport=dt_socket,address=localhost:9009,server=y,suspend=y - -telemetry_env=local -installation.id=local - -# Configuration for default channel ID -channel.default=in.ekstep - -# Definition update window -definitions.update.window.ms=300000 - -# Filter Metadata based on Definition while indexing into ES. -restrict.metadata.objectTypes=Content,ContentImage - -kp.content_service.base_url=localhost:3000 -cassandra.keyspace=sunbirddev_content_store \ No newline at end of file diff --git a/platform-jobs/samza/mvc-processor-indexer/src/main/config/mvc-processor-indexer.properties b/platform-jobs/samza/mvc-processor-indexer/src/main/config/mvc-processor-indexer.properties deleted file mode 100644 index f1123305c7..0000000000 --- a/platform-jobs/samza/mvc-processor-indexer/src/main/config/mvc-processor-indexer.properties +++ /dev/null @@ -1,99 +0,0 @@ -# Job -job.factory.class=org.apache.samza.job.yarn.YarnJobFactory -job.name=__env__.mvc-processor-indexer -job.container.count=__mvc_search_indexer_container_count__ - - -# YARN -yarn.package.path=http://__yarn_host__:__yarn_port__/__env__/${project.artifactId}-${pom.version}-distribution.tar.gz - -# Metrics -metrics.reporters=snapshot,jmx -metrics.reporter.snapshot.class=org.apache.samza.metrics.reporter.MetricsSnapshotReporterFactory -metrics.reporter.snapshot.stream=kafka.__env__.metrics -metrics.reporter.jmx.class=org.apache.samza.metrics.reporter.JmxReporterFactory - -# Task -task.class=org.sunbird.mvcjobs.samza.task.MVCSearchIndexerTask -task.inputs=kafka.__env__.mvc.processor.job.request -task.checkpoint.factory=org.apache.samza.checkpoint.kafka.KafkaCheckpointManagerFactory -task.checkpoint.system=kafka -task.checkpoint.replication.factor=__samza_checkpoint_replication_factor__ -task.commit.ms=60000 -task.window.ms=300000 -task.opts=-Dfile.encoding=UTF8 -#task.broadcast.inputs=kafka.__env__.system.command#0 - -# Serializers -serializers.registry.json.class=org.sunbird.jobs.samza.serializers.EkstepJsonSerdeFactory -serializers.registry.metrics.class=org.apache.samza.serializers.MetricsSnapshotSerdeFactory - -# Systems -systems.kafka.samza.factory=org.apache.samza.system.kafka.KafkaSystemFactory -systems.kafka.samza.msg.serde=json -systems.kafka.streams.metrics.samza.msg.serde=metrics -systems.kafka.consumer.zookeeper.connect=__zookeepers__ -systems.kafka.consumer.auto.offset.reset=smallest -systems.kafka.samza.offset.default=oldest -systems.kafka.producer.bootstrap.servers=__kafka_brokers__ - -# Job Coordinator -job.coordinator.system=kafka -# Normally, this would be 3, but we have only one broker. -job.coordinator.replication.factor=__samza_coordinator_replication_factor__ - -# Job specific config properties -search.es_conn_info=__search_es7_host__ -platform-api-url=__lp_url__ -ekstepPlatformApiUserId=ilimi - -# neo4j configurations -redis.host=__redis_host__ -redis.port=__redis_port__ -redis.maxConnections=128 -akka.request_timeout=30 -environment.id=__environment_id__ -graph.passport.key.base=__graph_passport_key__ -route.domain=__lp_bolt_url__ -route.bolt.read.domain=__lp_bolt_read_url__ -route.bolt.write.domain=__lp_bolt_write_url__ -route.all=__other_bolt_url__ -route.bolt.read.all=__other_bolt_read_url__ -route.bolt.write.all=__other_bolt_write_url__ -shard.id=__mw_shard_id__ -graph.dir="/data/graphDB" -graph.ids=domain,language,en,hi,ka,te,ta -platform.auth.check.enabled=false -platform.cache.ttl=3600000 - -# Metrics -output.metrics.job.name=mvc-processor-indexer -output.metrics.topic.name=__env__.pipeline_metrics - -# Nested Fields -nested.fields=trackable,credentials - -#Failed Topic Config -output.failed.events.topic.name=__env__.mvc.events.failed - -telemetry_env=__env_name__ -installation.id=__installation_id__ - -# Configuration for default channel ID -channel.default=in.ekstep - -# Definition update window -definitions.update.window.ms=300000 - -# Filter Metadata based on Definition while indexing into ES. -#restrict.metadata.objectTypes=Content,ContentImage,AssessmentItem,Channel,Framework,Category,CategoryInstance,Term,Concept,Dimension,Domain - -#kafka.topic.system.command=__env__.system.command - -kp.content_service.base_url=__kp_content_service_base_url__ - -cassandra.lp.connection=__cassandra_lp_connection__ -cassandra.keyspace = __keyspace_name__ - -ml.keyword.api=__ml-keywordapi__ -ml.vector.api=__ml-keywordapi__ \ No newline at end of file diff --git a/platform-jobs/samza/mvc-processor-indexer/src/main/java/org/sunbird/mvcjobs/samza/service/MVCProcessorService.java b/platform-jobs/samza/mvc-processor-indexer/src/main/java/org/sunbird/mvcjobs/samza/service/MVCProcessorService.java deleted file mode 100644 index 006c514342..0000000000 --- a/platform-jobs/samza/mvc-processor-indexer/src/main/java/org/sunbird/mvcjobs/samza/service/MVCProcessorService.java +++ /dev/null @@ -1,96 +0,0 @@ -package org.sunbird.mvcjobs.samza.service; - -import org.apache.commons.lang3.BooleanUtils; -import org.apache.samza.config.Config; -import org.apache.samza.system.SystemStream; -import org.apache.samza.task.MessageCollector; -import org.sunbird.jobs.samza.exception.PlatformErrorCodes; -import org.sunbird.jobs.samza.exception.PlatformException; -import org.sunbird.jobs.samza.service.ISamzaService; -import org.sunbird.jobs.samza.service.task.JobMetrics; -import org.sunbird.mvcjobs.samza.service.util.ContentUtil; -import org.sunbird.mvcjobs.samza.service.util.MVCProcessorCassandraIndexer; -import org.sunbird.mvcjobs.samza.service.util.MVCProcessorESIndexer; -import org.sunbird.jobs.samza.util.FailedEventsUtil; -import org.sunbird.jobs.samza.util.JSONUtils; -import org.sunbird.jobs.samza.util.JobLogger; -import org.sunbird.searchindex.util.CompositeSearchConstants; -import org.elasticsearch.client.transport.NoNodeAvailableException; - -import java.util.Map; - -public class MVCProcessorService implements ISamzaService { - - private JobLogger LOGGER = new JobLogger(MVCProcessorService.class); - private Config config = null; - private MVCProcessorESIndexer mvcIndexer = null; - private SystemStream systemStream = null; - private MVCProcessorCassandraIndexer cassandraManager ; - public MVCProcessorService() {} - - public MVCProcessorService(MVCProcessorESIndexer mvcIndexer) throws Exception { - this.mvcIndexer = mvcIndexer; - } - - @Override - public void initialize(Config config) throws Exception { - this.config = config; - JSONUtils.loadProperties(config); - LOGGER.info("Service config initialized"); - systemStream = new SystemStream("kafka", config.get("output.failed.events.topic.name")); - mvcIndexer = mvcIndexer == null ? new MVCProcessorESIndexer(): mvcIndexer; - mvcIndexer.createMVCSearchIndex(); - LOGGER.info(CompositeSearchConstants.MVC_SEARCH_INDEX + " created"); - cassandraManager = new MVCProcessorCassandraIndexer(); - } - - @Override - public void processMessage(Map message, JobMetrics metrics, MessageCollector collector) - throws Exception { - Object index = message.get("index"); - Boolean shouldindex = BooleanUtils.toBoolean(null == index ? "true" : index.toString()); - String identifier = (String) ((Map) message.get("object")).get("id"); - if (!BooleanUtils.isFalse(shouldindex)) { - LOGGER.debug("Indexing event into ES"); - try { - processMessage(message); - LOGGER.debug("Record Added/Updated into mvc index for " + identifier); - metrics.incSuccessCounter(); - } catch (PlatformException ex) { - LOGGER.error("Error while processing message:", message, ex); - metrics.incFailedCounter(); - FailedEventsUtil.pushEventForRetry(systemStream, message, metrics, collector, - PlatformErrorCodes.SYSTEM_ERROR.name(), ex); - } catch (Exception ex) { - LOGGER.error("Error while processing message:", message, ex); - metrics.incErrorCounter(); - if (null != message) { - String errorCode = ex instanceof NoNodeAvailableException ? PlatformErrorCodes.SYSTEM_ERROR.name() - : PlatformErrorCodes.PROCESSING_ERROR.name(); - FailedEventsUtil.pushEventForRetry(systemStream, message, metrics, collector, - errorCode, ex); - } - } - } else { - LOGGER.info("Learning event not qualified for indexing"); - } - } - - public void processMessage(Map message) throws Exception { - if (message != null && message.get("eventData") != null) { - Map eventData = (Map) message.get("eventData"); - String action = eventData.get("action").toString(); - String objectId = (String) ((Map) message.get("object")).get("id"); - if(!action.equalsIgnoreCase("update-content-rating")) { - if (action.equalsIgnoreCase("update-es-index")) { - eventData = ContentUtil.getContentMetaData(eventData, objectId); - } - LOGGER.info("MVCProcessorService :: processMessage ::: Calling cassandra insertion for " + objectId); - cassandraManager.insertIntoCassandra(eventData, objectId); - } - LOGGER.info("MVCProcessorService :: processMessage ::: Calling elasticsearch insertion for " + objectId); - mvcIndexer.upsertDocument(objectId, eventData); - } - } - -} diff --git a/platform-jobs/samza/mvc-processor-indexer/src/main/java/org/sunbird/mvcjobs/samza/service/util/CassandraConnector.java b/platform-jobs/samza/mvc-processor-indexer/src/main/java/org/sunbird/mvcjobs/samza/service/util/CassandraConnector.java deleted file mode 100644 index 093369c5ad..0000000000 --- a/platform-jobs/samza/mvc-processor-indexer/src/main/java/org/sunbird/mvcjobs/samza/service/util/CassandraConnector.java +++ /dev/null @@ -1,95 +0,0 @@ -package org.sunbird.mvcjobs.samza.service.util; - -import com.datastax.driver.core.BoundStatement; -import com.datastax.driver.core.Cluster; -import com.datastax.driver.core.PreparedStatement; -import com.datastax.driver.core.Session; -import org.apache.commons.lang3.StringUtils; -import org.sunbird.common.Platform; -import org.sunbird.jobs.samza.util.JobLogger; - -import java.net.InetSocketAddress; -import java.util.*; - -public class CassandraConnector { - private static JobLogger LOGGER = new JobLogger(CassandraConnector.class); - - static String arr[],table = "content_data"; - static Session session; - static public Session getSession() { - if(session != null) { - LOGGER.info("CassandraSession Exists"); - return session; - } - String serverIP = Platform.config.getString("cassandra.lp.connection"); - LOGGER.info("Cassandra keyspace is " + Platform.config.getString("cassandra.keyspace")); - if(serverIP == null) { - LOGGER.info("Server ip of cassandra is null"); - } - LOGGER.info("Server ip of cassandra is " + serverIP); - List connectionInfo = Arrays.asList(serverIP.split(",")); - List addressList = getSocketAddress(connectionInfo); - Cluster cluster = Cluster.builder() - .addContactPointsWithPorts(addressList) - .build(); - - session = cluster.connect(Platform.config.getString("cassandra.keyspace")); - LOGGER.info("The server IP " + serverIP + "\n Session created " + session); - return session; - } - public static void updateContentProperties(String contentId, Map map) { - Session session = getSession(); - if (null == map || map.isEmpty()) - return; - String query = getUpdateQuery(map.keySet()); - if(query == null) - return; - PreparedStatement ps = session.prepare(query); - Object[] values = new Object[map.size() + 1]; - try { - int i = 0; - for (Map.Entry entry : map.entrySet()) { - - if (null == entry.getValue()) { - continue; - } else { - values[i] = entry.getValue(); - } - - i += 1; - } - values[i] = contentId; - BoundStatement bound = ps.bind(values); - LOGGER.info("Executing the statement to insert into cassandra for identifier " + contentId); - session.execute(bound); - } catch (Exception e) { - System.out.println("Exception " + e); - LOGGER.info("Exception while inserting data into cassandra " + e); - } - } - private static String getUpdateQuery(Set properties) { - StringBuilder sb = new StringBuilder(); - if (null != properties && !properties.isEmpty()) { - sb.append("UPDATE " + table + " SET last_updated_on = dateOf(now()), "); - StringBuilder updateFields = new StringBuilder(); - for (String property : properties) { - if (StringUtils.isBlank(property)) - return null; - updateFields.append(property.trim()).append(" = ?, "); - } - sb.append(StringUtils.removeEnd(updateFields.toString(), ", ")); - sb.append(" where content_id = ?"); - } - return sb.toString(); - } - private static List getSocketAddress(List hosts) { - List connectionList = new ArrayList<>(); - for (String connection : hosts) { - String[] conn = connection.split(":"); - String host = conn[0]; - int port = Integer.valueOf(conn[1]); - connectionList.add(new InetSocketAddress(host, port)); - } - return connectionList; - } -} diff --git a/platform-jobs/samza/mvc-processor-indexer/src/main/java/org/sunbird/mvcjobs/samza/service/util/ContentUtil.java b/platform-jobs/samza/mvc-processor-indexer/src/main/java/org/sunbird/mvcjobs/samza/service/util/ContentUtil.java deleted file mode 100644 index c4940b5681..0000000000 --- a/platform-jobs/samza/mvc-processor-indexer/src/main/java/org/sunbird/mvcjobs/samza/service/util/ContentUtil.java +++ /dev/null @@ -1,47 +0,0 @@ -package org.sunbird.mvcjobs.samza.service.util; - -import com.fasterxml.jackson.databind.ObjectMapper; -import org.sunbird.common.Platform; -import org.sunbird.jobs.samza.util.JobLogger; -import org.sunbird.searchindex.util.HTTPUtil; - -import java.util.HashMap; -import java.util.Map; - -public class ContentUtil { - private static JobLogger LOGGER = new JobLogger(ContentUtil.class); - - public static Map getContentMetaData(Map newmap , String identifer) throws Exception { - ObjectMapper mapper = new ObjectMapper(); - String contentReadURL = ""; - try { - contentReadURL = Platform.config.hasPath("kp.content_service.base_url") ? Platform.config.getString("kp.content_service.base_url") : "" ; - LOGGER.info("MVCProcessorCassandraIndexer :: getContentMetaData ::: Making API call to read content " + contentReadURL + "/content/v3/read/"); - String content = HTTPUtil.makeGetRequest(contentReadURL + "/content/v3/read/" +identifer); - LOGGER.info("MVCProcessorCassandraIndexer :: getContentMetaData ::: retrieved content meta " + content); - Map obj = mapper.readValue(content,Map.class); - Map contentobj = (HashMap) (((HashMap)obj.get("result")).get("content")); - newmap = filterData(newmap,contentobj); - - }catch (Exception e) { - LOGGER.info("MVCProcessorCassandraIndexer :: getContentDefinition ::: Error in getContentDefinitionFunction " + e); - throw new Exception("Get content metdata failed"); - } - return newmap; - } - public static Map filterData(Map obj ,Map content) { - String elasticSearchParamArr[] = {"organisation","channel","framework","board","medium","subject","gradeLevel","name","description","language","appId","appIcon","appIconLabel","contentEncoding","identifier","node_id","nodeType","mimeType","resourceType","contentType","allowedContentTypes","objectType","posterImage","artifactUrl","launchUrl","previewUrl","streamingUrl","downloadUrl","status","pkgVersion","source","lastUpdatedOn","ml_contentText","ml_contentTextVector","ml_Keywords","level1Name","level1Concept","level2Name","level2Concept","level3Name","level3Concept","textbook_name","sourceURL","label","all_fields"}; - String key = null; - Object value = null; - for(int i = 0 ; i < elasticSearchParamArr.length ; i++ ) { - key = (elasticSearchParamArr[i]); - value = content.containsKey(key) ? content.get(key) : null; - if(value != null) { - obj.put(key,value); - value = null; - } - } - return obj; - - } -} diff --git a/platform-jobs/samza/mvc-processor-indexer/src/main/java/org/sunbird/mvcjobs/samza/service/util/MVCProcessorCassandraIndexer.java b/platform-jobs/samza/mvc-processor-indexer/src/main/java/org/sunbird/mvcjobs/samza/service/util/MVCProcessorCassandraIndexer.java deleted file mode 100644 index f042a15151..0000000000 --- a/platform-jobs/samza/mvc-processor-indexer/src/main/java/org/sunbird/mvcjobs/samza/service/util/MVCProcessorCassandraIndexer.java +++ /dev/null @@ -1,149 +0,0 @@ -package org.sunbird.mvcjobs.samza.service.util; - -import org.apache.commons.lang3.StringUtils; -import org.sunbird.common.Platform; -import org.sunbird.jobs.samza.util.JobLogger; -import org.sunbird.searchindex.util.HTTPUtil; -import org.json.JSONArray; -import org.json.JSONObject; - -import java.util.*; -import java.util.concurrent.CompletableFuture; - -public class MVCProcessorCassandraIndexer { - String mlworkbenchapirequest = "", mlvectorListRequest = "" , jobname = "" ; - Map mapStage1 = new HashMap<>(); - List level1concept = null,level2concept = null, level3concept = null , textbook_name , level1_name , level2_name , level3_name ; - private JobLogger LOGGER = new JobLogger(MVCProcessorCassandraIndexer.class); - public MVCProcessorCassandraIndexer() { - mlworkbenchapirequest = "{\"request\":{ \"input\" :{ \"content\" : [] } } }"; - mlvectorListRequest = "{\"request\":{\"text\":[],\"cid\": \"\",\"language\":\"en\",\"method\":\"BERT\",\"params\":{\"dim\":768,\"seq_len\":25}}}"; - jobname = "vidyadaan_content_keyword_tagging"; - - } - // Insert to cassandra - public void insertIntoCassandra(Map obj, String identifier) throws Exception { - String action = obj.get("action").toString(); - - if(StringUtils.isNotBlank(action)) { - if(action.equalsIgnoreCase("update-es-index")) { - LOGGER.info("MVCProcessorCassandraIndexer :: getContentMetaData ::: extracting required fields" + obj); - extractFieldsToBeInserted(obj); - LOGGER.info("MVCProcessorCassandraIndexer :: getContentMetaData ::: making ml workbench api request"); - getMLKeywords(obj); - LOGGER.info("MVCProcessorCassandraIndexer :: insertIntoCassandra ::: update-es-index-1 event"); - LOGGER.info("MVCProcessorCassandraIndexer :: insertIntoCassandra ::: Inserting into cassandra stage-1"); - CassandraConnector.updateContentProperties(identifier,mapStage1); - } else if(action.equalsIgnoreCase("update-ml-keywords")) { - LOGGER.info("MVCProcessorCassandraIndexer :: insertIntoCassandra ::: update-ml-keywords"); - String ml_contentText; - List ml_Keywords; - ml_contentText = obj.get("ml_contentText") != null ? obj.get("ml_contentText").toString() : null; - ml_Keywords = obj.get("ml_Keywords") != null ? (List) obj.get("ml_Keywords") : null; - - getMLVectors(ml_contentText,identifier); - Map mapForStage2 = new HashMap<>(); - mapForStage2.put("ml_keywords",ml_Keywords); - mapForStage2.put("ml_content_text",ml_contentText); - CassandraConnector.updateContentProperties(identifier,mapForStage2); - - } - else if(action.equalsIgnoreCase("update-ml-contenttextvector")) { - LOGGER.info("MVCProcessorCassandraIndexer :: insertIntoCassandra ::: update-ml-contenttextvector event"); - List> ml_contentTextVectorList; - Set ml_contentTextVector = null; - ml_contentTextVectorList = obj.get("ml_contentTextVector") != null ? (List>) obj.get("ml_contentTextVector") : null; - if(ml_contentTextVectorList != null) - { - ml_contentTextVector = new HashSet(ml_contentTextVectorList.get(0)); - - } - Map mapForStage3 = new HashMap<>(); - mapForStage3.put("ml_content_text_vector",ml_contentTextVector); - CassandraConnector.updateContentProperties(identifier,mapForStage3); - - - } - } - } - - //Getting Fields to be inserted into cassandra - private void extractFieldsToBeInserted(Map contentobj) { - if(contentobj.containsKey("level1Concept")){ - level1concept = (List)contentobj.get("level1Concept"); - mapStage1.put("level1_concept", level1concept); - } - if(contentobj.containsKey("level2Concept")){ - level2concept = (List)contentobj.get("level2Concept"); - mapStage1.put("level2_concept", level2concept); - } - if(contentobj.containsKey("level3Concept")){ - level3concept = (List)contentobj.get("level3Concept"); - mapStage1.put("level3_concept",level3concept ); - } - if(contentobj.containsKey("textbook_name")){ - textbook_name = (List)contentobj.get("textbook_name"); - mapStage1.put("textbook_name", textbook_name); - } - if(contentobj.containsKey("level1Name")){ - level1_name = (List)contentobj.get("level1Name"); - mapStage1.put("level1_name", level1_name); - } - if(contentobj.containsKey("level2Name")){ - level2_name = (List)contentobj.get("level2Name"); - mapStage1.put("level2_name", level2_name); - } - if(contentobj.containsKey("level3Name")){ - level3_name = (List)contentobj.get("level3Name"); - mapStage1.put("level3_name", level3_name); - } - if(contentobj.containsKey("source")){ - mapStage1.put("source",contentobj.get("source")); - } - if(contentobj.containsKey("sourceURL")){ - mapStage1.put("sourceurl",contentobj.get("sourceURL")); - } - LOGGER.info("MVCProcessorCassandraIndexer :: extractedmetadata"); - - } - - // POST reqeuest for ml keywords api - void getMLKeywords(Map contentdef) throws Exception { - JSONObject obj = new JSONObject(mlworkbenchapirequest); - JSONObject req = ((JSONObject) (obj.get("request"))); - JSONObject input = (JSONObject) req.get("input"); - JSONArray content = (JSONArray) input.get("content"); - content.put(contentdef); - req.put("job", jobname); - LOGGER.info("MVCProcessorCassandraIndexer :: getMLKeywords ::: The ML workbench URL is " + "http://" + Platform.config.getString("ml.keyword.api") + ":3579/daggit/submit"); - - try { - String resp = HTTPUtil.makePostRequest("http://" + Platform.config.getString("ml.keyword.api") + ":3579/daggit/submit", obj.toString()); - LOGGER.info("MVCProcessorCassandraIndexer :: getMLKeywords ::: The ML workbench response is " + resp); - - } catch (Exception e) { - LOGGER.info("MVCProcessorCassandraIndexer :: getMLKeywords ::: ML workbench api request failed "); - } - - } - - - // Post reqeuest for vector api - public void getMLVectors(String contentText, String identifier) throws Exception { - String mlVectorApi = Platform.config.hasPath("ml.vector.api") ? Platform.config.getString("ml.vector.api") : ""; - JSONObject obj = new JSONObject(mlvectorListRequest); - JSONObject req = ((JSONObject) (obj.get("request"))); - JSONArray text = (JSONArray) req.get("text"); - req.put("cid", identifier); - text.put(contentText); - LOGGER.info("MVCProcessorCassandraIndexer :: getMLVectors ::: The ML vector URL is " + "http://" + mlVectorApi + ":1729/ml/vector/ContentText"); - - try { - String resp = HTTPUtil.makePostRequest("http://" + mlVectorApi + ":1729/ml/vector/ContentText", obj.toString()); - LOGGER.info("MVCProcessorCassandraIndexer :: getMLVectors ::: ML vector api request response is " + resp); - } catch (Exception e) { - LOGGER.info("MVCProcessorCassandraIndexer :: getMLVectors ::: ML vector api request failed "); - } - } - -} diff --git a/platform-jobs/samza/mvc-processor-indexer/src/main/java/org/sunbird/mvcjobs/samza/service/util/MVCProcessorESIndexer.java b/platform-jobs/samza/mvc-processor-indexer/src/main/java/org/sunbird/mvcjobs/samza/service/util/MVCProcessorESIndexer.java deleted file mode 100644 index f2e4a35fee..0000000000 --- a/platform-jobs/samza/mvc-processor-indexer/src/main/java/org/sunbird/mvcjobs/samza/service/util/MVCProcessorESIndexer.java +++ /dev/null @@ -1,130 +0,0 @@ -/** - * - */ -package org.sunbird.mvcjobs.samza.service.util; - -import org.apache.commons.collections4.MapUtils; -import org.codehaus.jackson.map.ObjectMapper; -import org.codehaus.jackson.type.TypeReference; -import org.sunbird.common.Platform; -import org.sunbird.jobs.samza.service.util.AbstractESIndexer; -import org.sunbird.jobs.samza.util.JobLogger; -import org.sunbird.learning.util.ControllerUtil; -import org.sunbird.mvcsearchindex.elasticsearch.ElasticSearchUtil; -import org.sunbird.searchindex.util.CompositeSearchConstants; - -import java.io.IOException; -import java.util.*; -import java.util.concurrent.CompletableFuture; - - -/** - * @author pradyumna - * - */ -public class MVCProcessorESIndexer extends AbstractESIndexer { - - private JobLogger LOGGER = new JobLogger(MVCProcessorESIndexer.class); - private ObjectMapper mapper = new ObjectMapper(); - private ControllerUtil util = new ControllerUtil(); - private static List NESTED_FIELDS = Platform.config.hasPath("nested.fields")? Arrays.asList(Platform.config.getString("nested.fields").split(",")): new ArrayList(); - - @Override - public void init() { - ElasticSearchUtil.initialiseESClient(CompositeSearchConstants.MVC_SEARCH_INDEX, - Platform.config.getString("search.es_conn_info")); - } - - /** - * @return - */ - - - public void createMVCSearchIndex() throws IOException { - String alias = "mvc-content"; - String settings = "{\"settings\":{\"index\":{\"max_ngram_diff\":\"29\",\"mapping\":{\"total_fields\":{\"limit\":\"1500\"}},\"number_of_shards\":\"5\",\"provided_name\":\"mvc-content-v1\",\"creation_date\":\"1593168273071\",\"analysis\":{\"filter\":{\"mynGram\":{\"token_chars\":[\"letter\",\"digit\",\"whitespace\",\"punctuation\",\"symbol\"],\"min_gram\":\"1\",\"type\":\"nGram\",\"max_gram\":\"30\"}},\"analyzer\":{\"cs_index_analyzer\":{\"filter\":[\"lowercase\",\"mynGram\"],\"type\":\"custom\",\"tokenizer\":\"standard\"},\"keylower\":{\"filter\":\"lowercase\",\"tokenizer\":\"keyword\"},\"ml_custom_analyzer\":{\"type\":\"standard\",\"stopwords\":[\"_english_\",\"_hindi_\"]},\"cs_search_analyzer\":{\"filter\":[\"lowercase\"],\"type\":\"custom\",\"tokenizer\":\"standard\"}}},\"number_of_replicas\":\"1\",\"uuid\":\"esGBPk9aQiqeRWrJA4wu9g\",\"version\":{\"created\":\"7050099\"}}}}"; - String mappings = "{\"mappings\":{\"dynamic\":\"strict\",\"properties\":{\"all_fields\":{\"type\":\"text\",\"fields\":{\"raw\":{\"type\":\"text\",\"analyzer\":\"keylower\",\"fielddata\":true}},\"analyzer\":\"cs_index_analyzer\",\"search_analyzer\":\"cs_search_analyzer\"},\"allowedContentTypes\":{\"type\":\"text\",\"fields\":{\"raw\":{\"type\":\"text\",\"analyzer\":\"keylower\",\"fielddata\":true}},\"copy_to\":[\"all_fields\"],\"analyzer\":\"cs_index_analyzer\",\"search_analyzer\":\"cs_search_analyzer\"},\"appIcon\":{\"type\":\"text\",\"index\":false},\"appIconLabel\":{\"type\":\"text\",\"fields\":{\"raw\":{\"type\":\"text\",\"analyzer\":\"keylower\",\"fielddata\":true}},\"copy_to\":[\"all_fields\"],\"analyzer\":\"cs_index_analyzer\",\"search_analyzer\":\"cs_search_analyzer\"},\"appId\":{\"type\":\"text\",\"fields\":{\"raw\":{\"type\":\"text\",\"analyzer\":\"keylower\",\"fielddata\":true}},\"copy_to\":[\"all_fields\"],\"analyzer\":\"cs_index_analyzer\",\"search_analyzer\":\"cs_search_analyzer\"},\"artifactUrl\":{\"type\":\"text\",\"fields\":{\"raw\":{\"type\":\"text\",\"analyzer\":\"keylower\",\"fielddata\":true}},\"copy_to\":[\"all_fields\"],\"analyzer\":\"cs_index_analyzer\",\"search_analyzer\":\"cs_search_analyzer\"},\"board\":{\"type\":\"text\",\"fields\":{\"raw\":{\"type\":\"text\",\"analyzer\":\"keylower\",\"fielddata\":true}},\"copy_to\":[\"all_fields\"],\"analyzer\":\"cs_index_analyzer\",\"search_analyzer\":\"cs_search_analyzer\"},\"channel\":{\"type\":\"text\",\"fields\":{\"raw\":{\"type\":\"text\",\"analyzer\":\"keylower\",\"fielddata\":true}},\"copy_to\":[\"all_fields\"],\"analyzer\":\"cs_index_analyzer\",\"search_analyzer\":\"cs_search_analyzer\"},\"contentEncoding\":{\"type\":\"text\",\"fields\":{\"raw\":{\"type\":\"text\",\"analyzer\":\"keylower\",\"fielddata\":true}},\"copy_to\":[\"all_fields\"],\"analyzer\":\"cs_index_analyzer\",\"search_analyzer\":\"cs_search_analyzer\"},\"contentType\":{\"type\":\"text\",\"fields\":{\"raw\":{\"type\":\"text\",\"analyzer\":\"keylower\",\"fielddata\":true}},\"copy_to\":[\"all_fields\"],\"analyzer\":\"cs_index_analyzer\",\"search_analyzer\":\"cs_search_analyzer\"},\"description\":{\"type\":\"text\",\"fields\":{\"raw\":{\"type\":\"text\",\"analyzer\":\"keylower\",\"fielddata\":true}},\"copy_to\":[\"all_fields\"],\"analyzer\":\"cs_index_analyzer\",\"search_analyzer\":\"cs_search_analyzer\"},\"downloadUrl\":{\"type\":\"text\",\"fields\":{\"raw\":{\"type\":\"text\",\"analyzer\":\"keylower\",\"fielddata\":true}},\"copy_to\":[\"all_fields\"],\"analyzer\":\"cs_index_analyzer\",\"search_analyzer\":\"cs_search_analyzer\"},\"framework\":{\"type\":\"text\",\"fields\":{\"raw\":{\"type\":\"text\",\"analyzer\":\"keylower\",\"fielddata\":true}},\"copy_to\":[\"all_fields\"],\"analyzer\":\"cs_index_analyzer\",\"search_analyzer\":\"cs_search_analyzer\"},\"gradeLevel\":{\"type\":\"text\",\"fields\":{\"raw\":{\"type\":\"text\",\"analyzer\":\"keylower\",\"fielddata\":true}},\"copy_to\":[\"all_fields\"],\"analyzer\":\"cs_index_analyzer\",\"search_analyzer\":\"cs_search_analyzer\"},\"identifier\":{\"type\":\"text\",\"fields\":{\"raw\":{\"type\":\"text\",\"analyzer\":\"keylower\",\"fielddata\":true}},\"copy_to\":[\"all_fields\"],\"analyzer\":\"cs_index_analyzer\",\"search_analyzer\":\"cs_search_analyzer\"},\"label\":{\"type\":\"text\",\"analyzer\":\"cs_index_analyzer\",\"search_analyzer\":\"standard\"},\"language\":{\"type\":\"text\",\"fields\":{\"raw\":{\"type\":\"text\",\"analyzer\":\"keylower\",\"fielddata\":true}},\"copy_to\":[\"all_fields\"],\"analyzer\":\"cs_index_analyzer\",\"search_analyzer\":\"cs_search_analyzer\"},\"lastUpdatedOn\":{\"type\":\"date\",\"fields\":{\"raw\":{\"type\":\"date\"}}},\"launchUrl\":{\"type\":\"text\",\"fields\":{\"raw\":{\"type\":\"text\",\"analyzer\":\"keylower\",\"fielddata\":true}},\"copy_to\":[\"all_fields\"],\"analyzer\":\"cs_index_analyzer\",\"search_analyzer\":\"cs_search_analyzer\"},\"level1Concept\":{\"type\":\"text\",\"fields\":{\"raw\":{\"type\":\"text\",\"analyzer\":\"keylower\",\"fielddata\":true}},\"copy_to\":[\"all_fields\"],\"analyzer\":\"cs_index_analyzer\",\"search_analyzer\":\"cs_search_analyzer\"},\"level1Name\":{\"type\":\"text\",\"fields\":{\"raw\":{\"type\":\"text\",\"analyzer\":\"keylower\",\"fielddata\":true}},\"copy_to\":[\"all_fields\"],\"analyzer\":\"cs_index_analyzer\",\"search_analyzer\":\"cs_search_analyzer\"},\"level2Concept\":{\"type\":\"text\",\"fields\":{\"raw\":{\"type\":\"text\",\"analyzer\":\"keylower\",\"fielddata\":true}},\"copy_to\":[\"all_fields\"],\"analyzer\":\"cs_index_analyzer\",\"search_analyzer\":\"cs_search_analyzer\"},\"level2Name\":{\"type\":\"text\",\"fields\":{\"raw\":{\"type\":\"text\",\"analyzer\":\"keylower\",\"fielddata\":true}},\"copy_to\":[\"all_fields\"],\"analyzer\":\"cs_index_analyzer\",\"search_analyzer\":\"cs_search_analyzer\"},\"level3Concept\":{\"type\":\"text\",\"fields\":{\"raw\":{\"type\":\"text\",\"analyzer\":\"keylower\",\"fielddata\":true}},\"copy_to\":[\"all_fields\"],\"analyzer\":\"cs_index_analyzer\",\"search_analyzer\":\"cs_search_analyzer\"},\"level3Name\":{\"type\":\"text\",\"fields\":{\"raw\":{\"type\":\"text\",\"analyzer\":\"keylower\",\"fielddata\":true}},\"copy_to\":[\"all_fields\"],\"analyzer\":\"cs_index_analyzer\",\"search_analyzer\":\"cs_search_analyzer\"},\"medium\":{\"type\":\"text\",\"fields\":{\"raw\":{\"type\":\"text\",\"analyzer\":\"keylower\",\"fielddata\":true}},\"copy_to\":[\"all_fields\"],\"analyzer\":\"cs_index_analyzer\",\"search_analyzer\":\"cs_search_analyzer\"},\"mimeType\":{\"type\":\"text\",\"fields\":{\"raw\":{\"type\":\"text\",\"analyzer\":\"keylower\",\"fielddata\":true}},\"copy_to\":[\"all_fields\"],\"analyzer\":\"cs_index_analyzer\",\"search_analyzer\":\"cs_search_analyzer\"},\"ml_Keywords\":{\"type\":\"text\",\"analyzer\":\"ml_custom_analyzer\",\"search_analyzer\":\"standard\"},\"ml_contentText\":{\"type\":\"text\",\"analyzer\":\"ml_custom_analyzer\",\"search_analyzer\":\"standard\"},\"ml_contentTextVector\":{\"type\":\"dense_vector\",\"dims\":768},\"name\":{\"type\":\"text\",\"fields\":{\"raw\":{\"type\":\"text\",\"analyzer\":\"keylower\",\"fielddata\":true}},\"copy_to\":[\"all_fields\"],\"analyzer\":\"cs_index_analyzer\",\"search_analyzer\":\"cs_search_analyzer\"},\"nodeType\":{\"type\":\"text\",\"fields\":{\"raw\":{\"type\":\"text\",\"analyzer\":\"keylower\",\"fielddata\":true}},\"copy_to\":[\"all_fields\"],\"analyzer\":\"cs_index_analyzer\",\"search_analyzer\":\"cs_search_analyzer\"},\"node_id\":{\"type\":\"long\",\"fields\":{\"raw\":{\"type\":\"long\"}}},\"objectType\":{\"type\":\"text\",\"fields\":{\"raw\":{\"type\":\"text\",\"analyzer\":\"keylower\",\"fielddata\":true}},\"copy_to\":[\"all_fields\"],\"analyzer\":\"cs_index_analyzer\",\"search_analyzer\":\"cs_search_analyzer\"},\"organisation\":{\"type\":\"text\",\"fields\":{\"raw\":{\"type\":\"text\",\"analyzer\":\"keylower\",\"fielddata\":true}},\"copy_to\":[\"all_fields\"],\"analyzer\":\"cs_index_analyzer\",\"search_analyzer\":\"cs_search_analyzer\"},\"pkgVersion\":{\"type\":\"double\",\"fields\":{\"raw\":{\"type\":\"double\"}}},\"posterImage\":{\"type\":\"text\",\"fields\":{\"raw\":{\"type\":\"text\",\"analyzer\":\"keylower\",\"fielddata\":true}},\"copy_to\":[\"all_fields\"],\"analyzer\":\"cs_index_analyzer\",\"search_analyzer\":\"cs_search_analyzer\"},\"previewUrl\":{\"type\":\"text\",\"fields\":{\"raw\":{\"type\":\"text\",\"analyzer\":\"keylower\",\"fielddata\":true}},\"copy_to\":[\"all_fields\"],\"analyzer\":\"cs_index_analyzer\",\"search_analyzer\":\"cs_search_analyzer\"},\"resourceType\":{\"type\":\"text\",\"fields\":{\"raw\":{\"type\":\"text\",\"analyzer\":\"keylower\",\"fielddata\":true}},\"copy_to\":[\"all_fields\"],\"analyzer\":\"cs_index_analyzer\",\"search_analyzer\":\"cs_search_analyzer\"},\"source\":{\"type\":\"text\",\"fields\":{\"raw\":{\"type\":\"text\",\"analyzer\":\"keylower\",\"fielddata\":true}},\"copy_to\":[\"all_fields\"],\"analyzer\":\"cs_index_analyzer\",\"search_analyzer\":\"cs_search_analyzer\"},\"sourceURL\":{\"type\":\"text\",\"fields\":{\"raw\":{\"type\":\"text\",\"analyzer\":\"keylower\",\"fielddata\":true}},\"copy_to\":[\"all_fields\"],\"analyzer\":\"cs_index_analyzer\",\"search_analyzer\":\"cs_search_analyzer\"},\"status\":{\"type\":\"text\",\"fields\":{\"raw\":{\"type\":\"text\",\"analyzer\":\"keylower\",\"fielddata\":true}},\"copy_to\":[\"all_fields\"],\"analyzer\":\"cs_index_analyzer\",\"search_analyzer\":\"cs_search_analyzer\"},\"streamingUrl\":{\"type\":\"text\",\"fields\":{\"raw\":{\"type\":\"text\",\"analyzer\":\"keylower\",\"fielddata\":true}},\"copy_to\":[\"all_fields\"],\"analyzer\":\"cs_index_analyzer\",\"search_analyzer\":\"cs_search_analyzer\"},\"subject\":{\"type\":\"text\",\"fields\":{\"raw\":{\"type\":\"text\",\"analyzer\":\"keylower\",\"fielddata\":true}},\"copy_to\":[\"all_fields\"],\"analyzer\":\"cs_index_analyzer\",\"search_analyzer\":\"cs_search_analyzer\"},\"textbook_name\":{\"type\":\"text\",\"fields\":{\"raw\":{\"type\":\"text\",\"analyzer\":\"keylower\",\"fielddata\":true}},\"copy_to\":[\"all_fields\"],\"analyzer\":\"cs_index_analyzer\",\"search_analyzer\":\"cs_search_analyzer\"}}}}"; - String esindex = "{\"aliases\":{\"mvc-content\":{}},\"settings\":{\"index\":{\"max_ngram_diff\":\"29\",\"mapping\":{\"total_fields\":{\"limit\":\"1500\"}},\"number_of_shards\":\"5\",\"analysis\":{\"filter\":{\"mynGram\":{\"token_chars\":[\"letter\",\"digit\",\"whitespace\",\"punctuation\",\"symbol\"],\"min_gram\":\"1\",\"type\":\"nGram\",\"max_gram\":\"30\"}},\"analyzer\":{\"cs_index_analyzer\":{\"filter\":[\"lowercase\",\"mynGram\"],\"type\":\"custom\",\"tokenizer\":\"standard\"},\"keylower\":{\"filter\":\"lowercase\",\"tokenizer\":\"keyword\"},\"cs_search_analyzer\":{\"filter\":[\"lowercase\"],\"type\":\"custom\",\"tokenizer\":\"standard\"},\"ml_custom_analyzer\":{\"type\":\"standard\",\"stopwords\":[\"_english_\",\"_hindi_\"]}}},\"number_of_replicas\":\"1\"}},\"mappings\":{\"dynamic\":\"strict\",\"properties\":{\"organisation\":{\"type\":\"text\",\"fields\":{\"raw\":{\"type\":\"text\",\"analyzer\":\"keylower\",\"fielddata\":true}},\"copy_to\":[\"all_fields\"],\"analyzer\":\"cs_index_analyzer\",\"search_analyzer\":\"cs_search_analyzer\"},\"channel\":{\"type\":\"text\",\"fields\":{\"raw\":{\"type\":\"text\",\"analyzer\":\"keylower\",\"fielddata\":true}},\"copy_to\":[\"all_fields\"],\"analyzer\":\"cs_index_analyzer\",\"search_analyzer\":\"cs_search_analyzer\"},\"framework\":{\"type\":\"text\",\"fields\":{\"raw\":{\"type\":\"text\",\"analyzer\":\"keylower\",\"fielddata\":true}},\"copy_to\":[\"all_fields\"],\"analyzer\":\"cs_index_analyzer\",\"search_analyzer\":\"cs_search_analyzer\"},\"board\":{\"type\":\"text\",\"fields\":{\"raw\":{\"type\":\"text\",\"analyzer\":\"keylower\",\"fielddata\":true}},\"copy_to\":[\"all_fields\"],\"analyzer\":\"cs_index_analyzer\",\"search_analyzer\":\"cs_search_analyzer\"},\"medium\":{\"type\":\"text\",\"fields\":{\"raw\":{\"type\":\"text\",\"analyzer\":\"keylower\",\"fielddata\":true}},\"copy_to\":[\"all_fields\"],\"analyzer\":\"cs_index_analyzer\",\"search_analyzer\":\"cs_search_analyzer\"},\"subject\":{\"type\":\"text\",\"fields\":{\"raw\":{\"type\":\"text\",\"analyzer\":\"keylower\",\"fielddata\":true}},\"copy_to\":[\"all_fields\"],\"analyzer\":\"cs_index_analyzer\",\"search_analyzer\":\"cs_search_analyzer\"},\"gradeLevel\":{\"type\":\"text\",\"fields\":{\"raw\":{\"type\":\"text\",\"analyzer\":\"keylower\",\"fielddata\":true}},\"copy_to\":[\"all_fields\"],\"analyzer\":\"cs_index_analyzer\",\"search_analyzer\":\"cs_search_analyzer\"},\"name\":{\"type\":\"text\",\"fields\":{\"raw\":{\"type\":\"text\",\"analyzer\":\"keylower\",\"fielddata\":true}},\"copy_to\":[\"all_fields\"],\"analyzer\":\"cs_index_analyzer\",\"search_analyzer\":\"cs_search_analyzer\"},\"description\":{\"type\":\"text\",\"fields\":{\"raw\":{\"type\":\"text\",\"analyzer\":\"keylower\",\"fielddata\":true}},\"copy_to\":[\"all_fields\"],\"analyzer\":\"cs_index_analyzer\",\"search_analyzer\":\"cs_search_analyzer\"},\"language\":{\"type\":\"text\",\"fields\":{\"raw\":{\"type\":\"text\",\"analyzer\":\"keylower\",\"fielddata\":true}},\"copy_to\":[\"all_fields\"],\"analyzer\":\"cs_index_analyzer\",\"search_analyzer\":\"cs_search_analyzer\"},\"appId\":{\"type\":\"text\",\"fields\":{\"raw\":{\"type\":\"text\",\"analyzer\":\"keylower\",\"fielddata\":true}},\"copy_to\":[\"all_fields\"],\"analyzer\":\"cs_index_analyzer\",\"search_analyzer\":\"cs_search_analyzer\"},\"appIcon\":{\"type\":\"text\",\"index\":false},\"appIconLabel\":{\"type\":\"text\",\"fields\":{\"raw\":{\"type\":\"text\",\"analyzer\":\"keylower\",\"fielddata\":true}},\"copy_to\":[\"all_fields\"],\"analyzer\":\"cs_index_analyzer\",\"search_analyzer\":\"cs_search_analyzer\"},\"contentEncoding\":{\"type\":\"text\",\"fields\":{\"raw\":{\"type\":\"text\",\"analyzer\":\"keylower\",\"fielddata\":true}},\"copy_to\":[\"all_fields\"],\"analyzer\":\"cs_index_analyzer\",\"search_analyzer\":\"cs_search_analyzer\"},\"identifier\":{\"type\":\"text\",\"fields\":{\"raw\":{\"type\":\"text\",\"analyzer\":\"keylower\",\"fielddata\":true}},\"copy_to\":[\"all_fields\"],\"analyzer\":\"cs_index_analyzer\",\"search_analyzer\":\"cs_search_analyzer\"},\"node_id\":{\"type\":\"long\",\"fields\":{\"raw\":{\"type\":\"long\"}}},\"nodeType\":{\"type\":\"text\",\"fields\":{\"raw\":{\"type\":\"text\",\"analyzer\":\"keylower\",\"fielddata\":true}},\"copy_to\":[\"all_fields\"],\"analyzer\":\"cs_index_analyzer\",\"search_analyzer\":\"cs_search_analyzer\"},\"mimeType\":{\"type\":\"text\",\"fields\":{\"raw\":{\"type\":\"text\",\"analyzer\":\"keylower\",\"fielddata\":true}},\"copy_to\":[\"all_fields\"],\"analyzer\":\"cs_index_analyzer\",\"search_analyzer\":\"cs_search_analyzer\"},\"resourceType\":{\"type\":\"text\",\"fields\":{\"raw\":{\"type\":\"text\",\"analyzer\":\"keylower\",\"fielddata\":true}},\"copy_to\":[\"all_fields\"],\"analyzer\":\"cs_index_analyzer\",\"search_analyzer\":\"cs_search_analyzer\"},\"contentType\":{\"type\":\"text\",\"fields\":{\"raw\":{\"type\":\"text\",\"analyzer\":\"keylower\",\"fielddata\":true}},\"copy_to\":[\"all_fields\"],\"analyzer\":\"cs_index_analyzer\",\"search_analyzer\":\"cs_search_analyzer\"},\"allowedContentTypes\":{\"type\":\"text\",\"fields\":{\"raw\":{\"type\":\"text\",\"analyzer\":\"keylower\",\"fielddata\":true}},\"copy_to\":[\"all_fields\"],\"analyzer\":\"cs_index_analyzer\",\"search_analyzer\":\"cs_search_analyzer\"},\"objectType\":{\"type\":\"text\",\"fields\":{\"raw\":{\"type\":\"text\",\"analyzer\":\"keylower\",\"fielddata\":true}},\"copy_to\":[\"all_fields\"],\"analyzer\":\"cs_index_analyzer\",\"search_analyzer\":\"cs_search_analyzer\"},\"posterImage\":{\"type\":\"text\",\"fields\":{\"raw\":{\"type\":\"text\",\"analyzer\":\"keylower\",\"fielddata\":true}},\"copy_to\":[\"all_fields\"],\"analyzer\":\"cs_index_analyzer\",\"search_analyzer\":\"cs_search_analyzer\"},\"artifactUrl\":{\"type\":\"text\",\"fields\":{\"raw\":{\"type\":\"text\",\"analyzer\":\"keylower\",\"fielddata\":true}},\"copy_to\":[\"all_fields\"],\"analyzer\":\"cs_index_analyzer\",\"search_analyzer\":\"cs_search_analyzer\"},\"launchUrl\":{\"type\":\"text\",\"fields\":{\"raw\":{\"type\":\"text\",\"analyzer\":\"keylower\",\"fielddata\":true}},\"copy_to\":[\"all_fields\"],\"analyzer\":\"cs_index_analyzer\",\"search_analyzer\":\"cs_search_analyzer\"},\"previewUrl\":{\"type\":\"text\",\"fields\":{\"raw\":{\"type\":\"text\",\"analyzer\":\"keylower\",\"fielddata\":true}},\"copy_to\":[\"all_fields\"],\"analyzer\":\"cs_index_analyzer\",\"search_analyzer\":\"cs_search_analyzer\"},\"streamingUrl\":{\"type\":\"text\",\"fields\":{\"raw\":{\"type\":\"text\",\"analyzer\":\"keylower\",\"fielddata\":true}},\"copy_to\":[\"all_fields\"],\"analyzer\":\"cs_index_analyzer\",\"search_analyzer\":\"cs_search_analyzer\"},\"downloadUrl\":{\"type\":\"text\",\"fields\":{\"raw\":{\"type\":\"text\",\"analyzer\":\"keylower\",\"fielddata\":true}},\"copy_to\":[\"all_fields\"],\"analyzer\":\"cs_index_analyzer\",\"search_analyzer\":\"cs_search_analyzer\"},\"status\":{\"type\":\"text\",\"fields\":{\"raw\":{\"type\":\"text\",\"analyzer\":\"keylower\",\"fielddata\":true}},\"copy_to\":[\"all_fields\"],\"analyzer\":\"cs_index_analyzer\",\"search_analyzer\":\"cs_search_analyzer\"},\"pkgVersion\":{\"type\":\"double\",\"fields\":{\"raw\":{\"type\":\"double\"}}},\"lastUpdatedOn\":{\"type\":\"date\",\"fields\":{\"raw\":{\"type\":\"date\"}}},\"textbook_name\":{\"type\":\"text\",\"fields\":{\"raw\":{\"type\":\"text\",\"analyzer\":\"keylower\",\"fielddata\":true}},\"copy_to\":[\"all_fields\"],\"analyzer\":\"cs_index_analyzer\",\"search_analyzer\":\"cs_search_analyzer\"},\"level1Name\":{\"type\":\"text\",\"fields\":{\"raw\":{\"type\":\"text\",\"analyzer\":\"keylower\",\"fielddata\":true}},\"copy_to\":[\"all_fields\"],\"analyzer\":\"cs_index_analyzer\",\"search_analyzer\":\"cs_search_analyzer\"},\"level1Concept\":{\"type\":\"text\",\"fields\":{\"raw\":{\"type\":\"text\",\"analyzer\":\"keylower\",\"fielddata\":true}},\"copy_to\":[\"all_fields\"],\"analyzer\":\"cs_index_analyzer\",\"search_analyzer\":\"cs_search_analyzer\"},\"level2Name\":{\"type\":\"text\",\"fields\":{\"raw\":{\"type\":\"text\",\"analyzer\":\"keylower\",\"fielddata\":true}},\"copy_to\":[\"all_fields\"],\"analyzer\":\"cs_index_analyzer\",\"search_analyzer\":\"cs_search_analyzer\"},\"level2Concept\":{\"type\":\"text\",\"fields\":{\"raw\":{\"type\":\"text\",\"analyzer\":\"keylower\",\"fielddata\":true}},\"copy_to\":[\"all_fields\"],\"analyzer\":\"cs_index_analyzer\",\"search_analyzer\":\"cs_search_analyzer\"},\"level3Name\":{\"type\":\"text\",\"fields\":{\"raw\":{\"type\":\"text\",\"analyzer\":\"keylower\",\"fielddata\":true}},\"copy_to\":[\"all_fields\"],\"analyzer\":\"cs_index_analyzer\",\"search_analyzer\":\"cs_search_analyzer\"},\"level3Concept\":{\"type\":\"text\",\"fields\":{\"raw\":{\"type\":\"text\",\"analyzer\":\"keylower\",\"fielddata\":true}},\"copy_to\":[\"all_fields\"],\"analyzer\":\"cs_index_analyzer\",\"search_analyzer\":\"cs_search_analyzer\"},\"source\":{\"type\":\"text\",\"fields\":{\"raw\":{\"type\":\"text\",\"analyzer\":\"keylower\",\"fielddata\":true}},\"copy_to\":[\"all_fields\"],\"analyzer\":\"cs_index_analyzer\",\"search_analyzer\":\"cs_search_analyzer\"},\"sourceURL\":{\"type\":\"text\",\"fields\":{\"raw\":{\"type\":\"text\",\"analyzer\":\"keylower\",\"fielddata\":true}},\"copy_to\":[\"all_fields\"],\"analyzer\":\"cs_index_analyzer\",\"search_analyzer\":\"cs_search_analyzer\"},\"ml_Keywords\":{\"type\":\"text\",\"analyzer\":\"ml_custom_analyzer\",\"search_analyzer\":\"standard\"},\"ml_contentText\":{\"type\":\"text\",\"analyzer\":\"ml_custom_analyzer\",\"search_analyzer\":\"standard\"},\"ml_contentTextVector\":{\"type\":\"dense_vector\",\"dims\":768},\"label\":{\"type\":\"text\",\"analyzer\":\"cs_index_analyzer\",\"search_analyzer\":\"standard\"},\"all_fields\":{\"type\":\"text\",\"fields\":{\"raw\":{\"type\":\"text\",\"analyzer\":\"keylower\",\"fielddata\":true}},\"analyzer\":\"cs_index_analyzer\",\"search_analyzer\":\"cs_search_analyzer\"}}}}"; - ElasticSearchUtil.addIndex(CompositeSearchConstants.MVC_SEARCH_INDEX, - CompositeSearchConstants.MVC_SEARCH_INDEX_TYPE, settings, mappings,alias,esindex); - } - - @SuppressWarnings({ "rawtypes", "unchecked" }) - - - public void upsertDocument(String uniqueId, Map jsonIndexDocument) throws Exception { - - - String action = jsonIndexDocument.get("action").toString(); - jsonIndexDocument = removeExtraParams(jsonIndexDocument); - proocessNestedProps(jsonIndexDocument); - String jsonAsString = mapper.writeValueAsString(jsonIndexDocument); - switch (action) { - case "update-es-index": { - // Insert a new doc - ElasticSearchUtil.addDocumentWithId(CompositeSearchConstants.MVC_SEARCH_INDEX, - uniqueId, jsonAsString); - break; - } - case "update-content-rating" : { - String resp = ElasticSearchUtil.getDocumentAsStringById(CompositeSearchConstants.MVC_SEARCH_INDEX, - uniqueId); - if (null != resp && resp.contains(uniqueId)) { - LOGGER.info("ES Document Found With Identifier " + uniqueId + " | Updating Content Rating."); - Map metadata = (Map) jsonIndexDocument.get("metadata"); - String finalJsonindexasString = mapper.writeValueAsString(metadata); - CompletableFuture.runAsync(() -> { - ElasticSearchUtil.updateDocument(CompositeSearchConstants.MVC_SEARCH_INDEX, - uniqueId, finalJsonindexasString); - - }); - } else - LOGGER.info("ES Document Not Found With Identifier " + uniqueId + " | Skipped Updating Content Rating."); - } - case "update-ml-contenttextvector": { - List> ml_contentTextVectorList; - Set ml_contentTextVector = null; - ml_contentTextVectorList = jsonIndexDocument.get("ml_contentTextVector") != null ? (List>) jsonIndexDocument.get("ml_contentTextVector") : null; - if (ml_contentTextVectorList != null) { - ml_contentTextVector = new HashSet(ml_contentTextVectorList.get(0)); - - } - jsonIndexDocument.put("ml_contentTextVector", ml_contentTextVector); - jsonAsString = mapper.writeValueAsString(jsonIndexDocument); - } - case "update-ml-keywords": { - // Update a doc - ElasticSearchUtil.updateDocument(CompositeSearchConstants.MVC_SEARCH_INDEX, - uniqueId, jsonAsString); - - break; - } - default: - LOGGER.info("No Action Matched. Skipped Processing Event For " + uniqueId); - } - - } - - private void proocessNestedProps(Map jsonIndexDocument) throws IOException { - if (MapUtils.isNotEmpty(jsonIndexDocument)) { - for (String propertyName : jsonIndexDocument.keySet()) { - if (NESTED_FIELDS.contains(propertyName)) { - Map propertyNewValue = mapper.readValue((String) jsonIndexDocument.get(propertyName), - new TypeReference() { - }); - jsonIndexDocument.put(propertyName, propertyNewValue); - } - } - } - } - - - // Remove params which should not be inserted into ES - public Map removeExtraParams(Map obj) { - obj.remove("action"); - obj.remove("stage"); - return obj; - } - - -} diff --git a/platform-jobs/samza/mvc-processor-indexer/src/main/java/org/sunbird/mvcjobs/samza/task/MVCSearchIndexerTask.java b/platform-jobs/samza/mvc-processor-indexer/src/main/java/org/sunbird/mvcjobs/samza/task/MVCSearchIndexerTask.java deleted file mode 100644 index 88d84d2469..0000000000 --- a/platform-jobs/samza/mvc-processor-indexer/src/main/java/org/sunbird/mvcjobs/samza/task/MVCSearchIndexerTask.java +++ /dev/null @@ -1,100 +0,0 @@ -package org.sunbird.mvcjobs.samza.task; - -import org.apache.commons.collections.MapUtils; -import org.apache.commons.lang.StringUtils; -import org.apache.samza.config.Config; -import org.apache.samza.system.IncomingMessageEnvelope; -import org.apache.samza.system.OutgoingMessageEnvelope; -import org.apache.samza.system.SystemStream; -import org.apache.samza.task.MessageCollector; -import org.apache.samza.task.TaskContext; -import org.apache.samza.task.TaskCoordinator; -import org.sunbird.jobs.samza.task.BaseTask; -import org.sunbird.mvcjobs.samza.service.MVCProcessorService; -import org.sunbird.jobs.samza.service.ISamzaService; -import org.sunbird.jobs.samza.service.task.JobMetrics; -import org.sunbird.jobs.samza.util.JobLogger; -import org.sunbird.jobs.samza.util.SamzaCommonParams; -import org.sunbird.learning.util.ControllerUtil; - -import java.util.HashMap; -import java.util.Map; - - -public class MVCSearchIndexerTask extends BaseTask { - - private JobLogger LOGGER = new JobLogger(MVCSearchIndexerTask.class); - private ControllerUtil controllerUtil = new ControllerUtil(); - - private ISamzaService service; - private JobMetrics metrics; - - public ISamzaService getService() { - return service; - } - - public MVCSearchIndexerTask(Config config, TaskContext context, ISamzaService service) throws Exception { - init(config, context, service); - } - - public MVCSearchIndexerTask() { - - } - - public void init(Config config, TaskContext context, ISamzaService service) throws Exception { - try { - metrics = new JobMetrics(context, config.get("output.metrics.job.name"), config.get("output.metrics.topic.name")); - this.service = (service == null ? new MVCProcessorService() : service); - this.service.initialize(config); - LOGGER.info("Task initialized"); - } catch (Exception ex) { - LOGGER.error("Task initialization failed", ex); - throw ex; - } - } - - @Override - public void init(Config config, TaskContext context) throws Exception { - init(config, context, null); - } - - - @Override - public void process(IncomingMessageEnvelope envelope, MessageCollector collector, TaskCoordinator coordinator) throws Exception { - Map outgoingMap = getMessage(envelope); - try { - if (outgoingMap.containsKey(SamzaCommonParams.edata.name())) { - Map edata = (Map) outgoingMap.getOrDefault(SamzaCommonParams.edata.name(), new HashMap()); - if (MapUtils.isNotEmpty(edata) && StringUtils.equalsIgnoreCase("definition_update", edata.getOrDefault("action", "").toString())) { - LOGGER.info("definition_update event received for objectType: " + edata.getOrDefault("objectType", "").toString()); - String graphId = edata.getOrDefault("graphId", "").toString(); - String objectType = edata.getOrDefault("objectType", "").toString(); - controllerUtil.updateDefinitionCache(graphId, objectType); - } - } else { - service.processMessage(outgoingMap, metrics, collector); - } - } catch (Exception e) { - metrics.incErrorCounter(); - LOGGER.error("Error while processing message:", outgoingMap, e); - } - } - - @SuppressWarnings("unchecked") - private Map getMessage(IncomingMessageEnvelope envelope) { - try { - return (Map) envelope.getMessage(); - } catch (Exception e) { - e.printStackTrace(); - LOGGER.error("Invalid message:" + envelope.getMessage(), e); - return new HashMap(); - } - } - - @Override - public void window(MessageCollector collector, TaskCoordinator coordinator) { - Map event = metrics.collect(); - collector.send(new OutgoingMessageEnvelope(new SystemStream("kafka", metrics.getTopic()), event)); - metrics.clear(); - } -} \ No newline at end of file diff --git a/platform-jobs/samza/mvc-processor-indexer/src/main/resources/application.conf b/platform-jobs/samza/mvc-processor-indexer/src/main/resources/application.conf deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/platform-jobs/samza/mvc-processor-indexer/src/main/resources/log4j.xml b/platform-jobs/samza/mvc-processor-indexer/src/main/resources/log4j.xml deleted file mode 100644 index d2db3940cc..0000000000 --- a/platform-jobs/samza/mvc-processor-indexer/src/main/resources/log4j.xml +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/platform-jobs/samza/mvc-processor-indexer/src/test/java/org/sunbird/mvcjobs/samza/test/ContentUtilTest.java b/platform-jobs/samza/mvc-processor-indexer/src/test/java/org/sunbird/mvcjobs/samza/test/ContentUtilTest.java deleted file mode 100644 index a511d1816e..0000000000 --- a/platform-jobs/samza/mvc-processor-indexer/src/test/java/org/sunbird/mvcjobs/samza/test/ContentUtilTest.java +++ /dev/null @@ -1,48 +0,0 @@ -package org.sunbird.mvcjobs.samza.test; - -import com.google.gson.Gson; -import org.sunbird.mvcjobs.samza.service.util.ContentUtil; -import org.sunbird.searchindex.util.HTTPUtil; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.Mockito; -import org.mockito.MockitoAnnotations; -import org.powermock.api.mockito.PowerMockito; -import org.powermock.core.classloader.annotations.PowerMockIgnore; -import org.powermock.core.classloader.annotations.PrepareForTest; -import org.powermock.modules.junit4.PowerMockRunner; -import org.apache.samza.config.Config; - -import java.io.IOException; -import java.util.Map; - -import static org.mockito.Mockito.*; - -@RunWith(PowerMockRunner.class) -@PrepareForTest({HTTPUtil.class}) -@PowerMockIgnore({"javax.management.*", "sun.security.ssl.*", "javax.net.ssl.*" , "javax.crypto.*"}) -public class ContentUtilTest { - private Config configMock; - private String getResp = "{\"id\":\"api.content.read\",\"ver\":\"1.0\",\"ts\":\"2020-07-21T05:38:46.301Z\",\"params\":{\"resmsgid\":\"7224a4d0-cb14-11ea-9313-0912071b8abe\",\"msgid\":\"722281f0-cb14-11ea-9313-0912071b8abe\",\"status\":\"successful\",\"err\":null,\"errmsg\":null},\"responseCode\":\"OK\",\"result\":{\"content\":{\"ownershipType\":[\"createdBy\"],\"code\":\"test.res.1\",\"channel\":\"in.ekstep\",\"language\":[\"English\"],\"mediaType\":\"content\",\"osId\":\"org.sunbird.quiz.app\",\"languageCode\":[\"en\"],\"version\":2,\"versionKey\":\"1591949601174\",\"license\":\"CC BY 4.0\",\"idealScreenDensity\":\"hdpi\",\"framework\":\"NCFCOPY\",\"s3Key\":\"content/do_113041248230580224116/artifact/validecml_1591949596304.zip\",\"createdBy\":\"95e4942d-cbe8-477d-aebd-ad8e6de4bfc8\",\"compatibilityLevel\":1,\"name\":\"Resource Content 1\",\"status\":\"Draft\",\"level1Concept\":[\"Addition\"],\"level1Name\":[\"Math-Magic\"],\"textbook_name\":[\"How Many Times?\"],\"sourceURL\":\"https://diksha.gov.in/play/content/do_30030488\",\"source\":[\"Diksha 1\"]}}}"; - private String eventData = "{\"identifier\":\"do_113041248230580224116\",\"action\":\"update-es-index\",\"stage\":1}"; - private String uniqueId = "do_113041248230580224116"; - @Before - public void setup(){ - MockitoAnnotations.initMocks(this); - configMock = mock(Config.class); - stub(configMock.get("nested.fields")).toReturn("badgeAssertions,targets,badgeAssociations,plugins,me_totalTimeSpent,me_totalPlaySessionCount,me_totalTimeSpentInSec,batches"); - } - - @Test - public void getContentMetaData()throws Exception { - PowerMockito.mockStatic(HTTPUtil.class); - when(HTTPUtil.makeGetRequest(Mockito.anyString())).thenReturn(getResp); - ContentUtil.getContentMetaData(getEvent(eventData),uniqueId); - } - - public Map getEvent(String message) throws IOException { - return new Gson().fromJson(message, Map.class); - } -} - diff --git a/platform-jobs/samza/mvc-processor-indexer/src/test/java/org/sunbird/mvcjobs/samza/test/MVCProcessorCassandraTest.java b/platform-jobs/samza/mvc-processor-indexer/src/test/java/org/sunbird/mvcjobs/samza/test/MVCProcessorCassandraTest.java deleted file mode 100644 index 570839cb57..0000000000 --- a/platform-jobs/samza/mvc-processor-indexer/src/test/java/org/sunbird/mvcjobs/samza/test/MVCProcessorCassandraTest.java +++ /dev/null @@ -1,77 +0,0 @@ -package org.sunbird.mvcjobs.samza.test; - -import com.google.gson.Gson; -import org.sunbird.mvcjobs.samza.service.util.CassandraConnector; -import org.sunbird.mvcjobs.samza.service.util.MVCProcessorCassandraIndexer; -import org.sunbird.mvcsearchindex.elasticsearch.ElasticSearchUtil; -import org.sunbird.searchindex.util.HTTPUtil; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.Mockito; -import org.mockito.MockitoAnnotations; -import org.powermock.api.mockito.PowerMockito; -import org.powermock.core.classloader.annotations.PowerMockIgnore; -import org.powermock.core.classloader.annotations.PrepareForTest; -import org.powermock.modules.junit4.PowerMockRunner; -import org.apache.samza.config.Config; - -import java.io.IOException; -import java.util.Map; - -import static org.mockito.Mockito.*; - -@RunWith(PowerMockRunner.class) -@PrepareForTest({ElasticSearchUtil.class, Config.class,MVCProcessorCassandraIndexer.class,CassandraConnector.class, HTTPUtil.class}) -@PowerMockIgnore({"javax.management.*", "sun.security.ssl.*", "javax.net.ssl.*" , "javax.crypto.*"}) -public class MVCProcessorCassandraTest { - private String uniqueId = "do_113041248230580224116"; - private String eventData = "{\"identifier\":\"do_113041248230580224116\",\"action\":\"update-es-index\",\"stage\":1,\"ownershipType\":[\"createdBy\"],\"code\":\"test.res.1\",\"channel\":\"in.ekstep\",\"language\":[\"English\"],\"mediaType\":\"content\",\"osId\":\"org.sunbird.quiz.app\",\"languageCode\":[\"en\"],\"version\":2,\"versionKey\":\"1591949601174\",\"license\":\"CC BY 4.0\",\"idealScreenDensity\":\"hdpi\",\"framework\":\"NCFCOPY\",\"s3Key\":\"content/do_113041248230580224116/artifact/validecml_1591949596304.zip\",\"createdBy\":\"95e4942d-cbe8-477d-aebd-ad8e6de4bfc8\",\"compatibilityLevel\":1,\"name\":\"Resource Content 1\",\"status\":\"Draft\",\"level1Concept\":[\"Addition\"],\"level1Name\":[\"Math-Magic\"],\"textbook_name\":[\"How Many Times?\"],\"sourceURL\":\"https://diksha.gov.in/play/content/do_30030488\",\"source\":[\"Diksha 1\"]}"; - private String eventData2 = "{\"action\":\"update-ml-keywords\",\"stage\":\"2\",\"ml_Keywords\":[\"maths\",\"addition\",\"add\"],\"ml_contentText\":\"This is the content text for addition of two numbers.\"}"; - private String eventData3 = "{\"action\":\"update-ml-contenttextvector\",\"stage\":3,\"ml_contentTextVector\":[[0.2961231768131256, 0.13621050119400024, 0.655802309513092, -0.33641257882118225]]}"; - private String getResp = "{\"id\":\"api.content.read\",\"ver\":\"1.0\",\"ts\":\"2020-07-21T05:38:46.301Z\",\"params\":{\"resmsgid\":\"7224a4d0-cb14-11ea-9313-0912071b8abe\",\"msgid\":\"722281f0-cb14-11ea-9313-0912071b8abe\",\"status\":\"successful\",\"err\":null,\"errmsg\":null},\"responseCode\":\"OK\",\"result\":{\"content\":{\"ownershipType\":[\"createdBy\"],\"code\":\"test.res.1\",\"channel\":\"in.ekstep\",\"language\":[\"English\"],\"mediaType\":\"content\",\"osId\":\"org.sunbird.quiz.app\",\"languageCode\":[\"en\"],\"version\":2,\"versionKey\":\"1591949601174\",\"license\":\"CC BY 4.0\",\"idealScreenDensity\":\"hdpi\",\"framework\":\"NCFCOPY\",\"s3Key\":\"content/do_113041248230580224116/artifact/validecml_1591949596304.zip\",\"createdBy\":\"95e4942d-cbe8-477d-aebd-ad8e6de4bfc8\",\"compatibilityLevel\":1,\"name\":\"Resource Content 1\",\"status\":\"Draft\",\"level1Concept\":[\"Addition\"],\"level1Name\":[\"Math-Magic\"],\"textbook_name\":[\"How Many Times?\"],\"sourceURL\":\"https://diksha.gov.in/play/content/do_30030488\",\"source\":[\"Diksha 1\"]}}}"; - private String postReqStage1Resp = "{\"id\":\"api.daggit\",\"params\":{\"err\":\"null\",\"errmsg\":\"Dag Initialization failed\",\"msgid\":\"\",\"resmsgid\":\"null\",\"status\":\"success\"},\"responseCode\":\"OK\",\"result\":{\"execution_date\":\"2020-07-08\",\"experiment_name\":\"Content_tagging_20200708-141923\",\"status\":200},\"ts\":\"2020-07-08 14:19:23:1594198163\",\"ver\":\"v1\"}"; - private String postReqStage2Resp = "{\"ets\":\"2020-07-14 15:27:23:1594720643\",\"id\":\"api.ml.vector\",\"params\":{\"err\":\"null\",\"errmsg\":\"null\",\"msgid\":\"\",\"resmsgid\":\"null\",\"status\":\"success\"},\"result\":{\"action\":\"get_BERT_embedding\",\"vector\":[[]]}}"; - private Config configMock; - - @Before - public void setup(){ - MockitoAnnotations.initMocks(this); - configMock = mock(Config.class); - stub(configMock.get("nested.fields")).toReturn("badgeAssertions,targets,badgeAssociations,plugins,me_totalTimeSpent,me_totalPlaySessionCount,me_totalTimeSpentInSec,batches"); - } - - @Test - public void testInsertToCassandraForStage1() throws Exception { - PowerMockito.mockStatic(HTTPUtil.class); - when(HTTPUtil.makePostRequest(Mockito.anyString(),Mockito.anyString())).thenReturn(postReqStage1Resp); - PowerMockito.mockStatic(CassandraConnector.class); - PowerMockito.doNothing().when(CassandraConnector.class); - CassandraConnector.updateContentProperties(Mockito.anyString(),Mockito.anyMap()); - MVCProcessorCassandraIndexer cassandraManager = new MVCProcessorCassandraIndexer(); - cassandraManager.insertIntoCassandra(getEvent(eventData),uniqueId); - } - @Test - public void testInsertToCassandraForStage2() throws Exception { - PowerMockito.mockStatic(HTTPUtil.class); - when(HTTPUtil.makePostRequest(Mockito.anyString(),Mockito.anyString())).thenReturn(postReqStage2Resp); - PowerMockito.mockStatic(CassandraConnector.class); - PowerMockito.doNothing().when(CassandraConnector.class); - CassandraConnector.updateContentProperties(Mockito.anyString(),Mockito.anyMap()); - MVCProcessorCassandraIndexer cassandraManager = new MVCProcessorCassandraIndexer(); - cassandraManager.insertIntoCassandra(getEvent(eventData2),uniqueId); - } - @Test - public void testInsertToCassandraForStage3() throws Exception { - PowerMockito.mockStatic(CassandraConnector.class); - PowerMockito.doNothing().when(CassandraConnector.class); - CassandraConnector.updateContentProperties(Mockito.anyString(),Mockito.anyMap()); - MVCProcessorCassandraIndexer cassandraManager = new MVCProcessorCassandraIndexer(); - cassandraManager.insertIntoCassandra(getEvent(eventData3),uniqueId); - } - - public Map getEvent(String message) throws IOException { - return new Gson().fromJson(message, Map.class); - } - -} diff --git a/platform-jobs/samza/mvc-processor-indexer/src/test/java/org/sunbird/mvcjobs/samza/test/MVCProcessorESIndexerTest.java b/platform-jobs/samza/mvc-processor-indexer/src/test/java/org/sunbird/mvcjobs/samza/test/MVCProcessorESIndexerTest.java deleted file mode 100644 index 848317d98f..0000000000 --- a/platform-jobs/samza/mvc-processor-indexer/src/test/java/org/sunbird/mvcjobs/samza/test/MVCProcessorESIndexerTest.java +++ /dev/null @@ -1,89 +0,0 @@ - -package org.sunbird.mvcjobs.samza.test; - -import com.google.gson.Gson; -import org.apache.commons.lang.StringUtils; -import org.sunbird.mvcjobs.samza.service.util.MVCProcessorESIndexer; -import org.sunbird.mvcsearchindex.elasticsearch.ElasticSearchUtil; -import static org.junit.Assert.assertTrue; - -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.Mockito; -import org.mockito.MockitoAnnotations; -import org.powermock.api.mockito.PowerMockito; -import org.powermock.core.classloader.annotations.PowerMockIgnore; -import org.powermock.core.classloader.annotations.PrepareForTest; -import org.powermock.modules.junit4.PowerMockRunner; -import org.apache.samza.config.Config; - -import java.io.IOException; -import java.util.Map; - -import static org.mockito.Mockito.*; - -@RunWith(PowerMockRunner.class) -@PrepareForTest({ElasticSearchUtil.class, Config.class, MVCProcessorESIndexer.class}) -@PowerMockIgnore({"javax.management.*", "sun.security.ssl.*", "javax.net.ssl.*" , "javax.crypto.*"}) -public class MVCProcessorESIndexerTest { - private String uniqueId = "do_113041248230580224116"; - private String eventDataNewDoc = "{\"identifier\":\"do_113041248230580224116\",\"action\":\"update-es-index\",\"stage\":1}"; - private String eventDataMlKeywords = "{\"action\":\"update-ml-keywords\",\"stage\":\"2\",\"ml_Keywords\":[\"maths\",\"addition\",\"add\"],\"ml_contentText\":\"This is the content text for addition of two numbers.\"}"; - private String eventDataContentRating = "{\"action\":\"update-content-rating\",\"stage\":4,\"metadata\":{\"me_averageRating\":\"1\",\"me_total_time_spent_in_app\":\"2\",\"me_total_time_spent_in_portal\":\"3\",\"me_total_time_spent_in_desktop\":\"4\",\"me_total_play_sessions_in_app\":\"5\",\"me_total_play_sessions_in_portal\":\"6\",\"me_total_play_sessions_in_desktop\":\"7\"}}"; - private String eventDataContentTextVector = "{\"action\":\"update-ml-contenttextvector\",\"stage\":3,\"ml_contentTextVector\":[[1.1,2,7.4,68]]}"; - private Config configMock; - private MVCProcessorESIndexer mvcProcessorESIndexer = new MVCProcessorESIndexer(); - - @Before - public void setup(){ - MockitoAnnotations.initMocks(this); - configMock = mock(Config.class); - stub(configMock.get("nested.fields")).toReturn("badgeAssertions,targets,badgeAssociations,plugins,me_totalTimeSpent,me_totalPlaySessionCount,me_totalTimeSpentInSec,batches"); - PowerMockito.mockStatic(ElasticSearchUtil.class); - PowerMockito.doNothing().when(ElasticSearchUtil.class); - } - - @Test - public void testUpsertDocumentCaseUpdateEsIndex() throws Exception { - ElasticSearchUtil.addDocumentWithId(Mockito.anyString(),Mockito.anyString(),Mockito.anyString()); - mvcProcessorESIndexer.upsertDocument(uniqueId,getEvent(eventDataNewDoc)); - when(ElasticSearchUtil.getDocumentAsStringById(Mockito.anyString(),Mockito.anyString())).thenReturn(uniqueId); - String doc = ElasticSearchUtil.getDocumentAsStringById(Mockito.anyString(),Mockito.anyString()); - assertTrue(StringUtils.contains(doc, uniqueId)); - } - - @Test - public void testUpsertDocumentUpdateMlKeywords() throws Exception { - ElasticSearchUtil.updateDocument(Mockito.anyString(),Mockito.anyString(),Mockito.anyString()); - mvcProcessorESIndexer.upsertDocument(uniqueId,getEvent(eventDataMlKeywords)); - when(ElasticSearchUtil.getDocumentAsStringById(Mockito.anyString(),Mockito.anyString())).thenReturn(uniqueId); - String doc = ElasticSearchUtil.getDocumentAsStringById(Mockito.anyString(),Mockito.anyString()); - assertTrue(StringUtils.contains(doc, uniqueId)); - } - - @Test - public void testUpsertDocumentUpdateMlContentTextVector() throws Exception { - ElasticSearchUtil.updateDocument(Mockito.anyString(),Mockito.anyString(),Mockito.anyString()); - mvcProcessorESIndexer.upsertDocument(uniqueId,getEvent(eventDataContentTextVector)); - when(ElasticSearchUtil.getDocumentAsStringById(Mockito.anyString(),Mockito.anyString())).thenReturn(uniqueId); - String doc = ElasticSearchUtil.getDocumentAsStringById(Mockito.anyString(),Mockito.anyString()); - assertTrue(StringUtils.contains(doc, uniqueId)); - } - - @Test - public void testUpsertDocumentUpdateContentRating() throws Exception { - ElasticSearchUtil.updateDocument(Mockito.anyString(),Mockito.anyString(),Mockito.anyString()); - when(ElasticSearchUtil.getDocumentAsStringById(Mockito.anyString(),Mockito.anyString())).thenReturn(uniqueId); - mvcProcessorESIndexer.upsertDocument(uniqueId,getEvent(eventDataContentRating)); - String doc = ElasticSearchUtil.getDocumentAsStringById(Mockito.anyString(),Mockito.anyString()); - assertTrue(StringUtils.contains(doc, uniqueId)); - } - - - public Map getEvent(String message) throws IOException { - return new Gson().fromJson(message, Map.class); - } - -} - diff --git a/platform-jobs/samza/mvc-processor-indexer/src/test/resources/application.conf b/platform-jobs/samza/mvc-processor-indexer/src/test/resources/application.conf deleted file mode 100644 index ca201c8819..0000000000 --- a/platform-jobs/samza/mvc-processor-indexer/src/test/resources/application.conf +++ /dev/null @@ -1,40 +0,0 @@ -# Graph Configuration -graph.dir=/data/graphDB -akka.request_timeout=30 -environment.id=10000000 -graph.ids=domain -graph.passport.key.base=31b6fd1c4d64e745c867e61a45edc34a -route.domain="bolt://localhost:7687" -route.bolt.write.domain="bolt://localhost:7687" -route.bolt.read.domain="bolt://localhost:7687" -route.bolt.comment.domain="bolt://localhost:7687" -route.all="bolt://localhost:7687" -route.bolt.write.all="bolt://localhost:7687" -route.bolt.read.all="bolt://localhost:7687" -route.bolt.comment.all="bolt://localhost:7687" -shard.id=1 -platform.auth.check.enabled=false -platform.cache.ttl=3600000 - -# Elasticsearch properties -search.es_conn_info="localhost:9200" -search.fields.query=["name^100","title^100","lemma^100","code^100","tags^100","domain","subject","description^10","keywords^25","ageGroup^10","filter^10","theme^10","genre^10","objects^25","contentType^100","language^200","teachingMode^25","skills^10","learningObjective^10","curriculum^100","gradeLevel^100","developer^100","attributions^10","owner^50","text","words","releaseNotes"] -search.fields.date=["lastUpdatedOn","createdOn","versionDate","lastSubmittedOn","lastPublishedOn"] -search.batch.size=500 -search.connection.timeout=30 -search.connection.timeout=30 -platform-api-url="http://localhost:8080/language-service" - -LearningActorSystem{ - default-dispatcher { - type = "Dispatcher" - executor = "fork-join-executor" - fork-join-executor { - parallelism-min = 1 - parallelism-factor = 2.0 - parallelism-max = 4 - } - # Throughput for default Dispatcher, set to 1 for as fair as possible - throughput = 1 - } -} diff --git a/platform-jobs/samza/pom.xml b/platform-jobs/samza/pom.xml deleted file mode 100644 index 051a354bbc..0000000000 --- a/platform-jobs/samza/pom.xml +++ /dev/null @@ -1,45 +0,0 @@ - - 4.0.0 - - org.sunbird - platform-jobs - 1.1-SNAPSHOT - - - UTF-8 - 0.14.1 - 1.8 - 2.11 - 2.6.2 - 1.1.0 - - org.sunbird - samza - pom - EkStep Platform Samza Jobs - This Project Contains all the backend jobs, they may be the Pipeline Consumers. - - common - course-common - publish-pipeline - qrcode-image-generator - distribution - qr-image-generator - auto-creator - mvc-processor-indexer - - - - - - org.apache.maven.plugins - maven-compiler-plugin - - ${java.version} - ${java.version} - - - - - \ No newline at end of file diff --git a/platform-jobs/samza/publish-pipeline/.gitignore b/platform-jobs/samza/publish-pipeline/.gitignore deleted file mode 100644 index b83d22266a..0000000000 --- a/platform-jobs/samza/publish-pipeline/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/target/ diff --git a/platform-jobs/samza/publish-pipeline/pom.xml b/platform-jobs/samza/publish-pipeline/pom.xml deleted file mode 100644 index 01f8da356f..0000000000 --- a/platform-jobs/samza/publish-pipeline/pom.xml +++ /dev/null @@ -1,87 +0,0 @@ - - 4.0.0 - - org.sunbird - samza - 1.1-SNAPSHOT - - publish-pipeline - 0.0.386 - - - org.sunbird - samza-common - 1.1-SNAPSHOT - - - org.sunbird - content-manager - 1.1-beta - jar - - - org.apache.kafka - kafka-clients - - - - - org.sunbird - unit-tests - 1.1-SNAPSHOT - test - - - org.mockito - mockito-all - 1.10.19 - test - - - com.fasterxml.jackson.core - jackson-databind - 2.7.8 - - - com.fasterxml.jackson.core - jackson-core - 2.6.0 - - - com.fasterxml.jackson.core - jackson-annotations - 2.7.8 - - - - - - org.apache.maven.plugins - maven-compiler-plugin - - 1.8 - 1.8 - - - - - maven-assembly-plugin - - - src/main/assembly/src.xml - - - - - make-assembly - package - - single - - - - - - - diff --git a/platform-jobs/samza/publish-pipeline/src/main/assembly/src.xml b/platform-jobs/samza/publish-pipeline/src/main/assembly/src.xml deleted file mode 100644 index 70c2e6a187..0000000000 --- a/platform-jobs/samza/publish-pipeline/src/main/assembly/src.xml +++ /dev/null @@ -1,69 +0,0 @@ - - - - - distribution - - tar.gz - - false - - - ${basedir} - - README* - LICENSE* - NOTICE* - - - - - - ${basedir}/src/main/resources/log4j.xml - lib - - - - ${basedir}/src/main/config/publish-pipeline.properties - config - true - - - - - bin - - org.apache.samza:samza-shell:tgz:dist:* - - 0744 - true - - - lib - - org.apache.samza:samza-api - org.sunbird:publish-pipeline - org.apache.samza:samza-core_2.11 - org.apache.samza:samza-kafka_2.11 - org.apache.samza:samza-yarn_2.11 - org.apache.samza:samza-log4j - org.apache.kafka:kafka_2.11 - org.apache.hadoop:hadoop-hdfs - - true - - - diff --git a/platform-jobs/samza/publish-pipeline/src/main/config/local.publish-pipeline.properties b/platform-jobs/samza/publish-pipeline/src/main/config/local.publish-pipeline.properties deleted file mode 100644 index a3049416de..0000000000 --- a/platform-jobs/samza/publish-pipeline/src/main/config/local.publish-pipeline.properties +++ /dev/null @@ -1,167 +0,0 @@ -# Job -job.factory.class=org.apache.samza.job.yarn.YarnJobFactory -job.name=dev.publish.pipeline - -# YARN -yarn.package.path=file://${basedir}/target/${project.artifactId}-${pom.version}-distribution.tar.gz - -# Metrics -metrics.reporters=snapshot,jmx -metrics.reporter.snapshot.class=org.apache.samza.metrics.reporter.MetricsSnapshotReporterFactory -metrics.reporter.snapshot.stream=kafka.dev.lp.metrics -metrics.reporter.jmx.class=org.apache.samza.metrics.reporter.JmxReporterFactory - -# Task -task.class=org.sunbird.jobs.samza.task.PublishPipelineTask -#task.inputs=kafka.telemetry.raw -task.inputs=kafka.local.learning.job.request -task.checkpoint.factory=org.apache.samza.checkpoint.kafka.KafkaCheckpointManagerFactory -task.checkpoint.system=kafka -task.checkpoint.replication.factor=1 -task.commit.ms=60000 -task.opts=-agentlib:jdwp=transport=dt_socket,address=localhost:9009,server=y,suspend=y -task.window.ms=300000 -#task.opts=-Dfile.encoding=UTF8 -task.broadcast.inputs=kafka.dev.system.command#0 - -# Serializers -serializers.registry.json.class=org.sunbird.jobs.samza.serializers.EkstepJsonSerdeFactory -serializers.registry.metrics.class=org.apache.samza.serializers.MetricsSnapshotSerdeFactory - -# Systems -systems.kafka.samza.factory=org.apache.samza.system.kafka.KafkaSystemFactory -systems.kafka.samza.msg.serde=json -systems.kafka.streams.metrics.samza.msg.serde=metrics -systems.kafka.consumer.zookeeper.connect=localhost:2181 -systems.kafka.consumer.auto.offset.reset=largest -systems.kafka.producer.bootstrap.servers=localhost:9092 - -# Job Coordinator -job.coordinator.system=kafka -# Normally, this would be 3, but we have only one broker. -job.coordinator.replication.factor=1 - -# Job specific config properties -graph.dir="/data/graphDB" -redis.host=localhost -redis.port=6379 -redis.maxConnections=128 -akka.request_timeout=30 -environment.id=10000000 -graph.ids=domain -graph.passport.key.base=31b6fd1c4d64e745c867e61a45edc34a -route.domain=bolt://localhost:7687 -route.bolt.write.domain=bolt://localhost:7687 -route.bolt.read.domain=bolt://localhost:7687 -route.bolt.comment.domain=bolt://localhost:7687 -route.all=bolt://localhost:7687 -route.bolt.write.all=bolt://localhost:7687 -route.bolt.read.all=bolt://localhost:7687 -route.bolt.comment.all=bolt://localhost:7687 -shard.id=1 -platform.auth.check.enabled=false -platform.cache.ttl=3600000 -backend_telemetry_topic=local.telemetry.backend -failed_event_topic=local.learning.job.request - -#Current environment -cloud_storage.env=dev - -#Folder configuration -cloud_storage.content.folder=content -cloud_storage.itemset.folder = "itemset" -cloud_storage.asset.folder=assets -cloud_storage.artefact.folder=artifact -cloud_storage.bundle.folder=bundle -cloud_storage.media.folder=media -cloud_storage.ecar.folder=ecar_files -cloud_storage.upload.url.ttl=600 - - -# Media download configuration -content.media.base.url=https://dev.ekstep.in -plugin.media.base.url=https://dev.ekstep.in - -#directory location where store unzip file -dist.directory = /data/tmp/dist/ -output.zipfile = /data/tmp/story.zip -source.folder = /data/tmp/temp2/ -save.directory = /data/tmp/temp/ - -MAX_CONTENT_PACKAGE_FILE_SIZE_LIMIT = 52428800 -MAX_ASSET_FILE_SIZE_LIMIT = 20971520 -RETRY_ASSET_DOWNLOAD_COUNT = 1 - -platform-api-url=http://localhost:8080/learning-service - -lp.tempfile.location=__lp_tmpfile_location__ -publish.collection.fullecar.disable=true -max.iteration.count.samza.job=2 -publish.content.limit=200 - -#Remote Debug Configuration -#task.opts=-agentlib:jdwp=transport=dt_socket,address=localhost:9009,server=y,suspend=y - -# Metrics -output.metrics.job.name=publish-pipeline -output.metrics.topic.name=__env__.pipeline_metrics - -#Failed Topic Config -output.failed.events.topic.name=local.learning.events.failed - -telemetry_env=LOCAL -# Configuration for default channel ID -channel.default=in.ekstep - -#Streamable media type list -stream.mime.type=video/mp4,video/webm -stream.keyspace.name=platform_db -stream.keyspace.table=job_request - -cassandra.lp.connection=localhost:9042 -cassandra.lpa.connection=localhost:9042 - -search.es_conn_info=localhost:9200 - -#restrict.metadata.objectTypes=Content,ContentImage - -content.nested.fields=badgeAssertions,targets,badgeAssociations - - -# Max size(width/height) of thumbnail in pixels -max.thumbnail.size.pixels=150 - -installation.id= - -# Cloud store details (Please replace them for local testing) -cloud_storage_type= -azure_storage_key= -azure_storage_secret= - -azure_storage_container= -aws_storage_key= -aws_storage_secret= -aws_storage_container= - -#Post publish Job topic name -post.publish.event.topic=local.content.postpublish.request -post.publish.mvc.topic=local.mvc.processor.job.request -kp.print.service.base.url=http://11.2.2.4:5001 -lp.assessment.tmp_file_location=/tmp/ -lp.assessment.template_name=questionSetTemplate.vm - -content.tagging.backward_enable=true -content.tagging.property=subject,medium - -# For enabling transfer of content from one path to other -content.upload.context.driven=true - -# PDF generation for contents linked to ItemSet -itemset.generate.pdf=true -content.streaming_enabled=true - -# This is added to handle large artifacts sizes differently -content.artifact.size.for_online=209715200 - -#Content Type Primary Category mapping -contentTypeToPrimaryCategory={\"ClassroomTeachingVideo\":\"Explanation Content\",\"ConceptMap\":\"Learning Resource\",\"Course\":\"Course\",\"CuriosityQuestionSet\":\"Practice Question Set\",\"eTextBook\":\"eTextbook\",\"ExperientialResource\":\"Learning Resource\",\"ExplanationResource\":\"Explanation Content\",\"ExplanationVideo\":\"Explanation Content\",\"FocusSpot\":\"Teacher Resource\",\"LearningOutcomeDefinition\":\"Teacher Resource\",\"MarkingSchemeRubric\":\"Teacher Resource\",\"PedagogyFlow\":\"Teacher Resource\",\"PracticeQuestionSet\":\"Practice Question Set\",\"PracticeResource\":\"Practice Question Set\",\"SelfAssess\":\"Course Assessment\",\"TeachingMethod\":\"Teacher Resource\",\"TextBook\":\"Digital Textbook\",\"Collection\":\"Content Playlist\",\"ExplanationReadingMaterial\":\"Learning Resource\",\"LearningActivity\":\"Learning Resource\",\"LessonPlan\":\"Content Playlist\",\"LessonPlanResource\":\"Teacher Resource\",\"PreviousBoardExamPapers\":\"Learning Resource\",\"TVLesson\":\"Explanation Content\",\"OnboardingResource\":\"Learning Resource\",\"ReadingMaterial\":\"Learning Resource\",\"Template\":\"Template\",\"Asset\":\"Asset\",\"Plugin\":\"Plugin\",\"LessonPlanUnit\":\"Lesson Plan Unit\",\"CourseUnit\":\"Course Unit\",\"TextBookUnit\":\"Textbook Unit\"} \ No newline at end of file diff --git a/platform-jobs/samza/publish-pipeline/src/main/config/publish-pipeline.properties b/platform-jobs/samza/publish-pipeline/src/main/config/publish-pipeline.properties deleted file mode 100644 index fc8c47972b..0000000000 --- a/platform-jobs/samza/publish-pipeline/src/main/config/publish-pipeline.properties +++ /dev/null @@ -1,192 +0,0 @@ -# Job -job.factory.class=org.apache.samza.job.yarn.YarnJobFactory -job.name=__env__.publish-pipeline -job.container.count=__publish_pipeline_container_count__ - -# YARN -yarn.package.path=http://__yarn_host__:__yarn_port__/__env__/${project.artifactId}-${pom.version}-distribution.tar.gz -yarn.container.memory.mb=__yarn_container_memory_mb__ - -# Metrics -metrics.reporters=snapshot,jmx -metrics.reporter.snapshot.class=org.apache.samza.metrics.reporter.MetricsSnapshotReporterFactory -metrics.reporter.snapshot.stream=kafka.__env__.metrics -metrics.reporter.jmx.class=org.apache.samza.metrics.reporter.JmxReporterFactory - -# Task -task.class=org.sunbird.jobs.samza.task.PublishPipelineTask -task.inputs=kafka.__env__.learning.job.request -task.checkpoint.factory=org.apache.samza.checkpoint.kafka.KafkaCheckpointManagerFactory -task.checkpoint.system=kafka -task.checkpoint.replication.factor=__samza_checkpoint_replication_factor__ -task.commit.ms=60000 -task.window.ms=300000 -task.opts=__publish_pipeline_task_opts__ -task.broadcast.inputs=kafka.__env__.system.command#0 - -# Serializers -serializers.registry.json.class=org.sunbird.jobs.samza.serializers.EkstepJsonSerdeFactory -serializers.registry.metrics.class=org.apache.samza.serializers.MetricsSnapshotSerdeFactory - -# Systems -systems.kafka.samza.factory=org.apache.samza.system.kafka.KafkaSystemFactory -systems.kafka.samza.msg.serde=json -systems.kafka.streams.metrics.samza.msg.serde=metrics -systems.kafka.consumer.zookeeper.connect=__zookeepers__ -systems.kafka.consumer.auto.offset.reset=smallest -systems.kafka.samza.offset.default=oldest -systems.kafka.producer.bootstrap.servers=__kafka_brokers__ - - -# Job Coordinator -job.coordinator.system=kafka -# Normally, this would be 3, but we have only one broker. -job.coordinator.replication.factor=__samza_coordinator_replication_factor__ - -#Job specif configuration -redis.host=__redis_host__ -redis.port=__redis_port__ -redis.maxConnections=128 -akka.request_timeout=600 -environment.id=__environment_id__ -graph.passport.key.base=__graph_passport_key__ -route.domain=__lp_bolt_url__ -route.bolt.read.domain=__lp_bolt_read_url__ -route.bolt.write.domain=__lp_bolt_write_url__ -route.all=__other_bolt_url__ -route.bolt.read.all=__other_bolt_read_url__ -route.bolt.write.all=__other_bolt_write_url__ -shard.id=__mw_shard_id__ - -content.keyspace.name=__keyspace_name__ -content.keyspace.table=__keyspace_table__ -assessment.keyspace.name=__keyspace_name__ -hierarchy.keyspace.name=__hierarchy_keyspace_name__ -content.hierarchy.table=content_hierarchy -CONTENT_TO_VEC_URL=__content_to_vec_url__ -platform-api-url=__lp_url__ -ekstepPlatformApiUserId=ilimi -graph.dir="/data/graphDB" -graph.ids=["domain","language","en","hi","ka","te","ta"] -platform.auth.check.enabled=false -platform.cache.ttl=3600000 -kafka.topics.backend.telemetry=__env__.telemetry.raw -kafka.topics.failed=__env__.learning.job.request - -#Current environment -cloud_storage.env=__cloud_storage_config_environment__ - -#Folder configuration -cloud_storage.content.folder=content -cloud_storage.itemset.folder=itemset -cloud_storage.asset.folder=assets -cloud_storage.artefact.folder=artifact -cloud_storage.bundle.folder=bundle -cloud_storage.media.folder=media -cloud_storage.ecar.folder=ecar_files -cloud_storage.upload.url.ttl=600 - - -# Media download configuration -content.media.base.url=__content_media_base_url__ -plugin.media.base.url=__plugin_media_base_url__ - -#directory location where store unzip file -dist.directory=/tmp/dist/ -output.zipfile=/tmp/story.zip -source.folder=/tmp/temp2/ -save.directory=/tmp/temp/ - -MAX_CONTENT_PACKAGE_FILE_SIZE_LIMIT=52428800 -MAX_ASSET_FILE_SIZE_LIMIT=20971520 -RETRY_ASSET_DOWNLOAD_COUNT=1 - -lp.tempfile.location=__lp_tmpfile_location__ -max.iteration.count.samza.job=__max_iteration_count_for_samza_job__ -publish.content.limit=200 - - -# Metrics -output.metrics.job.name=publish-pipeline -output.metrics.topic.name=__env__.pipeline_metrics - -#Failed Topic Config -output.failed.events.topic.name=__env__.learning.events.failed - -telemetry_env=__env_name__ -installation.id=__installation_id__ - -# Cloud store details -cloud_storage_type=__cloud_storage_type__ -azure_storage_key=__azure_storage_key__ -azure_storage_secret=__azure_storage_secret__ -azure_storage_container=__azure_storage_container__ -aws_storage_key=__aws_access_key_id__ -aws_storage_secret=__aws_secret_access_key__ -aws_storage_container=__aws_storage_container__ - -# Configuration for default channel ID -channel.default=in.ekstep - - -content.publish.invoke_web_hook=__invoke_web_hook__ - -#Streamable media type list -stream.mime.type=__streaming_mime_type__ -stream.keyspace.name=__env___platform_db -stream.keyspace.table=job_request - -cassandra.lp.connection=__cassandra_lp_connection__ -cassandra.lpa.connection=__cassandra_lpa_connection__ - -#restrict.metadata.objectTypes=Content,ContentImage - -kafka.topic.system.command=__env__.system.command - -# Consistency Level for Multi Node Cassandra cluster -cassandra.lp.consistency.level=QUORUM - -compositesearch.index.name=__compositesearch_index_name__ - -content.nested.fields=badgeAssertions,targets,badgeAssociations - -search.es_conn_info=__search_es_host__ - -# Content Tagging Config for Backward Compatibility in Mobile App - -content.cache.read=true -content.cache.hierarchy=true - -# Max size(width/height) of thumbnail in pixels -max.thumbnail.size.pixels=150 - -#Post publish Job topic name -post.publish.event.topic=__env__.content.postpublish.request -post.publish.mvc.topic=__env__.mvc.processor.job.request -# Print service config -#kp.print.service.base.url=__kp_print_service_base_url__ -kp.print.service.base.url=__kp_print_service_base_url__ -lp.assessment.tmp_file_location=/tmp/ -lp.assessment.template_name=questionSetTemplate.vm - -# Content Tagging Config for Backward Compatibility in Mobile App -content.tagging.backward_enable=true -content.tagging.property=subject,medium - -# For enabling transfer of content from one path to other -content.upload.context.driven=true - -# PDF generation for contents linked to ItemSet -itemset.generate.pdf=__itemset_generate_pdf__ -content.streaming_enabled=__content_streaming_enabled__ - -#Configuration added to handle large artifacts -content.artifact.size.for_online=209715200 - -#Content Type Primary Category mapping -contentTypeToPrimaryCategory={\"ClassroomTeachingVideo\":\"Explanation Content\",\"ConceptMap\":\"Learning Resource\",\"Course\":\"Course\",\"CuriosityQuestionSet\":\"Practice Question Set\",\"eTextBook\":\"eTextbook\",\"ExperientialResource\":\"Learning Resource\",\"ExplanationResource\":\"Explanation Content\",\"ExplanationVideo\":\"Explanation Content\",\"FocusSpot\":\"Teacher Resource\",\"LearningOutcomeDefinition\":\"Teacher Resource\",\"MarkingSchemeRubric\":\"Teacher Resource\",\"PedagogyFlow\":\"Teacher Resource\",\"PracticeQuestionSet\":\"Practice Question Set\",\"PracticeResource\":\"Practice Question Set\",\"SelfAssess\":\"Course Assessment\",\"TeachingMethod\":\"Teacher Resource\",\"TextBook\":\"Digital Textbook\",\"Collection\":\"Content Playlist\",\"ExplanationReadingMaterial\":\"Learning Resource\",\"LearningActivity\":\"Learning Resource\",\"LessonPlan\":\"Content Playlist\",\"LessonPlanResource\":\"Teacher Resource\",\"PreviousBoardExamPapers\":\"Learning Resource\",\"TVLesson\":\"Explanation Content\",\"OnboardingResource\":\"Learning Resource\",\"ReadingMaterial\":\"Learning Resource\",\"Template\":\"Template\",\"Asset\":\"Asset\",\"Plugin\":\"Plugin\",\"LessonPlanUnit\":\"Lesson Plan Unit\",\"CourseUnit\":\"Course Unit\",\"TextBookUnit\":\"Textbook Unit\"} - -# master Category Cache Properties -master.category.cache.read=true -master.category.cache.ttl=86400 -master.category.validation.enabled=__master_category_validation_enabled__ \ No newline at end of file diff --git a/platform-jobs/samza/publish-pipeline/src/main/java/org/sunbird/jobs/samza/service/PublishPipelineService.java b/platform-jobs/samza/publish-pipeline/src/main/java/org/sunbird/jobs/samza/service/PublishPipelineService.java deleted file mode 100644 index 7926c45733..0000000000 --- a/platform-jobs/samza/publish-pipeline/src/main/java/org/sunbird/jobs/samza/service/PublishPipelineService.java +++ /dev/null @@ -1,366 +0,0 @@ -package org.sunbird.jobs.samza.service; - -import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.ObjectMapper; -import org.apache.commons.collections.MapUtils; -import org.apache.commons.io.FileUtils; -import org.apache.commons.lang3.StringUtils; -import org.apache.samza.config.Config; -import org.apache.samza.system.OutgoingMessageEnvelope; -import org.apache.samza.system.SystemStream; -import org.apache.samza.task.MessageCollector; -import org.sunbird.common.Platform; -import org.sunbird.common.exception.ClientException; -import org.sunbird.common.exception.ServerException; -import org.sunbird.content.common.ContentErrorMessageConstants; -import org.sunbird.content.enums.ContentErrorCodeConstants; -import org.sunbird.content.enums.ContentWorkflowPipelineParams; -import org.sunbird.content.pipeline.initializer.InitializePipeline; -import org.sunbird.content.publish.PublishManager; -import org.sunbird.graph.dac.model.Node; -import org.sunbird.jobs.samza.exception.PlatformErrorCodes; -import org.sunbird.jobs.samza.exception.PlatformException; -import org.sunbird.jobs.samza.service.task.JobMetrics; -import org.sunbird.jobs.samza.util.FailedEventsUtil; -import org.sunbird.jobs.samza.util.JSONUtils; -import org.sunbird.jobs.samza.util.JobLogger; -import org.sunbird.jobs.samza.util.PublishPipelineParams; -import org.sunbird.learning.router.LearningRequestRouterPool; -import org.sunbird.learning.util.ControllerUtil; -import org.sunbird.telemetry.dto.TelemetryBJREvent; -import org.sunbird.telemetry.logger.TelemetryManager; - -import com.rits.cloning.Cloner; - -import java.io.File; -import java.text.SimpleDateFormat; -import java.util.*; - -public class PublishPipelineService implements ISamzaService { - - private static JobLogger LOGGER = new JobLogger(PublishPipelineService.class); - - private Map parameterMap = new HashMap(); - - protected static final String DEFAULT_CONTENT_IMAGE_OBJECT_SUFFIX = ".img"; - - private ControllerUtil util = new ControllerUtil(); - - private Config config = null; - - private static int MAXITERTIONCOUNT = 2; - - private SystemStream systemStream = null; - private SystemStream postPublishStream = null; - private SystemStream postPublishMVCStream = null; - private static SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ"); - - private static ObjectMapper mapper = new ObjectMapper(); - - protected int getMaxIterations() { - if (Platform.config.hasPath("max.iteration.count.samza.job")) - return Platform.config.getInt("max.iteration.count.samza.job"); - else - return MAXITERTIONCOUNT; - } - - @Override - public void initialize(Config config) throws Exception { - this.config = config; - JSONUtils.loadProperties(config); - LOGGER.info("Service config initialized"); - LearningRequestRouterPool.init(); - LOGGER.info("Akka actors initialized"); - systemStream = new SystemStream("kafka", config.get("output.failed.events.topic.name")); - LOGGER.info("Stream initialized for Failed Events"); - postPublishStream = new SystemStream("kafka", config.get("post.publish.event.topic")); - LOGGER.info("Stream initialized for Post Publish Events"); - postPublishMVCStream = new SystemStream("kafka",config.get("post.publish.mvc.topic")); - LOGGER.info("Stream initialized for Post Publish MVC Content Events"); - - } - - @Override - @SuppressWarnings("unchecked") - public void processMessage(Map message, JobMetrics metrics, MessageCollector collector) - throws Exception { - - if (null == message) { - LOGGER.info("Ignoring the message because it is not valid for publishing."); - return; - } - Map edata = (Map) message.get(PublishPipelineParams.edata.name()); - Map object = (Map) message.get(PublishPipelineParams.object.name()); - - if (!validateObject(edata) || null == object) { - LOGGER.info("Ignoring the message because it is not valid for publishing."); - return; - } - - String nodeId = (String) object.get(PublishPipelineParams.id.name()); - if (StringUtils.isNotBlank(nodeId)) { - try { - Node node = getNode(nodeId); - if (null != node) { - if (prePublishValidation(node, (Map) edata.get("metadata"))) { - LOGGER.info( - "Node fetched for publish and content enrichment operation : " + node.getIdentifier()); - prePublishUpdate(edata, node); - - processJob(edata, nodeId, metrics, collector); - } - } else { - metrics.incSkippedCounter(); - FailedEventsUtil.pushEventForRetry(systemStream, message, metrics, collector, - PlatformErrorCodes.PROCESSING_ERROR.name(), new ServerException("ERR_PUBLISH_PIPELINE", "Please check neo4j connection or identfier to publish")); - LOGGER.debug("Invalid Node Object. Unable to process the event", message); - } - } catch (PlatformException e) { - LOGGER.error("Failed to process message", message, e); - metrics.incFailedCounter(); - FailedEventsUtil.pushEventForRetry(systemStream, message, metrics, collector, - PlatformErrorCodes.PROCESSING_ERROR.name(), e); - } catch (Exception e) { - LOGGER.error("Failed to process message", message, e); - metrics.incErrorCounter(); - FailedEventsUtil.pushEventForRetry(systemStream, message, metrics, collector, - PlatformErrorCodes.SYSTEM_ERROR.name(), e); - } - } else { - FailedEventsUtil.pushEventForRetry(systemStream, message, metrics, collector, - PlatformErrorCodes.SYSTEM_ERROR.name(), new ServerException("ERR_PUBLISH_PIPELINE", "Id is blank")); - metrics.incSkippedCounter(); - LOGGER.debug("Invalid NodeId. Unable to process the event", message); - } - } - - private boolean prePublishValidation(Node node, Map eventMetadata) { - Map objMetadata = (Map) node.getMetadata(); - - double eventPkgVersion = ((eventMetadata.get("pkgVersion") == null) ? 0d - : ((Number)eventMetadata.get("pkgVersion")).doubleValue()); - double objPkgVersion = ((objMetadata.get("pkgVersion") == null) ? 0d : ((Number) objMetadata.get("pkgVersion")).doubleValue()); - - return (objPkgVersion <= eventPkgVersion); - } - - private void processJob(Map edata, String contentId, JobMetrics metrics, MessageCollector collector) throws Exception { - - Node node = getNode(contentId); - String publishType = (String) edata.get(PublishPipelineParams.publish_type.name()); - node.getMetadata().put(PublishPipelineParams.publish_type.name(), publishType); - publishContent(node, edata, metrics, collector); - } - - @SuppressWarnings("unchecked") - private void prePublishUpdate(Map edata, Node node) { - Map metadata = (Map) edata.get("metadata"); - node.getMetadata().putAll(metadata); - - String prevState = (String) node.getMetadata().get(ContentWorkflowPipelineParams.status.name()); - node.getMetadata().put(ContentWorkflowPipelineParams.prevState.name(), prevState); - node.getMetadata().put("status", "Processing"); - - util.updateNode(node); - edata.put(PublishPipelineParams.status.name(), PublishPipelineParams.Processing.name()); - LOGGER.debug("Node status :: Processing for NodeId :: " + node.getIdentifier()); - } - - private Node getNode(String nodeId) { - Node node = null; - String imgNodeId = nodeId + DEFAULT_CONTENT_IMAGE_OBJECT_SUFFIX; - node = util.getNode(PublishPipelineParams.domain.name(), imgNodeId); - if (null == node) { - node = util.getNode(PublishPipelineParams.domain.name(), nodeId); - } - return node; - } - - private void publishContent(Node node, Map edata, JobMetrics metrics, MessageCollector collector) throws Exception { - boolean published = true; - LOGGER.debug("Publish processing start for content: " + node.getIdentifier()); - publishNode(node, (String) node.getMetadata().get(PublishPipelineParams.mimeType.name())); - Node publishedNode = getNode(node.getIdentifier().replace(".img", "")); - if (StringUtils.equalsIgnoreCase((String) publishedNode.getMetadata().get(PublishPipelineParams.status.name()), - PublishPipelineParams.Failed.name())) { - edata.put(PublishPipelineParams.status.name(), PublishPipelineParams.FAILED.name()); - LOGGER.debug("Node publish operation :: FAILED :: For NodeId :: " + node.getIdentifier()); - throw new PlatformException(PlatformErrorCodes.PUBLISH_FAILED.name(), - "Node publish operation failed for Node Id:" + node.getIdentifier()); - } else { - metrics.incSuccessCounter(); - edata.put(PublishPipelineParams.status.name(), PublishPipelineParams.SUCCESS.name()); - LOGGER.debug("Node publish operation :: SUCCESS :: For NodeId :: " + node.getIdentifier()); - pushInstructionEvent(publishedNode, collector); - } - } - - protected static String format(Date date) { - if (null != date) { - try { - return sdf.format(date); - } catch (Exception e) { - TelemetryManager.error("Error! While Converting the Date Format."+ date, e); - } - } - return null; - } - - private void publishNode(Node node, String mimeType) { - if (null == node) - throw new ClientException(ContentErrorCodeConstants.INVALID_CONTENT.name(), - ContentErrorMessageConstants.INVALID_CONTENT - + " | ['null' or Invalid Content Node (Object). Async Publish Operation Failed.]"); - Cloner cloner = new Cloner(); - Node cloneNode = cloner.deepClone(node); - String nodeId = node.getIdentifier().replace(".img", ""); - LOGGER.info("Publish processing start for node: " + nodeId); - String basePath = PublishManager.getBasePath(nodeId, this.config.get("lp.tempfile.location")); - LOGGER.info("Base path to store files: " + basePath); - try { - setContentBody(node, mimeType); - LOGGER.debug("Fetched body from cassandra"); - parameterMap.put(PublishPipelineParams.node.name(), node); - parameterMap.put(PublishPipelineParams.ecmlType.name(), PublishManager.isECMLContent(mimeType)); - LOGGER.info("Initializing the publish pipeline for: " + node.getIdentifier()); - InitializePipeline pipeline = new InitializePipeline(basePath, nodeId); - pipeline.init(PublishPipelineParams.publish.name(), parameterMap); - } catch (Exception e) { - e.printStackTrace(); - LOGGER.info( - "Something Went Wrong While Performing 'Content Publish' Operation in Async Mode. | [Content Id: " - + nodeId + "]", - e.getMessage()); - cloneNode.getMetadata().put(PublishPipelineParams.publishError.name(), e.getMessage()); - cloneNode.getMetadata().put(PublishPipelineParams.status.name(), PublishPipelineParams.Failed.name()); - util.updateNode(cloneNode); - } finally { - try { - FileUtils.deleteDirectory(new File(basePath.replace(nodeId, ""))); - } catch (Exception e2) { - LOGGER.error("Error while deleting base Path: " + basePath, e2); - e2.printStackTrace(); - } - } - } - - private void setContentBody(Node node, String mimeType) { - if (PublishManager.isECMLContent(mimeType)) { - node.getMetadata().put(PublishPipelineParams.body.name(), - PublishManager.getContentBody(node.getIdentifier())); - } - } - - private boolean validateObject(Map edata) { - String action = (String) edata.get("action"); - String contentType = (String) edata.get(PublishPipelineParams.contentType.name()); - Integer iteration = (Integer) edata.get(PublishPipelineParams.iteration.name()); - //TODO: remove contentType validation - if (StringUtils.equalsIgnoreCase("publish", action) && (!StringUtils.equalsIgnoreCase(contentType, - PublishPipelineParams.Asset.name())) && (iteration <= getMaxIterations())) { - return true; - } - return false; - } - - private void pushInstructionEvent(Node node, MessageCollector collector) throws Exception { - Map actor = new HashMap(); - Map context = new HashMap(); - Map object = new HashMap(); - Map edata = new HashMap(); - String mimeType = (String) node.getMetadata().get("mimeType"); - String sourceURL = node.getMetadata().get("sourceURL") != null ? (String)node.getMetadata().get("sourceURL") : null; - if(StringUtils.isNotBlank(sourceURL)){ - Map mvcProcessorEvent = generateInstructionEventMetadata(actor, context, object, edata, node.getMetadata(), node.getIdentifier(), "link-dialcode"); - mvcProcessorEvent= updatevaluesForMVCEvent(mvcProcessorEvent); - if (MapUtils.isEmpty(mvcProcessorEvent)) { - TelemetryManager.error("Post Publish event is not generated properly. #postPublishJob : " + mvcProcessorEvent); - throw new ClientException("MVC_JOB_REQUEST_EXCEPTION", "Event is not generated properly."); - } - collector.send(new OutgoingMessageEnvelope(postPublishMVCStream, mvcProcessorEvent)); - LOGGER.info("All Events sent to post publish mvc event topic"); - } - - Map postPublishEvent = generateInstructionEventMetadata(actor, context, object, edata, node.getMetadata(), node.getIdentifier(), "post-publish-process"); - if (MapUtils.isEmpty(postPublishEvent)) { - TelemetryManager.error("Post Publish event is not generated properly. #postPublishJob : " + postPublishEvent); - throw new ClientException("BE_JOB_REQUEST_EXCEPTION", "Event is not generated properly."); - } - collector.send(new OutgoingMessageEnvelope(postPublishStream, postPublishEvent)); - - LOGGER.info("All Events sent to post publish event topic"); - } - - Map updatevaluesForMVCEvent(Map mvcProcessorEvent) { - mvcProcessorEvent.put("eventData",mvcProcessorEvent.get("edata")); - mvcProcessorEvent.put("eid","MVC_JOB_PROCESSOR"); - mvcProcessorEvent.remove("edata"); - Map eventData = (Map) mvcProcessorEvent.get("eventData"); - eventData.put("identifier",eventData.get("id")); - eventData.remove("id"); - eventData.remove("iteration"); - eventData.remove("mimeType"); - eventData.remove("contentType"); - eventData.remove("pkgVersion"); - eventData.remove("status"); - eventData.put("action","update-es-index"); - eventData.put("stage",1); - return mvcProcessorEvent; - } - - private Map generateInstructionEventMetadata(Map actor, Map context, - Map object, Map edata, Map metadata, String contentId, String action) { - TelemetryBJREvent te = new TelemetryBJREvent(); - actor.put("id", "Post Publish Processor"); - actor.put("type", "System"); - context.put("channel", metadata.get("channel")); - Map pdata = new HashMap<>(); - pdata.put("id", "org.sunbird.platform"); - pdata.put("ver", "1.0"); - context.put("pdata", pdata); - if (Platform.config.hasPath("cloud_storage.env")) { - String env = Platform.config.getString("cloud_storage.env"); - context.put("env", env); - } - - object.put("id", contentId); - object.put("ver", metadata.get("versionKey")); - - edata.put("action", action); - edata.put("contentType", metadata.get("contentType")); - edata.put("status", metadata.get("status")); - // TODO: remove 'id' after mvc-processor handled it. - edata.put("id", contentId); - edata.put("identifier", contentId); - edata.put("pkgVersion", metadata.get("pkgVersion")); - edata.put("mimeType", metadata.get("mimeType")); - edata.put("name", metadata.get("name")); - edata.put("createdBy", metadata.get("createdBy")); - edata.put("createdFor", metadata.get("createdFor")); - edata.put("trackable", metadata.get("trackable")); - if (metadata.get("artifactUrl") != null) { - edata.put("artifactUrl", metadata.get("artifactUrl")); - } - - // generate event structure - long unixTime = System.currentTimeMillis(); - String mid = "LP." + System.currentTimeMillis() + "." + UUID.randomUUID(); - edata.put("iteration", 1); - te.setEid("BE_JOB_REQUEST"); - te.setEts(unixTime); - te.setMid(mid); - te.setActor(actor); - te.setContext(context); - te.setObject(object); - te.setEdata(edata); - Map event = null; - try { - event = mapper.convertValue(te, new TypeReference>() { - }); - } catch (Exception e) { - TelemetryManager.error("Error Generating BE_JOB_REQUEST event: " + e.getMessage(), e); - } - return event; - } - -} diff --git a/platform-jobs/samza/publish-pipeline/src/main/java/org/sunbird/jobs/samza/task/PublishPipelineTask.java b/platform-jobs/samza/publish-pipeline/src/main/java/org/sunbird/jobs/samza/task/PublishPipelineTask.java deleted file mode 100644 index a78d5a4c98..0000000000 --- a/platform-jobs/samza/publish-pipeline/src/main/java/org/sunbird/jobs/samza/task/PublishPipelineTask.java +++ /dev/null @@ -1,42 +0,0 @@ -package org.sunbird.jobs.samza.task; - -import java.util.Map; - -import org.apache.samza.task.MessageCollector; -import org.apache.samza.task.TaskCoordinator; -import org.sunbird.jobs.samza.service.ISamzaService; -import org.sunbird.jobs.samza.service.PublishPipelineService; -import org.sunbird.jobs.samza.util.JobLogger; - -public class PublishPipelineTask extends AbstractTask { - - private static JobLogger LOGGER = new JobLogger(PublishPipelineTask.class); - private ISamzaService service = new PublishPipelineService(); - - public ISamzaService initialize() throws Exception { - LOGGER.info("Task initialized"); - this.jobType = "publish"; - this.jobStartMessage = "Started processing of publish samza job"; - this.jobEndMessage = "Publish job processing complete"; - this.jobClass = "org.sunbird.jobs.samza.task.PublishPipelineTask"; - - return service; - } - - @Override - public void process(Map message, MessageCollector collector, TaskCoordinator coordinator) throws Exception { - try { - //LOGGER.info("Starting of service.processMessage..."); - long startTime = System.currentTimeMillis(); - LOGGER.info("Starting of service.processMessage at :: " + startTime); - service.processMessage(message, metrics, collector); - //LOGGER.info("Completed service.processMessage..."); - long endTime = System.currentTimeMillis(); - LOGGER.info("Completed service.processMessage at :: " + endTime); - LOGGER.info("Total execution time to complete publish operation :: " + (endTime-startTime)); - } catch (Exception e) { - metrics.incErrorCounter(); - LOGGER.error("Message processing failed", message, e); - } - } -} \ No newline at end of file diff --git a/platform-jobs/samza/publish-pipeline/src/main/java/org/sunbird/jobs/samza/util/PublishPipelineParams.java b/platform-jobs/samza/publish-pipeline/src/main/java/org/sunbird/jobs/samza/util/PublishPipelineParams.java deleted file mode 100644 index b72f3dff04..0000000000 --- a/platform-jobs/samza/publish-pipeline/src/main/java/org/sunbird/jobs/samza/util/PublishPipelineParams.java +++ /dev/null @@ -1,11 +0,0 @@ -package org.sunbird.jobs.samza.util; - -public enum PublishPipelineParams { - - taxonomy, taxonomy_hierarchy,search_criteria, property_keys, unique_constraint, status, Live, Unlisted, isImageObject, node_id, - cwp_element_name, param, nodeUniqueId, nodeType, state, Asset, contentType, - transactionData,properties, Flagged, FlagDraft, domain, FlagReview, data, eid, id, ecmlType, - node, Processing, Draft, edata, eks, content, mimeType, publish, Failed, publishError, body, Collection, - gradeLevel, ageGroup, medium, subject, genre, theme, keywords, concepts, visibility, channel, Default, versionKey, - BE_JOB_REQUEST, Content, cid, object, Pending, FAILED, SUCCESS, iteration, publish_type, kafka, Parent, children; -} diff --git a/platform-jobs/samza/publish-pipeline/src/main/resources/actor-config.xml b/platform-jobs/samza/publish-pipeline/src/main/resources/actor-config.xml deleted file mode 100644 index f349225f43..0000000000 --- a/platform-jobs/samza/publish-pipeline/src/main/resources/actor-config.xml +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/platform-jobs/samza/publish-pipeline/src/main/resources/application.conf b/platform-jobs/samza/publish-pipeline/src/main/resources/application.conf deleted file mode 100644 index 55482aeae5..0000000000 --- a/platform-jobs/samza/publish-pipeline/src/main/resources/application.conf +++ /dev/null @@ -1,13 +0,0 @@ -LearningActorSystem{ - default-dispatcher { - type = "Dispatcher" - executor = "fork-join-executor" - fork-join-executor { - parallelism-min = 1 - parallelism-factor = 2.0 - parallelism-max = 4 - } - # Throughput for default Dispatcher, set to 1 for as fair as possible - throughput = 1 - } -} diff --git a/platform-jobs/samza/publish-pipeline/src/main/resources/log4j.xml b/platform-jobs/samza/publish-pipeline/src/main/resources/log4j.xml deleted file mode 100644 index d2db3940cc..0000000000 --- a/platform-jobs/samza/publish-pipeline/src/main/resources/log4j.xml +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/platform-jobs/samza/publish-pipeline/src/main/resources/questionSetTemplate.vm b/platform-jobs/samza/publish-pipeline/src/main/resources/questionSetTemplate.vm deleted file mode 100644 index baf0b5d378..0000000000 --- a/platform-jobs/samza/publish-pipeline/src/main/resources/questionSetTemplate.vm +++ /dev/null @@ -1,76 +0,0 @@ -
- -
- -
-
-

$title

-
$questions
-
-
-

Answers

-
$answers
-
- -
\ No newline at end of file diff --git a/platform-jobs/samza/qr-image-generator/pom.xml b/platform-jobs/samza/qr-image-generator/pom.xml deleted file mode 100644 index 3980c3fc02..0000000000 --- a/platform-jobs/samza/qr-image-generator/pom.xml +++ /dev/null @@ -1,45 +0,0 @@ - - - - samza - org.sunbird - 1.1-SNAPSHOT - - 4.0.0 - qr-image-generator - 1.1-SNAPSHOT - - - - com.google.zxing - core - 3.3.3 - - - com.google.zxing - javase - 3.3.3 - - - org.apache.commons - commons-lang3 - 3.8.1 - - - - - - - org.apache.maven.plugins - maven-compiler-plugin - - 1.8 - 1.8 - - - - - - \ No newline at end of file diff --git a/platform-jobs/samza/qr-image-generator/src/main/java/org/sunbird/qrimage/generator/QRImageGenerator.java b/platform-jobs/samza/qr-image-generator/src/main/java/org/sunbird/qrimage/generator/QRImageGenerator.java deleted file mode 100644 index 7076baaa66..0000000000 --- a/platform-jobs/samza/qr-image-generator/src/main/java/org/sunbird/qrimage/generator/QRImageGenerator.java +++ /dev/null @@ -1,281 +0,0 @@ -package org.sunbird.qrimage.generator; - -import com.google.zxing.BarcodeFormat; -import com.google.zxing.EncodeHintType; -import com.google.zxing.NotFoundException; -import com.google.zxing.WriterException; -import com.google.zxing.client.j2se.BufferedImageLuminanceSource; -import com.google.zxing.common.BitMatrix; -import com.google.zxing.common.HybridBinarizer; -import com.google.zxing.qrcode.QRCodeWriter; -import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel; -import org.apache.commons.lang3.StringUtils; -import org.sunbird.qrimage.request.QRImageConfig; -import org.sunbird.qrimage.request.QRImageRequest; - -import javax.imageio.ImageIO; -import java.awt.*; -import java.awt.font.TextAttribute; -import java.awt.image.BufferedImage; -import java.io.File; -import java.io.IOException; -import java.io.InputStream; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.stream.Collectors; - -public class QRImageGenerator { - - private static QRImageConfig config = getDefaultConfig(); - private static QRCodeWriter qrCodeWriter = new QRCodeWriter(); - private static Map fontStore = new HashMap(); - - - public static File generateQRImage(QRImageRequest request) throws Exception { - if (null != request && null == request.getConfig()) - request.setConfig(config); - - List dataList = request.getData(); - String data = dataList.stream().collect(Collectors.joining(",")); - String text = request.getText(); - String fileName = request.getFileName(); - - String errorCorrectionLevel = request.getConfig().getErrorCorrectionLevel(); - int pixelsPerBlock = request.getConfig().getPixelsPerBlock(); - int qrMargin = request.getConfig().getQrCodeMargin(); - String fontName = request.getConfig().getTextFontName(); - int fontSize = request.getConfig().getTextFontSize(); - double tracking = request.getConfig().getTextCharacterSpacing(); - String imageFormat = request.getConfig().getFileFormat(); - String colorModel = request.getConfig().getColorModel(); - int borderSize = request.getConfig().getImageBorderSize(); - int qrMarginBottom = request.getConfig().getQrCodeMarginBottom(); - int imageMargin = request.getConfig().getImageMargin(); - - BufferedImage qrImage = generateBaseImage(data, errorCorrectionLevel, pixelsPerBlock, qrMargin, colorModel); - - if (StringUtils.isNotBlank(text)) { - BufferedImage textImage = getTextImage(text, fontName, fontSize, tracking, colorModel); - qrImage = addTextToBaseImage(qrImage, textImage, colorModel, qrMargin, pixelsPerBlock, qrMarginBottom, imageMargin); - } - - if (borderSize > 0) { - drawBorder(qrImage, borderSize, imageMargin); - } - - File finalImageFile = new File(request.getTempFileLocation() + File.separator + fileName + "." + imageFormat); - finalImageFile.createNewFile(); - ImageIO.write(qrImage, imageFormat, finalImageFile); - return finalImageFile; - } - - - private static QRImageConfig getDefaultConfig(){ - QRImageConfig config = new QRImageConfig(); - config.setFileFormat("png"); - config.setErrorCorrectionLevel("H"); - config.setPixelsPerBlock(2); - config.setColorModel("Grayscale"); - config.setTextFontName("Verdana"); - config.setTextFontSize(11); - config.setTextCharacterSpacing(0.1); - config.setQrCodeMargin(3); - config.setImageBorderSize(1); - config.setImageMargin(1); - config.setQrCodeMarginBottom(1); - return config; - } - - private static BufferedImage generateBaseImage(String data, String errorCorrectionLevel, int pixelsPerBlock, int qrMargin, String colorModel) throws WriterException { - Map hintsMap = getHintsMap(errorCorrectionLevel, qrMargin); - BitMatrix defaultBitMatrix = getDefaultBitMatrix(data, hintsMap); - BitMatrix largeBitMatrix = getBitMatrix(data, defaultBitMatrix.getWidth() * pixelsPerBlock, defaultBitMatrix.getHeight() * pixelsPerBlock, hintsMap); - BufferedImage qrImage = getImage(largeBitMatrix, colorModel); - return qrImage; - } - - //Sample = 2A42UH , Verdana, 11, 0.1, Grayscale - private static BufferedImage getTextImage(String text, String fontName, int fontSize, double tracking, String colorModel) throws IOException, FontFormatException { - BufferedImage image = new BufferedImage(1, 1, getImageType(colorModel)); - Font basicFont = getFontFromStore(fontName); - - Map attributes = new HashMap(); - attributes.put(TextAttribute.TRACKING, tracking); - attributes.put(TextAttribute.WEIGHT, TextAttribute.WEIGHT_BOLD); - attributes.put(TextAttribute.SIZE, fontSize); - Font font = basicFont.deriveFont(attributes); - - Graphics2D graphics2d = image.createGraphics(); - graphics2d.setFont(font); - FontMetrics fontmetrics = graphics2d.getFontMetrics(); - int width = fontmetrics.stringWidth(text); - int height = fontmetrics.getHeight(); - graphics2d.dispose(); - - image = new BufferedImage(width, height, getImageType(colorModel)); - graphics2d = image.createGraphics(); - graphics2d.setRenderingHint(RenderingHints.KEY_ALPHA_INTERPOLATION, RenderingHints.VALUE_ALPHA_INTERPOLATION_QUALITY); - graphics2d.setRenderingHint(RenderingHints.KEY_COLOR_RENDERING, RenderingHints.VALUE_COLOR_RENDER_QUALITY); - graphics2d.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_OFF); - graphics2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_OFF); - - graphics2d.setColor(Color.WHITE); - graphics2d.fillRect(0, 0, image.getWidth(), image.getHeight()); - graphics2d.setColor(Color.BLACK); - - graphics2d.setFont(font); - fontmetrics = graphics2d.getFontMetrics(); - graphics2d.drawString(text, 0, fontmetrics.getAscent()); - graphics2d.dispose(); - - return image; - } - - private static BufferedImage addTextToBaseImage(BufferedImage qrImage, BufferedImage textImage, String colorModel, int qrMargin, int pixelsPerBlock, int qrMarginBottom, int imageMargin) throws NotFoundException { - BufferedImageLuminanceSource qrSource = new BufferedImageLuminanceSource(qrImage); - HybridBinarizer qrBinarizer = new HybridBinarizer(qrSource); - BitMatrix qrBits = qrBinarizer.getBlackMatrix(); - - BufferedImageLuminanceSource textSource = new BufferedImageLuminanceSource(textImage); - HybridBinarizer textBinarizer = new HybridBinarizer(textSource); - BitMatrix textBits = textBinarizer.getBlackMatrix(); - - if (qrBits.getWidth() > textBits.getWidth()) { - BitMatrix tempTextMatrix = new BitMatrix(qrBits.getWidth(), textBits.getHeight()); - copyMatrixDataToBiggerMatrix(textBits, tempTextMatrix); - textBits = tempTextMatrix; - } else if (qrBits.getWidth() < textBits.getWidth()) { - BitMatrix tempQrMatrix = new BitMatrix(textBits.getWidth(), qrBits.getHeight()); - copyMatrixDataToBiggerMatrix(qrBits, tempQrMatrix); - qrBits = tempQrMatrix; - } - - BitMatrix mergedMatrix = mergeMatricesOfSameWidth(qrBits, textBits, qrMargin, pixelsPerBlock, qrMarginBottom, imageMargin); - return getImage(mergedMatrix, colorModel); - } - - private static BitMatrix mergeMatricesOfSameWidth(BitMatrix firstMatrix, BitMatrix secondMatrix, int qrMargin, int pixelsPerBlock, int qrMarginBottom, int imageMargin) { - int mergedWidth = firstMatrix.getWidth() + (2 * imageMargin); - int mergedHeight = firstMatrix.getHeight() + secondMatrix.getHeight() + (2 * imageMargin); - int defaultBottomMargin = pixelsPerBlock * qrMargin; - int marginToBeRemoved = qrMarginBottom > defaultBottomMargin ? 0 : (defaultBottomMargin-qrMarginBottom); - BitMatrix mergedMatrix = new BitMatrix(mergedWidth, mergedHeight - marginToBeRemoved); - - for (int x = 0; x < firstMatrix.getWidth(); x++) { - for (int y = 0; y < firstMatrix.getHeight() - marginToBeRemoved; y++) { - if (firstMatrix.get(x, y)) { - mergedMatrix.set(x + imageMargin, y + imageMargin); - } - } - } - for (int x = 0; x < secondMatrix.getWidth(); x++) { - for (int y = 0; y < secondMatrix.getHeight(); y++) { - if (secondMatrix.get(x, y)) { - mergedMatrix.set(x + imageMargin, y + firstMatrix.getHeight() - marginToBeRemoved + imageMargin); - } - } - } - return mergedMatrix; - } - - private static void copyMatrixDataToBiggerMatrix(BitMatrix fromMatrix, BitMatrix toMatrix) { - int widthDiff = toMatrix.getWidth() - fromMatrix.getWidth(); - int leftMargin = widthDiff / 2; - for (int x = 0; x < fromMatrix.getWidth(); x++) { - for (int y = 0; y < fromMatrix.getHeight(); y++) { - if (fromMatrix.get(x, y)) { - toMatrix.set(x + leftMargin, y); - } - } - } - } - - private static void drawBorder(BufferedImage image, int borderSize, int imageMargin) { - image.createGraphics(); - Graphics2D graphics = (Graphics2D) image.getGraphics(); - graphics.setColor(Color.BLACK); - for (int i = 0; i < borderSize; i++) { - graphics.drawRect(i + imageMargin, i + imageMargin, image.getWidth() - 1 - (2 * i) - (2 * imageMargin), image.getHeight() - 1 - (2 * i) - (2 * imageMargin)); - } - graphics.dispose(); - } - - private static BufferedImage getImage(BitMatrix bitMatrix, String colorModel) { - int imageWidth = bitMatrix.getWidth(); - int imageHeight = bitMatrix.getHeight(); - BufferedImage image = new BufferedImage(imageWidth, imageHeight, getImageType(colorModel)); - image.createGraphics(); - - Graphics2D graphics = (Graphics2D) image.getGraphics(); - graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_OFF); - graphics.setColor(Color.WHITE); - graphics.fillRect(0, 0, imageWidth, imageHeight); - - graphics.setColor(Color.BLACK); - - for (int i = 0; i < imageWidth; i++) { - for (int j = 0; j < imageHeight; j++) { - if (bitMatrix.get(i, j)) { - graphics.fillRect(i, j, 1, 1); - } - } - } - graphics.dispose(); - return image; - } - - private static BitMatrix getBitMatrix(String data, int width, int height, Map hintsMap) throws WriterException { - BitMatrix bitMatrix = qrCodeWriter.encode(data, BarcodeFormat.QR_CODE, width, height, hintsMap); - return bitMatrix; - } - - private static BitMatrix getDefaultBitMatrix(String data, Map hintsMap) throws WriterException { - BitMatrix defaultBitMatrix = qrCodeWriter.encode(data, BarcodeFormat.QR_CODE, 0, 0, hintsMap); - return defaultBitMatrix; - } - - private static Map getHintsMap(String errorCorrectionLevel, int qrMargin) { - Map hintsMap = new HashMap(); - switch (errorCorrectionLevel) { - case "H": - hintsMap.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.H); - break; - case "Q": - hintsMap.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.Q); - break; - case "M": - hintsMap.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.M); - break; - case "L": - hintsMap.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.L); - break; - } - hintsMap.put(EncodeHintType.MARGIN, qrMargin); - return hintsMap; - } - - - - private static int getImageType(String colorModel) { - if (colorModel.equalsIgnoreCase("RGB")) { - return BufferedImage.TYPE_INT_RGB; - } else { - return BufferedImage.TYPE_BYTE_GRAY; - } - } - - private static Font loadFontStore(String fontName) throws IOException, FontFormatException { - //load the packaged font file from the root dir - String fontFile = "/"+fontName+".ttf"; - InputStream fontStream = QRImageGenerator.class.getResourceAsStream(fontFile); - Font basicFont = Font.createFont(Font.TRUETYPE_FONT, fontStream); - fontStore.put(fontName, basicFont); - - return basicFont; - } - - private static Font getFontFromStore(String fontName) throws IOException, FontFormatException { - return null != fontStore.get(fontName) ? fontStore.get(fontName) : loadFontStore(fontName); - } -} diff --git a/platform-jobs/samza/qr-image-generator/src/main/java/org/sunbird/qrimage/request/QRImageConfig.java b/platform-jobs/samza/qr-image-generator/src/main/java/org/sunbird/qrimage/request/QRImageConfig.java deleted file mode 100644 index 8a0d0cc239..0000000000 --- a/platform-jobs/samza/qr-image-generator/src/main/java/org/sunbird/qrimage/request/QRImageConfig.java +++ /dev/null @@ -1,104 +0,0 @@ -package org.sunbird.qrimage.request; - -public class QRImageConfig { - - private String fileFormat; - private String errorCorrectionLevel; - private int pixelsPerBlock; - private String colorModel; - private String textFontName; - private int textFontSize; - private double textCharacterSpacing; - private int qrCodeMargin; - private int imageBorderSize; - private int imageMargin; - private int qrCodeMarginBottom; - - public String getFileFormat() { - return fileFormat; - } - - public void setFileFormat(String fileFormat) { - this.fileFormat = fileFormat; - } - - public String getErrorCorrectionLevel() { - return errorCorrectionLevel; - } - - public void setErrorCorrectionLevel(String errorCorrectionLevel) { - this.errorCorrectionLevel = errorCorrectionLevel; - } - - public int getPixelsPerBlock() { - return pixelsPerBlock; - } - - public void setPixelsPerBlock(int pixelsPerBlock) { - this.pixelsPerBlock = pixelsPerBlock; - } - - public String getColorModel() { - return colorModel; - } - - public void setColorModel(String colorModel) { - this.colorModel = colorModel; - } - - public String getTextFontName() { - return textFontName; - } - - public void setTextFontName(String textFontName) { - this.textFontName = textFontName; - } - - public int getTextFontSize() { - return textFontSize; - } - - public void setTextFontSize(int textFontSize) { - this.textFontSize = textFontSize; - } - - public double getTextCharacterSpacing() { - return textCharacterSpacing; - } - - public void setTextCharacterSpacing(double textCharacterSpacing) { - this.textCharacterSpacing = textCharacterSpacing; - } - - public int getQrCodeMargin() { - return qrCodeMargin; - } - - public void setQrCodeMargin(int qrCodeMargin) { - this.qrCodeMargin = qrCodeMargin; - } - - public int getImageBorderSize() { - return imageBorderSize; - } - - public void setImageBorderSize(int imageBorderSize) { - this.imageBorderSize = imageBorderSize; - } - - public int getImageMargin() { - return imageMargin; - } - - public void setImageMargin(int imageMargin) { - this.imageMargin = imageMargin; - } - - public int getQrCodeMarginBottom() { - return qrCodeMarginBottom; - } - - public void setQrCodeMarginBottom(int qrCodeMarginBottom) { - this.qrCodeMarginBottom = qrCodeMarginBottom; - } -} diff --git a/platform-jobs/samza/qr-image-generator/src/main/java/org/sunbird/qrimage/request/QRImageRequest.java b/platform-jobs/samza/qr-image-generator/src/main/java/org/sunbird/qrimage/request/QRImageRequest.java deleted file mode 100644 index e55528c4a6..0000000000 --- a/platform-jobs/samza/qr-image-generator/src/main/java/org/sunbird/qrimage/request/QRImageRequest.java +++ /dev/null @@ -1,54 +0,0 @@ -package org.sunbird.qrimage.request; - -import java.util.List; - -public class QRImageRequest { - - private List data; - private String text; - private String fileName; - private QRImageConfig config; - private String tempFileLocation; - - public QRImageRequest(){} - - public QRImageRequest(String tempFileLocation) { - this.tempFileLocation = tempFileLocation; - } - - public List getData() { - return data; - } - - public void setData(List data) { - this.data = data; - } - - public String getText() { - return text; - } - - public void setText(String text) { - this.text = text; - } - - public String getFileName() { - return fileName; - } - - public void setFileName(String fileName) { - this.fileName = fileName; - } - - public QRImageConfig getConfig() { - return config; - } - - public void setConfig(QRImageConfig config) { - this.config = config; - } - - public String getTempFileLocation() { - return this.tempFileLocation; - } -} diff --git a/platform-jobs/samza/qr-image-generator/src/main/resources/Verdana.ttf b/platform-jobs/samza/qr-image-generator/src/main/resources/Verdana.ttf deleted file mode 100755 index 18ef6e8f1fc1a57750e17b2c611454d010f3ee51..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 129364 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�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 t3)KzWf_Z(5kW%B)&PZ - - - samza - org.sunbird - 1.1-SNAPSHOT - - 4.0.0 - - qrcode-image-generator - 0.0.31 - - - com.google.zxing - core - 3.3.3 - - - com.google.zxing - javase - 3.3.3 - - - org.sunbird - samza-common - 1.1-SNAPSHOT - - - - - - - org.apache.maven.plugins - maven-compiler-plugin - - 1.8 - 1.8 - - - - - maven-assembly-plugin - - - src/main/assembly/src.xml - - - - - make-assembly - package - - single - - - - - - - - diff --git a/platform-jobs/samza/qrcode-image-generator/src/main/assembly/src.xml b/platform-jobs/samza/qrcode-image-generator/src/main/assembly/src.xml deleted file mode 100644 index 813101d8e0..0000000000 --- a/platform-jobs/samza/qrcode-image-generator/src/main/assembly/src.xml +++ /dev/null @@ -1,69 +0,0 @@ - - - - - distribution - - tar.gz - - false - - - ${basedir} - - README* - LICENSE* - NOTICE* - - - - - - ${basedir}/src/main/resources/log4j.xml - lib - - - - ${basedir}/src/main/config/qrcode-image-generator.properties - config - true - - - - - bin - - org.apache.samza:samza-shell:tgz:dist:* - - 0744 - true - - - lib - - org.apache.samza:samza-api - org.sunbird:qrcode-image-generator - org.apache.samza:samza-core_2.11 - org.apache.samza:samza-kafka_2.11 - org.apache.samza:samza-yarn_2.11 - org.apache.samza:samza-log4j - org.apache.kafka:kafka_2.11 - org.apache.hadoop:hadoop-hdfs - - true - - - diff --git a/platform-jobs/samza/qrcode-image-generator/src/main/config/local.qrcode-image-generator.properties b/platform-jobs/samza/qrcode-image-generator/src/main/config/local.qrcode-image-generator.properties deleted file mode 100644 index 55f84fae02..0000000000 --- a/platform-jobs/samza/qrcode-image-generator/src/main/config/local.qrcode-image-generator.properties +++ /dev/null @@ -1,73 +0,0 @@ -# Job -job.factory.class=org.apache.samza.job.yarn.YarnJobFactory -job.name=local.qrcode-image-generator - -# YARN -yarn.package.path=file://${basedir}/target/${project.artifactId}-${pom.version}-distribution.tar.gz - -# Metrics -metrics.reporters=snapshot,jmx -metrics.reporter.snapshot.class=org.apache.samza.metrics.reporter.MetricsSnapshotReporterFactory -metrics.reporter.snapshot.stream=kafka.dev.lp.metrics -metrics.reporter.jmx.class=org.apache.samza.metrics.reporter.JmxReporterFactory - -# Task -task.class=org.sunbird.jobs.samza.task.QRCodeImageGeneratorTask -task.inputs=kafka.local.qrimage.request -task.checkpoint.factory=org.apache.samza.checkpoint.kafka.KafkaCheckpointManagerFactory -task.checkpoint.system=kafka -task.checkpoint.replication.factor=1 -task.commit.ms=60000 -task.window.ms=300000 - -# Serializers -serializers.registry.json.class=org.sunbird.jobs.samza.serializers.EkstepJsonSerdeFactory -serializers.registry.metrics.class=org.apache.samza.serializers.MetricsSnapshotSerdeFactory - -# Systems -systems.kafka.samza.factory=org.apache.samza.system.kafka.KafkaSystemFactory -systems.kafka.samza.msg.serde=json -systems.kafka.streams.metrics.samza.msg.serde=metrics -systems.kafka.consumer.zookeeper.connect=localhost:2181 -systems.kafka.consumer.auto.offset.reset=smallest -systems.kafka.samza.offset.default=oldest -systems.kafka.producer.bootstrap.servers=localhost:9092 - -# Job Coordinator -job.coordinator.system=kafka - -# Normally, this would be 3, but we have only one broker. -job.coordinator.replication.factor=1 - -# Job specific configuration - -# Metrics -output.metrics.job.name=qrcode-image-generator -output.metrics.topic.name=local.qrimage.request - -# Cloud store details -cloud_storage_type=__cloud_storage_type__ -azure_storage_key=__azure_storage_key__ -azure_storage_secret=__azure_storage_secret__ -azure_storage_container=__azure_storage_container__ -aws_storage_key=__aws_access_key_id__ -aws_storage_secret=__aws_secret_access_key__ -aws_storage_container=__aws_storage_container__ -cloud_upload_retry_count=3 - -# Cassandra connection details -cassandra.lp.connection=localhost:9042 -cassandra.lpa.connection=localhost:9042 -cassandra.sunbird.connection=localhost:9042 - -# QR Image generation default configurations -# Thickness of white border(in pixels) around the black border of the qr image -qr_image_margin=1 -# Spacing(in pixels) between qrcode and text in the qr image -qr_image_margin_bottom=0 - -# Remote Debug Configuration -task.opts=-agentlib:jdwp=transport=dt_socket,address=localhost:9009,server=y,suspend=y - -# Temp file path to generate files -lp_tempfile_location=/tmp \ No newline at end of file diff --git a/platform-jobs/samza/qrcode-image-generator/src/main/config/qrcode-image-generator.properties b/platform-jobs/samza/qrcode-image-generator/src/main/config/qrcode-image-generator.properties deleted file mode 100644 index b5954fd766..0000000000 --- a/platform-jobs/samza/qrcode-image-generator/src/main/config/qrcode-image-generator.properties +++ /dev/null @@ -1,73 +0,0 @@ -# Job -job.factory.class=org.apache.samza.job.yarn.YarnJobFactory -job.name=__env__.qrcode-image-generator - -# YARN -yarn.package.path=http://__yarn_host__:__yarn_port__/__env__/${project.artifactId}-${pom.version}-distribution.tar.gz - -# Metrics -metrics.reporters=snapshot,jmx -metrics.reporter.snapshot.class=org.apache.samza.metrics.reporter.MetricsSnapshotReporterFactory -metrics.reporter.snapshot.stream=kafka.__env__.metrics -metrics.reporter.jmx.class=org.apache.samza.metrics.reporter.JmxReporterFactory - -# Task -task.class=org.sunbird.jobs.samza.task.QRCodeImageGeneratorTask -task.inputs=kafka.__env__.qrimage.request -task.checkpoint.factory=org.apache.samza.checkpoint.kafka.KafkaCheckpointManagerFactory -task.checkpoint.system=kafka -task.checkpoint.replication.factor=__samza_checkpoint_replication_factor__ -task.commit.ms=60000 -task.window.ms=300000 - -# Serializers -serializers.registry.json.class=org.sunbird.jobs.samza.serializers.EkstepJsonSerdeFactory -serializers.registry.metrics.class=org.apache.samza.serializers.MetricsSnapshotSerdeFactory - -# Systems -systems.kafka.samza.factory=org.apache.samza.system.kafka.KafkaSystemFactory -systems.kafka.samza.msg.serde=json -systems.kafka.streams.metrics.samza.msg.serde=metrics -systems.kafka.consumer.zookeeper.connect=__zookeepers__ -systems.kafka.consumer.auto.offset.reset=smallest -systems.kafka.samza.offset.default=oldest -systems.kafka.producer.bootstrap.servers=__kafka_brokers__ - -# Job Coordinator -job.coordinator.system=kafka - -# Normally, this would be 3, but we have only one broker. -job.coordinator.replication.factor=__samza_coordinator_replication_factor__ - -# Job specific configuration - -# Metrics -output.metrics.job.name=qrcode-image-generator -output.metrics.topic.name=__env__.qrimage.request - -# Cloud store details -cloud_storage_type=__cloud_storage_type__ -azure_storage_key=__azure_storage_key__ -azure_storage_secret=__azure_storage_secret__ -azure_storage_container=__azure_storage_container__ -aws_storage_key=__aws_access_key_id__ -aws_storage_secret=__aws_secret_access_key__ -aws_storage_container=__aws_storage_container__ -cloud_upload_retry_count=__cloud_upload_retry_count__ - -# Cassandra connection details -cassandra.lp.connection=__cassandra_lp_connection__ -cassandra.lpa.connection=__cassandra_lpa_connection__ -cassandra.sunbird.connection=__cassandra_sunbird_connection__ - -# QR Image generation default configurations -# Thickness of white border(in pixels) around the black border of the qr image -qr_image_margin=1 -# Spacing(in pixels) between qrcode and text in the qr image -qr_image_margin_bottom=0 - -# Consistency Level for Multi Node Cassandra cluster -cassandra.sunbird.consistency.level=QUORUM - -# Temp file path to generate files -lp_tempfile_location=/tmp \ No newline at end of file diff --git a/platform-jobs/samza/qrcode-image-generator/src/main/java/org/sunbird/jobs/samza/model/QRCodeGenerationRequest.java b/platform-jobs/samza/qrcode-image-generator/src/main/java/org/sunbird/jobs/samza/model/QRCodeGenerationRequest.java deleted file mode 100644 index a1f7963bc1..0000000000 --- a/platform-jobs/samza/qrcode-image-generator/src/main/java/org/sunbird/jobs/samza/model/QRCodeGenerationRequest.java +++ /dev/null @@ -1,134 +0,0 @@ -package org.sunbird.jobs.samza.model; - -import java.util.List; - -public class QRCodeGenerationRequest { - - private List data; - private String errorCorrectionLevel; - private int pixelsPerBlock; - private int qrCodeMargin; - private List text; - private String textFontName; - private int textFontSize; - private double textCharacterSpacing; - private int imageBorderSize; - private String colorModel; - private List fileName; - private String fileFormat; - private int qrCodeMarginBottom; - private int imageMargin; - private String tempFilePath; - - public String getTempFilePath() { return tempFilePath; } - - public void setTempFilePath(String tempFilePath) { this.tempFilePath = tempFilePath; } - - public int getImageMargin() { return imageMargin; } - - public void setImageMargin(int imageMargin) { this.imageMargin = imageMargin; } - - public int getQrCodeMarginBottom() { - return qrCodeMarginBottom; - } - - public void setQrCodeMarginBottom(int qrCodeMarginBottom) { - this.qrCodeMarginBottom = qrCodeMarginBottom; - } - - public List getData() { - return data; - } - - public void setData(List data) { - this.data = data; - } - - public String getErrorCorrectionLevel() { - return errorCorrectionLevel; - } - - public void setErrorCorrectionLevel(String errorCorrectionLevel) { - this.errorCorrectionLevel = errorCorrectionLevel; - } - - public int getPixelsPerBlock() { - return pixelsPerBlock; - } - - public void setPixelsPerBlock(int pixelsPerBlock) { - this.pixelsPerBlock = pixelsPerBlock; - } - - public int getQrCodeMargin() { - return qrCodeMargin; - } - - public void setQrCodeMargin(int qrCodeMargin) { - this.qrCodeMargin = qrCodeMargin; - } - - public List getText() { - return text; - } - - public void setText(List text) { - this.text = text; - } - - public String getTextFontName() { - return textFontName; - } - - public void setTextFontName(String textFontName) { - this.textFontName = textFontName; - } - - public int getTextFontSize() { - return textFontSize; - } - - public void setTextFontSize(int textFontSize) { - this.textFontSize = textFontSize; - } - - public double getTextCharacterSpacing() { - return textCharacterSpacing; - } - - public void setTextCharacterSpacing(double textCharacterSpacing) { - this.textCharacterSpacing = textCharacterSpacing; - } - - public int getImageBorderSize() { - return imageBorderSize; - } - - public void setImageBorderSize(int imageBorderSize) { - this.imageBorderSize = imageBorderSize; - } - - public String getColorModel() { - return colorModel; - } - - public void setColorModel(String colorModel) { - this.colorModel = colorModel; - } - - public List getFileName() { - return fileName; - } - - public void setFileName(List fileName) { - this.fileName = fileName; - } - - public String getFileFormat() { - return fileFormat; - } - - public void setFileFormat(String fileFormat) { - this.fileFormat = fileFormat; - } -} diff --git a/platform-jobs/samza/qrcode-image-generator/src/main/java/org/sunbird/jobs/samza/service/QRCodeImageGeneratorService.java b/platform-jobs/samza/qrcode-image-generator/src/main/java/org/sunbird/jobs/samza/service/QRCodeImageGeneratorService.java deleted file mode 100644 index 994f95ed63..0000000000 --- a/platform-jobs/samza/qrcode-image-generator/src/main/java/org/sunbird/jobs/samza/service/QRCodeImageGeneratorService.java +++ /dev/null @@ -1,154 +0,0 @@ -package org.sunbird.jobs.samza.service; - -import org.apache.commons.lang3.StringUtils; -import org.apache.samza.config.Config; -import org.apache.samza.task.MessageCollector; -import org.sunbird.jobs.samza.model.QRCodeGenerationRequest; -import org.sunbird.jobs.samza.util.JSONUtils; -import org.sunbird.jobs.samza.util.JobLogger; -import org.sunbird.jobs.samza.util.QRCodeImageGeneratorParams; -import org.sunbird.jobs.samza.util.QRCodeImageGeneratorUtil; -import org.sunbird.jobs.samza.util.QRCodeCassandraConnector; -import org.sunbird.jobs.samza.util.CloudStorageUtil; -import org.sunbird.jobs.samza.util.ZipEditorUtil; -import org.sunbird.jobs.samza.service.task.JobMetrics; - -import java.io.File; -import java.util.ArrayList; -import java.util.Map; -import java.util.List; - -public class QRCodeImageGeneratorService implements ISamzaService { - - private JobLogger LOGGER = new JobLogger(QRCodeImageGeneratorService.class); - - private Config appConfig = null; - - @Override - public void initialize(Config config) throws Exception { - JSONUtils.loadProperties(config); - appConfig = config; - LOGGER.info("QRCodeImageGeneratorService:initialize: Service config initialized"); - } - - @Override - public void processMessage(Map message, JobMetrics metrics, MessageCollector collector) throws Exception { - List availableImages = new ArrayList(); - File zipFile = null; - try{ - LOGGER.info("QRCodeImageGeneratorService:processMessage: Processing request: "+message); - LOGGER.info("QRCodeImageGeneratorService:processMessage: Starting message processing at "+System.currentTimeMillis()); - - if(!message.containsKey(QRCodeImageGeneratorParams.eid.name())) { - return; - } - - String eid = (String) message.get(QRCodeImageGeneratorParams.eid.name()); - if(!eid.equalsIgnoreCase(QRCodeImageGeneratorParams.BE_QR_IMAGE_GENERATOR.name())) { - return; - } - - List> dialCodes = (List>) message.get(QRCodeImageGeneratorParams.dialcodes.name()); - if(null == dialCodes || dialCodes.size()==0) { - return; - } - - Map config = (Map) message.get(QRCodeImageGeneratorParams.config.name()); - String imageFormat = (String) config.get(QRCodeImageGeneratorParams.imageFormat.name()); - - List dataList = new ArrayList(); - List textList = new ArrayList(); - List fileNameList = new ArrayList(); - String downloadUrl = null; - String tempFilePath = appConfig.getOrDefault(QRCodeImageGeneratorParams.lp_tempfile_location.name(), "/tmp"); - - for(Map dialCode : dialCodes) { - if(dialCode.containsKey(QRCodeImageGeneratorParams.location.name())) { - try { - downloadUrl = (String) dialCode.get(QRCodeImageGeneratorParams.location.name()); - String fileName = (String) dialCode.get(QRCodeImageGeneratorParams.id.name()); - File fileToSave = new File(tempFilePath + File.separator + fileName+"."+imageFormat); - LOGGER.info("QRCodeImageGeneratorService:processMessage: creating file - " + fileToSave.getAbsolutePath()); - fileToSave.createNewFile(); - LOGGER.info("QRCodeImageGeneratorService:processMessage: created file - " + fileToSave.getAbsolutePath()); - CloudStorageUtil.downloadFile(downloadUrl, fileToSave); - availableImages.add(fileToSave); - continue; - } catch(Exception e) { - LOGGER.error("QRCodeImageGeneratorService:processMessage: Error while downloading image:", downloadUrl, e); - } - } - - dataList.add((String)dialCode.get(QRCodeImageGeneratorParams.data.name())); - textList.add((String)dialCode.get(QRCodeImageGeneratorParams.text.name())); - fileNameList.add((String)dialCode.get(QRCodeImageGeneratorParams.id.name())); - - } - - Map storage = (Map) message.get(QRCodeImageGeneratorParams.storage.name()); - String container = storage.get(QRCodeImageGeneratorParams.container.name()); - String path = storage.get(QRCodeImageGeneratorParams.path.name()); - String zipFileName = storage.get(QRCodeImageGeneratorParams.fileName.name()); - String processId = (String) message.get(QRCodeImageGeneratorParams.processId.name()); - - QRCodeGenerationRequest qrGenRequest = getQRCodeGenerationRequest(config, dataList, textList, fileNameList); - List generatedImages = QRCodeImageGeneratorUtil.createQRImages(qrGenRequest, appConfig, container, path); - - if(!StringUtils.isBlank(processId)) { - LOGGER.info("QRCodeImageGeneratorService:processMessage: Generating zip for QR codes with processId " + processId); - if(StringUtils.isBlank(zipFileName)) { - zipFileName = processId; - } - availableImages.addAll(generatedImages); - zipFile = ZipEditorUtil.zipFiles(availableImages, zipFileName, tempFilePath); - - String zipDownloadUrl = CloudStorageUtil.uploadFile(container, path, zipFile, false); - QRCodeCassandraConnector.updateDownloadZIPUrl(processId, zipDownloadUrl); - } else { - LOGGER.info("QRCodeImageGeneratorService:processMessage: Skipping zip creation due to missing processId."); - } - LOGGER.info("QRCodeImageGeneratorService:processMessage: Message processed successfully at "+System.currentTimeMillis()); - } catch (Exception e) { - QRCodeCassandraConnector.updateFailure((String) message.get(QRCodeImageGeneratorParams.processId.name()), - e.getMessage()); - throw e; - } finally { - if(null != zipFile) { - zipFile.delete(); - } - for(File imageFile : availableImages) { - if(null != imageFile) { - imageFile.delete(); - } - } - } - } - - private QRCodeGenerationRequest getQRCodeGenerationRequest(Map config, List dataList, List textList, List fileNameList) { - QRCodeGenerationRequest qrGenRequest = new QRCodeGenerationRequest(); - qrGenRequest.setData(dataList); - qrGenRequest.setText(textList); - qrGenRequest.setFileName(fileNameList); - qrGenRequest.setErrorCorrectionLevel((String) config.get(QRCodeImageGeneratorParams.errorCorrectionLevel.name())); - qrGenRequest.setPixelsPerBlock((Integer) config.get(QRCodeImageGeneratorParams.pixelsPerBlock.name())); - qrGenRequest.setQrCodeMargin((Integer) config.get(QRCodeImageGeneratorParams.qrCodeMargin.name())); - qrGenRequest.setTextFontName((String) config.get(QRCodeImageGeneratorParams.textFontName.name())); - qrGenRequest.setTextFontSize((Integer) config.get(QRCodeImageGeneratorParams.textFontSize.name())); - qrGenRequest.setTextCharacterSpacing((Double) config.get(QRCodeImageGeneratorParams.textCharacterSpacing.name())); - qrGenRequest.setFileFormat((String) config.get(QRCodeImageGeneratorParams.imageFormat.name())); - qrGenRequest.setColorModel((String) config.get(QRCodeImageGeneratorParams.colourModel.name())); - qrGenRequest.setImageBorderSize((Integer) config.get(QRCodeImageGeneratorParams.imageBorderSize.name())); - if(config.containsKey(QRCodeImageGeneratorParams.qrCodeMarginBottom.name())) { - qrGenRequest.setQrCodeMarginBottom((Integer) config.get(QRCodeImageGeneratorParams.qrCodeMarginBottom.name())); - } else { - qrGenRequest.setQrCodeMarginBottom(appConfig.getInt(QRCodeImageGeneratorParams.qr_image_margin_bottom.name())); - } - if(config.containsKey(QRCodeImageGeneratorParams.imageMargin.name())) { - qrGenRequest.setImageMargin((Integer) config.get(QRCodeImageGeneratorParams.imageMargin.name())); - } else { - qrGenRequest.setImageMargin(appConfig.getInt(QRCodeImageGeneratorParams.qr_image_margin.name())); - } - qrGenRequest.setTempFilePath(appConfig.getOrDefault(QRCodeImageGeneratorParams.lp_tempfile_location.name(), "/tmp")); - return qrGenRequest; - } -} diff --git a/platform-jobs/samza/qrcode-image-generator/src/main/java/org/sunbird/jobs/samza/task/QRCodeImageGeneratorTask.java b/platform-jobs/samza/qrcode-image-generator/src/main/java/org/sunbird/jobs/samza/task/QRCodeImageGeneratorTask.java deleted file mode 100644 index de743c33e8..0000000000 --- a/platform-jobs/samza/qrcode-image-generator/src/main/java/org/sunbird/jobs/samza/task/QRCodeImageGeneratorTask.java +++ /dev/null @@ -1,64 +0,0 @@ -package org.sunbird.jobs.samza.task; - -import org.apache.samza.config.Config; -import org.apache.samza.system.IncomingMessageEnvelope; -import org.apache.samza.task.StreamTask; -import org.apache.samza.task.InitableTask; -import org.apache.samza.task.TaskContext; -import org.apache.samza.task.MessageCollector; -import org.apache.samza.task.TaskCoordinator; -import org.sunbird.jobs.samza.service.ISamzaService; -import org.sunbird.jobs.samza.service.QRCodeImageGeneratorService; -import org.sunbird.jobs.samza.service.task.JobMetrics; -import org.sunbird.jobs.samza.util.JobLogger; -import org.sunbird.jobs.samza.util.QRCodeImageGeneratorParams; - -import java.util.HashMap; -import java.util.Map; - -public class QRCodeImageGeneratorTask implements StreamTask, InitableTask { - - private JobLogger LOGGER = new JobLogger(QRCodeImageGeneratorTask.class); - - private JobMetrics metrics; - - private ISamzaService service = new QRCodeImageGeneratorService(); - - @Override - public void init(Config config, TaskContext context) throws Exception { - try { - metrics = new JobMetrics(context, config.get("output.metrics.job.name"), config.get("output.metrics.topic.name")); - service.initialize(config); - LOGGER.info("QRCodeImageGeneratorTask:init: Task initialized"); - } catch (Exception ex) { - LOGGER.error("QRCodeImageGeneratorTask:init: Task initialization failed", ex); - throw ex; - } - } - - - @Override - public void process(IncomingMessageEnvelope envelope, MessageCollector collector, TaskCoordinator coordinator) throws Exception { - Map outgoingMap = getMessage(envelope); - try { - service.processMessage(outgoingMap, metrics, collector); - } catch (Exception e) { - LOGGER.error("QRCodeImageGeneratorTask:process: Error while processing message for process_id:: " + - (String) outgoingMap.get(QRCodeImageGeneratorParams.processId.name()), outgoingMap, e); - e.printStackTrace(); - //throw e; - } - } - - @SuppressWarnings("unchecked") - private Map getMessage(IncomingMessageEnvelope envelope) { - try { - return (Map) envelope.getMessage(); - } catch (Exception e) { - e.printStackTrace(); - LOGGER.error("QRCodeImageGeneratorTask:getMessage: Invalid message = " + envelope.getMessage(), e); - return new HashMap(); - } - } - -} diff --git a/platform-jobs/samza/qrcode-image-generator/src/main/java/org/sunbird/jobs/samza/util/CloudStorageUtil.java b/platform-jobs/samza/qrcode-image-generator/src/main/java/org/sunbird/jobs/samza/util/CloudStorageUtil.java deleted file mode 100644 index 81493cae58..0000000000 --- a/platform-jobs/samza/qrcode-image-generator/src/main/java/org/sunbird/jobs/samza/util/CloudStorageUtil.java +++ /dev/null @@ -1,62 +0,0 @@ -package org.sunbird.jobs.samza.util; - -import org.apache.commons.lang3.StringUtils; -import org.sunbird.common.Platform; -import org.sunbird.common.exception.ServerException; -import org.sunbird.cloud.storage.BaseStorageService; -import org.sunbird.cloud.storage.factory.StorageConfig; -import org.sunbird.cloud.storage.factory.StorageServiceFactory; - -import scala.Option; - -import java.io.File; -import java.io.FileOutputStream; -import java.io.IOException; -import java.net.URL; -import java.nio.channels.Channels; -import java.nio.channels.FileChannel; -import java.nio.channels.ReadableByteChannel; - -public class CloudStorageUtil { - - private static BaseStorageService storageService = null; - private static String cloudStoreType = Platform.config.getString("cloud_storage_type"); - static { - - if(StringUtils.equalsIgnoreCase(cloudStoreType, "azure")) { - String storageKey = Platform.config.getString("azure_storage_key"); - String storageSecret = Platform.config.getString("azure_storage_secret"); - storageService = StorageServiceFactory.getStorageService(new StorageConfig(cloudStoreType, storageKey, storageSecret)); - }else if(StringUtils.equalsIgnoreCase(cloudStoreType, "aws")) { - String storageKey = Platform.config.getString("aws_storage_key"); - String storageSecret = Platform.config.getString("aws_storage_secret"); - storageService = StorageServiceFactory.getStorageService(new StorageConfig(cloudStoreType, storageKey, storageSecret)); - }else { - throw new ServerException("ERR_INVALID_CLOUD_STORAGE", "Error while initialising cloud storage"); - } - } - - public static String uploadFile(String container, String path, File file, boolean isDirectory) { - int retryCount = Platform.config.getInt("cloud_upload_retry_count"); - String objectKey = path + file.getName(); - String url = storageService.upload(container, - file.getAbsolutePath(), - objectKey, - Option.apply(isDirectory), - Option.apply(1), - Option.apply(retryCount),Option.empty()); - return url; - } - - public static void downloadFile(String downloadUrl, File fileToSave) throws IOException { - URL url = new URL(downloadUrl); - ReadableByteChannel readableByteChannel = Channels.newChannel(url.openStream()); - FileOutputStream fileOutputStream = new FileOutputStream(fileToSave); - FileChannel fileChannel = fileOutputStream.getChannel(); - fileOutputStream.getChannel().transferFrom(readableByteChannel, 0, Long.MAX_VALUE); - fileChannel.close(); - fileOutputStream.close(); - readableByteChannel.close(); - } - -} diff --git a/platform-jobs/samza/qrcode-image-generator/src/main/java/org/sunbird/jobs/samza/util/QRCodeCassandraConnector.java b/platform-jobs/samza/qrcode-image-generator/src/main/java/org/sunbird/jobs/samza/util/QRCodeCassandraConnector.java deleted file mode 100644 index cded052963..0000000000 --- a/platform-jobs/samza/qrcode-image-generator/src/main/java/org/sunbird/jobs/samza/util/QRCodeCassandraConnector.java +++ /dev/null @@ -1,27 +0,0 @@ -package org.sunbird.jobs.samza.util; - -import com.datastax.driver.core.Session; -import org.sunbird.cassandra.connector.util.CassandraConnector; - -public class QRCodeCassandraConnector { - - public static void updateDownloadUrl(String id, String downloadUrl) { - String query = "update dialcodes.dialcode_images set status=2, url='"+downloadUrl+"' where filename='"+id+"'"; - executeQuery(query); - } - - public static void updateDownloadZIPUrl(String id, String downloadZIPUrl) { - String query = "update dialcodes.dialcode_batch set status=2, url='"+downloadZIPUrl+"' where processid="+id; - executeQuery(query); - } - - public static void updateFailure(String id, String errMsg) { - String query = "update dialcodes.dialcode_batch set status=3, url='' where processid="+id; - executeQuery(query); - } - - private static void executeQuery(String query) { - Session session = CassandraConnector.getSession("sunbird"); - session.execute(query); - } -} diff --git a/platform-jobs/samza/qrcode-image-generator/src/main/java/org/sunbird/jobs/samza/util/QRCodeImageGeneratorParams.java b/platform-jobs/samza/qrcode-image-generator/src/main/java/org/sunbird/jobs/samza/util/QRCodeImageGeneratorParams.java deleted file mode 100644 index d0a950d912..0000000000 --- a/platform-jobs/samza/qrcode-image-generator/src/main/java/org/sunbird/jobs/samza/util/QRCodeImageGeneratorParams.java +++ /dev/null @@ -1,10 +0,0 @@ -package org.sunbird.jobs.samza.util; - -public enum QRCodeImageGeneratorParams { - - eid, processId, objectId, dialcodes, data, text, id, location, storage, container, path, config, - errorCorrectionLevel, pixelsPerBlock, qrCodeMargin, textFontName, textFontSize, textCharacterSpacing, - imageFormat, colourModel, imageBorderSize, qrCodeMarginBottom, BE_QR_IMAGE_GENERATOR, fileName, imageMargin, - qr_image_margin_bottom, qr_image_margin, lp_tempfile_location; - -} diff --git a/platform-jobs/samza/qrcode-image-generator/src/main/java/org/sunbird/jobs/samza/util/QRCodeImageGeneratorUtil.java b/platform-jobs/samza/qrcode-image-generator/src/main/java/org/sunbird/jobs/samza/util/QRCodeImageGeneratorUtil.java deleted file mode 100644 index 352b9b10fb..0000000000 --- a/platform-jobs/samza/qrcode-image-generator/src/main/java/org/sunbird/jobs/samza/util/QRCodeImageGeneratorUtil.java +++ /dev/null @@ -1,288 +0,0 @@ -package org.sunbird.jobs.samza.util; - -import com.google.zxing.BarcodeFormat; -import com.google.zxing.EncodeHintType; -import com.google.zxing.NotFoundException; -import com.google.zxing.WriterException; -import com.google.zxing.client.j2se.BufferedImageLuminanceSource; -import com.google.zxing.common.BitMatrix; -import com.google.zxing.common.HybridBinarizer; -import com.google.zxing.qrcode.QRCodeWriter; -import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel; -import org.apache.samza.config.Config; -import org.sunbird.jobs.samza.model.QRCodeGenerationRequest; - -import javax.imageio.ImageIO; -import java.awt.FontMetrics; -import java.awt.Font; -import java.awt.Color; -import java.awt.RenderingHints; -import java.awt.Graphics2D; -import java.awt.font.TextAttribute; -import java.awt.image.BufferedImage; -import java.awt.FontFormatException; -import java.io.File; -import java.io.IOException; -import java.io.InputStream; -import java.util.HashMap; -import java.util.Map; -import java.util.List; -import java.util.ArrayList; - -public class QRCodeImageGeneratorUtil { - - private static QRCodeWriter qrCodeWriter = new QRCodeWriter(); - private static Map fontStore = new HashMap(); - private static JobLogger LOGGER = new JobLogger(QRCodeImageGeneratorUtil.class); - - public static List createQRImages(QRCodeGenerationRequest qrGenRequest, Config appConfig, String container, String path) throws WriterException, IOException, NotFoundException, FontFormatException { - - List fileList = new ArrayList(); - - List dataList = qrGenRequest.getData(); - List textList = qrGenRequest.getText(); - List fileNameList = qrGenRequest.getFileName(); - - String errorCorrectionLevel = qrGenRequest.getErrorCorrectionLevel(); - int pixelsPerBlock = qrGenRequest.getPixelsPerBlock(); - int qrMargin = qrGenRequest.getQrCodeMargin(); - String fontName = qrGenRequest.getTextFontName(); - int fontSize = qrGenRequest.getTextFontSize(); - double tracking = qrGenRequest.getTextCharacterSpacing(); - String imageFormat = qrGenRequest.getFileFormat(); - String colorModel = qrGenRequest.getColorModel(); - int borderSize = qrGenRequest.getImageBorderSize(); - int qrMarginBottom = qrGenRequest.getQrCodeMarginBottom(); - int imageMargin = qrGenRequest.getImageMargin(); - String tempFilePath = qrGenRequest.getTempFilePath(); - - for (int i = 0; i < dataList.size(); i++) { - String data = dataList.get(i); - String text = textList.get(i); - String fileName = fileNameList.get(i); - - BufferedImage qrImage = generateBaseImage(data, errorCorrectionLevel, pixelsPerBlock, qrMargin, colorModel); - - if (null != text || "" != text) { - BufferedImage textImage = getTextImage(text, fontName, fontSize, tracking, colorModel); - qrImage = addTextToBaseImage(qrImage, textImage, colorModel, qrMargin, pixelsPerBlock, qrMarginBottom, imageMargin); - } - - if (borderSize > 0) { - drawBorder(qrImage, borderSize, imageMargin); - } - - File finalImageFile = new File(tempFilePath + File.separator + fileName + "." + imageFormat); - LOGGER.info("QRCodeImageGeneratorUtil:createQRImages: creating file - " + finalImageFile.getAbsolutePath()); - finalImageFile.createNewFile(); - LOGGER.info("QRCodeImageGeneratorUtil:createQRImages: created file - " + finalImageFile.getAbsolutePath()); - ImageIO.write(qrImage, imageFormat, finalImageFile); - fileList.add(finalImageFile); - - try { - String imageDownloadUrl = CloudStorageUtil.uploadFile(container, path, finalImageFile, false); - QRCodeCassandraConnector.updateDownloadUrl(fileName, imageDownloadUrl); - } catch(Exception e) { - //ignore exception and proceed - } - } - - return fileList; - - } - - private static BufferedImage addTextToBaseImage(BufferedImage qrImage, BufferedImage textImage, String colorModel, int qrMargin, int pixelsPerBlock, int qrMarginBottom, int imageMargin) throws NotFoundException { - BufferedImageLuminanceSource qrSource = new BufferedImageLuminanceSource(qrImage); - HybridBinarizer qrBinarizer = new HybridBinarizer(qrSource); - BitMatrix qrBits = qrBinarizer.getBlackMatrix(); - - BufferedImageLuminanceSource textSource = new BufferedImageLuminanceSource(textImage); - HybridBinarizer textBinarizer = new HybridBinarizer(textSource); - BitMatrix textBits = textBinarizer.getBlackMatrix(); - - if (qrBits.getWidth() > textBits.getWidth()) { - BitMatrix tempTextMatrix = new BitMatrix(qrBits.getWidth(), textBits.getHeight()); - copyMatrixDataToBiggerMatrix(textBits, tempTextMatrix); - textBits = tempTextMatrix; - } else if (qrBits.getWidth() < textBits.getWidth()) { - BitMatrix tempQrMatrix = new BitMatrix(textBits.getWidth(), qrBits.getHeight()); - copyMatrixDataToBiggerMatrix(qrBits, tempQrMatrix); - qrBits = tempQrMatrix; - } - - BitMatrix mergedMatrix = mergeMatricesOfSameWidth(qrBits, textBits, qrMargin, pixelsPerBlock, qrMarginBottom, imageMargin); - return getImage(mergedMatrix, colorModel); - } - - private static BufferedImage generateBaseImage(String data, String errorCorrectionLevel, int pixelsPerBlock, int qrMargin, String colorModel) throws WriterException { - Map hintsMap = getHintsMap(errorCorrectionLevel, qrMargin); - BitMatrix defaultBitMatrix = getDefaultBitMatrix(data, hintsMap); - BitMatrix largeBitMatrix = getBitMatrix(data, defaultBitMatrix.getWidth() * pixelsPerBlock, defaultBitMatrix.getHeight() * pixelsPerBlock, hintsMap); - BufferedImage qrImage = getImage(largeBitMatrix, colorModel); - return qrImage; - } - - //To remove extra spaces between text and qrcode, margin below qrcode is removed - //Parameter, qrCodeMarginBottom, is introduced to add custom margin(in pixels) between qrcode and text - //Parameter, imageMargin is introduced, to add custom margin(in pixels) outside the black border of the image - private static BitMatrix mergeMatricesOfSameWidth(BitMatrix firstMatrix, BitMatrix secondMatrix, int qrMargin, int pixelsPerBlock, int qrMarginBottom, int imageMargin) { - int mergedWidth = firstMatrix.getWidth() + (2 * imageMargin); - int mergedHeight = firstMatrix.getHeight() + secondMatrix.getHeight() + (2 * imageMargin); - int defaultBottomMargin = pixelsPerBlock * qrMargin; - int marginToBeRemoved = qrMarginBottom > defaultBottomMargin ? 0 : (defaultBottomMargin-qrMarginBottom); - BitMatrix mergedMatrix = new BitMatrix(mergedWidth, mergedHeight - marginToBeRemoved); - - for (int x = 0; x < firstMatrix.getWidth(); x++) { - for (int y = 0; y < firstMatrix.getHeight() - marginToBeRemoved; y++) { - if (firstMatrix.get(x, y)) { - mergedMatrix.set(x + imageMargin, y + imageMargin); - } - } - } - for (int x = 0; x < secondMatrix.getWidth(); x++) { - for (int y = 0; y < secondMatrix.getHeight(); y++) { - if (secondMatrix.get(x, y)) { - mergedMatrix.set(x + imageMargin, y + firstMatrix.getHeight() - marginToBeRemoved + imageMargin); - } - } - } - return mergedMatrix; - } - - private static void copyMatrixDataToBiggerMatrix(BitMatrix fromMatrix, BitMatrix toMatrix) { - int widthDiff = toMatrix.getWidth() - fromMatrix.getWidth(); - int leftMargin = widthDiff / 2; - for (int x = 0; x < fromMatrix.getWidth(); x++) { - for (int y = 0; y < fromMatrix.getHeight(); y++) { - if (fromMatrix.get(x, y)) { - toMatrix.set(x + leftMargin, y); - } - } - } - } - - private static void drawBorder(BufferedImage image, int borderSize, int imageMargin) { - image.createGraphics(); - Graphics2D graphics = (Graphics2D) image.getGraphics(); - graphics.setColor(Color.BLACK); - for (int i = 0; i < borderSize; i++) { - graphics.drawRect(i + imageMargin, i + imageMargin, image.getWidth() - 1 - (2 * i) - (2 * imageMargin), image.getHeight() - 1 - (2 * i) - (2 * imageMargin)); - } - graphics.dispose(); - } - - private static BufferedImage getImage(BitMatrix bitMatrix, String colorModel) { - int imageWidth = bitMatrix.getWidth(); - int imageHeight = bitMatrix.getHeight(); - BufferedImage image = new BufferedImage(imageWidth, imageHeight, getImageType(colorModel)); - image.createGraphics(); - - Graphics2D graphics = (Graphics2D) image.getGraphics(); - graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_OFF); - graphics.setColor(Color.WHITE); - graphics.fillRect(0, 0, imageWidth, imageHeight); - - graphics.setColor(Color.BLACK); - - for (int i = 0; i < imageWidth; i++) { - for (int j = 0; j < imageHeight; j++) { - if (bitMatrix.get(i, j)) { - graphics.fillRect(i, j, 1, 1); - } - } - } - graphics.dispose(); - return image; - } - - private static BitMatrix getBitMatrix(String data, int width, int height, Map hintsMap) throws WriterException { - BitMatrix bitMatrix = qrCodeWriter.encode(data, BarcodeFormat.QR_CODE, width, height, hintsMap); - return bitMatrix; - } - - private static BitMatrix getDefaultBitMatrix(String data, Map hintsMap) throws WriterException { - BitMatrix defaultBitMatrix = qrCodeWriter.encode(data, BarcodeFormat.QR_CODE, 0, 0, hintsMap); - return defaultBitMatrix; - } - - private static Map getHintsMap(String errorCorrectionLevel, int qrMargin) { - Map hintsMap = new HashMap(); - switch (errorCorrectionLevel) { - case "H": - hintsMap.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.H); - break; - case "Q": - hintsMap.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.Q); - break; - case "M": - hintsMap.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.M); - break; - case "L": - hintsMap.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.L); - break; - } - hintsMap.put(EncodeHintType.MARGIN, qrMargin); - return hintsMap; - } - - //Sample = 2A42UH , Verdana, 11, 0.1, Grayscale - private static BufferedImage getTextImage(String text, String fontName, int fontSize, double tracking, String colorModel) throws IOException, FontFormatException { - - BufferedImage image = new BufferedImage(1, 1, getImageType(colorModel)); - - Font basicFont = getFontFromStore(fontName); - - Map attributes = new HashMap(); - attributes.put(TextAttribute.TRACKING, tracking); - attributes.put(TextAttribute.WEIGHT, TextAttribute.WEIGHT_BOLD); - attributes.put(TextAttribute.SIZE, fontSize); - Font font = basicFont.deriveFont(attributes); - - Graphics2D graphics2d = image.createGraphics(); - graphics2d.setFont(font); - FontMetrics fontmetrics = graphics2d.getFontMetrics(); - int width = fontmetrics.stringWidth(text); - int height = fontmetrics.getHeight(); - graphics2d.dispose(); - - image = new BufferedImage(width, height, getImageType(colorModel)); - graphics2d = image.createGraphics(); - graphics2d.setRenderingHint(RenderingHints.KEY_ALPHA_INTERPOLATION, RenderingHints.VALUE_ALPHA_INTERPOLATION_QUALITY); - graphics2d.setRenderingHint(RenderingHints.KEY_COLOR_RENDERING, RenderingHints.VALUE_COLOR_RENDER_QUALITY); - graphics2d.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_OFF); - graphics2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_OFF); - - graphics2d.setColor(Color.WHITE); - graphics2d.fillRect(0, 0, image.getWidth(), image.getHeight()); - graphics2d.setColor(Color.BLACK); - - graphics2d.setFont(font); - fontmetrics = graphics2d.getFontMetrics(); - graphics2d.drawString(text, 0, fontmetrics.getAscent()); - graphics2d.dispose(); - - return image; - } - - private static int getImageType(String colorModel) { - if (colorModel.equalsIgnoreCase("RGB")) { - return BufferedImage.TYPE_INT_RGB; - } else { - return BufferedImage.TYPE_BYTE_GRAY; - } - } - - private static Font loadFontStore(String fontName) throws IOException, FontFormatException { - //load the packaged font file from the root dir - String fontFile = "/"+fontName+".ttf"; - InputStream fontStream = QRCodeImageGeneratorUtil.class.getResourceAsStream(fontFile); - Font basicFont = Font.createFont(Font.TRUETYPE_FONT, fontStream); - fontStore.put(fontName, basicFont); - - return basicFont; - } - - private static Font getFontFromStore(String fontName) throws IOException, FontFormatException { - return null != fontStore.get(fontName) ? fontStore.get(fontName) : loadFontStore(fontName); - } -} diff --git a/platform-jobs/samza/qrcode-image-generator/src/main/java/org/sunbird/jobs/samza/util/ZipEditorUtil.java b/platform-jobs/samza/qrcode-image-generator/src/main/java/org/sunbird/jobs/samza/util/ZipEditorUtil.java deleted file mode 100644 index e0b72c7293..0000000000 --- a/platform-jobs/samza/qrcode-image-generator/src/main/java/org/sunbird/jobs/samza/util/ZipEditorUtil.java +++ /dev/null @@ -1,40 +0,0 @@ -package org.sunbird.jobs.samza.util; - -import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; -import java.io.IOException; -import java.util.zip.ZipEntry; -import java.util.zip.ZipOutputStream; -import java.util.List; - -public class ZipEditorUtil { - - private static JobLogger LOGGER = new JobLogger(ZipEditorUtil.class); - - public static File zipFiles(List files, String zipName, String basePath) throws IOException { - File zipFile = new File(basePath + File.separator + zipName + ".zip"); - LOGGER.info("ZipEditorUtil:zipFiles: creating file - " + zipFile.getAbsolutePath()); - zipFile.createNewFile(); - LOGGER.info("ZipEditorUtil:zipFiles: created file - " + zipFile.getAbsolutePath()); - FileOutputStream fos = new FileOutputStream(zipFile); - ZipOutputStream zos = new ZipOutputStream(fos); - for (File file : files) { - String filePath = file.getAbsolutePath(); - ZipEntry ze = new ZipEntry(file.getName()); - zos.putNextEntry(ze); - FileInputStream fis = new FileInputStream(filePath); - byte[] buffer = new byte[1024]; - int len; - while ((len = fis.read(buffer)) > 0) { - zos.write(buffer, 0, len); - } - zos.closeEntry(); - fis.close(); - } - zos.close(); - fos.close(); - - return zipFile; - } -} \ No newline at end of file diff --git a/platform-jobs/samza/qrcode-image-generator/src/main/resources/Verdana.ttf b/platform-jobs/samza/qrcode-image-generator/src/main/resources/Verdana.ttf deleted file mode 100755 index 18ef6e8f1fc1a57750e17b2c611454d010f3ee51..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 129364 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�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 t3)KzWf_Z(5kW%B)&PZ - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/pom.xml b/pom.xml index 50ac54bcce..c95893d4c5 100644 --- a/pom.xml +++ b/pom.xml @@ -44,15 +44,6 @@ platform-modules - - samza-jobs - - platform-core - platform-modules/actors - platform-modules/content-manager - platform-jobs - - From f09f35a8950033a4fc2cc9efc054096ad3ea24f7 Mon Sep 17 00:00:00 2001 From: Kartheek Palla Date: Mon, 6 Feb 2023 15:39:32 +0530 Subject: [PATCH 175/222] Issue KN-808 fix: removing dead code from repo --- .../mvcsearchindex-elasticsearch/.gitignore | 1 - .../mvcsearchindex-elasticsearch/pom.xml | 103 ------ .../elasticsearch/ElasticSearchUtil.java | 324 ------------------ .../AggregationsResultTransformer.java | 35 -- .../transformer/IESResultTransformer.java | 5 - .../sunbird/test/ElasticSearchUtilTest.java | 105 ------ .../src/test/resources/log4j2.xml | 36 -- searchIndex-platform/pom.xml | 1 - 8 files changed, 610 deletions(-) delete mode 100644 searchIndex-platform/module/mvcsearchindex-elasticsearch/.gitignore delete mode 100644 searchIndex-platform/module/mvcsearchindex-elasticsearch/pom.xml delete mode 100644 searchIndex-platform/module/mvcsearchindex-elasticsearch/src/main/java/org/sunbird/mvcsearchindex/elasticsearch/ElasticSearchUtil.java delete mode 100644 searchIndex-platform/module/mvcsearchindex-elasticsearch/src/main/java/org/sunbird/mvcsearchindex/transformer/AggregationsResultTransformer.java delete mode 100644 searchIndex-platform/module/mvcsearchindex-elasticsearch/src/main/java/org/sunbird/mvcsearchindex/transformer/IESResultTransformer.java delete mode 100644 searchIndex-platform/module/mvcsearchindex-elasticsearch/src/test/java/org/sunbird/test/ElasticSearchUtilTest.java delete mode 100644 searchIndex-platform/module/mvcsearchindex-elasticsearch/src/test/resources/log4j2.xml diff --git a/searchIndex-platform/module/mvcsearchindex-elasticsearch/.gitignore b/searchIndex-platform/module/mvcsearchindex-elasticsearch/.gitignore deleted file mode 100644 index b83d22266a..0000000000 --- a/searchIndex-platform/module/mvcsearchindex-elasticsearch/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/target/ diff --git a/searchIndex-platform/module/mvcsearchindex-elasticsearch/pom.xml b/searchIndex-platform/module/mvcsearchindex-elasticsearch/pom.xml deleted file mode 100644 index 75226166bc..0000000000 --- a/searchIndex-platform/module/mvcsearchindex-elasticsearch/pom.xml +++ /dev/null @@ -1,103 +0,0 @@ - - 4.0.0 - - org.sunbird - searchindex-platform - 1.1-SNAPSHOT - ../../pom.xml - - mvcsearchindex-elasticsearch - jar - mvcsearchindex-elasticsearch - - UTF-8 - - - - - org.sunbird - unit-tests - 1.1-SNAPSHOT - test-jar - test - - - org.sunbird - searchindex-common - 1.1-SNAPSHOT - jar - - - org.codehaus.jackson - jackson-mapper-asl - 1.9.13 - - - commons-lang - commons-lang - 2.6 - - - org.apache.httpcomponents - httpclient - 4.5.2 - - - net.sf.json-lib - json-lib - 2.4 - jdk15 - - - org.elasticsearch - elasticsearch - 7.5.0 - - - org.elasticsearch.client - elasticsearch-rest-high-level-client - 7.5.0 - - - org.elasticsearch.client - transport - 7.5.0 - - - org.apache.logging.log4j - log4j-api - ${log4j.version} - - - org.apache.logging.log4j - log4j-core - ${log4j.version} - - - org.powermock - powermock-module-junit4 - 1.7.4 - test - - - org.powermock - powermock-api-mockito - 1.7.4 - test - - - org.slf4j - slf4j-api - 1.6.1 - compile - - - - - - - - - - diff --git a/searchIndex-platform/module/mvcsearchindex-elasticsearch/src/main/java/org/sunbird/mvcsearchindex/elasticsearch/ElasticSearchUtil.java b/searchIndex-platform/module/mvcsearchindex-elasticsearch/src/main/java/org/sunbird/mvcsearchindex/elasticsearch/ElasticSearchUtil.java deleted file mode 100644 index a9d29ff0d3..0000000000 --- a/searchIndex-platform/module/mvcsearchindex-elasticsearch/src/main/java/org/sunbird/mvcsearchindex/elasticsearch/ElasticSearchUtil.java +++ /dev/null @@ -1,324 +0,0 @@ -/** - * - */ -package org.sunbird.mvcsearchindex.elasticsearch; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.concurrent.ExecutionException; -import org.apache.commons.lang3.StringUtils; -import org.apache.http.HttpHost; -import org.apache.http.client.config.RequestConfig; -import org.codehaus.jackson.JsonGenerationException; -import org.codehaus.jackson.map.JsonMappingException; -import org.sunbird.searchindex.util.CompositeSearchConstants; -import org.sunbird.telemetry.logger.TelemetryManager; -import org.elasticsearch.action.ActionListener; -import org.elasticsearch.action.admin.indices.alias.Alias; -import org.elasticsearch.action.support.master.AcknowledgedResponse; -import org.elasticsearch.client.indices.CreateIndexRequest; -import org.elasticsearch.client.indices.CreateIndexResponse; -import org.elasticsearch.action.admin.indices.delete.DeleteIndexRequest; -import org.elasticsearch.action.get.GetRequest; -import org.elasticsearch.action.get.GetResponse; -import org.elasticsearch.action.index.IndexRequest; -import org.elasticsearch.action.index.IndexResponse; -import org.elasticsearch.action.search.SearchRequest; -import org.elasticsearch.action.search.SearchResponse; -import org.elasticsearch.action.update.UpdateRequest; -import org.elasticsearch.action.update.UpdateResponse; -import org.elasticsearch.client.*; -import org.elasticsearch.common.settings.Settings; -import org.elasticsearch.common.xcontent.XContentType; -import org.elasticsearch.index.query.BoolQueryBuilder; -import org.elasticsearch.index.query.QueryBuilders; -import org.elasticsearch.search.aggregations.AggregationBuilders; -import org.elasticsearch.search.aggregations.bucket.terms.TermsAggregationBuilder; -import org.elasticsearch.search.builder.SearchSourceBuilder; - -import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.ObjectMapper; - -import akka.dispatch.Futures; -import scala.concurrent.Future; -import scala.concurrent.Promise; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * @author pradyumna - * - */ -public class ElasticSearchUtil { - private static final Logger logger = LoggerFactory.getLogger(ElasticSearchUtil.class); - static { - System.setProperty("es.set.netty.runtime.available.processors", "false"); - registerShutdownHook(); - } - - private static Map esClient = new HashMap(); - - private static ObjectMapper mapper = new ObjectMapper(); - - public static void initialiseESClient(String indexName, String connectionInfo) { - if (StringUtils.isBlank(indexName)) - indexName = CompositeSearchConstants.MVC_SEARCH_INDEX; - createClient(indexName, connectionInfo); - } - - /** - * - */ - private static void createClient(String indexName, String connectionInfo) { - if (!esClient.containsKey(indexName)) { - Map hostPort = new HashMap(); - for (String info : connectionInfo.split(",")) { - hostPort.put(info.split(":")[0], Integer.valueOf(info.split(":")[1])); - } - List httpHosts = new ArrayList<>(); - for (String host : hostPort.keySet()) { - httpHosts.add(new HttpHost(host, hostPort.get(host))); - } - RestClientBuilder builder = RestClient.builder(httpHosts.toArray(new HttpHost[httpHosts.size()])) - .setRequestConfigCallback(new RestClientBuilder.RequestConfigCallback() { - @Override - public RequestConfig.Builder customizeRequestConfig(RequestConfig.Builder requestConfigBuilder) { - return requestConfigBuilder.setConnectionRequestTimeout(-1); - } - }); - RestHighLevelClient client = new RestHighLevelClient(builder); - if (null != client) - esClient.put(indexName, client); - } - } - - private static RestHighLevelClient getClient(String indexName) { - if (StringUtils.isBlank(indexName)) - indexName = CompositeSearchConstants.MVC_SEARCH_INDEX; - if (StringUtils.startsWith(indexName,"kp_audit_log")) - return esClient.get("kp_audit_log"); - return esClient.get(indexName); - } - - public void finalize() { - cleanESClient(); - } - - - - public static boolean isIndexExists(String indexName) { - Response response; - try { - Request request = new Request("HEAD", "/" + indexName); - response = getClient(indexName).getLowLevelClient().performRequest(request); - return (200 == response.getStatusLine().getStatusCode()); - } catch (IOException e) { - return false; - } - - } - - - public static boolean addIndex(String indexName, String documentType, String settings, String mappings,String alias , String esvalues) - throws IOException { - boolean response = false; - RestHighLevelClient client = getClient(indexName); - if (!isIndexExists(indexName)) { - CreateIndexRequest createRequest = new CreateIndexRequest(indexName); - if(esvalues != null) { - createRequest.source(esvalues,XContentType.JSON); - } - else { - if (StringUtils.isNotBlank(alias)) - createRequest.alias(new Alias(alias)); - if (StringUtils.isNotBlank(settings)) - createRequest.settings(Settings.builder().loadFromSource(settings, XContentType.JSON)); - if (StringUtils.isNotBlank(documentType) && StringUtils.isNotBlank(mappings)) - createRequest.mapping(mappings, XContentType.JSON); - } - CreateIndexResponse createIndexResponse = client.indices().create(createRequest,RequestOptions.DEFAULT); - - response = createIndexResponse.isAcknowledged(); - } - return response; - } - - - public static void addDocumentWithId(String indexName, String documentId, String document) { - try { - logger.info("Inside addDocuemntwithId"); - IndexRequest indexRequest = new IndexRequest(indexName); - indexRequest.id(documentId); - indexRequest.source(document,XContentType.JSON); - IndexResponse indexResponse = getClient(indexName).index(indexRequest,RequestOptions.DEFAULT); - logger.info("Response after inserting inside ES :: " + indexResponse.toString()); - TelemetryManager.log("Added " + indexResponse.getId() + " to index " + indexResponse.getIndex()); - } catch (IOException e) { - logger.info("Error after inserting inside ES :: " + indexName + " " + e); - TelemetryManager.error("Error while adding document to index :" + indexName, e); - } - } - - - public static void updateDocument(String indexName, String documentId, String document) - { - try {Map doc = mapper.readValue(document, new TypeReference>() { - }); - logger.info("Inside updateDocument"); - UpdateRequest updateRequest = new UpdateRequest(); - updateRequest.index(indexName); - updateRequest.id(documentId); - updateRequest.doc(doc); - UpdateResponse response = getClient(indexName).update(updateRequest,RequestOptions.DEFAULT); - TelemetryManager.log("Updated " + response.getId() + " to index " + response.getIndex()); - logger.info("Response after updating inside ES :: " + response.toString()); - } catch (IOException e) { - logger.info("Error after updating inside ES :: " + indexName + " " + e); - TelemetryManager.error("Error while updating document to index :" + indexName, e); - } - - } - - - - - - public static void deleteIndex(String indexName) throws InterruptedException, ExecutionException, IOException { - AcknowledgedResponse response = getClient(indexName).indices().delete(new DeleteIndexRequest(indexName),RequestOptions.DEFAULT); - esClient.remove(indexName); - TelemetryManager.log("Deleted Index" + indexName + " : " + response.isAcknowledged()); - } - - public static String getDocumentAsStringById(String indexName, String documentId) - throws IOException { - GetResponse response = getClient(indexName).get(new GetRequest(indexName, documentId),RequestOptions.DEFAULT); - return response.getSourceAsString(); - } - - public static SearchResponse search(Map matchCriterias, Map textFiltersMap, - String indexName, String indexType, List> groupBy, boolean isDistinct, int limit) - throws Exception { - SearchSourceBuilder query = buildJsonForQuery(matchCriterias, textFiltersMap, groupBy, isDistinct, indexName); - query.size(limit); - return search(indexName, indexType, query); - } - - public static SearchResponse search(String indexName, String indexType, SearchSourceBuilder query) - throws Exception { - return getClient(indexName).search(new SearchRequest(indexName).source(query),RequestOptions.DEFAULT); - } - - public static Future search(String indexName, SearchSourceBuilder searchSourceBuilder) - throws IOException { - TelemetryManager.log("searching in ES index: " + indexName); - Promise promise = Futures.promise(); - getClient(indexName).searchAsync(new SearchRequest().indices(indexName).source(searchSourceBuilder),RequestOptions.DEFAULT, - new ActionListener() { - - @Override - public void onResponse(SearchResponse response) { - promise.success(response); - } - - @Override - public void onFailure(Exception e) { - promise.failure(e); - } - }); - return promise.future(); - } - - - - - @SuppressWarnings("unchecked") - public static SearchSourceBuilder buildJsonForQuery(Map matchCriterias, - Map textFiltersMap, List> groupByList, boolean isDistinct, - String indexName) - throws JsonGenerationException, JsonMappingException, IOException { - - SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder(); - - BoolQueryBuilder queryBuilder = QueryBuilders.boolQuery(); - if (matchCriterias != null) { - - for (Map.Entry entry : matchCriterias.entrySet()) { - if (entry.getValue() instanceof List) { - for (String matchText : (ArrayList) entry.getValue()) { - queryBuilder.should(QueryBuilders.matchQuery(entry.getKey(), matchText)); - } - } - } - } - - if (textFiltersMap != null && !textFiltersMap.isEmpty()) { - BoolQueryBuilder boolQuery = QueryBuilders.boolQuery(); - for (Map.Entry entry : textFiltersMap.entrySet()) { - ArrayList termValues = (ArrayList) entry.getValue(); - for (String termValue : termValues) { - boolQuery.must(QueryBuilders.termQuery(entry.getKey(), termValue)); - } - } - queryBuilder.filter(boolQuery); - } - - searchSourceBuilder.query(QueryBuilders.boolQuery().filter(queryBuilder)); - - if (groupByList != null && !groupByList.isEmpty()) { - if (!isDistinct) { - for (Map groupByMap : groupByList) { - String groupByParent = (String) groupByMap.get("groupByParent"); - List groupByChildList = (List) groupByMap.get("groupByChildList"); - TermsAggregationBuilder termBuilder = AggregationBuilders.terms(groupByParent).field(groupByParent); - if (groupByChildList != null && !groupByChildList.isEmpty()) { - for (String childGroupBy : groupByChildList) { - termBuilder.subAggregation(AggregationBuilders.terms(childGroupBy).field(childGroupBy)); - } - - } - searchSourceBuilder.aggregation(termBuilder); - } - } else { - for (Map groupByMap : groupByList) { - String groupBy = (String) groupByMap.get("groupBy"); - String distinctKey = (String) groupByMap.get("distinctKey"); - searchSourceBuilder.aggregation( - AggregationBuilders.terms(groupBy).field(groupBy).subAggregation(AggregationBuilders - .cardinality("distinct_" + distinctKey + "s").field(distinctKey))); - } - } - } - - return searchSourceBuilder; - } - - - private static void registerShutdownHook() { - Runtime.getRuntime().addShutdownHook(new Thread() { - @Override - public void run() { - try { - cleanESClient(); - } catch (Exception e) { - e.printStackTrace(); - } - } - }); - } - - public static void cleanESClient() { - if (!esClient.isEmpty()) - for (RestHighLevelClient client : esClient.values()) { - if (null != client) - try { - client.close(); - } catch (IOException e) { - } - } - } - - -} \ No newline at end of file diff --git a/searchIndex-platform/module/mvcsearchindex-elasticsearch/src/main/java/org/sunbird/mvcsearchindex/transformer/AggregationsResultTransformer.java b/searchIndex-platform/module/mvcsearchindex-elasticsearch/src/main/java/org/sunbird/mvcsearchindex/transformer/AggregationsResultTransformer.java deleted file mode 100644 index 396bc271f7..0000000000 --- a/searchIndex-platform/module/mvcsearchindex-elasticsearch/src/main/java/org/sunbird/mvcsearchindex/transformer/AggregationsResultTransformer.java +++ /dev/null @@ -1,35 +0,0 @@ -package org.sunbird.mvcsearchindex.transformer; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -public class AggregationsResultTransformer implements IESResultTransformer{ - - - @SuppressWarnings("unchecked") - public Object getTransformedObject(Object obj){ - Map aggObj = (Map) obj; - List> transformedObj = new ArrayList>(); - for(Map.Entry entry: aggObj.entrySet()){ - Map facetMap = new HashMap(); - String facetName = entry.getKey(); - facetMap.put("name", facetName); - Map aggKeyMap = (Map) entry.getValue(); - List> facetKeys = new ArrayList>(); - for(Map.Entry aggKeyEntry: aggKeyMap.entrySet()){ - Map facetKeyMap = new HashMap(); - String facetKeyName = aggKeyEntry.getKey(); - facetKeyMap.put("name", facetKeyName); - Map facetKeyCountMap = (Map) aggKeyEntry.getValue(); - long count = (long) facetKeyCountMap.get("count"); - facetKeyMap.put("count", count); - facetKeys.add(facetKeyMap); - } - facetMap.put("values", facetKeys); - transformedObj.add(facetMap); - } - return transformedObj; - } -} diff --git a/searchIndex-platform/module/mvcsearchindex-elasticsearch/src/main/java/org/sunbird/mvcsearchindex/transformer/IESResultTransformer.java b/searchIndex-platform/module/mvcsearchindex-elasticsearch/src/main/java/org/sunbird/mvcsearchindex/transformer/IESResultTransformer.java deleted file mode 100644 index b8115799b6..0000000000 --- a/searchIndex-platform/module/mvcsearchindex-elasticsearch/src/main/java/org/sunbird/mvcsearchindex/transformer/IESResultTransformer.java +++ /dev/null @@ -1,5 +0,0 @@ -package org.sunbird.mvcsearchindex.transformer; - -public interface IESResultTransformer { - public Object getTransformedObject(Object obj); -} diff --git a/searchIndex-platform/module/mvcsearchindex-elasticsearch/src/test/java/org/sunbird/test/ElasticSearchUtilTest.java b/searchIndex-platform/module/mvcsearchindex-elasticsearch/src/test/java/org/sunbird/test/ElasticSearchUtilTest.java deleted file mode 100644 index b74a71cefd..0000000000 --- a/searchIndex-platform/module/mvcsearchindex-elasticsearch/src/test/java/org/sunbird/test/ElasticSearchUtilTest.java +++ /dev/null @@ -1,105 +0,0 @@ -/** - * - */ - -package org.sunbird.test; - -import static org.junit.Assert.assertTrue; -import static org.mockito.Mockito.when; - -import java.util.Date; -import java.util.HashMap; -import java.util.Map; -import java.util.Random; -import org.apache.commons.lang.StringUtils; -import org.codehaus.jackson.map.ObjectMapper; -import org.sunbird.mvcsearchindex.elasticsearch.ElasticSearchUtil; -import org.sunbird.searchindex.util.CompositeSearchConstants; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.Mockito; -import org.mockito.MockitoAnnotations; -import org.powermock.api.mockito.PowerMockito; -import org.powermock.core.classloader.annotations.PowerMockIgnore; -import org.powermock.core.classloader.annotations.PrepareForTest; -import org.powermock.modules.junit4.PowerMockRunner; - -/** - * @author pradyumna - * - */ - -@RunWith(PowerMockRunner.class) -@PrepareForTest({ElasticSearchUtil.class}) -@PowerMockIgnore({"javax.management.*", "sun.security.ssl.*", "javax.net.ssl.*" , "javax.crypto.*"}) -public class ElasticSearchUtilTest { - - - private static ObjectMapper mapper = new ObjectMapper(); - private static Random random = new Random(); - - @Before - public void setup() { - MockitoAnnotations.initMocks(this); - } - - @Test - public void testAddDocumentWithId() throws Exception { - Map content = getContentTestRecord(); - String id = (String) content.get("identifier"); - // addToIndex(id, content); - String jsonIndexDocument = mapper.writeValueAsString(content); - PowerMockito.mockStatic(ElasticSearchUtil.class); - PowerMockito.doNothing().when(ElasticSearchUtil.class); - ElasticSearchUtil.addDocumentWithId(CompositeSearchConstants.MVC_SEARCH_INDEX, - id, jsonIndexDocument); - when(ElasticSearchUtil.getDocumentAsStringById(Mockito.anyString(),Mockito.anyString())).thenReturn(id); - String doc = ElasticSearchUtil.getDocumentAsStringById(CompositeSearchConstants.MVC_SEARCH_INDEX, id); - assertTrue(StringUtils.contains(doc, id)); - } - - - @Test - public void testUpdateDocument() throws Exception { - Map content = getContentTestRecord(); - String id = (String) content.get("identifier"); - String jsonIndexDocument = mapper.writeValueAsString(content); - PowerMockito.mockStatic(ElasticSearchUtil.class); - PowerMockito.doNothing().when(ElasticSearchUtil.class); - ElasticSearchUtil.addDocumentWithId(CompositeSearchConstants.MVC_SEARCH_INDEX, - id, jsonIndexDocument); - content.put("name", "Content_" + System.currentTimeMillis() + "_name"); - PowerMockito.mockStatic(ElasticSearchUtil.class); - PowerMockito.doNothing().when(ElasticSearchUtil.class); - ElasticSearchUtil.updateDocument(CompositeSearchConstants.MVC_SEARCH_INDEX, - mapper.writeValueAsString(content), id); - when(ElasticSearchUtil.getDocumentAsStringById(Mockito.anyString(),Mockito.anyString())).thenReturn(id); - String doc = ElasticSearchUtil.getDocumentAsStringById(CompositeSearchConstants.MVC_SEARCH_INDEX, id); - assertTrue(StringUtils.contains(doc, id)); - } - - - - - private static Map getContentTestRecord() { - String objectType = "Content"; - Date d = new Date(); - Map map = new HashMap(); - long suffix = (long) (10000000 + random.nextInt(1000000)); - map.put("identifier", "do_" + suffix); - map.put("objectType", objectType); - map.put("name", "Content_" + System.currentTimeMillis() + "_name"); - map.put("contentType", "Content"); - map.put("status", "Draft"); - return map; - } - /*private static void addToIndex(String uniqueId, Map doc) throws Exception { - String jsonIndexDocument = mapper.writeValueAsString(doc); - PowerMockito.mockStatic(ElasticSearchUtil.class); - PowerMockito.doNothing().when(ElasticSearchUtil.class); - ElasticSearchUtil.addDocumentWithId(CompositeSearchConstants.MVC_SEARCH_INDEX, - uniqueId, jsonIndexDocument); - }*/ - -} diff --git a/searchIndex-platform/module/mvcsearchindex-elasticsearch/src/test/resources/log4j2.xml b/searchIndex-platform/module/mvcsearchindex-elasticsearch/src/test/resources/log4j2.xml deleted file mode 100644 index fd6a9ef4a9..0000000000 --- a/searchIndex-platform/module/mvcsearchindex-elasticsearch/src/test/resources/log4j2.xml +++ /dev/null @@ -1,36 +0,0 @@ - - - - - - - - - %d [%t] %-5level %logger{36} - %msg%n - - - - - - - - - %d %msg%n - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/searchIndex-platform/pom.xml b/searchIndex-platform/pom.xml index 3d4709b867..c9079ea4f7 100644 --- a/searchIndex-platform/pom.xml +++ b/searchIndex-platform/pom.xml @@ -24,7 +24,6 @@ module/searchindex-elasticsearch module/searchindex-common - module/mvcsearchindex-elasticsearch From 4304f7f2730d593b55c0d0cd56b1ae7da0edcb1e Mon Sep 17 00:00:00 2001 From: Kartheek Palla Date: Mon, 6 Feb 2023 15:49:58 +0530 Subject: [PATCH 176/222] Issue KN-808 fix: removing dead code from repo --- platform-modules/manager/pom.xml | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/platform-modules/manager/pom.xml b/platform-modules/manager/pom.xml index 55cb295a79..8c913b5120 100644 --- a/platform-modules/manager/pom.xml +++ b/platform-modules/manager/pom.xml @@ -9,18 +9,6 @@ sunbird-manager - - org.sunbird - searchindex-elasticsearch - 1.1-SNAPSHOT - jar - - - org.apache.logging.log4j - log4j-core - - - org.sunbird content-manager From b7376d69aadbd9fcddcb52358fe9e21c56e000e5 Mon Sep 17 00:00:00 2001 From: Jayaprakash8887 Date: Tue, 21 Feb 2023 18:48:37 +0530 Subject: [PATCH 177/222] Issue #KN-802 feat: Sync QR Image to DIAL code ES index --- .../sync/tool/mgr/CassandraESSyncManager.java | 6 +++--- .../sunbird/sync/tool/util/DialcodeSync.java | 20 +++++++++++++++++++ 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/platform-tools/spikes/sync-tool/src/main/java/org/sunbird/sync/tool/mgr/CassandraESSyncManager.java b/platform-tools/spikes/sync-tool/src/main/java/org/sunbird/sync/tool/mgr/CassandraESSyncManager.java index c5309efedc..16a8b9b805 100644 --- a/platform-tools/spikes/sync-tool/src/main/java/org/sunbird/sync/tool/mgr/CassandraESSyncManager.java +++ b/platform-tools/spikes/sync-tool/src/main/java/org/sunbird/sync/tool/mgr/CassandraESSyncManager.java @@ -32,6 +32,7 @@ import org.sunbird.content.entity.Media; import org.sunbird.content.entity.Plugin; import org.sunbird.content.operation.initializer.BaseInitializer; +import org.sunbird.content.util.SyncMessageGenerator; import org.sunbird.graph.cache.util.RedisStoreUtil; import org.sunbird.graph.dac.model.Node; import org.sunbird.graph.model.node.DefinitionDTO; @@ -43,7 +44,6 @@ import org.sunbird.sync.tool.util.DialcodeSync; import org.sunbird.sync.tool.util.ElasticSearchConnector; import org.sunbird.sync.tool.util.GraphUtil; -import org.sunbird.sync.tool.util.SyncMessageGenerator; import org.sunbird.telemetry.logger.TelemetryManager; import org.springframework.stereotype.Component; @@ -230,7 +230,7 @@ private void populateESDoc(Map unitsMetadata, Map nodeMap = SyncMessageGenerator.getMessage(node); - Map message = SyncMessageGenerator.getJSONMessage(nodeMap, relationMap); + Map message = SyncMessageGenerator.getJSONMessage(nodeMap, relationMap, new ArrayList()); childData = refactorUnit(child); Object variants = message.get("variants"); if(null != variants && !(variants instanceof String)) @@ -316,7 +316,7 @@ private Map getESDocuments(List> units) thro return null; }).filter(node -> null!=node).collect(Collectors.toList()); - Map esDocument = SyncMessageGenerator.getMessages(nodes, "Content", new HashMap<>()); + Map esDocument = SyncMessageGenerator.getMessages(nodes, "Content", new HashMap<>(), new HashMap<>(), false); return esDocument; } diff --git a/platform-tools/spikes/sync-tool/src/main/java/org/sunbird/sync/tool/util/DialcodeSync.java b/platform-tools/spikes/sync-tool/src/main/java/org/sunbird/sync/tool/util/DialcodeSync.java index 1070a611aa..ec12b1497f 100644 --- a/platform-tools/spikes/sync-tool/src/main/java/org/sunbird/sync/tool/util/DialcodeSync.java +++ b/platform-tools/spikes/sync-tool/src/main/java/org/sunbird/sync/tool/util/DialcodeSync.java @@ -23,6 +23,8 @@ public class DialcodeSync { private static String documentType = null; private static String keyspace = null; private static String table = null; + private static String qrImageKeyspace = null; + private static String qrImageTable = null; public DialcodeSync() { @@ -34,6 +36,10 @@ public DialcodeSync() { ? Platform.config.getString("dialcode.keyspace.name") : "sunbirddev_dialcode_store"; table = Platform.config.hasPath("dialcode.table") ? Platform.config.getString("dialcode.table") : "dial_code"; + qrImageKeyspace = Platform.config.hasPath("dialcode.qrImageKeyspace.name") + ? Platform.config.getString("dialcode.qrImageKeyspace.name") : "dialcodes"; + qrImageTable = Platform.config.hasPath("dialcode.qrImageTable") + ? Platform.config.getString("dialcode.qrImageTable") : "dialcode_images"; ElasticSearchUtil.initialiseESClient(indexName, Platform.config.getString("search.es_conn_info")); } @@ -77,6 +83,9 @@ public Map getDialcodesFromIds(List identifiers) { put("published_on", row.getString("published_on")); put("objectType", "DialCode"); }}; + + String imageUrl = getQRImageFromDB(dialcodeId); + if(imageUrl != null && !imageUrl.isEmpty()) syncRequest.put("imageUrl", imageUrl); messages.put(dialcodeId, syncRequest); } System.out.println("total dialcodes fetched from cassandra: " + dialCodesFromDB); @@ -98,4 +107,15 @@ private ResultSet getDialcodesFromDB(List identifiers) { Session session = CassandraConnector.getSession(); return session.execute(query); } + + private String getQRImageFromDB(String dialcodeId) { + String query = "SELECT url FROM " + qrImageKeyspace + "." + qrImageTable + " WHERE dialcode ='" + dialcodeId + "';"; + Session session = CassandraConnector.getSession(); + ResultSet rs = session.execute(query); + while(rs.iterator().hasNext()) { + Row row = rs.iterator().next(); + return row.getString("url"); + } + return ""; + } } From 7a14aa3f5cdfe73040ede93f222a4274306d9a19 Mon Sep 17 00:00:00 2001 From: Jayaprakash8887 Date: Tue, 21 Feb 2023 18:58:48 +0530 Subject: [PATCH 178/222] Issue #KN-802 feat: Sync QR Image to DIAL code ES index --- .../java/org/sunbird/sync/tool/util/DialcodeSync.java | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/platform-tools/spikes/sync-tool/src/main/java/org/sunbird/sync/tool/util/DialcodeSync.java b/platform-tools/spikes/sync-tool/src/main/java/org/sunbird/sync/tool/util/DialcodeSync.java index ec12b1497f..2b053c76eb 100644 --- a/platform-tools/spikes/sync-tool/src/main/java/org/sunbird/sync/tool/util/DialcodeSync.java +++ b/platform-tools/spikes/sync-tool/src/main/java/org/sunbird/sync/tool/util/DialcodeSync.java @@ -5,6 +5,7 @@ import java.util.Map; import org.apache.commons.collections.MapUtils; +import org.apache.commons.lang3.StringUtils; import org.sunbird.cassandra.connector.util.CassandraConnector; import org.sunbird.common.Platform; import org.sunbird.common.exception.ServerException; @@ -25,7 +26,10 @@ public class DialcodeSync { private static String table = null; private static String qrImageKeyspace = null; private static String qrImageTable = null; - + + private static boolean isReplaceString = Platform.config.getBoolean("is_replace_string"); + private static String replaceSrcStringDIALStore = Platform.config.getString("replace_src_string_DIAL_store"); + private static String replaceDestStringDIALStore = Platform.config.getString("replace_dest_string_DIAL_store"); public DialcodeSync() { indexName = Platform.config.hasPath("dialcode.index.name") @@ -85,6 +89,11 @@ public Map getDialcodesFromIds(List identifiers) { }}; String imageUrl = getQRImageFromDB(dialcodeId); + + if(isReplaceString) { + imageUrl = StringUtils.replaceEach(imageUrl, new String[]{replaceSrcStringDIALStore}, new String[]{replaceDestStringDIALStore}); + } + if(imageUrl != null && !imageUrl.isEmpty()) syncRequest.put("imageUrl", imageUrl); messages.put(dialcodeId, syncRequest); } From 5dbfcfa7110a0cbe4b4d2b43169c737fddc65317 Mon Sep 17 00:00:00 2001 From: Jayaprakash8887 Date: Tue, 21 Feb 2023 19:10:23 +0530 Subject: [PATCH 179/222] Issue #KN-802 feat: Sync QR Image to DIAL code ES index --- .../src/main/java/org/sunbird/sync/tool/util/DialcodeSync.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform-tools/spikes/sync-tool/src/main/java/org/sunbird/sync/tool/util/DialcodeSync.java b/platform-tools/spikes/sync-tool/src/main/java/org/sunbird/sync/tool/util/DialcodeSync.java index 2b053c76eb..aa9c0ebc26 100644 --- a/platform-tools/spikes/sync-tool/src/main/java/org/sunbird/sync/tool/util/DialcodeSync.java +++ b/platform-tools/spikes/sync-tool/src/main/java/org/sunbird/sync/tool/util/DialcodeSync.java @@ -118,7 +118,7 @@ private ResultSet getDialcodesFromDB(List identifiers) { } private String getQRImageFromDB(String dialcodeId) { - String query = "SELECT url FROM " + qrImageKeyspace + "." + qrImageTable + " WHERE dialcode ='" + dialcodeId + "';"; + String query = "SELECT url FROM " + qrImageKeyspace + "." + qrImageTable + " WHERE dialcode ='" + dialcodeId + "' ALLOW FILTERING;"; Session session = CassandraConnector.getSession(); ResultSet rs = session.execute(query); while(rs.iterator().hasNext()) { From 2bad5388c5f0f09776275f1dccb0b064ec72e249 Mon Sep 17 00:00:00 2001 From: Jayaprakash8887 Date: Tue, 21 Feb 2023 19:14:19 +0530 Subject: [PATCH 180/222] Issue #KN-802 feat: Sync QR Image to DIAL code ES index --- .../roles/lp-synctool-deploy/templates/application.conf.j2 | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/ansible/roles/lp-synctool-deploy/templates/application.conf.j2 b/ansible/roles/lp-synctool-deploy/templates/application.conf.j2 index 5b590cd909..72298c8257 100644 --- a/ansible/roles/lp-synctool-deploy/templates/application.conf.j2 +++ b/ansible/roles/lp-synctool-deploy/templates/application.conf.j2 @@ -120,4 +120,7 @@ csp.migration.request.topic="{{ csp_migration_topic_name }}" csp.migration.batch.size={{ csp_migration_batch_size }} is_replace_string={{ sync_tool_is_replace_string | default('false') }} replace_src_string= "{{ sync_tool_replace_src_string | default('') }}" -replace_dest_string="{{ sync_tool_replace_dest_string | default('') }}" \ No newline at end of file +replace_dest_string="{{ sync_tool_replace_dest_string | default('') }}" + +replace_src_string_DIAL_store= "{{ sync_tool_replace_src_string | default('') }}" +replace_dest_string_DIAL_store="{{ sync_tool_replace_dest_string | default('') }}" \ No newline at end of file From 3f68c45f0532080f65c451e8cbf878b95c123a03 Mon Sep 17 00:00:00 2001 From: Jayaprakash8887 Date: Tue, 21 Feb 2023 19:19:10 +0530 Subject: [PATCH 181/222] Issue #KN-802 feat: Sync QR Image to DIAL code ES index --- .../roles/lp-synctool-deploy/templates/application.conf.j2 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ansible/roles/lp-synctool-deploy/templates/application.conf.j2 b/ansible/roles/lp-synctool-deploy/templates/application.conf.j2 index 72298c8257..aa264fcd2a 100644 --- a/ansible/roles/lp-synctool-deploy/templates/application.conf.j2 +++ b/ansible/roles/lp-synctool-deploy/templates/application.conf.j2 @@ -122,5 +122,5 @@ is_replace_string={{ sync_tool_is_replace_string | default('false') }} replace_src_string= "{{ sync_tool_replace_src_string | default('') }}" replace_dest_string="{{ sync_tool_replace_dest_string | default('') }}" -replace_src_string_DIAL_store= "{{ sync_tool_replace_src_string | default('') }}" -replace_dest_string_DIAL_store="{{ sync_tool_replace_dest_string | default('') }}" \ No newline at end of file +replace_src_string_DIAL_store= "{{ sync_tool_replace_src_string_DIAL_store | default('') }}" +replace_dest_string_DIAL_store="{{ sync_tool_replace_dest_string_DIAL_store | default('') }}" \ No newline at end of file From 725bd275d8d82bc7b4c874126cabc150d7942ead Mon Sep 17 00:00:00 2001 From: Jayaprakash8887 Date: Tue, 21 Feb 2023 19:38:33 +0530 Subject: [PATCH 182/222] Issue #KN-802 feat: Sync QR Image to DIAL code ES index --- .../main/java/org/sunbird/sync/tool/util/DialcodeSync.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/platform-tools/spikes/sync-tool/src/main/java/org/sunbird/sync/tool/util/DialcodeSync.java b/platform-tools/spikes/sync-tool/src/main/java/org/sunbird/sync/tool/util/DialcodeSync.java index aa9c0ebc26..fc58221907 100644 --- a/platform-tools/spikes/sync-tool/src/main/java/org/sunbird/sync/tool/util/DialcodeSync.java +++ b/platform-tools/spikes/sync-tool/src/main/java/org/sunbird/sync/tool/util/DialcodeSync.java @@ -89,7 +89,7 @@ public Map getDialcodesFromIds(List identifiers) { }}; String imageUrl = getQRImageFromDB(dialcodeId); - + System.out.println("Returned imageUrl: " + imageUrl); if(isReplaceString) { imageUrl = StringUtils.replaceEach(imageUrl, new String[]{replaceSrcStringDIALStore}, new String[]{replaceDestStringDIALStore}); } @@ -119,10 +119,12 @@ private ResultSet getDialcodesFromDB(List identifiers) { private String getQRImageFromDB(String dialcodeId) { String query = "SELECT url FROM " + qrImageKeyspace + "." + qrImageTable + " WHERE dialcode ='" + dialcodeId + "' ALLOW FILTERING;"; + System.out.println("getQRImageFromDB query: " + query); Session session = CassandraConnector.getSession(); ResultSet rs = session.execute(query); while(rs.iterator().hasNext()) { Row row = rs.iterator().next(); + System.out.println("getQRImageFromDB url: " + row.getString("url")); return row.getString("url"); } return ""; From 75de98d59bf476fec11747d3fc1dfca46e310fa4 Mon Sep 17 00:00:00 2001 From: Jayaprakash8887 Date: Tue, 21 Feb 2023 19:39:13 +0530 Subject: [PATCH 183/222] Issue #KN-802 feat: Sync QR Image to DIAL code ES index --- .../src/main/java/org/sunbird/sync/tool/util/DialcodeSync.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform-tools/spikes/sync-tool/src/main/java/org/sunbird/sync/tool/util/DialcodeSync.java b/platform-tools/spikes/sync-tool/src/main/java/org/sunbird/sync/tool/util/DialcodeSync.java index fc58221907..0ebb5b0925 100644 --- a/platform-tools/spikes/sync-tool/src/main/java/org/sunbird/sync/tool/util/DialcodeSync.java +++ b/platform-tools/spikes/sync-tool/src/main/java/org/sunbird/sync/tool/util/DialcodeSync.java @@ -93,7 +93,7 @@ public Map getDialcodesFromIds(List identifiers) { if(isReplaceString) { imageUrl = StringUtils.replaceEach(imageUrl, new String[]{replaceSrcStringDIALStore}, new String[]{replaceDestStringDIALStore}); } - + System.out.println("Replaced imageUrl: " + imageUrl); if(imageUrl != null && !imageUrl.isEmpty()) syncRequest.put("imageUrl", imageUrl); messages.put(dialcodeId, syncRequest); } From 405a50a77e7be407d7f7f83b60035bc96c31f6b0 Mon Sep 17 00:00:00 2001 From: Jayaprakash8887 Date: Tue, 21 Feb 2023 19:40:34 +0530 Subject: [PATCH 184/222] Issue #KN-802 feat: Sync QR Image to DIAL code ES index --- .../src/main/java/org/sunbird/sync/tool/util/DialcodeSync.java | 1 + 1 file changed, 1 insertion(+) diff --git a/platform-tools/spikes/sync-tool/src/main/java/org/sunbird/sync/tool/util/DialcodeSync.java b/platform-tools/spikes/sync-tool/src/main/java/org/sunbird/sync/tool/util/DialcodeSync.java index 0ebb5b0925..62326e79a0 100644 --- a/platform-tools/spikes/sync-tool/src/main/java/org/sunbird/sync/tool/util/DialcodeSync.java +++ b/platform-tools/spikes/sync-tool/src/main/java/org/sunbird/sync/tool/util/DialcodeSync.java @@ -98,6 +98,7 @@ public Map getDialcodesFromIds(List identifiers) { messages.put(dialcodeId, syncRequest); } System.out.println("total dialcodes fetched from cassandra: " + dialCodesFromDB); + System.out.println("messages: " + JSONUtils.serialize(messages)); return messages; } else { From aa71a0442b800e36514a6fa3fe5a37e3ef76e085 Mon Sep 17 00:00:00 2001 From: Jayaprakash8887 Date: Wed, 22 Feb 2023 10:21:14 +0530 Subject: [PATCH 185/222] Issue #KN-802 feat: Sync QR Image to DIAL code ES index --- .../java/org/sunbird/sync/tool/util/DialcodeSync.java | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/platform-tools/spikes/sync-tool/src/main/java/org/sunbird/sync/tool/util/DialcodeSync.java b/platform-tools/spikes/sync-tool/src/main/java/org/sunbird/sync/tool/util/DialcodeSync.java index 62326e79a0..646611dc4b 100644 --- a/platform-tools/spikes/sync-tool/src/main/java/org/sunbird/sync/tool/util/DialcodeSync.java +++ b/platform-tools/spikes/sync-tool/src/main/java/org/sunbird/sync/tool/util/DialcodeSync.java @@ -1,5 +1,6 @@ package org.sunbird.sync.tool.util; +import java.io.IOException; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -95,7 +96,13 @@ public Map getDialcodesFromIds(List identifiers) { } System.out.println("Replaced imageUrl: " + imageUrl); if(imageUrl != null && !imageUrl.isEmpty()) syncRequest.put("imageUrl", imageUrl); - messages.put(dialcodeId, syncRequest); + + String documentJson = ElasticSearchUtil.getDocumentAsStringById(indexName, documentType, dialcodeId); + if (documentJson != null && !documentJson.isEmpty()) { + String updatedDocString = JSONUtils.serialize(syncRequest); + ElasticSearchUtil.updateDocument(indexName, documentType, updatedDocString, dialcodeId); + System.out.println("Document Updated: " + updatedDocString); + } else messages.put(dialcodeId, syncRequest); } System.out.println("total dialcodes fetched from cassandra: " + dialCodesFromDB); System.out.println("messages: " + JSONUtils.serialize(messages)); @@ -130,4 +137,6 @@ private String getQRImageFromDB(String dialcodeId) { } return ""; } + + } From 71a0a73c753650558ca5a8ebb47ef14eabdd5cde Mon Sep 17 00:00:00 2001 From: Jayaprakash8887 Date: Wed, 22 Feb 2023 10:22:18 +0530 Subject: [PATCH 186/222] Issue #KN-802 feat: Sync QR Image to DIAL code ES index --- .../src/main/java/org/sunbird/sync/tool/util/DialcodeSync.java | 1 + 1 file changed, 1 insertion(+) diff --git a/platform-tools/spikes/sync-tool/src/main/java/org/sunbird/sync/tool/util/DialcodeSync.java b/platform-tools/spikes/sync-tool/src/main/java/org/sunbird/sync/tool/util/DialcodeSync.java index 646611dc4b..35989f5d8d 100644 --- a/platform-tools/spikes/sync-tool/src/main/java/org/sunbird/sync/tool/util/DialcodeSync.java +++ b/platform-tools/spikes/sync-tool/src/main/java/org/sunbird/sync/tool/util/DialcodeSync.java @@ -98,6 +98,7 @@ public Map getDialcodesFromIds(List identifiers) { if(imageUrl != null && !imageUrl.isEmpty()) syncRequest.put("imageUrl", imageUrl); String documentJson = ElasticSearchUtil.getDocumentAsStringById(indexName, documentType, dialcodeId); + System.out.println("Fetched Document: " + documentJson); if (documentJson != null && !documentJson.isEmpty()) { String updatedDocString = JSONUtils.serialize(syncRequest); ElasticSearchUtil.updateDocument(indexName, documentType, updatedDocString, dialcodeId); From 15660cc47976ecbd592ae1becd51c61959bd21de Mon Sep 17 00:00:00 2001 From: Jayaprakash8887 Date: Wed, 22 Feb 2023 11:11:06 +0530 Subject: [PATCH 187/222] Issue #KN-802 feat: Sync QR Image to DIAL code ES index --- .../java/org/sunbird/sync/tool/util/DialcodeSync.java | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/platform-tools/spikes/sync-tool/src/main/java/org/sunbird/sync/tool/util/DialcodeSync.java b/platform-tools/spikes/sync-tool/src/main/java/org/sunbird/sync/tool/util/DialcodeSync.java index 35989f5d8d..14b388424e 100644 --- a/platform-tools/spikes/sync-tool/src/main/java/org/sunbird/sync/tool/util/DialcodeSync.java +++ b/platform-tools/spikes/sync-tool/src/main/java/org/sunbird/sync/tool/util/DialcodeSync.java @@ -96,14 +96,7 @@ public Map getDialcodesFromIds(List identifiers) { } System.out.println("Replaced imageUrl: " + imageUrl); if(imageUrl != null && !imageUrl.isEmpty()) syncRequest.put("imageUrl", imageUrl); - - String documentJson = ElasticSearchUtil.getDocumentAsStringById(indexName, documentType, dialcodeId); - System.out.println("Fetched Document: " + documentJson); - if (documentJson != null && !documentJson.isEmpty()) { - String updatedDocString = JSONUtils.serialize(syncRequest); - ElasticSearchUtil.updateDocument(indexName, documentType, updatedDocString, dialcodeId); - System.out.println("Document Updated: " + updatedDocString); - } else messages.put(dialcodeId, syncRequest); + messages.put(dialcodeId, syncRequest); } System.out.println("total dialcodes fetched from cassandra: " + dialCodesFromDB); System.out.println("messages: " + JSONUtils.serialize(messages)); From b671304c56baea82f7ce8458b2f89d7e28b487c8 Mon Sep 17 00:00:00 2001 From: Kartheek Palla Date: Thu, 23 Feb 2023 14:43:38 +0530 Subject: [PATCH 188/222] Issue #KN-810 feat: Learning service docker file --- Dockerfile | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 Dockerfile diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000000..02f6971f19 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,5 @@ +FROM tomcat:9.0.62-jdk11-openjdk +RUN rm -rf /usr/local/tomcat/webapps/* +COPY ./platform-modules/service/target/learning-service.war /usr/local/tomcat/webapps/ +# COPY ./platform-modules/service/src/main/resources/application.conf /usr/local/tomcat/config/ +CMD ["catalina.sh", "run", "-Dconfig.file=/usr/local/tomcat/config/application.conf"] From 9d7883f7261f8f14aeb018c70292990596ba45e6 Mon Sep 17 00:00:00 2001 From: Jayaprakash8887 Date: Mon, 27 Feb 2023 12:48:23 +0530 Subject: [PATCH 189/222] Issue #KN-802 feat: Sync QR Image to DIAL code ES index --- .../lp-synctool-deploy/templates/application.conf.j2 | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/ansible/roles/lp-synctool-deploy/templates/application.conf.j2 b/ansible/roles/lp-synctool-deploy/templates/application.conf.j2 index aa264fcd2a..a751276de6 100644 --- a/ansible/roles/lp-synctool-deploy/templates/application.conf.j2 +++ b/ansible/roles/lp-synctool-deploy/templates/application.conf.j2 @@ -119,8 +119,8 @@ contentTypeToPrimaryCategory { csp.migration.request.topic="{{ csp_migration_topic_name }}" csp.migration.batch.size={{ csp_migration_batch_size }} is_replace_string={{ sync_tool_is_replace_string | default('false') }} -replace_src_string= "{{ sync_tool_replace_src_string | default('') }}" -replace_dest_string="{{ sync_tool_replace_dest_string | default('') }}" +replace_src_string= "{{ sync_tool_replace_src_string | default('CONTENT_STORAGE_BASE_PATH') }}" +replace_dest_string="{{ sync_tool_replace_dest_string | default('https://sunbirddevbbpublic.blob.core.windows.net/sunbird-content-dev') }}" -replace_src_string_DIAL_store= "{{ sync_tool_replace_src_string_DIAL_store | default('') }}" -replace_dest_string_DIAL_store="{{ sync_tool_replace_dest_string_DIAL_store | default('') }}" \ No newline at end of file +replace_src_string_DIAL_store= "{{ sync_tool_replace_src_string_DIAL_store | default('DIAL_STORAE_BASE_PATH') }}" +replace_dest_string_DIAL_store="{{ sync_tool_replace_dest_string_DIAL_store | default('https://sunbirddevbbpublic.blob.core.windows.net/dial') }}" \ No newline at end of file From b1d90d8c6e922936e30aefc0d8f1292d00e0285f Mon Sep 17 00:00:00 2001 From: Jayaprakash8887 Date: Mon, 27 Feb 2023 12:49:11 +0530 Subject: [PATCH 190/222] Issue #KN-802 feat: Sync QR Image to DIAL code ES index --- ansible/roles/lp-synctool-deploy/templates/application.conf.j2 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ansible/roles/lp-synctool-deploy/templates/application.conf.j2 b/ansible/roles/lp-synctool-deploy/templates/application.conf.j2 index a751276de6..c2a8e679fc 100644 --- a/ansible/roles/lp-synctool-deploy/templates/application.conf.j2 +++ b/ansible/roles/lp-synctool-deploy/templates/application.conf.j2 @@ -122,5 +122,5 @@ is_replace_string={{ sync_tool_is_replace_string | default('false') }} replace_src_string= "{{ sync_tool_replace_src_string | default('CONTENT_STORAGE_BASE_PATH') }}" replace_dest_string="{{ sync_tool_replace_dest_string | default('https://sunbirddevbbpublic.blob.core.windows.net/sunbird-content-dev') }}" -replace_src_string_DIAL_store= "{{ sync_tool_replace_src_string_DIAL_store | default('DIAL_STORAE_BASE_PATH') }}" +replace_src_string_DIAL_store= "{{ sync_tool_replace_src_string_DIAL_store | default('DIAL_STORAGE_BASE_PATH') }}" replace_dest_string_DIAL_store="{{ sync_tool_replace_dest_string_DIAL_store | default('https://sunbirddevbbpublic.blob.core.windows.net/dial') }}" \ No newline at end of file From 284652374a06371b75abe32bba73ccd3b6077c4e Mon Sep 17 00:00:00 2001 From: Kartheek Palla Date: Wed, 8 Mar 2023 17:33:14 +0530 Subject: [PATCH 191/222] Issue #KN-810 fix: build script added for docker build --- pipelines/build/build.sh | 11 +++++++++++ pipelines/build/learning/Dockerfile | 5 +++++ 2 files changed, 16 insertions(+) create mode 100755 pipelines/build/build.sh create mode 100644 pipelines/build/learning/Dockerfile diff --git a/pipelines/build/build.sh b/pipelines/build/build.sh new file mode 100755 index 0000000000..0e24196e8d --- /dev/null +++ b/pipelines/build/build.sh @@ -0,0 +1,11 @@ +#!/bin/bash +# Build script +set -eo pipefail + +build_tag=$1 +name=$2 +node=$3 +org=$4 + +docker build -f build/${name}/Dockerfile --label commitHash=$(git rev-parse --short HEAD) -t ${org}/${name}:${build_tag} . +echo {\"image_name\" : \"${name}\", \"image_tag\" : \"${build_tag}\", \"node_name\" : \"$node\"} > metadata.json diff --git a/pipelines/build/learning/Dockerfile b/pipelines/build/learning/Dockerfile new file mode 100644 index 0000000000..91018eab6f --- /dev/null +++ b/pipelines/build/learning/Dockerfile @@ -0,0 +1,5 @@ +FROM tomcat:9.0.62-jdk11-openjdk +RUN rm -rf /usr/local/tomcat/webapps/* +COPY platform-modules/service/target/learning-service.war /usr/local/tomcat/webapps/ +# COPY ./platform-modules/service/src/main/resources/application.conf /usr/local/tomcat/config/ +CMD ["catalina.sh", "run", "-Dconfig.file=/usr/local/tomcat/config/application.conf"] From 33433067446a2bd75f3d4692d41c746d47d70f31 Mon Sep 17 00:00:00 2001 From: Kartheek Palla Date: Wed, 8 Mar 2023 17:35:04 +0530 Subject: [PATCH 192/222] Issue #KN-810 fix: build script added for docker build --- pipelines/build/learning/Jenkinsfile | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pipelines/build/learning/Jenkinsfile b/pipelines/build/learning/Jenkinsfile index 4df6e90574..b282039653 100644 --- a/pipelines/build/learning/Jenkinsfile +++ b/pipelines/build/learning/Jenkinsfile @@ -62,6 +62,11 @@ node() { currentBuild.result = "SUCCESS" currentBuild.description = "Artifact: ${artifact_version}, Public: ${params.github_release_tag}" } + + stage('Package') { + sh('chmod 777 build/build.sh') + sh("build/build.sh ${build_tag} ${"learning"} ${env.NODE_NAME} ${hub_org}") + } } catch (err) { currentBuild.result = "FAILURE" From c7a64f7d7e6416da53374bc3adba4057ed60588a Mon Sep 17 00:00:00 2001 From: Kartheek Palla Date: Wed, 8 Mar 2023 17:42:28 +0530 Subject: [PATCH 193/222] Issue #KN-810 fix: build script added for docker build --- pipelines/build/learning/Jenkinsfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pipelines/build/learning/Jenkinsfile b/pipelines/build/learning/Jenkinsfile index b282039653..00dc4cea28 100644 --- a/pipelines/build/learning/Jenkinsfile +++ b/pipelines/build/learning/Jenkinsfile @@ -64,8 +64,8 @@ node() { } stage('Package') { - sh('chmod 777 build/build.sh') - sh("build/build.sh ${build_tag} ${"learning"} ${env.NODE_NAME} ${hub_org}") + sh('chmod 777 pipelines/build/build.sh') + sh("pipelines/build/build.sh ${build_tag} ${"learning"} ${env.NODE_NAME} ${hub_org}") } } catch (err) { From 4c4a6c621f6b31d9a98e5c19c57d6110c0ef3981 Mon Sep 17 00:00:00 2001 From: Kartheek Palla Date: Wed, 8 Mar 2023 17:46:05 +0530 Subject: [PATCH 194/222] Issue #KN-810 fix: build script added for docker build --- pipelines/build/build.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pipelines/build/build.sh b/pipelines/build/build.sh index 0e24196e8d..269930c4ae 100755 --- a/pipelines/build/build.sh +++ b/pipelines/build/build.sh @@ -7,5 +7,5 @@ name=$2 node=$3 org=$4 -docker build -f build/${name}/Dockerfile --label commitHash=$(git rev-parse --short HEAD) -t ${org}/${name}:${build_tag} . +docker build -f pipelines/build/${name}/Dockerfile --label commitHash=$(git rev-parse --short HEAD) -t ${org}/${name}:${build_tag} . echo {\"image_name\" : \"${name}\", \"image_tag\" : \"${build_tag}\", \"node_name\" : \"$node\"} > metadata.json From f0b97aa5dca6e90069b33a40930cd5257c4f06a2 Mon Sep 17 00:00:00 2001 From: Kartheek Palla Date: Wed, 8 Mar 2023 18:04:48 +0530 Subject: [PATCH 195/222] Issue #KN-810 fix: build script added for docker build --- pipelines/build/learning/Jenkinsfile | 51 +++++++++++++++------------- 1 file changed, 28 insertions(+), 23 deletions(-) diff --git a/pipelines/build/learning/Jenkinsfile b/pipelines/build/learning/Jenkinsfile index 00dc4cea28..32b9886d98 100644 --- a/pipelines/build/learning/Jenkinsfile +++ b/pipelines/build/learning/Jenkinsfile @@ -38,35 +38,40 @@ node() { } - stage('Archive artifacts'){ - if(params.profile_id == "platform-services"){ - sh """ - mkdir lp_artifacts - cp platform-modules/service/target/learning-service.war lp_artifacts - cp platform-tools/spikes/sync-tool/target/sync-tool*.jar lp_artifacts - cp platform-tools/spikes/content-tool/target/content-tool-*.jar lp_artifacts - zip -j lp_artifacts.zip:${artifact_version} lp_artifacts/* - """ - } - else { - sh """ - mkdir lp_artifacts - cp platform-modules/service/target/learning-service.war lp_artifacts - zip -j lp_artifacts.zip:${artifact_version} lp_artifacts/* - """ - } + // stage('Archive artifacts'){ + // if(params.profile_id == "platform-services"){ + // sh """ + // mkdir lp_artifacts + // cp platform-modules/service/target/learning-service.war lp_artifacts + // cp platform-tools/spikes/sync-tool/target/sync-tool*.jar lp_artifacts + // cp platform-tools/spikes/content-tool/target/content-tool-*.jar lp_artifacts + // zip -j lp_artifacts.zip:${artifact_version} lp_artifacts/* + // """ + // } + // else { + // sh """ + // mkdir lp_artifacts + // cp platform-modules/service/target/learning-service.war lp_artifacts + // zip -j lp_artifacts.zip:${artifact_version} lp_artifacts/* + // """ + // } - archiveArtifacts artifacts: "lp_artifacts.zip:${artifact_version}", fingerprint: true, onlyIfSuccessful: true - sh """echo {\\"artifact_name\\" : \\"lp_artifacts.zip\\", \\"artifact_version\\" : \\"${artifact_version}\\", \\"node_name\\" : \\"${env.NODE_NAME}\\"} > metadata.json""" - archiveArtifacts artifacts: 'metadata.json', onlyIfSuccessful: true - currentBuild.result = "SUCCESS" - currentBuild.description = "Artifact: ${artifact_version}, Public: ${params.github_release_tag}" - } + // archiveArtifacts artifacts: "lp_artifacts.zip:${artifact_version}", fingerprint: true, onlyIfSuccessful: true + // sh """echo {\\"artifact_name\\" : \\"lp_artifacts.zip\\", \\"artifact_version\\" : \\"${artifact_version}\\", \\"node_name\\" : \\"${env.NODE_NAME}\\"} > metadata.json""" + // archiveArtifacts artifacts: 'metadata.json', onlyIfSuccessful: true + // currentBuild.result = "SUCCESS" + // currentBuild.description = "Artifact: ${artifact_version}, Public: ${params.github_release_tag}" + // } stage('Package') { sh('chmod 777 pipelines/build/build.sh') sh("pipelines/build/build.sh ${build_tag} ${"learning"} ${env.NODE_NAME} ${hub_org}") } + + stage('ArchiveArtifacts') { + archiveArtifacts "metadata.json" + currentBuild.description = "${build_tag}" + } } catch (err) { currentBuild.result = "FAILURE" From 436855bd1242953d76d724823ddb5ccb5ca10cee Mon Sep 17 00:00:00 2001 From: Kartheek Palla Date: Wed, 8 Mar 2023 18:11:04 +0530 Subject: [PATCH 196/222] Issue #KN-810 fix: build script added for docker build --- pipelines/build/learning/Jenkinsfile | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pipelines/build/learning/Jenkinsfile b/pipelines/build/learning/Jenkinsfile index 32b9886d98..aacafa8aa7 100644 --- a/pipelines/build/learning/Jenkinsfile +++ b/pipelines/build/learning/Jenkinsfile @@ -12,6 +12,8 @@ node() { cleanWs() checkout scm commit_hash = sh(script: 'git rev-parse --short HEAD', returnStdout: true).trim() + build_tag = sh(script: "echo " + params.github_release_tag.split('/')[-1] + "_" + commit_hash + "_" + env.BUILD_NUMBER, returnStdout: true).trim() + echo "build_tag: " + build_tag artifact_version = sh(script: "echo " + params.github_release_tag.split('/')[-1] + "_" + commit_hash + "_" + env.BUILD_NUMBER, returnStdout: true).trim() echo "artifact_version: "+ artifact_version } @@ -30,6 +32,8 @@ node() { } stage('Build') { + env.NODE_ENV = "build" + print "Environment will be : ${env.NODE_ENV}" sh 'mvn clean install -DskipTests -P ${profile_id} -T10' } From 143597f095f0a62879b83ffc128b981b79222ca4 Mon Sep 17 00:00:00 2001 From: Kartheek Palla Date: Wed, 29 Mar 2023 23:16:32 +0530 Subject: [PATCH 197/222] Issue #KN-810 feat: Learning service docker file --- Dockerfile | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 02f6971f19..b44892cd1e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,6 @@ FROM tomcat:9.0.62-jdk11-openjdk RUN rm -rf /usr/local/tomcat/webapps/* COPY ./platform-modules/service/target/learning-service.war /usr/local/tomcat/webapps/ +ENV JAVA_OPTS -Dconfig.file=/usr/local/tomcat/config/application.conf # COPY ./platform-modules/service/src/main/resources/application.conf /usr/local/tomcat/config/ -CMD ["catalina.sh", "run", "-Dconfig.file=/usr/local/tomcat/config/application.conf"] +CMD ["catalina.sh", "run"] From b8dd7b933ad92f9ff95751e2bb45bae707099800 Mon Sep 17 00:00:00 2001 From: Kartheek Palla Date: Thu, 30 Mar 2023 11:03:23 +0530 Subject: [PATCH 198/222] Issue #KN-810 feat: Learning service docker file --- Dockerfile | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index b44892cd1e..03ae6aa64d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,7 @@ FROM tomcat:9.0.62-jdk11-openjdk RUN rm -rf /usr/local/tomcat/webapps/* COPY ./platform-modules/service/target/learning-service.war /usr/local/tomcat/webapps/ -ENV JAVA_OPTS -Dconfig.file=/usr/local/tomcat/config/application.conf # COPY ./platform-modules/service/src/main/resources/application.conf /usr/local/tomcat/config/ -CMD ["catalina.sh", "run"] +# CMD ["catalina.sh", "run", "-Dconfig.file=/usr/local/tomcat/config/application.conf"] +# CMD '/usr/bin/java' -Djava.util.logging.config.file=/usr/share/tomcat/conf/logging.properties -Djava.util.logging.manager=org.apache.juli.ClassLoaderLogManager -Xms2048m -Xmx5096m -Dconfig.file=/data/properties/application.conf -XX:NewRatio=3 -XX:+CMSClassUnloadingEnabled -XX:+UseConcMarkSweepGC -XX:+CMSParallelRemarkEnabled -Dfile.encoding=UTF8 -Djdk.tls.ephemeralDHKeySize=2048 -Djava.endorsed.dirs=/usr/share/tomcat/endorsed -classpath /usr/share/tomcat/bin/bootstrap.jar:/usr/share/tomcat/bin/tomcat-juli.jar -Dcatalina.base=/usr/share/tomcat -Dcatalina.home=/usr/share/tomcat -Djava.io.tmpdir=/usr/share/tomcat/temp org.apache.catalina.startup.Bootstrap start +CMD '/usr/local/openjdk-11/bin/java' -Djava.util.logging.config.file=/usr/local/tomcat/conf/logging.properties -Djava.util.logging.manager=org.apache.juli.ClassLoaderLogManager -Djdk.tls.ephemeralDHKeySize=2048 -Djava.protocol.handler.pkgs=org.apache.catalina.webresources -Dorg.apache.catalina.security.SecurityListener.UMASK=0027 -Dignore.endorsed.dirs= -classpath /usr/local/tomcat/bin/bootstrap.jar:/usr/local/tomcat/bin/tomcat-juli.jar -Dcatalina.base=/usr/local/tomcat -Dcatalina.home=/usr/local/tomcat -Djava.io.tmpdir=/usr/local/tomcat/temp -Dconfig.file=/usr/local/tomcat/config/application.conf org.apache.catalina.startup.Bootstrap start From e14a3a9eaeccb41b6751544565ed84a0f7e815c5 Mon Sep 17 00:00:00 2001 From: Kartheek Palla Date: Thu, 30 Mar 2023 15:02:56 +0530 Subject: [PATCH 199/222] Issue #KN-810 feat: Learning service docker file --- Dockerfile | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 03ae6aa64d..0722fe5e89 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,7 +1,9 @@ FROM tomcat:9.0.62-jdk11-openjdk RUN rm -rf /usr/local/tomcat/webapps/* COPY ./platform-modules/service/target/learning-service.war /usr/local/tomcat/webapps/ +ENV JAVA_OPTS -Dconfig.file=/usr/local/tomcat/config/application.conf +CMD ["catalina.sh", "run"] # COPY ./platform-modules/service/src/main/resources/application.conf /usr/local/tomcat/config/ # CMD ["catalina.sh", "run", "-Dconfig.file=/usr/local/tomcat/config/application.conf"] # CMD '/usr/bin/java' -Djava.util.logging.config.file=/usr/share/tomcat/conf/logging.properties -Djava.util.logging.manager=org.apache.juli.ClassLoaderLogManager -Xms2048m -Xmx5096m -Dconfig.file=/data/properties/application.conf -XX:NewRatio=3 -XX:+CMSClassUnloadingEnabled -XX:+UseConcMarkSweepGC -XX:+CMSParallelRemarkEnabled -Dfile.encoding=UTF8 -Djdk.tls.ephemeralDHKeySize=2048 -Djava.endorsed.dirs=/usr/share/tomcat/endorsed -classpath /usr/share/tomcat/bin/bootstrap.jar:/usr/share/tomcat/bin/tomcat-juli.jar -Dcatalina.base=/usr/share/tomcat -Dcatalina.home=/usr/share/tomcat -Djava.io.tmpdir=/usr/share/tomcat/temp org.apache.catalina.startup.Bootstrap start -CMD '/usr/local/openjdk-11/bin/java' -Djava.util.logging.config.file=/usr/local/tomcat/conf/logging.properties -Djava.util.logging.manager=org.apache.juli.ClassLoaderLogManager -Djdk.tls.ephemeralDHKeySize=2048 -Djava.protocol.handler.pkgs=org.apache.catalina.webresources -Dorg.apache.catalina.security.SecurityListener.UMASK=0027 -Dignore.endorsed.dirs= -classpath /usr/local/tomcat/bin/bootstrap.jar:/usr/local/tomcat/bin/tomcat-juli.jar -Dcatalina.base=/usr/local/tomcat -Dcatalina.home=/usr/local/tomcat -Djava.io.tmpdir=/usr/local/tomcat/temp -Dconfig.file=/usr/local/tomcat/config/application.conf org.apache.catalina.startup.Bootstrap start +#CMD '/usr/local/openjdk-11/bin/java' -Djava.util.logging.config.file=/usr/local/tomcat/conf/logging.properties -Djava.util.logging.manager=org.apache.juli.ClassLoaderLogManager -Djdk.tls.ephemeralDHKeySize=2048 -Djava.protocol.handler.pkgs=org.apache.catalina.webresources -Dorg.apache.catalina.security.SecurityListener.UMASK=0027 -Dignore.endorsed.dirs= -classpath /usr/local/tomcat/bin/bootstrap.jar:/usr/local/tomcat/bin/tomcat-juli.jar -Dcatalina.base=/usr/local/tomcat -Dcatalina.home=/usr/local/tomcat -Djava.io.tmpdir=/usr/local/tomcat/temp -Dconfig.file=/usr/local/tomcat/config/application.conf org.apache.catalina.startup.Bootstrap start From af89597d62ad16f980bd3655d2c7883901adcae9 Mon Sep 17 00:00:00 2001 From: Kartheek Palla Date: Thu, 30 Mar 2023 15:15:32 +0530 Subject: [PATCH 200/222] Issue #KN-810 feat: Learning service docker file --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 0722fe5e89..c076a47e6b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,7 +1,7 @@ FROM tomcat:9.0.62-jdk11-openjdk RUN rm -rf /usr/local/tomcat/webapps/* COPY ./platform-modules/service/target/learning-service.war /usr/local/tomcat/webapps/ -ENV JAVA_OPTS -Dconfig.file=/usr/local/tomcat/config/application.conf +# ENV JAVA_OPTS -Dconfig.file=/usr/local/tomcat/config/application.conf CMD ["catalina.sh", "run"] # COPY ./platform-modules/service/src/main/resources/application.conf /usr/local/tomcat/config/ # CMD ["catalina.sh", "run", "-Dconfig.file=/usr/local/tomcat/config/application.conf"] From e6b3adfd05cf2d8d78a0b062b02141bca426e42e Mon Sep 17 00:00:00 2001 From: Kartheek Palla Date: Thu, 30 Mar 2023 15:44:30 +0530 Subject: [PATCH 201/222] Issue #KN-810 feat: Learning service docker file --- Dockerfile | 1 + 1 file changed, 1 insertion(+) diff --git a/Dockerfile b/Dockerfile index c076a47e6b..bac295805d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,7 @@ FROM tomcat:9.0.62-jdk11-openjdk RUN rm -rf /usr/local/tomcat/webapps/* COPY ./platform-modules/service/target/learning-service.war /usr/local/tomcat/webapps/ +RUN sed -i 's/org.apache.catalina.startup.Bootstrap/-Dconfig.file=\/usr\/local\/tomcat\/config\/application.conf org.apache.catalina.startup.Bootstrap/g' /usr/local/tomcat/bin/catalina.sh # ENV JAVA_OPTS -Dconfig.file=/usr/local/tomcat/config/application.conf CMD ["catalina.sh", "run"] # COPY ./platform-modules/service/src/main/resources/application.conf /usr/local/tomcat/config/ From 746ba4051d1da7293231d6d0ddfb7ff95a730c92 Mon Sep 17 00:00:00 2001 From: Kartheek Palla Date: Thu, 30 Mar 2023 15:54:00 +0530 Subject: [PATCH 202/222] Issue #KN-810 feat: env variable added --- pipelines/build/learning/Dockerfile | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pipelines/build/learning/Dockerfile b/pipelines/build/learning/Dockerfile index 91018eab6f..86bfbf142d 100644 --- a/pipelines/build/learning/Dockerfile +++ b/pipelines/build/learning/Dockerfile @@ -2,4 +2,5 @@ FROM tomcat:9.0.62-jdk11-openjdk RUN rm -rf /usr/local/tomcat/webapps/* COPY platform-modules/service/target/learning-service.war /usr/local/tomcat/webapps/ # COPY ./platform-modules/service/src/main/resources/application.conf /usr/local/tomcat/config/ -CMD ["catalina.sh", "run", "-Dconfig.file=/usr/local/tomcat/config/application.conf"] +ENV JAVA_OPTS -Dconfig.file=/usr/local/tomcat/config/application.conf +CMD ["catalina.sh", "run"] From 3ed020fbcdda802cdec388de7e18fde3a5c2834c Mon Sep 17 00:00:00 2001 From: Kartheek Palla Date: Thu, 30 Mar 2023 16:01:54 +0530 Subject: [PATCH 203/222] Issue #KN-810 feat: remove unused docker file --- Dockerfile | 10 ---------- 1 file changed, 10 deletions(-) delete mode 100644 Dockerfile diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index bac295805d..0000000000 --- a/Dockerfile +++ /dev/null @@ -1,10 +0,0 @@ -FROM tomcat:9.0.62-jdk11-openjdk -RUN rm -rf /usr/local/tomcat/webapps/* -COPY ./platform-modules/service/target/learning-service.war /usr/local/tomcat/webapps/ -RUN sed -i 's/org.apache.catalina.startup.Bootstrap/-Dconfig.file=\/usr\/local\/tomcat\/config\/application.conf org.apache.catalina.startup.Bootstrap/g' /usr/local/tomcat/bin/catalina.sh -# ENV JAVA_OPTS -Dconfig.file=/usr/local/tomcat/config/application.conf -CMD ["catalina.sh", "run"] -# COPY ./platform-modules/service/src/main/resources/application.conf /usr/local/tomcat/config/ -# CMD ["catalina.sh", "run", "-Dconfig.file=/usr/local/tomcat/config/application.conf"] -# CMD '/usr/bin/java' -Djava.util.logging.config.file=/usr/share/tomcat/conf/logging.properties -Djava.util.logging.manager=org.apache.juli.ClassLoaderLogManager -Xms2048m -Xmx5096m -Dconfig.file=/data/properties/application.conf -XX:NewRatio=3 -XX:+CMSClassUnloadingEnabled -XX:+UseConcMarkSweepGC -XX:+CMSParallelRemarkEnabled -Dfile.encoding=UTF8 -Djdk.tls.ephemeralDHKeySize=2048 -Djava.endorsed.dirs=/usr/share/tomcat/endorsed -classpath /usr/share/tomcat/bin/bootstrap.jar:/usr/share/tomcat/bin/tomcat-juli.jar -Dcatalina.base=/usr/share/tomcat -Dcatalina.home=/usr/share/tomcat -Djava.io.tmpdir=/usr/share/tomcat/temp org.apache.catalina.startup.Bootstrap start -#CMD '/usr/local/openjdk-11/bin/java' -Djava.util.logging.config.file=/usr/local/tomcat/conf/logging.properties -Djava.util.logging.manager=org.apache.juli.ClassLoaderLogManager -Djdk.tls.ephemeralDHKeySize=2048 -Djava.protocol.handler.pkgs=org.apache.catalina.webresources -Dorg.apache.catalina.security.SecurityListener.UMASK=0027 -Dignore.endorsed.dirs= -classpath /usr/local/tomcat/bin/bootstrap.jar:/usr/local/tomcat/bin/tomcat-juli.jar -Dcatalina.base=/usr/local/tomcat -Dcatalina.home=/usr/local/tomcat -Djava.io.tmpdir=/usr/local/tomcat/temp -Dconfig.file=/usr/local/tomcat/config/application.conf org.apache.catalina.startup.Bootstrap start From de86e70b2c5c8cf137717ee52847c4acbca5bc86 Mon Sep 17 00:00:00 2001 From: Kartheek Palla Date: Thu, 30 Mar 2023 16:03:52 +0530 Subject: [PATCH 204/222] Issue #KN-810 fix: remove unused code --- pipelines/build/learning/Jenkinsfile | 25 ------------------------- 1 file changed, 25 deletions(-) diff --git a/pipelines/build/learning/Jenkinsfile b/pipelines/build/learning/Jenkinsfile index aacafa8aa7..37aa3acf52 100644 --- a/pipelines/build/learning/Jenkinsfile +++ b/pipelines/build/learning/Jenkinsfile @@ -42,31 +42,6 @@ node() { } - // stage('Archive artifacts'){ - // if(params.profile_id == "platform-services"){ - // sh """ - // mkdir lp_artifacts - // cp platform-modules/service/target/learning-service.war lp_artifacts - // cp platform-tools/spikes/sync-tool/target/sync-tool*.jar lp_artifacts - // cp platform-tools/spikes/content-tool/target/content-tool-*.jar lp_artifacts - // zip -j lp_artifacts.zip:${artifact_version} lp_artifacts/* - // """ - // } - // else { - // sh """ - // mkdir lp_artifacts - // cp platform-modules/service/target/learning-service.war lp_artifacts - // zip -j lp_artifacts.zip:${artifact_version} lp_artifacts/* - // """ - // } - - // archiveArtifacts artifacts: "lp_artifacts.zip:${artifact_version}", fingerprint: true, onlyIfSuccessful: true - // sh """echo {\\"artifact_name\\" : \\"lp_artifacts.zip\\", \\"artifact_version\\" : \\"${artifact_version}\\", \\"node_name\\" : \\"${env.NODE_NAME}\\"} > metadata.json""" - // archiveArtifacts artifacts: 'metadata.json', onlyIfSuccessful: true - // currentBuild.result = "SUCCESS" - // currentBuild.description = "Artifact: ${artifact_version}, Public: ${params.github_release_tag}" - // } - stage('Package') { sh('chmod 777 pipelines/build/build.sh') sh("pipelines/build/build.sh ${build_tag} ${"learning"} ${env.NODE_NAME} ${hub_org}") From 04a3287fa8fd7cfc18fddade9adc96bcc4ec0ffc Mon Sep 17 00:00:00 2001 From: anilgupta Date: Fri, 5 May 2023 15:35:38 +0530 Subject: [PATCH 205/222] Issue #KN-522 chore: Adding the missing topic for qrimage.request --- ansible/roles/setup-kafka/defaults/main.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/ansible/roles/setup-kafka/defaults/main.yml b/ansible/roles/setup-kafka/defaults/main.yml index cee780aab3..410bf4c374 100644 --- a/ansible/roles/setup-kafka/defaults/main.yml +++ b/ansible/roles/setup-kafka/defaults/main.yml @@ -162,6 +162,10 @@ processing_kafka_topics: - name: republish.events.skipped num_of_partitions: 1 replication_factor: 1 + - name: qrimage.request + num_of_partitions: 1 + replication_factor: 1 + processing_kafka_overriden_topics: - name: telemetry.raw @@ -316,5 +320,8 @@ processing_kafka_overriden_topics: retention_time: 604800000 replication_factor: 1 - name: republish.events.skipped + retention_time: 604800000 + replication_factor: 1 + - name: qrimage.request retention_time: 604800000 replication_factor: 1 \ No newline at end of file From bdb56f6172f5f446189c72dba8a4f4f5ad81887f Mon Sep 17 00:00:00 2001 From: Kartheek Palla Date: Wed, 21 Jun 2023 18:23:45 +0530 Subject: [PATCH 206/222] Issue KN-10 fix: disable content publish, unlisted publish, reject and review APIs --- pipelines/build/learning/Jenkinsfile | 35 +++++++---- .../learning/Jenkinsfile_containerization | 63 +++++++++++++++++++ .../controller/ContentV3Controller.java | 36 +++++------ 3 files changed, 104 insertions(+), 30 deletions(-) create mode 100644 pipelines/build/learning/Jenkinsfile_containerization diff --git a/pipelines/build/learning/Jenkinsfile b/pipelines/build/learning/Jenkinsfile index 37aa3acf52..e57a054a7d 100644 --- a/pipelines/build/learning/Jenkinsfile +++ b/pipelines/build/learning/Jenkinsfile @@ -12,8 +12,6 @@ node() { cleanWs() checkout scm commit_hash = sh(script: 'git rev-parse --short HEAD', returnStdout: true).trim() - build_tag = sh(script: "echo " + params.github_release_tag.split('/')[-1] + "_" + commit_hash + "_" + env.BUILD_NUMBER, returnStdout: true).trim() - echo "build_tag: " + build_tag artifact_version = sh(script: "echo " + params.github_release_tag.split('/')[-1] + "_" + commit_hash + "_" + env.BUILD_NUMBER, returnStdout: true).trim() echo "artifact_version: "+ artifact_version } @@ -32,8 +30,6 @@ node() { } stage('Build') { - env.NODE_ENV = "build" - print "Environment will be : ${env.NODE_ENV}" sh 'mvn clean install -DskipTests -P ${profile_id} -T10' } @@ -42,14 +38,29 @@ node() { } - stage('Package') { - sh('chmod 777 pipelines/build/build.sh') - sh("pipelines/build/build.sh ${build_tag} ${"learning"} ${env.NODE_NAME} ${hub_org}") - } + stage('Archive artifacts'){ + if(params.profile_id == "platform-services"){ + sh """ + mkdir lp_artifacts + cp platform-modules/service/target/learning-service.war lp_artifacts + cp platform-tools/spikes/sync-tool/target/sync-tool*.jar lp_artifacts + cp platform-tools/spikes/content-tool/target/content-tool-*.jar lp_artifacts + zip -j lp_artifacts.zip:${artifact_version} lp_artifacts/* + """ + } + else { + sh """ + mkdir lp_artifacts + cp platform-modules/service/target/learning-service.war lp_artifacts + zip -j lp_artifacts.zip:${artifact_version} lp_artifacts/* + """ + } - stage('ArchiveArtifacts') { - archiveArtifacts "metadata.json" - currentBuild.description = "${build_tag}" + archiveArtifacts artifacts: "lp_artifacts.zip:${artifact_version}", fingerprint: true, onlyIfSuccessful: true + sh """echo {\\"artifact_name\\" : \\"lp_artifacts.zip\\", \\"artifact_version\\" : \\"${artifact_version}\\", \\"node_name\\" : \\"${env.NODE_NAME}\\"} > metadata.json""" + archiveArtifacts artifacts: 'metadata.json', onlyIfSuccessful: true + currentBuild.result = "SUCCESS" + currentBuild.description = "Artifact: ${artifact_version}, Public: ${params.github_release_tag}" } } catch (err) { @@ -60,4 +71,4 @@ node() { slack_notify(currentBuild.result) email_notify() } -} +} \ No newline at end of file diff --git a/pipelines/build/learning/Jenkinsfile_containerization b/pipelines/build/learning/Jenkinsfile_containerization new file mode 100644 index 0000000000..9cea000f79 --- /dev/null +++ b/pipelines/build/learning/Jenkinsfile_containerization @@ -0,0 +1,63 @@ +@Library('deploy-conf') _ +node() { + try { + String ANSI_GREEN = "\u001B[32m" + String ANSI_NORMAL = "\u001B[0m" + String ANSI_BOLD = "\u001B[1m" + String ANSI_RED = "\u001B[31m" + String ANSI_YELLOW = "\u001B[33m" + + ansiColor('xterm') { + stage('Checkout') { + cleanWs() + checkout scm + commit_hash = sh(script: 'git rev-parse --short HEAD', returnStdout: true).trim() + build_tag = sh(script: "echo " + params.github_release_tag.split('/')[-1] + "_" + commit_hash + "_" + env.BUILD_NUMBER, returnStdout: true).trim() + echo "build_tag: " + build_tag + artifact_version = sh(script: "echo " + params.github_release_tag.split('/')[-1] + "_" + commit_hash + "_" + env.BUILD_NUMBER, returnStdout: true).trim() + echo "artifact_version: "+ artifact_version + } + } + + stage('Pre-Build') { + sh """ + java -version + rm -rf /data/logs/* + rm -rf /data/graphDB/* + rm -rf /data/testgraphDB/* + rm -rf /data/testGraphDB/* + vim -esnc '%s/dialcode.es_conn_info="localhost:9200"/dialcode.es_conn_info="10.6.0.11:9200"/g|:wq' platform-core/unit-tests/src/test/resources/application.conf + vim -esnc '%s/search.es_conn_info="localhost:9200"/search.es_conn_info="10.6.0.11:9200"/g|:wq' platform-core/unit-tests/src/test/resources/application.conf + """ + } + + stage('Build') { + env.NODE_ENV = "build" + print "Environment will be : ${env.NODE_ENV}" + sh 'mvn clean install -DskipTests -P ${profile_id} -T10' + } + + stage('Post_Build-Action') { + jacoco exclusionPattern: '**/common/**,**/dto/**,**/enums/**,**/pipeline/**,**/servlet/**,**/interceptor/**,**/batch/**,**/models/**,**/model/**,**/EnrichActor*.class,**/language/controller/**,**/wordchain/**,**/importer/**,**/Base**,**/ControllerUtil**,**/Indowordnet**,**/Import**' + + } + + stage('Package') { + sh('chmod 777 pipelines/build/build.sh') + sh("pipelines/build/build.sh ${build_tag} ${"learning"} ${env.NODE_NAME} ${hub_org}") + } + + stage('ArchiveArtifacts') { + archiveArtifacts "metadata.json" + currentBuild.description = "${build_tag}" + } + } + catch (err) { + currentBuild.result = "FAILURE" + throw err + } + finally { + slack_notify(currentBuild.result) + email_notify() + } +} diff --git a/platform-modules/manager/src/main/java/org/sunbird/taxonomy/controller/ContentV3Controller.java b/platform-modules/manager/src/main/java/org/sunbird/taxonomy/controller/ContentV3Controller.java index 0495d3d738..f2fa149d18 100755 --- a/platform-modules/manager/src/main/java/org/sunbird/taxonomy/controller/ContentV3Controller.java +++ b/platform-modules/manager/src/main/java/org/sunbird/taxonomy/controller/ContentV3Controller.java @@ -184,8 +184,8 @@ public ResponseEntity bundle(@RequestBody Map map) { * Set. */ @SuppressWarnings("unchecked") - @RequestMapping(value = { "/publish/{id:.+}", "/public/publish/{id:.+}" }, method = RequestMethod.POST) - @ResponseBody +// @RequestMapping(value = { "/publish/{id:.+}", "/public/publish/{id:.+}" }, method = RequestMethod.POST) +// @ResponseBody public ResponseEntity publish(@PathVariable(value = "id") String contentId, @RequestBody Map map) { String apiId = "ekstep.learning.content.publish"; @@ -224,8 +224,8 @@ public ResponseEntity publish(@PathVariable(value = "id") String conte * Set. */ @SuppressWarnings("unchecked") - @RequestMapping(value = "/unlisted/publish/{id:.+}", method = RequestMethod.POST) - @ResponseBody +// @RequestMapping(value = "/unlisted/publish/{id:.+}", method = RequestMethod.POST) +// @ResponseBody public ResponseEntity publishUnlisted(@PathVariable(value = "id") String contentId, @RequestBody Map map) { String apiId = "ekstep.learning.content.unlisted.publish"; @@ -262,8 +262,8 @@ public ResponseEntity publishUnlisted(@PathVariable(value = "id") Stri * The Content Id which needs to be published. * @return The Response entity with Content Id in its Result Set. */ - @RequestMapping(value = "/review/{id:.+}", method = RequestMethod.POST) - @ResponseBody +// @RequestMapping(value = "/review/{id:.+}", method = RequestMethod.POST) +// @ResponseBody public ResponseEntity review(@PathVariable(value = "id") String contentId, @RequestBody Map map) { String apiId = "ekstep.learning.content.review"; @@ -446,8 +446,8 @@ public ResponseEntity syncHierarchy(@PathVariable(value = "id") String * @param requestMap * @return */ - @RequestMapping(value = "/dialcode/link", method = RequestMethod.POST) - @ResponseBody +// @RequestMapping(value = "/dialcode/link", method = RequestMethod.POST) +// @ResponseBody public ResponseEntity linkDialCode(@RequestBody Map requestMap, @RequestHeader(value = CHANNEL_ID, required = true) String channelId) { String apiId = "ekstep.content.dialcode.link"; @@ -470,8 +470,8 @@ public ResponseEntity linkDialCode(@RequestBody Map re * The Content Id for whom DIAL Codes have to be reserved * @return */ - @RequestMapping(value = "/dialcode/reserve/{id:.+}", method = RequestMethod.POST) - @ResponseBody +// @RequestMapping(value = "/dialcode/reserve/{id:.+}", method = RequestMethod.POST) +// @ResponseBody public ResponseEntity reserveDialCode( @PathVariable(value = "id") String contentId, @RequestBody Map requestMap, @@ -496,8 +496,8 @@ public ResponseEntity reserveDialCode( * The Content Id of the Textbook from which DIAL Codes have to be released * @return The Response Entity with list of Released QR Codes */ - @RequestMapping(value="/dialcode/release/{id}", method = RequestMethod.PATCH) - @ResponseBody +// @RequestMapping(value="/dialcode/release/{id}", method = RequestMethod.PATCH) +// @ResponseBody public ResponseEntity releaseDialcodes(@PathVariable(value="id") String contentId, @RequestHeader(value = CHANNEL_ID) String channelId) { String apiId = "ekstep.learning.content.dialcode.release"; @@ -545,8 +545,8 @@ protected String getAPIVersion() { return API_VERSION_3; } - @RequestMapping(value="/retire/{id:.+}", method = RequestMethod.DELETE) - @ResponseBody +// @RequestMapping(value="/retire/{id:.+}", method = RequestMethod.DELETE) +// @ResponseBody public ResponseEntity retire(@PathVariable(value = "id") String contentId) { String apiId = "ekstep.content.retire"; TelemetryManager.log("Retiring content | Content Id : " + contentId); @@ -583,8 +583,8 @@ public ResponseEntity acceptFlag(@PathVariable(value = "id") String co * @return The Response entity with Content Id and Version Key in its Result * Set. */ - @RequestMapping(value="/flag/reject/{id:.+}", method = RequestMethod.POST) - @ResponseBody +// @RequestMapping(value="/flag/reject/{id:.+}", method = RequestMethod.POST) +// @ResponseBody public ResponseEntity rejectFlag(@PathVariable(value = "id") String contentId){ String apiId = "ekstep.learning.content.rejectFlag"; TelemetryManager.log("Reject flagged content | Content Id : " + contentId); @@ -650,8 +650,8 @@ public ResponseEntity discard(@PathVariable(value = "id") String conte * @param contentId * @return */ - @RequestMapping(value = "/reject/{id:.+}", method = RequestMethod.POST) - @ResponseBody +// @RequestMapping(value = "/reject/{id:.+}", method = RequestMethod.POST) +// @ResponseBody public ResponseEntity rejectContent(@PathVariable(value = "id") String contentId, @RequestBody Map requestMap) { String apiId = "ekstep.learning.content.reject"; From f5f612775b725a8f2d2cd48b4e0132147ecc6236 Mon Sep 17 00:00:00 2001 From: Kartheek Palla Date: Wed, 21 Jun 2023 18:25:19 +0530 Subject: [PATCH 207/222] Revert "Issue KN-10 fix: disable content publish, unlisted publish, reject and review APIs" This reverts commit bdb56f6172f5f446189c72dba8a4f4f5ad81887f. --- pipelines/build/learning/Jenkinsfile | 35 ++++------- .../learning/Jenkinsfile_containerization | 63 ------------------- .../controller/ContentV3Controller.java | 36 +++++------ 3 files changed, 30 insertions(+), 104 deletions(-) delete mode 100644 pipelines/build/learning/Jenkinsfile_containerization diff --git a/pipelines/build/learning/Jenkinsfile b/pipelines/build/learning/Jenkinsfile index e57a054a7d..37aa3acf52 100644 --- a/pipelines/build/learning/Jenkinsfile +++ b/pipelines/build/learning/Jenkinsfile @@ -12,6 +12,8 @@ node() { cleanWs() checkout scm commit_hash = sh(script: 'git rev-parse --short HEAD', returnStdout: true).trim() + build_tag = sh(script: "echo " + params.github_release_tag.split('/')[-1] + "_" + commit_hash + "_" + env.BUILD_NUMBER, returnStdout: true).trim() + echo "build_tag: " + build_tag artifact_version = sh(script: "echo " + params.github_release_tag.split('/')[-1] + "_" + commit_hash + "_" + env.BUILD_NUMBER, returnStdout: true).trim() echo "artifact_version: "+ artifact_version } @@ -30,6 +32,8 @@ node() { } stage('Build') { + env.NODE_ENV = "build" + print "Environment will be : ${env.NODE_ENV}" sh 'mvn clean install -DskipTests -P ${profile_id} -T10' } @@ -38,29 +42,14 @@ node() { } - stage('Archive artifacts'){ - if(params.profile_id == "platform-services"){ - sh """ - mkdir lp_artifacts - cp platform-modules/service/target/learning-service.war lp_artifacts - cp platform-tools/spikes/sync-tool/target/sync-tool*.jar lp_artifacts - cp platform-tools/spikes/content-tool/target/content-tool-*.jar lp_artifacts - zip -j lp_artifacts.zip:${artifact_version} lp_artifacts/* - """ - } - else { - sh """ - mkdir lp_artifacts - cp platform-modules/service/target/learning-service.war lp_artifacts - zip -j lp_artifacts.zip:${artifact_version} lp_artifacts/* - """ - } + stage('Package') { + sh('chmod 777 pipelines/build/build.sh') + sh("pipelines/build/build.sh ${build_tag} ${"learning"} ${env.NODE_NAME} ${hub_org}") + } - archiveArtifacts artifacts: "lp_artifacts.zip:${artifact_version}", fingerprint: true, onlyIfSuccessful: true - sh """echo {\\"artifact_name\\" : \\"lp_artifacts.zip\\", \\"artifact_version\\" : \\"${artifact_version}\\", \\"node_name\\" : \\"${env.NODE_NAME}\\"} > metadata.json""" - archiveArtifacts artifacts: 'metadata.json', onlyIfSuccessful: true - currentBuild.result = "SUCCESS" - currentBuild.description = "Artifact: ${artifact_version}, Public: ${params.github_release_tag}" + stage('ArchiveArtifacts') { + archiveArtifacts "metadata.json" + currentBuild.description = "${build_tag}" } } catch (err) { @@ -71,4 +60,4 @@ node() { slack_notify(currentBuild.result) email_notify() } -} \ No newline at end of file +} diff --git a/pipelines/build/learning/Jenkinsfile_containerization b/pipelines/build/learning/Jenkinsfile_containerization deleted file mode 100644 index 9cea000f79..0000000000 --- a/pipelines/build/learning/Jenkinsfile_containerization +++ /dev/null @@ -1,63 +0,0 @@ -@Library('deploy-conf') _ -node() { - try { - String ANSI_GREEN = "\u001B[32m" - String ANSI_NORMAL = "\u001B[0m" - String ANSI_BOLD = "\u001B[1m" - String ANSI_RED = "\u001B[31m" - String ANSI_YELLOW = "\u001B[33m" - - ansiColor('xterm') { - stage('Checkout') { - cleanWs() - checkout scm - commit_hash = sh(script: 'git rev-parse --short HEAD', returnStdout: true).trim() - build_tag = sh(script: "echo " + params.github_release_tag.split('/')[-1] + "_" + commit_hash + "_" + env.BUILD_NUMBER, returnStdout: true).trim() - echo "build_tag: " + build_tag - artifact_version = sh(script: "echo " + params.github_release_tag.split('/')[-1] + "_" + commit_hash + "_" + env.BUILD_NUMBER, returnStdout: true).trim() - echo "artifact_version: "+ artifact_version - } - } - - stage('Pre-Build') { - sh """ - java -version - rm -rf /data/logs/* - rm -rf /data/graphDB/* - rm -rf /data/testgraphDB/* - rm -rf /data/testGraphDB/* - vim -esnc '%s/dialcode.es_conn_info="localhost:9200"/dialcode.es_conn_info="10.6.0.11:9200"/g|:wq' platform-core/unit-tests/src/test/resources/application.conf - vim -esnc '%s/search.es_conn_info="localhost:9200"/search.es_conn_info="10.6.0.11:9200"/g|:wq' platform-core/unit-tests/src/test/resources/application.conf - """ - } - - stage('Build') { - env.NODE_ENV = "build" - print "Environment will be : ${env.NODE_ENV}" - sh 'mvn clean install -DskipTests -P ${profile_id} -T10' - } - - stage('Post_Build-Action') { - jacoco exclusionPattern: '**/common/**,**/dto/**,**/enums/**,**/pipeline/**,**/servlet/**,**/interceptor/**,**/batch/**,**/models/**,**/model/**,**/EnrichActor*.class,**/language/controller/**,**/wordchain/**,**/importer/**,**/Base**,**/ControllerUtil**,**/Indowordnet**,**/Import**' - - } - - stage('Package') { - sh('chmod 777 pipelines/build/build.sh') - sh("pipelines/build/build.sh ${build_tag} ${"learning"} ${env.NODE_NAME} ${hub_org}") - } - - stage('ArchiveArtifacts') { - archiveArtifacts "metadata.json" - currentBuild.description = "${build_tag}" - } - } - catch (err) { - currentBuild.result = "FAILURE" - throw err - } - finally { - slack_notify(currentBuild.result) - email_notify() - } -} diff --git a/platform-modules/manager/src/main/java/org/sunbird/taxonomy/controller/ContentV3Controller.java b/platform-modules/manager/src/main/java/org/sunbird/taxonomy/controller/ContentV3Controller.java index f2fa149d18..0495d3d738 100755 --- a/platform-modules/manager/src/main/java/org/sunbird/taxonomy/controller/ContentV3Controller.java +++ b/platform-modules/manager/src/main/java/org/sunbird/taxonomy/controller/ContentV3Controller.java @@ -184,8 +184,8 @@ public ResponseEntity bundle(@RequestBody Map map) { * Set. */ @SuppressWarnings("unchecked") -// @RequestMapping(value = { "/publish/{id:.+}", "/public/publish/{id:.+}" }, method = RequestMethod.POST) -// @ResponseBody + @RequestMapping(value = { "/publish/{id:.+}", "/public/publish/{id:.+}" }, method = RequestMethod.POST) + @ResponseBody public ResponseEntity publish(@PathVariable(value = "id") String contentId, @RequestBody Map map) { String apiId = "ekstep.learning.content.publish"; @@ -224,8 +224,8 @@ public ResponseEntity publish(@PathVariable(value = "id") String conte * Set. */ @SuppressWarnings("unchecked") -// @RequestMapping(value = "/unlisted/publish/{id:.+}", method = RequestMethod.POST) -// @ResponseBody + @RequestMapping(value = "/unlisted/publish/{id:.+}", method = RequestMethod.POST) + @ResponseBody public ResponseEntity publishUnlisted(@PathVariable(value = "id") String contentId, @RequestBody Map map) { String apiId = "ekstep.learning.content.unlisted.publish"; @@ -262,8 +262,8 @@ public ResponseEntity publishUnlisted(@PathVariable(value = "id") Stri * The Content Id which needs to be published. * @return The Response entity with Content Id in its Result Set. */ -// @RequestMapping(value = "/review/{id:.+}", method = RequestMethod.POST) -// @ResponseBody + @RequestMapping(value = "/review/{id:.+}", method = RequestMethod.POST) + @ResponseBody public ResponseEntity review(@PathVariable(value = "id") String contentId, @RequestBody Map map) { String apiId = "ekstep.learning.content.review"; @@ -446,8 +446,8 @@ public ResponseEntity syncHierarchy(@PathVariable(value = "id") String * @param requestMap * @return */ -// @RequestMapping(value = "/dialcode/link", method = RequestMethod.POST) -// @ResponseBody + @RequestMapping(value = "/dialcode/link", method = RequestMethod.POST) + @ResponseBody public ResponseEntity linkDialCode(@RequestBody Map requestMap, @RequestHeader(value = CHANNEL_ID, required = true) String channelId) { String apiId = "ekstep.content.dialcode.link"; @@ -470,8 +470,8 @@ public ResponseEntity linkDialCode(@RequestBody Map re * The Content Id for whom DIAL Codes have to be reserved * @return */ -// @RequestMapping(value = "/dialcode/reserve/{id:.+}", method = RequestMethod.POST) -// @ResponseBody + @RequestMapping(value = "/dialcode/reserve/{id:.+}", method = RequestMethod.POST) + @ResponseBody public ResponseEntity reserveDialCode( @PathVariable(value = "id") String contentId, @RequestBody Map requestMap, @@ -496,8 +496,8 @@ public ResponseEntity reserveDialCode( * The Content Id of the Textbook from which DIAL Codes have to be released * @return The Response Entity with list of Released QR Codes */ -// @RequestMapping(value="/dialcode/release/{id}", method = RequestMethod.PATCH) -// @ResponseBody + @RequestMapping(value="/dialcode/release/{id}", method = RequestMethod.PATCH) + @ResponseBody public ResponseEntity releaseDialcodes(@PathVariable(value="id") String contentId, @RequestHeader(value = CHANNEL_ID) String channelId) { String apiId = "ekstep.learning.content.dialcode.release"; @@ -545,8 +545,8 @@ protected String getAPIVersion() { return API_VERSION_3; } -// @RequestMapping(value="/retire/{id:.+}", method = RequestMethod.DELETE) -// @ResponseBody + @RequestMapping(value="/retire/{id:.+}", method = RequestMethod.DELETE) + @ResponseBody public ResponseEntity retire(@PathVariable(value = "id") String contentId) { String apiId = "ekstep.content.retire"; TelemetryManager.log("Retiring content | Content Id : " + contentId); @@ -583,8 +583,8 @@ public ResponseEntity acceptFlag(@PathVariable(value = "id") String co * @return The Response entity with Content Id and Version Key in its Result * Set. */ -// @RequestMapping(value="/flag/reject/{id:.+}", method = RequestMethod.POST) -// @ResponseBody + @RequestMapping(value="/flag/reject/{id:.+}", method = RequestMethod.POST) + @ResponseBody public ResponseEntity rejectFlag(@PathVariable(value = "id") String contentId){ String apiId = "ekstep.learning.content.rejectFlag"; TelemetryManager.log("Reject flagged content | Content Id : " + contentId); @@ -650,8 +650,8 @@ public ResponseEntity discard(@PathVariable(value = "id") String conte * @param contentId * @return */ -// @RequestMapping(value = "/reject/{id:.+}", method = RequestMethod.POST) -// @ResponseBody + @RequestMapping(value = "/reject/{id:.+}", method = RequestMethod.POST) + @ResponseBody public ResponseEntity rejectContent(@PathVariable(value = "id") String contentId, @RequestBody Map requestMap) { String apiId = "ekstep.learning.content.reject"; From 61c288f0e280f9a6b0fbf6b2aff5e8b3a0fe73f0 Mon Sep 17 00:00:00 2001 From: Kartheek Palla Date: Wed, 21 Jun 2023 18:41:49 +0530 Subject: [PATCH 208/222] Issue KN-10 fix: disable content publish, unlisted publish, reject and review APIs --- pipelines/build/learning/Jenkinsfile | 35 +++++++---- .../learning/Jenkinsfile_containerization | 63 +++++++++++++++++++ .../controller/ContentV3Controller.java | 36 +++++------ 3 files changed, 104 insertions(+), 30 deletions(-) create mode 100644 pipelines/build/learning/Jenkinsfile_containerization diff --git a/pipelines/build/learning/Jenkinsfile b/pipelines/build/learning/Jenkinsfile index 37aa3acf52..e57a054a7d 100644 --- a/pipelines/build/learning/Jenkinsfile +++ b/pipelines/build/learning/Jenkinsfile @@ -12,8 +12,6 @@ node() { cleanWs() checkout scm commit_hash = sh(script: 'git rev-parse --short HEAD', returnStdout: true).trim() - build_tag = sh(script: "echo " + params.github_release_tag.split('/')[-1] + "_" + commit_hash + "_" + env.BUILD_NUMBER, returnStdout: true).trim() - echo "build_tag: " + build_tag artifact_version = sh(script: "echo " + params.github_release_tag.split('/')[-1] + "_" + commit_hash + "_" + env.BUILD_NUMBER, returnStdout: true).trim() echo "artifact_version: "+ artifact_version } @@ -32,8 +30,6 @@ node() { } stage('Build') { - env.NODE_ENV = "build" - print "Environment will be : ${env.NODE_ENV}" sh 'mvn clean install -DskipTests -P ${profile_id} -T10' } @@ -42,14 +38,29 @@ node() { } - stage('Package') { - sh('chmod 777 pipelines/build/build.sh') - sh("pipelines/build/build.sh ${build_tag} ${"learning"} ${env.NODE_NAME} ${hub_org}") - } + stage('Archive artifacts'){ + if(params.profile_id == "platform-services"){ + sh """ + mkdir lp_artifacts + cp platform-modules/service/target/learning-service.war lp_artifacts + cp platform-tools/spikes/sync-tool/target/sync-tool*.jar lp_artifacts + cp platform-tools/spikes/content-tool/target/content-tool-*.jar lp_artifacts + zip -j lp_artifacts.zip:${artifact_version} lp_artifacts/* + """ + } + else { + sh """ + mkdir lp_artifacts + cp platform-modules/service/target/learning-service.war lp_artifacts + zip -j lp_artifacts.zip:${artifact_version} lp_artifacts/* + """ + } - stage('ArchiveArtifacts') { - archiveArtifacts "metadata.json" - currentBuild.description = "${build_tag}" + archiveArtifacts artifacts: "lp_artifacts.zip:${artifact_version}", fingerprint: true, onlyIfSuccessful: true + sh """echo {\\"artifact_name\\" : \\"lp_artifacts.zip\\", \\"artifact_version\\" : \\"${artifact_version}\\", \\"node_name\\" : \\"${env.NODE_NAME}\\"} > metadata.json""" + archiveArtifacts artifacts: 'metadata.json', onlyIfSuccessful: true + currentBuild.result = "SUCCESS" + currentBuild.description = "Artifact: ${artifact_version}, Public: ${params.github_release_tag}" } } catch (err) { @@ -60,4 +71,4 @@ node() { slack_notify(currentBuild.result) email_notify() } -} +} \ No newline at end of file diff --git a/pipelines/build/learning/Jenkinsfile_containerization b/pipelines/build/learning/Jenkinsfile_containerization new file mode 100644 index 0000000000..3b0c581286 --- /dev/null +++ b/pipelines/build/learning/Jenkinsfile_containerization @@ -0,0 +1,63 @@ +@Library('deploy-conf') _ +node() { + try { + String ANSI_GREEN = "\u001B[32m" + String ANSI_NORMAL = "\u001B[0m" + String ANSI_BOLD = "\u001B[1m" + String ANSI_RED = "\u001B[31m" + String ANSI_YELLOW = "\u001B[33m" + + ansiColor('xterm') { + stage('Checkout') { + cleanWs() + checkout scm + commit_hash = sh(script: 'git rev-parse --short HEAD', returnStdout: true).trim() + build_tag = sh(script: "echo " + params.github_release_tag.split('/')[-1] + "_" + commit_hash + "_" + env.BUILD_NUMBER, returnStdout: true).trim() + echo "build_tag: " + build_tag + artifact_version = sh(script: "echo " + params.github_release_tag.split('/')[-1] + "_" + commit_hash + "_" + env.BUILD_NUMBER, returnStdout: true).trim() + echo "artifact_version: "+ artifact_version + } + } + + stage('Pre-Build') { + sh """ + java -version + rm -rf /data/logs/* + rm -rf /data/graphDB/* + rm -rf /data/testgraphDB/* + rm -rf /data/testGraphDB/* + vim -esnc '%s/dialcode.es_conn_info="localhost:9200"/dialcode.es_conn_info="10.6.0.11:9200"/g|:wq' platform-core/unit-tests/src/test/resources/application.conf + vim -esnc '%s/search.es_conn_info="localhost:9200"/search.es_conn_info="10.6.0.11:9200"/g|:wq' platform-core/unit-tests/src/test/resources/application.conf + """ + } + + stage('Build') { + env.NODE_ENV = "build" + print "Environment will be : ${env.NODE_ENV}" + sh 'mvn clean install -DskipTests -P ${profile_id} -T10' + } + + stage('Post_Build-Action') { + jacoco exclusionPattern: '**/common/**,**/dto/**,**/enums/**,**/pipeline/**,**/servlet/**,**/interceptor/**,**/batch/**,**/models/**,**/model/**,**/EnrichActor*.class,**/language/controller/**,**/wordchain/**,**/importer/**,**/Base**,**/ControllerUtil**,**/Indowordnet**,**/Import**' + + } + + stage('Package') { + sh('chmod 777 pipelines/build/build.sh') + sh("pipelines/build/build.sh ${build_tag} ${"learning"} ${env.NODE_NAME} ${hub_org}") + } + + stage('ArchiveArtifacts') { + archiveArtifacts "metadata.json" + currentBuild.description = "${build_tag}" + } + } + catch (err) { + currentBuild.result = "FAILURE" + throw err + } + finally { + slack_notify(currentBuild.result) + email_notify() + } +} \ No newline at end of file diff --git a/platform-modules/manager/src/main/java/org/sunbird/taxonomy/controller/ContentV3Controller.java b/platform-modules/manager/src/main/java/org/sunbird/taxonomy/controller/ContentV3Controller.java index 0495d3d738..f2fa149d18 100755 --- a/platform-modules/manager/src/main/java/org/sunbird/taxonomy/controller/ContentV3Controller.java +++ b/platform-modules/manager/src/main/java/org/sunbird/taxonomy/controller/ContentV3Controller.java @@ -184,8 +184,8 @@ public ResponseEntity bundle(@RequestBody Map map) { * Set. */ @SuppressWarnings("unchecked") - @RequestMapping(value = { "/publish/{id:.+}", "/public/publish/{id:.+}" }, method = RequestMethod.POST) - @ResponseBody +// @RequestMapping(value = { "/publish/{id:.+}", "/public/publish/{id:.+}" }, method = RequestMethod.POST) +// @ResponseBody public ResponseEntity publish(@PathVariable(value = "id") String contentId, @RequestBody Map map) { String apiId = "ekstep.learning.content.publish"; @@ -224,8 +224,8 @@ public ResponseEntity publish(@PathVariable(value = "id") String conte * Set. */ @SuppressWarnings("unchecked") - @RequestMapping(value = "/unlisted/publish/{id:.+}", method = RequestMethod.POST) - @ResponseBody +// @RequestMapping(value = "/unlisted/publish/{id:.+}", method = RequestMethod.POST) +// @ResponseBody public ResponseEntity publishUnlisted(@PathVariable(value = "id") String contentId, @RequestBody Map map) { String apiId = "ekstep.learning.content.unlisted.publish"; @@ -262,8 +262,8 @@ public ResponseEntity publishUnlisted(@PathVariable(value = "id") Stri * The Content Id which needs to be published. * @return The Response entity with Content Id in its Result Set. */ - @RequestMapping(value = "/review/{id:.+}", method = RequestMethod.POST) - @ResponseBody +// @RequestMapping(value = "/review/{id:.+}", method = RequestMethod.POST) +// @ResponseBody public ResponseEntity review(@PathVariable(value = "id") String contentId, @RequestBody Map map) { String apiId = "ekstep.learning.content.review"; @@ -446,8 +446,8 @@ public ResponseEntity syncHierarchy(@PathVariable(value = "id") String * @param requestMap * @return */ - @RequestMapping(value = "/dialcode/link", method = RequestMethod.POST) - @ResponseBody +// @RequestMapping(value = "/dialcode/link", method = RequestMethod.POST) +// @ResponseBody public ResponseEntity linkDialCode(@RequestBody Map requestMap, @RequestHeader(value = CHANNEL_ID, required = true) String channelId) { String apiId = "ekstep.content.dialcode.link"; @@ -470,8 +470,8 @@ public ResponseEntity linkDialCode(@RequestBody Map re * The Content Id for whom DIAL Codes have to be reserved * @return */ - @RequestMapping(value = "/dialcode/reserve/{id:.+}", method = RequestMethod.POST) - @ResponseBody +// @RequestMapping(value = "/dialcode/reserve/{id:.+}", method = RequestMethod.POST) +// @ResponseBody public ResponseEntity reserveDialCode( @PathVariable(value = "id") String contentId, @RequestBody Map requestMap, @@ -496,8 +496,8 @@ public ResponseEntity reserveDialCode( * The Content Id of the Textbook from which DIAL Codes have to be released * @return The Response Entity with list of Released QR Codes */ - @RequestMapping(value="/dialcode/release/{id}", method = RequestMethod.PATCH) - @ResponseBody +// @RequestMapping(value="/dialcode/release/{id}", method = RequestMethod.PATCH) +// @ResponseBody public ResponseEntity releaseDialcodes(@PathVariable(value="id") String contentId, @RequestHeader(value = CHANNEL_ID) String channelId) { String apiId = "ekstep.learning.content.dialcode.release"; @@ -545,8 +545,8 @@ protected String getAPIVersion() { return API_VERSION_3; } - @RequestMapping(value="/retire/{id:.+}", method = RequestMethod.DELETE) - @ResponseBody +// @RequestMapping(value="/retire/{id:.+}", method = RequestMethod.DELETE) +// @ResponseBody public ResponseEntity retire(@PathVariable(value = "id") String contentId) { String apiId = "ekstep.content.retire"; TelemetryManager.log("Retiring content | Content Id : " + contentId); @@ -583,8 +583,8 @@ public ResponseEntity acceptFlag(@PathVariable(value = "id") String co * @return The Response entity with Content Id and Version Key in its Result * Set. */ - @RequestMapping(value="/flag/reject/{id:.+}", method = RequestMethod.POST) - @ResponseBody +// @RequestMapping(value="/flag/reject/{id:.+}", method = RequestMethod.POST) +// @ResponseBody public ResponseEntity rejectFlag(@PathVariable(value = "id") String contentId){ String apiId = "ekstep.learning.content.rejectFlag"; TelemetryManager.log("Reject flagged content | Content Id : " + contentId); @@ -650,8 +650,8 @@ public ResponseEntity discard(@PathVariable(value = "id") String conte * @param contentId * @return */ - @RequestMapping(value = "/reject/{id:.+}", method = RequestMethod.POST) - @ResponseBody +// @RequestMapping(value = "/reject/{id:.+}", method = RequestMethod.POST) +// @ResponseBody public ResponseEntity rejectContent(@PathVariable(value = "id") String contentId, @RequestBody Map requestMap) { String apiId = "ekstep.learning.content.reject"; From cd823976d1d50bb6707994bf7be939001ce6fe08 Mon Sep 17 00:00:00 2001 From: Jayaprakash8887 Date: Tue, 4 Jul 2023 11:13:33 +0530 Subject: [PATCH 209/222] Issue #KN-889 fix: QR ImageURL bulk sync --- .../sync/tool/mgr/CassandraESSyncManager.java | 6 +-- .../sync/tool/shell/SyncShellCommands.java | 5 ++- .../sunbird/sync/tool/util/DialcodeSync.java | 39 ++++++++++++++----- .../tool/mgr/CassandraESSyncManagerTest.java | 8 ++-- .../sync/tool/util/DialcodeSyncTest.java | 4 +- .../src/test/resources/application.conf | 6 ++- 6 files changed, 47 insertions(+), 21 deletions(-) diff --git a/platform-tools/spikes/sync-tool/src/main/java/org/sunbird/sync/tool/mgr/CassandraESSyncManager.java b/platform-tools/spikes/sync-tool/src/main/java/org/sunbird/sync/tool/mgr/CassandraESSyncManager.java index 16a8b9b805..27c3e3e493 100644 --- a/platform-tools/spikes/sync-tool/src/main/java/org/sunbird/sync/tool/mgr/CassandraESSyncManager.java +++ b/platform-tools/spikes/sync-tool/src/main/java/org/sunbird/sync/tool/mgr/CassandraESSyncManager.java @@ -533,13 +533,13 @@ protected boolean validateAssetMediaForExternalLink(Media media){ return isExternal; } - public void syncDialcodesByIds(List dialcodes) throws Exception { - if(CollectionUtils.isEmpty(dialcodes)) { + public void syncDialcodesByIds(List dialcodes, List filenames) throws Exception { + if(CollectionUtils.isEmpty(dialcodes) && CollectionUtils.isEmpty(filenames)) { System.out.println("CassandraESSyncManager:syncDialcodesByIds:No dialcodes for syncing."); return; } System.out.println("CassandraESSyncManager:syncDialcodesByIds:No dialcodes for syncing: " + dialcodes.size()); - int dialcodeSyncedCount = dialcodeSync.sync(dialcodes); + int dialcodeSyncedCount = dialcodeSync.sync(dialcodes, filenames); System.out.println("CassandraESSyncManager:syncDialcodesByIds::dialcodeSyncedCount: " + dialcodeSyncedCount); } diff --git a/platform-tools/spikes/sync-tool/src/main/java/org/sunbird/sync/tool/shell/SyncShellCommands.java b/platform-tools/spikes/sync-tool/src/main/java/org/sunbird/sync/tool/shell/SyncShellCommands.java index 0045ba87ad..0ee8b6fdb8 100644 --- a/platform-tools/spikes/sync-tool/src/main/java/org/sunbird/sync/tool/shell/SyncShellCommands.java +++ b/platform-tools/spikes/sync-tool/src/main/java/org/sunbird/sync/tool/shell/SyncShellCommands.java @@ -143,7 +143,8 @@ public void syncLeafNodesByIds( @CliCommand(value = "syncdialcodes", help = "Refresh leafNodes by Id(s) for Collection MimeTypes") public void syncDialcodes( - @CliOption(key = {"id","ids"}, mandatory = false, help = "Unique Id of node object") final String[] ids) + @CliOption(key = {"id","ids"}, mandatory = false, help = "Unique Id of node object") final String[] ids, + @CliOption(key = {"filename", "filenames"}, mandatory = false, help = "dialcode filename") final String[] filenames) throws Exception { long startTime = System.currentTimeMillis(); DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm:ss"); @@ -151,7 +152,7 @@ public void syncDialcodes( if(null != ids && ids.length > 0) { System.out.println("SyncShellCommands:syncDialcodes:Total dialcodes for syncing:: " + ids); - syncManager.syncDialcodesByIds(new ArrayList(Arrays.asList(ids))); + syncManager.syncDialcodesByIds(new ArrayList(Arrays.asList(ids)), new ArrayList(Arrays.asList(filenames))); } long endTime = System.currentTimeMillis(); diff --git a/platform-tools/spikes/sync-tool/src/main/java/org/sunbird/sync/tool/util/DialcodeSync.java b/platform-tools/spikes/sync-tool/src/main/java/org/sunbird/sync/tool/util/DialcodeSync.java index 14b388424e..8c2220e27d 100644 --- a/platform-tools/spikes/sync-tool/src/main/java/org/sunbird/sync/tool/util/DialcodeSync.java +++ b/platform-tools/spikes/sync-tool/src/main/java/org/sunbird/sync/tool/util/DialcodeSync.java @@ -4,6 +4,8 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; import org.apache.commons.collections.MapUtils; import org.apache.commons.lang3.StringUtils; @@ -48,10 +50,10 @@ public DialcodeSync() { ElasticSearchUtil.initialiseESClient(indexName, Platform.config.getString("search.es_conn_info")); } - public int sync(List dialcodes) throws Exception { + public int sync(List dialcodes, List filenames) throws Exception { System.out.println("DialcodeSync:sync:message:: Total number of Dialcodes to be fetched from cassandra: " + dialcodes.size()); // Get dialcodes data from cassandra - Map messages = getDialcodesFromIds(dialcodes); + Map messages = getDialcodesFromIds(dialcodes, filenames); if(MapUtils.isEmpty(messages)) { System.out.println("DialcodeSync:sync:message:: No dialcodes data fetched from cassandra."); return 0; @@ -66,15 +68,27 @@ private void upsertDocument( Map messages) throws Exception { ElasticSearchUtil.bulkIndexWithIndexId(indexName, documentType, messages); } - public Map getDialcodesFromIds(List identifiers) { + public Map getDialcodesFromIds(List identifiers, List filenames) { try { - Map messages = new HashMap(); - ResultSet rs = getDialcodesFromDB(identifiers); + List updateddialcodes = null; + HashMap filenamesmap = null; + + if(identifiers != null || !identifiers.isEmpty()) + updateddialcodes = identifiers; + else { + filenamesmap = (HashMap)filenames.stream().collect(Collectors.toMap(s->s.split("_")[1], Function.identity())); + updateddialcodes = filenamesmap.keySet().stream().collect(Collectors.toList()); + } + + System.out.println("DialcodeSync:sync:message:: Total number of Dialcodes to be fetched from cassandra: " + updateddialcodes.size()); + + Map messages = new HashMap(); + ResultSet rs = getDialcodesFromDB(updateddialcodes); if (null != rs) { Map dialCodesFromDB = new HashMap(); while(rs.iterator().hasNext()) { Row row = rs.iterator().next(); - String dialcodeId = (String)row.getString("identifier"); + String dialcodeId = row.getString("identifier"); dialCodesFromDB.put(dialcodeId, row); Map syncRequest = new HashMap(){{ @@ -89,7 +103,10 @@ public Map getDialcodesFromIds(List identifiers) { put("objectType", "DialCode"); }}; - String imageUrl = getQRImageFromDB(dialcodeId); + String imageUrl = ""; + if(filenamesmap!=null && !filenamesmap.isEmpty()) + imageUrl = getQRImageFromDB(filenamesmap.get(dialcodeId), true); + else imageUrl = getQRImageFromDB(dialcodeId, false); System.out.println("Returned imageUrl: " + imageUrl); if(isReplaceString) { imageUrl = StringUtils.replaceEach(imageUrl, new String[]{replaceSrcStringDIALStore}, new String[]{replaceDestStringDIALStore}); @@ -119,8 +136,12 @@ private ResultSet getDialcodesFromDB(List identifiers) { return session.execute(query); } - private String getQRImageFromDB(String dialcodeId) { - String query = "SELECT url FROM " + qrImageKeyspace + "." + qrImageTable + " WHERE dialcode ='" + dialcodeId + "' ALLOW FILTERING;"; + private String getQRImageFromDB(String dialcodeId, boolean isFileName) { + String query = ""; + if(isFileName) + query = "SELECT url FROM " + qrImageKeyspace + "." + qrImageTable + " WHERE filename ='" + dialcodeId + "';"; + else + query = "SELECT url FROM " + qrImageKeyspace + "." + qrImageTable + " WHERE dialcode ='" + dialcodeId + "' ALLOW FILTERING;"; System.out.println("getQRImageFromDB query: " + query); Session session = CassandraConnector.getSession(); ResultSet rs = session.execute(query); diff --git a/platform-tools/spikes/sync-tool/src/test/java/org/sunbird/sync/tool/mgr/CassandraESSyncManagerTest.java b/platform-tools/spikes/sync-tool/src/test/java/org/sunbird/sync/tool/mgr/CassandraESSyncManagerTest.java index 015118d915..28774e18c2 100644 --- a/platform-tools/spikes/sync-tool/src/test/java/org/sunbird/sync/tool/mgr/CassandraESSyncManagerTest.java +++ b/platform-tools/spikes/sync-tool/src/test/java/org/sunbird/sync/tool/mgr/CassandraESSyncManagerTest.java @@ -31,21 +31,21 @@ public class CassandraESSyncManagerTest { @Test public void testsyncDialcodesByIdsWithDialcodes() throws Exception { DialcodeSync dialcodeSync = PowerMockito.mock(DialcodeSync.class); - PowerMockito.when(dialcodeSync.sync(Mockito.anyList())).thenReturn(1); + PowerMockito.when(dialcodeSync.sync(Mockito.anyList(), Mockito.anyList())).thenReturn(1); List dialcodes = Arrays.asList("A1B2C3"); CassandraESSyncManager cassandraESSyncManager = new CassandraESSyncManager(dialcodeSync); - cassandraESSyncManager.syncDialcodesByIds(dialcodes); + cassandraESSyncManager.syncDialcodesByIds(dialcodes, null); } @Test public void testsyncDialcodesByIdsWithoutDialcodes() throws Exception { DialcodeSync dialcodeSync = PowerMockito.mock(DialcodeSync.class); - PowerMockito.when(dialcodeSync.sync(Mockito.anyList())).thenReturn(1); + PowerMockito.when(dialcodeSync.sync(Mockito.anyList(), Mockito.anyList())).thenReturn(1); List dialcodes = null; CassandraESSyncManager cassandraESSyncManager = new CassandraESSyncManager(dialcodeSync); - cassandraESSyncManager.syncDialcodesByIds(dialcodes); + cassandraESSyncManager.syncDialcodesByIds(dialcodes, null); } @Test diff --git a/platform-tools/spikes/sync-tool/src/test/java/org/sunbird/sync/tool/util/DialcodeSyncTest.java b/platform-tools/spikes/sync-tool/src/test/java/org/sunbird/sync/tool/util/DialcodeSyncTest.java index 636d01c2ea..f5abfb26e3 100644 --- a/platform-tools/spikes/sync-tool/src/test/java/org/sunbird/sync/tool/util/DialcodeSyncTest.java +++ b/platform-tools/spikes/sync-tool/src/test/java/org/sunbird/sync/tool/util/DialcodeSyncTest.java @@ -37,7 +37,7 @@ public void testSyncWrongDialcodes() throws Exception { PowerMockito.when(CassandraConnector.getSession()).thenReturn(session); DialcodeSync dialcodeSync = new DialcodeSync(); - Assert.isTrue(dialcodeSync.sync(Arrays.asList("A1B2C3")) == 0); + Assert.isTrue(dialcodeSync.sync(Arrays.asList("A1B2C3"), null) == 0); } @Test @@ -73,6 +73,6 @@ public void testSyncCorrectDialcodes() throws Exception { DialcodeSync dialcodeSync = new DialcodeSync(); - Assert.isTrue(dialcodeSync.sync(Arrays.asList("A1B2C3")) == 1); + Assert.isTrue(dialcodeSync.sync(Arrays.asList("A1B2C3"),null) == 1); } } diff --git a/platform-tools/spikes/sync-tool/src/test/resources/application.conf b/platform-tools/spikes/sync-tool/src/test/resources/application.conf index 4c519eea34..a90ee410a3 100644 --- a/platform-tools/spikes/sync-tool/src/test/resources/application.conf +++ b/platform-tools/spikes/sync-tool/src/test/resources/application.conf @@ -40,4 +40,8 @@ search.fields.query=["name^100","title^100","lemma^100","code^100","tags^100","d search.fields.date=["lastUpdatedOn","createdOn","versionDate","lastSubmittedOn","lastPublishedOn"] search.batch.size=500 -batch.size=100 \ No newline at end of file +batch.size=100 + + +replace_src_string_DIAL_store= "{{ sync_tool_replace_src_string_DIAL_store | default('DIAL_STORAGE_BASE_PATH') }}" +replace_dest_string_DIAL_store="{{ sync_tool_replace_dest_string_DIAL_store | default('https://sunbirddevbbpublic.blob.core.windows.net/dial') }}" \ No newline at end of file From 78d326f8757e2223cb405f3469b90ed093afef07 Mon Sep 17 00:00:00 2001 From: Kumar Gauraw Date: Fri, 14 Jul 2023 11:20:52 +0530 Subject: [PATCH 210/222] Issue #IQ-493 feat: added code for quml migration event generation --- .../lp-synctool-deploy/defaults/main.yml | 4 + .../templates/application.conf.j2 | 6 +- .../sunbird/learning/util/ControllerUtil.java | 73 ++++++ .../mgr/QumlMigrationMessageGenerator.java | 215 ++++++++++++++++++ .../tool/shell/MigrateQumlDataCommand.java | 41 ++++ .../src/main/resources/application.conf | 6 + 6 files changed, 344 insertions(+), 1 deletion(-) create mode 100644 platform-tools/spikes/sync-tool/src/main/java/org/sunbird/sync/tool/mgr/QumlMigrationMessageGenerator.java create mode 100644 platform-tools/spikes/sync-tool/src/main/java/org/sunbird/sync/tool/shell/MigrateQumlDataCommand.java diff --git a/ansible/roles/lp-synctool-deploy/defaults/main.yml b/ansible/roles/lp-synctool-deploy/defaults/main.yml index b98b48a3e7..4eac4bf5ad 100644 --- a/ansible/roles/lp-synctool-deploy/defaults/main.yml +++ b/ansible/roles/lp-synctool-deploy/defaults/main.yml @@ -29,3 +29,7 @@ azure_account_name: csp_migration_batch_size: 100 csp_migration_topic_name: "{{ env }}.csp.migration.job.request" + +# Default Values For QuML Data Migration +quml_migration_batch_size: 50 +quml_migration_topic_name: "{{ env }}.quml.migration.job.request" \ No newline at end of file diff --git a/ansible/roles/lp-synctool-deploy/templates/application.conf.j2 b/ansible/roles/lp-synctool-deploy/templates/application.conf.j2 index c2a8e679fc..188f5e7211 100644 --- a/ansible/roles/lp-synctool-deploy/templates/application.conf.j2 +++ b/ansible/roles/lp-synctool-deploy/templates/application.conf.j2 @@ -123,4 +123,8 @@ replace_src_string= "{{ sync_tool_replace_src_string | default('CONTENT_STORAGE_ replace_dest_string="{{ sync_tool_replace_dest_string | default('https://sunbirddevbbpublic.blob.core.windows.net/sunbird-content-dev') }}" replace_src_string_DIAL_store= "{{ sync_tool_replace_src_string_DIAL_store | default('DIAL_STORAGE_BASE_PATH') }}" -replace_dest_string_DIAL_store="{{ sync_tool_replace_dest_string_DIAL_store | default('https://sunbirddevbbpublic.blob.core.windows.net/dial') }}" \ No newline at end of file +replace_dest_string_DIAL_store="{{ sync_tool_replace_dest_string_DIAL_store | default('https://sunbirddevbbpublic.blob.core.windows.net/dial') }}" + +# Config For QuML Data Migration +quml.migration.request.topic="{{ quml_migration_topic_name }}" +quml.migration.batch.size={{ quml_migration_batch_size }} \ No newline at end of file diff --git a/platform-modules/actors/src/main/java/org/sunbird/learning/util/ControllerUtil.java b/platform-modules/actors/src/main/java/org/sunbird/learning/util/ControllerUtil.java index 4acdae33be..5cd32ab3f1 100644 --- a/platform-modules/actors/src/main/java/org/sunbird/learning/util/ControllerUtil.java +++ b/platform-modules/actors/src/main/java/org/sunbird/learning/util/ControllerUtil.java @@ -444,6 +444,39 @@ public List getNodes(String graphId, String objectType, List mimeT } } + public List getNodes(String graphId, String objectType, List status, List contentIdsList, double migrationVersion, int startPosition, int batchSize) { + List filters = new ArrayList(); + if(!status.isEmpty()) + filters.add(new Filter("status", SearchConditions.OP_IN, status)); + if(!contentIdsList.isEmpty()) { + filters.add(new Filter("IL_UNIQUE_ID", SearchConditions.OP_IN, contentIdsList)); + } else { + if (migrationVersion == 0) { + filters.add(new Filter("qumlVersion", SearchConditions.OP_IS, Values.NULL)); + filters.add(new Filter("schemaVersion", SearchConditions.OP_IS, Values.NULL)); + } + else if (migrationVersion > 2) filters.add(new Filter("migrationVersion", SearchConditions.OP_EQUAL, migrationVersion)); + } + + SearchCriteria sc = new SearchCriteria(); + sc.setNodeType(SystemNodeTypes.DATA_NODE.name()); + sc.setObjectType(objectType); + sc.setResultSize(batchSize); + sc.setStartPosition(startPosition); + if(!filters.isEmpty() && filters.size()>0) + sc.addMetadata(MetadataCriterion.create(filters)); + Request req = getRequest(graphId, GraphEngineManagers.SEARCH_MANAGER, "searchNodes", + GraphDACParams.search_criteria.name(), sc); + req.put(GraphDACParams.get_tags.name(), true); + Response listRes = getResponse(req); + if (checkError(listRes)) + return null; + else { + List nodes = (List) listRes.get(GraphDACParams.node_list.name()); + return nodes; + } + } + public List getNodesWithInDateRange(String graphId, String objectType, String startDate, String endDate) { List nodeIds = new ArrayList<>(); @@ -840,4 +873,44 @@ public Map getCSPMigrationObjectCount(String graphId, List return counts; } + public Map getQumlMigrationObjectCount(String graphId, List objectTypes, List statusList, List objectIdList, double migrationVersion) { + Map counts = new HashMap(); + Request request = getRequest(graphId, GraphEngineManagers.SEARCH_MANAGER, "executeQueryForProps"); + StringBuilder queryString = new StringBuilder(); + queryString.append("MATCH (n:{0}) WHERE EXISTS(n.IL_FUNC_OBJECT_TYPE) AND n.IL_SYS_NODE_TYPE=\"DATA_NODE\" AND n.IL_FUNC_OBJECT_TYPE IN {1} "); + + if(statusList!=null && !statusList.isEmpty()) + queryString.append(" AND n.status IN {2} "); + + if(objectIdList!=null && !objectIdList.isEmpty()) + queryString.append(" AND n.IL_UNIQUE_ID IN {3} "); + + if(migrationVersion != 0 && migrationVersion > 2) + queryString.append(" AND n.migrationVersion={4} "); + + queryString.append("AND NOT EXISTS(n.qumlVersion) AND NOT EXISTS(n.schemaVersion) RETURN n.IL_FUNC_OBJECT_TYPE AS objectType, COUNT(n) AS count;"); + + System.out.println("Count queryString:: " + MessageFormat.format(queryString.toString(), graphId, new JSONArray(objectTypes), new JSONArray(statusList), migrationVersion, new JSONArray(objectIdList))); + + request.put(GraphDACParams.query.name(), MessageFormat.format(queryString.toString(), graphId, new JSONArray(objectTypes), new JSONArray(statusList), migrationVersion, new JSONArray(objectIdList))); + + List props = new ArrayList(); + props.add("objectType"); + props.add("count"); + request.put(GraphDACParams.property_keys.name(), props); + Response response = getResponse(request); + if (!checkError(response)) { + Map result = response.getResult(); + List> list = (List>) result.get("properties"); + if (null != list && !list.isEmpty()) { + for (int i = 0; i < list.size(); i++) { + Map properties = list.get(i); + counts.put((String) properties.get("objectType"), (Long) properties.get("count")); + } + } + + } + return counts; + } + } \ No newline at end of file diff --git a/platform-tools/spikes/sync-tool/src/main/java/org/sunbird/sync/tool/mgr/QumlMigrationMessageGenerator.java b/platform-tools/spikes/sync-tool/src/main/java/org/sunbird/sync/tool/mgr/QumlMigrationMessageGenerator.java new file mode 100644 index 0000000000..a8e45cce24 --- /dev/null +++ b/platform-tools/spikes/sync-tool/src/main/java/org/sunbird/sync/tool/mgr/QumlMigrationMessageGenerator.java @@ -0,0 +1,215 @@ +package org.sunbird.sync.tool.mgr; + +import org.apache.commons.collections.CollectionUtils; +import org.apache.commons.lang3.StringUtils; +import org.codehaus.jackson.map.ObjectMapper; +import org.springframework.stereotype.Component; +import org.sunbird.common.Platform; +import org.sunbird.common.exception.ClientException; +import org.sunbird.common.exception.ResourceNotFoundException; +import org.sunbird.graph.dac.enums.SystemNodeTypes; +import org.sunbird.graph.dac.model.Node; +import org.sunbird.learning.util.ControllerUtil; +import org.sunbird.sync.tool.util.KafkaUtil; +import org.sunbird.telemetry.util.LogTelemetryEventUtil; + +import javax.annotation.PostConstruct; +import java.util.*; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +@Component +public class QumlMigrationMessageGenerator { + + private ControllerUtil util = new ControllerUtil(); + private static int batchSize = 50; + private ObjectMapper mapper = new ObjectMapper(); + private static String actorId = "quml-migration"; + private static String actorType = "System"; + private static String pdataId = "org.sunbird.platform"; + private static String pdataVersion = "1.0"; + private static String action = "quml-migration"; + private static String migrationTopicName = Platform.config.getString("quml.migration.request.topic"); + + @PostConstruct + private void init() throws Exception { + int batch = Platform.config.hasPath("quml.migration.batch.size") ? Platform.config.getInt("quml.migration.batch.size") : 50; + batchSize = batch; + } + + public void generateMgrMsg(String graphId, String[] objectTypes, String[] status, String[] contentIds, double migrationVersion, Integer limit, Integer delay) throws Exception { + List mimeTypeList = new ArrayList(); + if (StringUtils.isBlank(graphId)) + throw new ClientException("ERR_INVALID_GRAPH_ID", "Graph Id is blank."); + if (null == objectTypes || objectTypes.length == 0) + throw new ClientException("ERR_EMPTY_OBJECT_TYPE", "Object Type is blank."); + List statusList = new ArrayList(); + List contentIdsList = new ArrayList(); + if (null != status && status.length > 0) + statusList = Arrays.asList(status); + if (null != contentIds && contentIds.length > 0) + contentIdsList = Arrays.asList(contentIds); + + Map errors = new HashMap<>(); + long startTime = System.currentTimeMillis(); + System.out.println("-----------------------------------------"); + System.out.println("\nQuML Migration Event Generation starting at " + startTime); + Map counts = util.getQumlMigrationObjectCount(graphId, Arrays.asList(objectTypes), statusList, contentIdsList, migrationVersion); + if (counts.isEmpty()) { + System.out.println("No objects found in this graph."); + } else { + List objTypes = counts.keySet().stream().filter(key -> Arrays.asList(objectTypes).contains(key)).collect(Collectors.toList()); + for (String objectType : objTypes) { + Long count = counts.get(objectType); + System.out.println(count + " - " + objectType + " nodes available for quml migration"); + } + } + for (String objectType : objectTypes) { + Long count = counts.get(objectType); + if (count > 0) { + System.out.println("-----------------------------------------"); + System.out.println("\nGenerating event for object of type " + objectType + " with batch size of " + batchSize + " having delay " + delay + "ms for each batch.\n"); + int start = 0; + int current = 0; + long total = counts.get(objectType); + long stopLimit; + if (limit > 0) { + if (limit < batchSize || limit % batchSize != 0) { + System.out.println("Limit value should be minimum " + batchSize + ". The limit value should be multiple of " + batchSize + ". Setting limit to minimum value. i.e " + batchSize); + stopLimit = batchSize; + } else stopLimit = limit; + } else stopLimit = total; + + System.out.println("QumlMigrationMessageGenerator:: generateMgrMsg:: stopLimit: " + stopLimit + " || total: " + total); + + boolean found = true; + while (found && start < stopLimit) { + List nodes = null; + try { + nodes = util.getNodes(graphId, objectType.trim(), statusList, contentIdsList, migrationVersion, start, batchSize); + } catch (ResourceNotFoundException e) { + System.out.println("Error while fetching neo4j records for objectType=" + objectType + ", start=" + start + ",batchSize=" + batchSize); + start += batchSize; + continue; + } + if (CollectionUtils.isNotEmpty(nodes)) { + System.out.println("QumlMigrationMessageGenerator:: generateMgrMsg:: nodes: " + nodes.size()); + start += batchSize; + Map events = generateMigrationEvent(nodes, errors); + System.out.println("QumlMigrationMessageGenerator:: generateMgrMsg:: events: " + events.size()); + sendEvent(events, errors); + current += events.size(); + printProgress(startTime, total, current); + if (delay > 0) { + Thread.sleep(delay); + } + } else { + System.out.println("QumlMigrationMessageGenerator:: generateMgrMsg:: Breaking Event Generation Loop!"); + found = false; + break; + } + } + if (!errors.isEmpty()) + System.out.println("Error! while generating migration event data from nodes, below nodes are ignored. \n" + errors); + long endTime = System.currentTimeMillis(); + System.out.println("\nQuML Migration Event Generation completed for object of type " + objectType + " in: " + (endTime - startTime) + "ms"); + } else { + System.out.println("\nSkipped Generating migration event for objectType: " + objectType); + } + } + System.out.println("-----------------------------------------"); + long endTime = System.currentTimeMillis(); + System.out.println("QuML Migration Event Generation completed at " + endTime); + System.out.println("Time taken to generate Events: " + (endTime - startTime) + "ms"); + } + + private void sendEvent(Map events, Map errors) { + for (String id : events.keySet()) { + try { + KafkaUtil.send(events.get(id), migrationTopicName); + } catch (Exception e) { + e.printStackTrace(); + System.out.println("Error Message :"+e.getMessage() ); + errors.put(id, "Error While Sending Migration Event for " + id); + } + } + } + + private Map generateMigrationEvent(List nodes, Map errors) { + Map events = new HashMap(); + for (Node node : nodes) { + String message = getEvent(node, errors); + if (StringUtils.isNotBlank(message)) + events.put(node.getIdentifier(), message); + } + return events; + } + + private String getEvent(Node node, Map errors) { + Map actor = new HashMap() {{ + put("id", actorId); + put("type", actorType); + }}; + Map context = new HashMap() {{ + put("channel", node.getMetadata().getOrDefault("channel", "")); + put("pdata", new HashMap() {{ + put("id", pdataId); + put("ver", pdataVersion); + }}); + }}; + if (Platform.config.hasPath("cloud_storage.env")) { + String env = Platform.config.getString("cloud_storage.env"); + context.put("env", env); + } + Map object = new HashMap() {{ + put("id", node.getIdentifier()); + put("ver", node.getMetadata().get("versionKey")); + }}; + Map edata = new HashMap() {{ + put("action", action); + put("metadata", new HashMap() {{ + put("pkgVersion", node.getMetadata().get("pkgVersion")); + put("mimeType", node.getMetadata().get("mimeType")); + put("status", node.getMetadata().get("status")); + put("qumlVersion", node.getMetadata().get("qumlVersion")); + put("schemaVersion", node.getMetadata().get("schemaVersion")); + put("identifier", node.getIdentifier()); + put("objectType", node.getObjectType()); + }}); + }}; + String beJobRequestEvent = LogTelemetryEventUtil.logInstructionEvent(actor, context, object, edata); + if (StringUtils.isBlank(beJobRequestEvent)) { + errors.put(node.getIdentifier(), "Error While Generating Migration Event for " + node.getIdentifier()); + } + return beJobRequestEvent; + } + + private static void printProgress(long startTime, long total, long current) { + long eta = current == 0 ? 0 : + (total - current) * (System.currentTimeMillis() - startTime) / current; + + String etaHms = current == 0 ? "N/A" : + String.format("%02d:%02d:%02d", TimeUnit.MILLISECONDS.toHours(eta), + TimeUnit.MILLISECONDS.toMinutes(eta) % TimeUnit.HOURS.toMinutes(1), + TimeUnit.MILLISECONDS.toSeconds(eta) % TimeUnit.MINUTES.toSeconds(1)); + + StringBuilder string = new StringBuilder(140); + int percent = (int) (current * 100 / total); + string + .append('\r') + .append(String.join("", Collections.nCopies(percent == 0 ? 2 : 2 - (int) (Math.log10(percent)), " "))) + .append(String.format(" %d%% [", percent)) + .append(String.join("", Collections.nCopies(percent, "="))) + .append('>') + .append(String.join("", Collections.nCopies(100 - percent, " "))) + .append(']') + .append(String.join("", Collections.nCopies((int) (Math.log10(total)) - (int) (Math.log10(current)), " "))) + .append(String.format(" %d/%d, ETA: %s", current, total, etaHms)); + + System.out.print(string); + } + + public static void filterMigrationNodes(List nodes, Integer limit) { + nodes.removeIf(n -> SystemNodeTypes.DEFINITION_NODE.name().equals(n.getNodeType())); + } +} diff --git a/platform-tools/spikes/sync-tool/src/main/java/org/sunbird/sync/tool/shell/MigrateQumlDataCommand.java b/platform-tools/spikes/sync-tool/src/main/java/org/sunbird/sync/tool/shell/MigrateQumlDataCommand.java new file mode 100644 index 0000000000..f7d5acd500 --- /dev/null +++ b/platform-tools/spikes/sync-tool/src/main/java/org/sunbird/sync/tool/shell/MigrateQumlDataCommand.java @@ -0,0 +1,41 @@ +package org.sunbird.sync.tool.shell; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.shell.core.CommandMarker; +import org.springframework.shell.core.annotation.CliCommand; +import org.springframework.shell.core.annotation.CliOption; +import org.springframework.stereotype.Component; +import org.sunbird.sync.tool.mgr.CSPMigrationMessageGenerator; +import org.sunbird.sync.tool.mgr.QumlMigrationMessageGenerator; + +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; + +@Component +public class MigrateQumlDataCommand implements CommandMarker { + + @Autowired + QumlMigrationMessageGenerator qumlMsgGenerator; + + @CliCommand(value = "migrateQuml", help = "Generate QuML Data Migration (from 1.0 to 1.1) Event") + public void migrateQuml( + @CliOption(key = {"graphId"}, mandatory = false, unspecifiedDefaultValue = "domain", help = "graphId of the object") final String graphId, + @CliOption(key = {"objectType"}, mandatory = true, help = "Object Type is Required") final String[] objectType, + @CliOption(key = {"status"}, mandatory = false, help = "Specific Status can be passed") final String[] status, + @CliOption(key = {"ids"}, mandatory = false, help = "Specific content Ids can be passed") final String[] contentIds, + @CliOption(key = {"migrationVersion"}, mandatory = false, unspecifiedDefaultValue = "0", help = "Specific migration version can be passed") final double migrationVersion, + @CliOption(key = {"limit"}, mandatory = false, unspecifiedDefaultValue = "0", help = "Specific Limit can be passed") final Integer limit, + @CliOption(key = {"delay"}, mandatory = false, unspecifiedDefaultValue = "10", help = "time gap between each batch") final Integer delay) + throws Exception { + + long startTime = System.currentTimeMillis(); + DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm:ss"); + LocalDateTime start = LocalDateTime.now(); + qumlMsgGenerator.generateMgrMsg(graphId, objectType, status, contentIds, migrationVersion, limit, delay); + long endTime = System.currentTimeMillis(); + long exeTime = endTime - startTime; + System.out.println("Total time of execution: " + exeTime + "ms"); + LocalDateTime end = LocalDateTime.now(); + System.out.println("START_TIME: " + dtf.format(start) + ", END_TIME: " + dtf.format(end)); + } +} diff --git a/platform-tools/spikes/sync-tool/src/main/resources/application.conf b/platform-tools/spikes/sync-tool/src/main/resources/application.conf index 66c49bdd9f..2508e05479 100644 --- a/platform-tools/spikes/sync-tool/src/main/resources/application.conf +++ b/platform-tools/spikes/sync-tool/src/main/resources/application.conf @@ -101,3 +101,9 @@ csp.migration.batch.size=50 is_replace_string=false replace_src_string= "" replace_dest_string="" +replace_src_string_DIAL_store="" +replace_dest_string_DIAL_store="" + +quml.migration.request.topic="dev.quml.migration.job.request" +quml.migration.batch.size=50 + From 2b4396b20aa45cb5c10d93029b1743f10f5a2be7 Mon Sep 17 00:00:00 2001 From: Kartheek Palla Date: Mon, 16 Oct 2023 17:58:55 +0530 Subject: [PATCH 211/222] merge Release 5.6.0 into 5.7.0 (#1973) * Add task to start neo4j * merge Release 5.5.0 into 5.6.0 (#1971) * Issue #KN-427 feat: Knowledge service cloud-agnostic * added artifact upload for oci oss storage Signed-off-by: Deepak Devadathan * owerwrite oss object if already exists Signed-off-by: Deepak Devadathan * testing with artifact download for neo4j from oci oss Signed-off-by: Deepak Devadathan * changed the oss object download command Signed-off-by: Deepak Devadathan * added checkpoint Signed-off-by: Deepak Devadathan * removed checkpoint for debuging Signed-off-by: Deepak Devadathan * added a cloud storage type selector for oci Signed-off-by: Deepak Devadathan * jinja indentation Signed-off-by: Deepak Devadathan * jinja indentation correction Signed-off-by: Deepak Devadathan * removed the extra line for cloud_storage_type Signed-off-by: Deepak Devadathan * added s3 credential configuration for flink Signed-off-by: Deepak Devadathan * disabled s3 storage temporarily Signed-off-by: Deepak Devadathan * s3 config for flink jobmanager only Signed-off-by: Deepak Devadathan * corrected if else syntax Signed-off-by: Deepak Devadathan * added s3 config for taskmanager Signed-off-by: Deepak Devadathan * added base url configuration for s3 Signed-off-by: Deepak Devadathan * jinja templating syntax error correction Signed-off-by: Deepak Devadathan * corrected base url for s3 Signed-off-by: Deepak Devadathan * updated flink-conf for questionset-publish Signed-off-by: Deepak Devadathan * testing with s3 configuration Signed-off-by: Deepak Devadathan * endpoint url changed home region Signed-off-by: Deepak Devadathan * updated values.j2 cloud_service_provider cloud_storage_endpoint Signed-off-by: Deepak Devadathan * added closing (") for variable cloud_service_provider Signed-off-by: Deepak Devadathan * added hadoop-fs parameters to flink-conf for questionset Signed-off-by: Deepak Devadathan * corrected the flink-conf for questionset Signed-off-by: Deepak Devadathan * updated presto specific variables in flink-conf.yaml Signed-off-by: Deepak Devadathan * added s3-fs-hadoop values in flink-conf for question set Signed-off-by: Deepak Devadathan * updated the correct values for flink-conf Signed-off-by: Deepak Devadathan * removed values form flink-conf.yaml Signed-off-by: Deepak Devadathan * updated the deployment template Signed-off-by: Deepak Devadathan * added hive.s3.path-style-access Signed-off-by: Deepak Devadathan * removed hive access path Signed-off-by: Deepak Devadathan * template value for path.style Signed-off-by: Deepak Devadathan * added s3.aws.cred provider Signed-off-by: Deepak Devadathan * removed s3.aws.credentials.provider Signed-off-by: Deepak Devadathan * testing with flink-conf.yaml Signed-off-by: Deepak Devadathan * added s3.aws.credentials.provider Signed-off-by: Deepak Devadathan * updated value of s3.aws.credentials.provider Signed-off-by: Deepak Devadathan * removed s3.aws.credentials.provider Signed-off-by: Deepak Devadathan * added s3.aws.credentials.provider for asset-enrichment Signed-off-by: Deepak Devadathan * added acces key for asset enrichment Signed-off-by: Deepak Devadathan * added provider in the jobmanager deployment Signed-off-by: Deepak Devadathan * removed aws provider from deployment and asset-enrichment Signed-off-by: Deepak Devadathan * added aws cred provider in job mgr deployment Signed-off-by: Deepak Devadathan * testing with signing algo Signed-off-by: Deepak Devadathan * testing with S3SignerType Signed-off-by: Deepak Devadathan * update AWS3SignerType Signed-off-by: Deepak Devadathan * removed signing algorithm Signed-off-by: Deepak Devadathan * trying with s3a Signed-off-by: Deepak Devadathan * reverted to s3 Signed-off-by: Deepak Devadathan * AWS4SignerType algo Signed-off-by: Deepak Devadathan * AWS4UnsignedPayloadSignerType Signed-off-by: Deepak Devadathan * NoOpSignerType algo Signed-off-by: Deepak Devadathan * S3SignerType Signed-off-by: Deepak Devadathan * removed signer algo Signed-off-by: Deepak Devadathan * added aws logging Signed-off-by: Deepak Devadathan * added http logging Signed-off-by: Deepak Devadathan * added flink debug logs Signed-off-by: Deepak Devadathan * added http logging Signed-off-by: Deepak Devadathan * removed http logging Signed-off-by: Deepak Devadathan * testing bucket probing Signed-off-by: Deepak Devadathan * bucketprobe=2 Signed-off-by: Deepak Devadathan * bucketprobe=1 Signed-off-by: Deepak Devadathan * disable bucket probe Signed-off-by: Deepak Devadathan * removed bucket probe Signed-off-by: Deepak Devadathan * testing with disabing bucket probe Signed-off-by: Deepak Devadathan * passing the region to hadoop configuration Signed-off-by: Deepak Devadathan * presto config Signed-off-by: Deepak Devadathan * added the region to deployment Signed-off-by: Deepak Devadathan * updated taskmanager with presto variables Signed-off-by: Deepak Devadathan * removed oss connection details removed from flink-conf.yaml for questionset-publish as it no longer required Signed-off-by: Deepak Devadathan * Added oci cloud-store-sdk updated artifact id to cloud-store-sdk_2.11 version: 1.45 Signed-off-by: Deepak Devadathan * cloud-store-sdk with exclusion Signed-off-by: Deepak Devadathan * removed exlusion from cloud-store-sdk dependency Signed-off-by: Deepak Devadathan * rearranged modules for service Signed-off-by: Deepak Devadathan * reverted modules ordering Signed-off-by: Deepak Devadathan * added exclusion for guava for cloud-store-sdk Signed-off-by: Deepak Devadathan * removed exlusion for guava Signed-off-by: Deepak Devadathan * updated cassandra driver version to 3.2.0 This is test guava 20 Signed-off-by: Deepak Devadathan * update cloud-store-sdk version to 1.4.6 Signed-off-by: Deepak Devadathan * passing an empty region in cloud-store-sdk 1.4.6 region is defined as an Option , so need to pass it as a optional string Signed-off-by: Deepak Devadathan * updated cassandra driver to 3.1.2 cloud-store-sdk 1.4.6 uses guava 19, so reverted the change from 3.2.0 Signed-off-by: Deepak Devadathan * Updated the oss_bucket_name * Updated the instance principal * Added env var for OCI * Corrected the oss bucket name key * added oci specs for video-stream deployment for kp flinkjobs Signed-off-by: Deepak Devadathan * updated the zookeeper ip from local host to the ingestion and processing Signed-off-by: Deepak Devadathan * disabled service_fact gathering Signed-off-by: Deepak Devadathan * disabled service_facts Signed-off-by: Deepak Devadathan * Update values.j2 To add the media workflow id * Issue #KN-0000 feat: Config update * Issue #KN-0000 feat: Config update * Issue #KN-0000 feat: Config update * Issue #KN-0000 feat: Config update * added endpoint to flinkjob configuration Signed-off-by: Deepak Devadathan * added the alter replication and partition Signed-off-by: Deepak Devadathan * testing list length condition Signed-off-by: Deepak Devadathan * added condition to create topic Signed-off-by: Deepak Devadathan * removed replication factor alter Signed-off-by: Deepak Devadathan * oci media streaming changes * added cloud_storage_type key Signed-off-by: Deepak Devadathan * udpated cloud-store-sdk id Signed-off-by: Deepak Devadathan * resolved conflicts Signed-off-by: Deepak Devadathan * added cloud_storage_proxy_host for qrcode job Signed-off-by: Deepak Devadathan * added value for proxy host Signed-off-by: Deepak Devadathan * Issue #KN-195 feat: CSP migrator parallelism increase * To troubleshoot download content issue * Adding the missing property * Reverting the changes made in this deploy branch * missing property * for testing publish unit test Signed-off-by: Deepak Devadathan * updated the continer name value for unit test case Signed-off-by: Deepak Devadathan * testing unit tests Signed-off-by: Deepak Devadathan * updated endpoint for unit testing Signed-off-by: Deepak Devadathan * youtube chcks Signed-off-by: Deepak Devadathan * for testing unit case with azure Signed-off-by: Deepak Devadathan * testing unit cse Signed-off-by: Deepak Devadathan * added cloud ep for unit test Signed-off-by: Deepak Devadathan * adding the missing config * updated generic variables Signed-off-by: Deepak Devadathan * for test runs in circleci Signed-off-by: Deepak Devadathan * updated test data Signed-off-by: Deepak Devadathan * updated youtube link Signed-off-by: Deepak Devadathan * updated invalid youtube links Signed-off-by: Deepak Devadathan * updated youtube link for test cases Signed-off-by: Deepak Devadathan * testing creative commons lic Signed-off-by: Deepak Devadathan * passing mime type field for test case Signed-off-by: Deepak Devadathan * reverted the mimetype value Signed-off-by: Deepak Devadathan * KN-CSP Changes * KN-CSP Changes * KN-Fix For CSP Changes * KN-Fix For CSP Changes * KN-Fix For CSP Changes * KN-Fix For CSP Changes * KN-Fix For CSP Changes * KN-Fix For CSP Changes * KN-Fix For CSP Changes * KN-Fix For CSP Changes * KN-Fix For CSP Changes * KN-Fix For CSP Changes * Issue #KN-920 fix: Update kubernetes/ansible/roles/flink-jobs-deploy/defaults/main.yml Co-authored-by: Mahesh Kumar Gangula * Revert "KN-CSP Changes" * Issue KN-921 fix: config issue fix * Issue KN-921 fix: config issue fix * Issue KN-921 fix: config issue fix * Issue KN-921 fix: AWS support for Flink jobs --------- Signed-off-by: Deepak Devadathan Co-authored-by: Jayaprakash8887 Co-authored-by: vinukumar-vs Co-authored-by: Deepak Devadathan Co-authored-by: Kenneth Heung Co-authored-by: subhash_chandra_budde Co-authored-by: rjanart <123344037+rjanart@users.noreply.github.com> Co-authored-by: Anil Gupta Co-authored-by: Ramya Co-authored-by: Aiman Sharief Co-authored-by: Mahesh Kumar Gangula --------- Signed-off-by: Deepak Devadathan Co-authored-by: santhosh-tg Co-authored-by: Jayaprakash8887 Co-authored-by: vinukumar-vs Co-authored-by: Deepak Devadathan Co-authored-by: Kenneth Heung Co-authored-by: subhash_chandra_budde Co-authored-by: rjanart <123344037+rjanart@users.noreply.github.com> Co-authored-by: Anil Gupta Co-authored-by: Ramya Co-authored-by: Aiman Sharief Co-authored-by: Mahesh Kumar Gangula --- ansible/artifacts-download.yml | 13 +++ ansible/artifacts-upload.yml | 14 +++ ansible/inventory/env/group_vars/all.yml | 2 +- ansible/roles/learning-service/tasks/main.yml | 4 +- .../templates/application.conf.j2 | 1 + ansible/roles/logstash-deploy/tasks/main.yml | 4 +- ansible/roles/neo4j-community/tasks/main.yml | 6 ++ .../roles/oci-cloud-storage/defaults/main.yml | 3 + .../oci-cloud-storage/tasks/delete-folder.yml | 5 ++ .../roles/oci-cloud-storage/tasks/delete.yml | 7 ++ .../oci-cloud-storage/tasks/download.yml | 7 ++ .../roles/oci-cloud-storage/tasks/main.yml | 18 ++++ .../oci-cloud-storage/tasks/upload-folder.yml | 8 ++ .../roles/oci-cloud-storage/tasks/upload.yml | 8 ++ ansible/roles/setup-kafka/defaults/main.yml | 3 + ansible/roles/setup-kafka/tasks/main.yml | 28 ++++-- .../roles/flink-jobs-deploy/defaults/main.yml | 3 +- .../templates/flink_job_deployment.yaml | 18 +++- .../helm_charts/datapipeline_jobs/values.j2 | 85 +++++++++++++++++-- .../src/test/resources/application.conf | 8 +- platform-modules/actors/pom.xml | 2 +- .../org/sunbird/learning/util/CloudStore.java | 16 +++- .../src/test/resources/application.conf | 4 + .../taxonomy/util/YouTubeUrlUtilTest.java | 4 +- .../Contents/testEcmlMediaYoutube/index.ecml | 4 +- platform-modules/pom.xml | 2 +- platform-tools/spikes/content-tool/pom.xml | 2 +- .../content/tool/CloudStoreManager.java | 17 +++- 28 files changed, 266 insertions(+), 30 deletions(-) create mode 100644 ansible/roles/oci-cloud-storage/defaults/main.yml create mode 100644 ansible/roles/oci-cloud-storage/tasks/delete-folder.yml create mode 100644 ansible/roles/oci-cloud-storage/tasks/delete.yml create mode 100644 ansible/roles/oci-cloud-storage/tasks/download.yml create mode 100644 ansible/roles/oci-cloud-storage/tasks/main.yml create mode 100644 ansible/roles/oci-cloud-storage/tasks/upload-folder.yml create mode 100644 ansible/roles/oci-cloud-storage/tasks/upload.yml diff --git a/ansible/artifacts-download.yml b/ansible/artifacts-download.yml index 4477368ec4..2fb5a11ffa 100644 --- a/ansible/artifacts-download.yml +++ b/ansible/artifacts-download.yml @@ -40,3 +40,16 @@ aws_access_key_id: "{{ cloud_artifact_storage_accountname }}" aws_secret_access_key: "{{ cloud_artifact_storage_secret }}" when: cloud_service_provider == "aws" + + - name: download artifact from oci oss storage + include_role: + name: oci-cloud-storage + apply: + environment: + OCI_CLI_AUTH: "instance_principal" + tasks_from: download.yml + vars: + oss_bucket_name: "{{ cloud_storage_artifacts_bucketname }}" + oss_object_name: "{{ artifact }}" + local_file_or_folder_path: "{{ artifact_path }}" + when: cloud_service_provider == "oci" \ No newline at end of file diff --git a/ansible/artifacts-upload.yml b/ansible/artifacts-upload.yml index 99833f9d10..cd296004e4 100644 --- a/ansible/artifacts-upload.yml +++ b/ansible/artifacts-upload.yml @@ -28,6 +28,7 @@ gcp_path: "{{ artifact }}" local_file_or_folder_path: "{{ artifact_path }}" when: cloud_service_provider == "gcloud" + - name: upload artifact to aws s3 include_role: @@ -41,3 +42,16 @@ aws_access_key_id: "{{ cloud_artifact_storage_accountname }}" aws_secret_access_key: "{{ cloud_artifact_storage_secret }}" when: cloud_service_provider == "aws" + + - name: upload artifact to oci oss + include_role: + name: oci-cloud-storage + apply: + environment: + OCI_CLI_AUTH: "instance_principal" + tasks_from: upload.yml + vars: + local_file_or_folder_path: "{{ artifact_path }}" + oss_bucket_name: "{{ cloud_storage_artifacts_bucketname }}" + oss_path: "{{ artifact }}" + when: cloud_service_provider == "oci" \ No newline at end of file diff --git a/ansible/inventory/env/group_vars/all.yml b/ansible/inventory/env/group_vars/all.yml index e022d3f800..781983feef 100644 --- a/ansible/inventory/env/group_vars/all.yml +++ b/ansible/inventory/env/group_vars/all.yml @@ -123,7 +123,7 @@ enable_rc_certificate: true # SB-31155 plugin_storage: "{{ plugin_container_name }}" - +cloud_storage_endpoint: "{{ cloud_public_storage_endpoint }}" cloudstorage_relative_path_prefix_content: "CONTENT_STORAGE_BASE_PATH" cloudstorage_relative_path_prefix_dial: "DIAL_STORAGE_BASE_PATH" cloudstorage_metadata_list: '["appIcon", "artifactUrl", "posterImage", "previewUrl", "thumbnail", "assetsMap", "certTemplate", "itemSetPreviewUrl", "grayScaleAppIcon", "sourceURL", "variants", "downloadUrl", "streamingUrl", "toc_url", "data", "question", "solutions", "editorState", "media", "pdfUrl", "transcripts"]' \ No newline at end of file diff --git a/ansible/roles/learning-service/tasks/main.yml b/ansible/roles/learning-service/tasks/main.yml index 00feb5ba6c..ca5e06ffa9 100644 --- a/ansible/roles/learning-service/tasks/main.yml +++ b/ansible/roles/learning-service/tasks/main.yml @@ -1,5 +1,5 @@ -- name: checking the list of installed services - service_facts: +# - name: checking the list of installed services +# service_facts: - name: Stop the monit service: name=monit state=stopped diff --git a/ansible/roles/learning-service/templates/application.conf.j2 b/ansible/roles/learning-service/templates/application.conf.j2 index 27feedd67e..4590e18bb7 100644 --- a/ansible/roles/learning-service/templates/application.conf.j2 +++ b/ansible/roles/learning-service/templates/application.conf.j2 @@ -253,6 +253,7 @@ cloud_storage_type="{{ cloud_service_provider }}" cloud_storage_key="{{ cloud_public_storage_accountname }}" cloud_storage_secret="{{ cloud_public_storage_secret }}" cloud_storage_container="{{ cloud_storage_content_bucketname }}" +cloud_storage_endpoint="{{ cloud_public_storage_endpoint }}" installation.id="{{ instance_name }}" diff --git a/ansible/roles/logstash-deploy/tasks/main.yml b/ansible/roles/logstash-deploy/tasks/main.yml index fdeb62fe28..350c02f979 100644 --- a/ansible/roles/logstash-deploy/tasks/main.yml +++ b/ansible/roles/logstash-deploy/tasks/main.yml @@ -1,5 +1,5 @@ -- name: checking the list of installed services - service_facts: +# - name: checking the list of installed services +# service_facts: - name: Stop the monit service: name=monit state=stopped diff --git a/ansible/roles/neo4j-community/tasks/main.yml b/ansible/roles/neo4j-community/tasks/main.yml index d2460ebd13..ca450efe78 100644 --- a/ansible/roles/neo4j-community/tasks/main.yml +++ b/ansible/roles/neo4j-community/tasks/main.yml @@ -62,3 +62,9 @@ template: src=neo4j-wrapper.conf.j2 dest={{ neo4j_home }}/conf/neo4j-wrapper.conf group={{learner_user}} owner={{learner_user}} when: dbms_mode != "ARBITER" +- name: Start neo4j + become: yes + become_user: "{{ learner_user }}" + shell: bin/neo4j start + args: + chdir: "{{ neo4j_home }}" diff --git a/ansible/roles/oci-cloud-storage/defaults/main.yml b/ansible/roles/oci-cloud-storage/defaults/main.yml new file mode 100644 index 0000000000..72727de167 --- /dev/null +++ b/ansible/roles/oci-cloud-storage/defaults/main.yml @@ -0,0 +1,3 @@ +oss_bucket_name: "" +oss_path: "" +local_file_or_folder_path: "" diff --git a/ansible/roles/oci-cloud-storage/tasks/delete-folder.yml b/ansible/roles/oci-cloud-storage/tasks/delete-folder.yml new file mode 100644 index 0000000000..6ed4e6b8b4 --- /dev/null +++ b/ansible/roles/oci-cloud-storage/tasks/delete-folder.yml @@ -0,0 +1,5 @@ +--- +- name: delete files and folders recursively + shell: "oci os object bulk-delete -ns {{oss_namespace}} -bn {{oss_bucket_name}} --prefix {{oss_path}} --force" + async: 3600 + poll: 10 diff --git a/ansible/roles/oci-cloud-storage/tasks/delete.yml b/ansible/roles/oci-cloud-storage/tasks/delete.yml new file mode 100644 index 0000000000..65d18843ca --- /dev/null +++ b/ansible/roles/oci-cloud-storage/tasks/delete.yml @@ -0,0 +1,7 @@ +- name: Ensure oci oss bucket exists + command: oci os bucket get --name {{ oss_bucket_name }} + +- name: Upload to oci oss bucket + command: oci os object delete -bn {{ oss_bucket_name }} --name {{ oss_path }} --force + async: 3600 + poll: 10 \ No newline at end of file diff --git a/ansible/roles/oci-cloud-storage/tasks/download.yml b/ansible/roles/oci-cloud-storage/tasks/download.yml new file mode 100644 index 0000000000..838ecd544e --- /dev/null +++ b/ansible/roles/oci-cloud-storage/tasks/download.yml @@ -0,0 +1,7 @@ +- name: Ensure oci oss bucket exists + command: oci os bucket get --name {{ oss_bucket_name }} + +- name: download files from oci oss bucket + command: oci os object get -bn {{ oss_bucket_name }} --name {{ oss_object_name }} --file {{ local_file_or_folder_path }} + async: 3600 + poll: 10 \ No newline at end of file diff --git a/ansible/roles/oci-cloud-storage/tasks/main.yml b/ansible/roles/oci-cloud-storage/tasks/main.yml new file mode 100644 index 0000000000..6f9dca6b63 --- /dev/null +++ b/ansible/roles/oci-cloud-storage/tasks/main.yml @@ -0,0 +1,18 @@ +--- +- name: delete files from oci oss bucket + include: delete.yml + +- name: delete folders from oci oss bucket recursively + include: delete-folder.yml + + +- name: download file from oss + include: download.yml + +- name: upload files from a local to oci oss + include: upload.yml + +- name: upload files and folder from local directory to oci oss + include: upload-folder.yml + + diff --git a/ansible/roles/oci-cloud-storage/tasks/upload-folder.yml b/ansible/roles/oci-cloud-storage/tasks/upload-folder.yml new file mode 100644 index 0000000000..6e4d06562c --- /dev/null +++ b/ansible/roles/oci-cloud-storage/tasks/upload-folder.yml @@ -0,0 +1,8 @@ +--- +- name: Ensure oci oss bucket exists + command: oci os bucket get --name {{ oss_bucket_name }} + +- name: Upload folder to oci oss bucket + command: oci os object bulk-upload -bn {{ oss_bucket_name }} --prefix {{ oss_path }} --src-dir {{ local_file_or_folder_path }} --content-type auto + async: 3600 + poll: 10 diff --git a/ansible/roles/oci-cloud-storage/tasks/upload.yml b/ansible/roles/oci-cloud-storage/tasks/upload.yml new file mode 100644 index 0000000000..9e1ceb4289 --- /dev/null +++ b/ansible/roles/oci-cloud-storage/tasks/upload.yml @@ -0,0 +1,8 @@ +--- +- name: Ensure oci oss bucket exists + command: oci os bucket get --name {{ oss_bucket_name }} + +- name: Upload to oci oss bucket + command: oci os object put -bn {{ oss_bucket_name }} --name {{ oss_path }} --file {{ local_file_or_folder_path }} --content-type auto --force + async: 3600 + poll: 10 diff --git a/ansible/roles/setup-kafka/defaults/main.yml b/ansible/roles/setup-kafka/defaults/main.yml index 410bf4c374..6c5d3e848a 100644 --- a/ansible/roles/setup-kafka/defaults/main.yml +++ b/ansible/roles/setup-kafka/defaults/main.yml @@ -2,6 +2,9 @@ env: dev ingestion_kafka_topics: "" ingestion_kafka_overriden_topics: "" +ingestion_zookeeper_ip: "{{ groups['ingestion-cluster-zookeeper'][0] }}" +processing_zookeeper_ip: "{{ groups['processing-cluster-zookeepers'][0] }}" + processing_kafka_topics: - name: telemetry.raw num_of_partitions: 4 diff --git a/ansible/roles/setup-kafka/tasks/main.yml b/ansible/roles/setup-kafka/tasks/main.yml index 5080f951e9..0b817e0a5e 100644 --- a/ansible/roles/setup-kafka/tasks/main.yml +++ b/ansible/roles/setup-kafka/tasks/main.yml @@ -1,29 +1,45 @@ - name: create topics - command: /opt/kafka/bin/kafka-topics.sh --zookeeper localhost:2181 --create --topic {{env}}.{{item.name}} --partitions {{ item.num_of_partitions }} --replication-factor {{ item.replication_factor }} + command: /opt/kafka/bin/kafka-topics.sh --zookeeper {{ingestion_zookeeper_ip}}:2181 --create --topic {{env}}.{{item.name}} --partitions {{ item.num_of_partitions }} --replication-factor {{ item.replication_factor }} with_items: "{{ingestion_kafka_topics}}" ignore_errors: true - when: kafka_id=="1" + when: kafka_id=="1" and ingestion_kafka_topics | length > 0 tags: - ingestion-kafka - name: override retention time - command: /opt/kafka/bin/kafka-topics.sh --zookeeper localhost:2181 --alter --topic {{env}}.{{item.name}} --config retention.ms={{ item.retention_time }} + command: /opt/kafka/bin/kafka-topics.sh --zookeeper {{ingestion_zookeeper_ip}}:2181 --alter --topic {{env}}.{{item.name}} --config retention.ms={{ item.retention_time }} with_items: "{{ingestion_kafka_overriden_topics}}" when: kafka_id=="1" and item.retention_time is defined tags: - ingestion-kafka + +- name: override partition count + command: /opt/kafka/bin/kafka-topics.sh --zookeeper {{ingestion_zookeeper_ip}}:2181 --alter --topic {{env}}.{{item.name}} --partitions {{ item.num_of_partitions }} + with_items: "{{ingestion_kafka_overriden_topics}}" + when: kafka_id=="1" and item.num_of_partitions is defined + tags: + - ingestion-kafka + - name: create topics - command: /opt/kafka/bin/kafka-topics.sh --zookeeper localhost:2181 --create --topic {{env}}.{{item.name}} --partitions {{ item.num_of_partitions }} --replication-factor {{ item.replication_factor }} + command: /opt/kafka/bin/kafka-topics.sh --zookeeper {{processing_zookeeper_ip}}:2181 --create --topic {{env}}.{{item.name}} --partitions {{ item.num_of_partitions }} --replication-factor {{ item.replication_factor }} with_items: "{{processing_kafka_topics}}" ignore_errors: true - when: kafka_id=="1" + when: kafka_id=="1" and processing_kafka_topics | length > 0 tags: - processing-kafka - name: override retention time - command: /opt/kafka/bin/kafka-topics.sh --zookeeper localhost:2181 --alter --topic {{env}}.{{item.name}} --config retention.ms={{ item.retention_time }} + command: /opt/kafka/bin/kafka-topics.sh --zookeeper {{processing_zookeeper_ip}}:2181 --alter --topic {{env}}.{{item.name}} --config retention.ms={{ item.retention_time }} with_items: "{{processing_kafka_overriden_topics}}" when: kafka_id=="1" and item.retention_time is defined tags: - processing-kafka + + +- name: override partition count + command: /opt/kafka/bin/kafka-topics.sh --zookeeper {{processing_zookeeper_ip}}:2181 --alter --topic {{env}}.{{item.name}} --partitions {{ item.num_of_partitions }} + with_items: "{{processing_kafka_overriden_topics}}" + when: kafka_id=="1" and item.num_of_partitions is defined + tags: + - processing-kafka \ No newline at end of file diff --git a/kubernetes/ansible/roles/flink-jobs-deploy/defaults/main.yml b/kubernetes/ansible/roles/flink-jobs-deploy/defaults/main.yml index 220e2f9fa7..dddf953d4e 100644 --- a/kubernetes/ansible/roles/flink-jobs-deploy/defaults/main.yml +++ b/kubernetes/ansible/roles/flink-jobs-deploy/defaults/main.yml @@ -277,7 +277,7 @@ flink_job_names: replica: 1 jobmanager_memory: "{{live_node_publisher_job_memory | default('2048m') }}" taskmanager_memory: "{{live_node_publisher_task_memory | default('2048m') }}" - taskslots: "{{live_node_publisher_taskslots | default('3') }}" + taskslots: "{{live_node_publisher_taskslots | default('1') }}" cpu_requests: "{{live_node_publisher_cpu_requests | default('0.7') }}" live-video-stream-generator: job_class_name: 'org.sunbird.job.livevideostream.task.LiveVideoStreamGeneratorStreamTask' @@ -388,6 +388,7 @@ csp_migrator_timer_duration: 1800 csp_migrator_max_retries: 10 csp_migrator_consumer_parallelism: 1 csp_migrator_cassandra_parallelism: 1 +csp_migrator_router_parallelism: 1 csp_migration_topic_name: "{{ env_name }}.csp.migration.job.request" csp_migrator_group_name: "{{ env_name }}-csp-migrator-group" csp_migrator_failed_topic_name: "{{ env_name }}.csp.migration.job.request.failed" diff --git a/kubernetes/helm_charts/datapipeline_jobs/templates/flink_job_deployment.yaml b/kubernetes/helm_charts/datapipeline_jobs/templates/flink_job_deployment.yaml index ebd851a454..18c4729df2 100644 --- a/kubernetes/helm_charts/datapipeline_jobs/templates/flink_job_deployment.yaml +++ b/kubernetes/helm_charts/datapipeline_jobs/templates/flink_job_deployment.yaml @@ -109,8 +109,16 @@ spec: workingDir: /opt/flink command: ["/opt/flink/bin/standalone-job.sh"] args: ["start-foreground", - "--job-classname={{ .Values.job_classname }}", + "--job-classname={{ .Values.job_classname }}", + {{- if eq .Values.csp "oci" }} + "-Dpresto.s3.access-key={{ .Values.s3_access_key}}", + "-Dpresto.s3.secret-key={{ .Values.s3_secret_key }}", + "-Dpresto.s3.endpoint={{ .Values.s3_endpoint }}", + "-Dpresto.s3.region={{ .Values.s3_region }}", + "-Dpresto.s3.path-style-access={{ .Values.s3_path_style_access }}", + {{- else}} "-Dfs.azure.account.key.{{ .Values.azure_account }}.blob.core.windows.net={{ .Values.azure_secret }}", + {{- end}} "-Dweb.submit.enable=false", "-Dmetrics.reporter.prom.class=org.apache.flink.metrics.prometheus.PrometheusReporter", "-Dmetrics.reporter.prom.port={{ .Values.jobmanager.prom_port }}", @@ -183,7 +191,15 @@ spec: workingDir: {{ .Values.taskmanager.flink_work_dir }} command: ["/opt/flink/bin/taskmanager.sh"] args: ["start-foreground", + {{- if eq .Values.csp "oci" }} + "-Dpresto.s3.access.key={{ .Values.s3_access_key}}", + "-Dpresto.s3.secret.key={{ .Values.s3_secret_key }}", + "-Dpresto.s3.endpoint={{ .Values.s3_endpoint }}", + "-Dpresto.s3.endpoint={{ .Values.s3_region }}", + "-Dpresto.s3.path.style.access={{ .Values.s3_path_style_access }}", + {{- else}} "-Dfs.azure.account.key.{{ .Values.azure_account }}.blob.core.windows.net={{ .Values.azure_secret }}", + {{- end}} "-Dweb.submit.enable=false", "-Dmetrics.reporter.prom.class=org.apache.flink.metrics.prometheus.PrometheusReporter", "-Dmetrics.reporter.prom.host={{ .Release.Name }}-taskmanager", diff --git a/kubernetes/helm_charts/datapipeline_jobs/values.j2 b/kubernetes/helm_charts/datapipeline_jobs/values.j2 index 301e0f411d..ff8fcb817d 100644 --- a/kubernetes/helm_charts/datapipeline_jobs/values.j2 +++ b/kubernetes/helm_charts/datapipeline_jobs/values.j2 @@ -3,8 +3,22 @@ imagepullsecrets: {{ imagepullsecrets }} dockerhub: {{ dockerhub }} repository: {{flink_repository|default('knowledge-platform-jobs')}} image_tag: {{ image_tag }} +csp: {{cloud_service_provider}} +cloud_storage_key: {{cloud_public_storage_accountname}} +cloud_storage_secret: {{cloud_public_storage_secret}} +cloud_storage_container: {{cloud_storage_content_bucketname}} +cloud_storage_endpoint: {{cloudstorage_sdk_endpoint}} azure_account: {{ azure_account }} azure_secret: {{ azure_secret }} +s3_access_key: {{ cloud_public_storage_accountname }} +s3_secret_key: {{cloud_public_storage_secret}} +{% if cloud_service_provider == "oci" %} +s3_endpoint: {{oci_flink_s3_storage_endpoint}} +s3_region: {{s3_region}} +s3_path_style_access: true +{% else %} +s3_endpoint: {{cloud_public_storage_endpoint}} +{% endif %} serviceMonitor: enabled: {{ service_monitor_enabled | lower}} @@ -38,8 +52,8 @@ log4j_console_properties: | rootLogger.appenderRef.console.ref = ConsoleAppender # Uncomment this if you want to _only_ change Flink's logging - #logger.flink.name = org.apache.flink - #logger.flink.level = {{ flink_jobs_console_log_level | default(INFO) }} + # logger.flink.name = org.apache.flink + # logger.flink.level = {{ flink_jobs_console_log_level | default(INFO) }} # The following lines keep the log level of common libraries/connectors on # log level INFO. The root logger does not override this. You have to manually @@ -53,6 +67,8 @@ log4j_console_properties: | logger.zookeeper.name = org.apache.zookeeper logger.zookeeper.level = {{ flink_libraries_log_level | default(INFO) }} + + # Log all infos to the console appender.console.name = ConsoleAppender appender.console.type = CONSOLE @@ -73,7 +89,19 @@ base_config: | } job { env = "{{ env_name }}" - enable.distributed.checkpointing = true + enable.distributed.checkpointing = false +{% if cloud_service_provider == "oci" %} + statebackend { + s3 { + storage { + endpoint = "{{ oci_flink_s3_storage_endpoint }}" + container = "{{ flink_container_name }}" + checkpointing.dir = "checkpoint" + } + } + base.url = "s3://"${job.statebackend.s3.storage.container}"/"${job.statebackend.s3.storage.checkpointing.dir} + } +{% else if cloud_service_provider == "azure" %} statebackend { blob { storage { @@ -84,6 +112,19 @@ base_config: | } base.url = "wasbs://"${job.statebackend.blob.storage.container}"@"${job.statebackend.blob.storage.account}"/"${job.statebackend.blob.storage.checkpointing.dir} } +{% else if cloud_service_provider == "aws" %} + statebackend { + s3 { + storage { + endpoint = "{{ cloud_storage_endpoint }}" + container = "{{ cloud_storage_container }}" + checkpointing.dir = "checkpoint" + } + } + base.url = "s3://"${job.statebackend.s3.storage.container}"/"${job.statebackend.s3.storage.checkpointing.dir} + } +{% endif %} + } task { parallelism = 1 @@ -232,7 +273,25 @@ video-stream-generator: protocol="Hls" } } - +{% if cloud_service_provider == "oci" %} + #OCI Elemental Media Convert Config + oci { + region="{{oci_media_region}}" + compartment_id="{{oci_media_compartment}}" + namespace="{{oci_media_namespace}}" + bucket { + content_bucket_name="{{oci_media_source_bucket}}" + processed_bucket_name="{{oci_media_target_bucket}}" + } + stream { + prefix_input="{{oci_media_prefix_input}}" + distribution_channel_id="{{oci_media_dist_channel_id}}" + work_flow_id="{{ oci_media_work_flow_id }}" + stream_package_config_id="{{oci_media_stream_config_id}}" + gateway_domain="{{oci_media_gateway_domain}}" + } + } +{% endif %} flink-conf: |+ jobmanager.memory.flink.size: {{ flink_job_names['video-stream-generator'].jobmanager_memory }} @@ -320,6 +379,7 @@ asset-enrichment: cloud_storage_key="{{ cloud_public_storage_accountname }}" cloud_storage_secret="{{ cloud_public_storage_secret }}" cloud_storage_container="{{ cloud_storage_content_bucketname }}" + cloud_storage_endpoint="{{cloudstorage_sdk_endpoint}}" cloudstorage { metadata.replace_absolute_path={{ cloudstorage_replace_absolute_path | default('false') }} @@ -337,6 +397,7 @@ asset-enrichment: jobmanager.execution.failover-strategy: region taskmanager.memory.network.fraction: 0.1 + audit-history-indexer: audit-history-indexer: |+ include file("/data/flink/conf/base-config.conf") @@ -388,6 +449,7 @@ auto-creator-v2: cloud_storage_key="{{ cloud_public_storage_accountname }}" cloud_storage_secret="{{ cloud_public_storage_secret }}" cloud_storage_container="{{ cloud_storage_content_bucketname }}" + cloud_storage_endpoint="{{cloudstorage_sdk_endpoint}}" source { baseUrl="{{ source_base_url }}" @@ -436,6 +498,7 @@ content-auto-creator: cloud_storage_key="{{ cloud_public_storage_accountname }}" cloud_storage_secret="{{ cloud_public_storage_secret }}" cloud_storage_container="{{ cloud_storage_content_bucketname }}" + cloud_storage_endpoint="{{cloudstorage_sdk_endpoint}}" cloudstorage { metadata.replace_absolute_path={{ cloudstorage_replace_absolute_path | default('false') }} @@ -732,6 +795,7 @@ content-publish: cloud_storage_key="{{ cloud_public_storage_accountname }}" cloud_storage_secret="{{ cloud_public_storage_secret }}" cloud_storage_container="{{ cloud_storage_content_bucketname }}" + cloud_storage_endpoint="{{cloudstorage_sdk_endpoint}}" cloudstorage { metadata.replace_absolute_path={{ cloudstorage_replace_absolute_path | default('false') }} @@ -789,6 +853,8 @@ qrcode-image-generator: cloud_storage_key="{{ cloud_public_storage_accountname }}" cloud_storage_secret="{{ cloud_public_storage_secret }}" cloud_storage_container="{{ cloud_storage_dial_bucketname | default('dial') }}" + cloud_storage_endpoint="{{cloudstorage_sdk_endpoint}}" + cloud_storage_proxy_host="{{cloud_storage_proxy_host}}" lms-cassandra { keyspace = "dialcodes" @@ -1001,6 +1067,7 @@ live-node-publisher: cloud_storage_key="{{ cloud_public_storage_accountname }}" cloud_storage_secret="{{ cloud_public_storage_secret }}" cloud_storage_container="{{ cloud_storage_content_bucketname }}" + cloud_storage_endpoint="{{cloudstorage_sdk_endpoint}}" master.category.validation.enabled ="{{ master_category_validation_enabled }}" service { @@ -1115,6 +1182,7 @@ csp-migrator: task { timer.duration = {{ csp_migrator_timer_duration }} consumer.parallelism = {{ csp_migrator_consumer_parallelism }} + router.parallelism = {{csp_migrator_router_parallelism }} parallelism = {{ csp_migrator_parallelism }} max.retries = {{ csp_migrator_max_retries }} cassandra-migrator.parallelism = {{csp_migrator_cassandra_parallelism}} @@ -1163,6 +1231,8 @@ csp-migrator: "https://sunbirdstagingpublic.blob.core.windows.net/sunbird-content-staging": "{{ cloudstorage_relative_path_prefix_content }}", "https://preprodall.blob.core.windows.net/ntp-content-preprod": "{{ cloudstorage_relative_path_prefix_content }}", "https://ntpproductionall.blob.core.windows.net/ntp-content-production": "{{ cloudstorage_relative_path_prefix_content }}", + "https://dockpreprodall.blob.core.windows.net/dock-content-preprod": "{{ cloudstorage_relative_path_prefix_content }}", + "https://dockprodall.blob.core.windows.net/dock-content-prod": "{{ cloudstorage_relative_path_prefix_content }}", "CLOUD_STORAGE_BASE_PATH": "{{ cloudstorage_relative_path_prefix_content }}" } @@ -1203,6 +1273,7 @@ csp-migrator: cloud_storage_key="{{ cloud_public_storage_accountname }}" cloud_storage_secret="{{ cloud_public_storage_secret }}" cloud_storage_container="{{ cloud_storage_content_bucketname }}" + cloud_storage_endpoint="{{cloudstorage_sdk_endpoint}}" flink-conf: |+ jobmanager.memory.flink.size: {{ flink_job_names['content-publish'].jobmanager_memory }} @@ -1228,7 +1299,9 @@ cassandra-data-migration: migrate = { key_value_strings_to_migrate = { - "https://sunbirdstagingpublic.blob.core.windows.net/dial": "{{ cloudstorage_relative_path_prefix_dial }}" + "https://sunbirdstagingpublic.blob.core.windows.net/dial": "{{ cloudstorage_relative_path_prefix_dial }}", + "https://preprodall.blob.core.windows.net/dial": "{{ cloudstorage_relative_path_prefix_dial }}", + "https://ntpproductionall.blob.core.windows.net/dial": "{{ cloudstorage_relative_path_prefix_dial }}" } } @@ -1246,4 +1319,4 @@ cassandra-data-migration: taskmanager.numberOfTaskSlots: {{ flink_job_names['cassandra-data-migration'].taskslots }} parallelism.default: 1 jobmanager.execution.failover-strategy: region - taskmanager.memory.network.fraction: 0.1 \ No newline at end of file + taskmanager.memory.network.fraction: 0.1 diff --git a/platform-core/unit-tests/src/test/resources/application.conf b/platform-core/unit-tests/src/test/resources/application.conf index 169b02f75e..a1d51f9470 100644 --- a/platform-core/unit-tests/src/test/resources/application.conf +++ b/platform-core/unit-tests/src/test/resources/application.conf @@ -226,4 +226,10 @@ content.license = "CC BY 4.0" content.tagging.backward_enable=true content.tagging.property="subject,medium" -kp.search_service.base_url="http://search-service" \ No newline at end of file +kp.search_service.base_url="http://search-service" + +cloud_storage_type="azure" +cloud_storage_key="accesskeyyyy" +cloud_storage_secret="secretxxx=" +cloud_storage_container="sunbird-content-dev" +cloud_storage_endpoint="" \ No newline at end of file diff --git a/platform-modules/actors/pom.xml b/platform-modules/actors/pom.xml index 3f38d54eb0..2c960a5bfd 100644 --- a/platform-modules/actors/pom.xml +++ b/platform-modules/actors/pom.xml @@ -63,7 +63,7 @@ org.sunbird cloud-store-sdk - ${cloud.store.version} + ${cloud.store.version} diff --git a/platform-modules/actors/src/main/java/org/sunbird/learning/util/CloudStore.java b/platform-modules/actors/src/main/java/org/sunbird/learning/util/CloudStore.java index 3e24ead27d..98b6ed803c 100644 --- a/platform-modules/actors/src/main/java/org/sunbird/learning/util/CloudStore.java +++ b/platform-modules/actors/src/main/java/org/sunbird/learning/util/CloudStore.java @@ -24,9 +24,23 @@ public class CloudStore { private static String cloudStoreType = Platform.config.getString("cloud_storage_type"); static { + try + { String storageKey = Platform.config.getString("cloud_storage_key"); + System.out.println("storageKey::"+storageKey); String storageSecret = Platform.config.getString("cloud_storage_secret"); - storageService = StorageServiceFactory.getStorageService(new StorageConfig(cloudStoreType, storageKey, storageSecret)); + System.out.println("storageSecret::"+storageSecret); + scala.Option storageEndpoint = scala.Option.apply(Platform.config.getString("cloud_storage_endpoint")); + System.out.println("storageEndpoint::"+storageEndpoint); + scala.Option storageRegion = scala.Option.apply(""); + System.out.println("storageRegion::"+storageRegion); + System.out.println("cloudStoreType::"+cloudStoreType); + storageService = StorageServiceFactory.getStorageService(new StorageConfig(cloudStoreType, storageKey, storageSecret,storageEndpoint,storageRegion)); + System.out.println("storageService::"+storageService); + }catch(Exception e) + { + e.printStackTrace(); + } } public static BaseStorageService getCloudStoreService() { diff --git a/platform-modules/content-manager/src/test/resources/application.conf b/platform-modules/content-manager/src/test/resources/application.conf index 308f99aa86..f655564d09 100644 --- a/platform-modules/content-manager/src/test/resources/application.conf +++ b/platform-modules/content-manager/src/test/resources/application.conf @@ -60,6 +60,10 @@ specialCharRegEx="^([$&+,:;=?@#|!]*)$" numberRegEx="^([+-]?\\d*\\.?\\d*)$" cloud_storage_type="azure" +cloud_storage_key="accesskeyyyy" +cloud_storage_secret="secretxxx=" +cloud_storage_container="sunbird-content-dev" + azure_storage_key="" azure_storage_secret="" azure_storage_container="sunbird-content-dev" diff --git a/platform-modules/manager/src/test/java/org/sunbird/taxonomy/util/YouTubeUrlUtilTest.java b/platform-modules/manager/src/test/java/org/sunbird/taxonomy/util/YouTubeUrlUtilTest.java index c8c5feafc3..0828c77617 100644 --- a/platform-modules/manager/src/test/java/org/sunbird/taxonomy/util/YouTubeUrlUtilTest.java +++ b/platform-modules/manager/src/test/java/org/sunbird/taxonomy/util/YouTubeUrlUtilTest.java @@ -84,7 +84,7 @@ private void createYoutubeContent() throws Exception { // check license of valid youtube url. @Test public void testYouTubeService_01() throws Exception { - String artifactUrl = "https://www.youtube.com/watch?v=owr198WQpM8"; + String artifactUrl = "https://www.youtube.com/watch?v=GHmQ8euNwv8"; String result = YouTubeUrlUtil.getLicense(artifactUrl); assertEquals("creativeCommon", result); } @@ -146,7 +146,7 @@ public void testYouTubeService_07() throws Exception { public void testYouTubeService_08() throws Exception { //upload content String mimeType = "video/x-youtube"; - String fileUrl = "https://www.youtube.com/watch?v=owr198WQpM8"; + String fileUrl = "https://www.youtube.com/watch?v=eKT1IbPjH1Q"; Response response = contentManager.upload(contentId, fileUrl, mimeType); String responseCode = (String) response.getResponseCode().toString(); assertEquals("OK", responseCode); diff --git a/platform-modules/manager/src/test/resources/Contents/testEcmlMediaYoutube/index.ecml b/platform-modules/manager/src/test/resources/Contents/testEcmlMediaYoutube/index.ecml index f87791d263..0994dc56d3 100644 --- a/platform-modules/manager/src/test/resources/Contents/testEcmlMediaYoutube/index.ecml +++ b/platform-modules/manager/src/test/resources/Contents/testEcmlMediaYoutube/index.ecml @@ -29,7 +29,7 @@ - + @@ -191,7 +191,7 @@ - + diff --git a/platform-modules/pom.xml b/platform-modules/pom.xml index 20ee3a4651..492c211627 100644 --- a/platform-modules/pom.xml +++ b/platform-modules/pom.xml @@ -19,7 +19,7 @@ 2.3.1 1.8 1.8 - 1.4.3 + 1.4.6 diff --git a/platform-tools/spikes/content-tool/pom.xml b/platform-tools/spikes/content-tool/pom.xml index a485ae2693..626d2e53d6 100644 --- a/platform-tools/spikes/content-tool/pom.xml +++ b/platform-tools/spikes/content-tool/pom.xml @@ -66,7 +66,7 @@ org.sunbird cloud-store-sdk - 1.2.5 + 1.4.6 diff --git a/platform-tools/spikes/content-tool/src/main/java/org/sunbird/content/tool/CloudStoreManager.java b/platform-tools/spikes/content-tool/src/main/java/org/sunbird/content/tool/CloudStoreManager.java index d4f0e23e60..47e769efbd 100644 --- a/platform-tools/spikes/content-tool/src/main/java/org/sunbird/content/tool/CloudStoreManager.java +++ b/platform-tools/spikes/content-tool/src/main/java/org/sunbird/content/tool/CloudStoreManager.java @@ -21,9 +21,18 @@ public class CloudStoreManager { protected String destStorageType = Platform.config.getString("destination.storage_type"); + protected scala.Option awsEndpoint = scala.Option.apply(""); + protected scala.Option awsRegion = scala.Option.apply(""); + protected BaseStorageService awsService = StorageServiceFactory.getStorageService(new StorageConfig("aws", Platform.config.getString("aws_storage_key"), Platform.config.getString("aws_storage_secret"),awsEndpoint,awsRegion)); + + protected scala.Option azureEndpoint = scala.Option.apply(""); + protected scala.Option azureRegion = scala.Option.apply(""); + protected BaseStorageService azureService = StorageServiceFactory.getStorageService(new StorageConfig("azure", Platform.config.getString("azure_storage_key"), Platform.config.getString("azure_storage_secret"),azureEndpoint,azureRegion)); + + protected scala.Option ociEndpoint = scala.Option.apply(Platform.config.getString("oci_storage_endpoint")); + protected scala.Option ociRegion = scala.Option.apply(""); + protected BaseStorageService ociService = StorageServiceFactory.getStorageService(new StorageConfig("oci", Platform.config.getString("oci_storage_key"), Platform.config.getString("oci_storage_secret"),ociEndpoint,ociRegion)); - protected BaseStorageService awsService = StorageServiceFactory.getStorageService(new StorageConfig("aws", Platform.config.getString("aws_storage_key"), Platform.config.getString("aws_storage_secret"))); - protected BaseStorageService azureService = StorageServiceFactory.getStorageService((new StorageConfig("azure", Platform.config.getString("azure_storage_key"), Platform.config.getString("azure_storage_secret")))); private String cloudSrcBaseURL = Platform.config.getString("cloud.src.baseurl"); private String cloudDestBaseURL = Platform.config.getString("cloud.dest.baseurl"); @@ -239,6 +248,8 @@ public String getContainerName(String cloudStoreType) { return Platform.config.getString("azure_storage_container"); }else if(StringUtils.equalsIgnoreCase(cloudStoreType, "aws")) { return Platform.config.getString("aws_storage_container"); + }else if(StringUtils.equalsIgnoreCase(cloudStoreType, "oci")) { + return Platform.config.getString("oci_storage_container"); }else { throw new ServerException("ERR_INVALID_CLOUD_STORAGE", "Error while getting container name"); } @@ -249,6 +260,8 @@ public BaseStorageService getcloudService(String cloudStoreType){ return azureService; }else if(StringUtils.equalsIgnoreCase(cloudStoreType, "aws")) { return awsService; + }else if(StringUtils.equalsIgnoreCase(cloudStoreType, "oci")) { + return ociService; }else { throw new ServerException("ERR_INVALID_CLOUD_STORAGE", "Error while getting container name"); } From 5749712f08561112e247bbb748d15c39e2bb1650 Mon Sep 17 00:00:00 2001 From: Kumar Gauraw Date: Fri, 2 Feb 2024 12:25:43 +0530 Subject: [PATCH 212/222] Issue #IQ-670 fix: fix for selective quml migration --- .../main/java/org/sunbird/learning/util/ControllerUtil.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/platform-modules/actors/src/main/java/org/sunbird/learning/util/ControllerUtil.java b/platform-modules/actors/src/main/java/org/sunbird/learning/util/ControllerUtil.java index 5cd32ab3f1..879551f51f 100644 --- a/platform-modules/actors/src/main/java/org/sunbird/learning/util/ControllerUtil.java +++ b/platform-modules/actors/src/main/java/org/sunbird/learning/util/ControllerUtil.java @@ -890,9 +890,9 @@ public Map getQumlMigrationObjectCount(String graphId, List props = new ArrayList(); props.add("objectType"); From a1f23c8915fba29faf9e1483a378ac5cbd734e51 Mon Sep 17 00:00:00 2001 From: Kartheek Palla Date: Thu, 18 Apr 2024 10:28:08 +0530 Subject: [PATCH 213/222] Update values.j2 --- kubernetes/helm_charts/datapipeline_jobs/values.j2 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kubernetes/helm_charts/datapipeline_jobs/values.j2 b/kubernetes/helm_charts/datapipeline_jobs/values.j2 index ff8fcb817d..0f1fbfabcc 100644 --- a/kubernetes/helm_charts/datapipeline_jobs/values.j2 +++ b/kubernetes/helm_charts/datapipeline_jobs/values.j2 @@ -888,7 +888,7 @@ dialcode-context-updater: dialcode_context_updater { actions="dialcode-context-update" search_mode="Collection" - context_map_path = "https://raw.githubusercontent.com/project-sunbird/knowledge-platform-jobs/release-5.0.0/dialcode-context-updater/src/main/resources/contextMapping.json" + context_map_path = "https://raw.githubusercontent.com/project-sunbird/knowledge-platform-jobs/master/dialcode-context-updater/src/main/resources/contextMapping.json" identifier_search_fields = ["identifier", "primaryCategory", "channel"] dial_code_context_read_api_path = "/dialcode/v4/read/" dial_code_context_update_api_path = "/dialcode/v4/update/" From 068a88109b3b64f0a49cf7e2891f17d3a871d115 Mon Sep 17 00:00:00 2001 From: Kartheek Palla Date: Thu, 18 Apr 2024 10:28:40 +0530 Subject: [PATCH 214/222] Update values.j2 --- kubernetes/helm_charts/datapipeline_jobs/values.j2 | 1 + 1 file changed, 1 insertion(+) diff --git a/kubernetes/helm_charts/datapipeline_jobs/values.j2 b/kubernetes/helm_charts/datapipeline_jobs/values.j2 index 0f1fbfabcc..dad23d5465 100644 --- a/kubernetes/helm_charts/datapipeline_jobs/values.j2 +++ b/kubernetes/helm_charts/datapipeline_jobs/values.j2 @@ -188,6 +188,7 @@ post-publish-processor: lms.basePath = "{{ lms_service_base_url }}" learning_service.basePath = "{{ kp_learning_service_base_url }}" dial.basePath = "https://{{domain_name}}/dial/" + content.basePath = "{{ kp_content_service_base_url }}" } cloudstorage { From f18122da9f8c8a62549725a3117f61a41637554b Mon Sep 17 00:00:00 2001 From: Sanket Nagdive <31030038+sanketnagdive@users.noreply.github.com> Date: Fri, 19 Apr 2024 18:38:42 +0530 Subject: [PATCH 215/222] updated syntax --- kubernetes/helm_charts/datapipeline_jobs/values.j2 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/kubernetes/helm_charts/datapipeline_jobs/values.j2 b/kubernetes/helm_charts/datapipeline_jobs/values.j2 index dad23d5465..dfede4b5cd 100644 --- a/kubernetes/helm_charts/datapipeline_jobs/values.j2 +++ b/kubernetes/helm_charts/datapipeline_jobs/values.j2 @@ -101,7 +101,7 @@ base_config: | } base.url = "s3://"${job.statebackend.s3.storage.container}"/"${job.statebackend.s3.storage.checkpointing.dir} } -{% else if cloud_service_provider == "azure" %} +{% elif cloud_service_provider == "azure" %} statebackend { blob { storage { @@ -112,7 +112,7 @@ base_config: | } base.url = "wasbs://"${job.statebackend.blob.storage.container}"@"${job.statebackend.blob.storage.account}"/"${job.statebackend.blob.storage.checkpointing.dir} } -{% else if cloud_service_provider == "aws" %} +{% elif cloud_service_provider == "aws" %} statebackend { s3 { storage { From 95f13505551d1e24eebf12b5da4e8ec426ababc0 Mon Sep 17 00:00:00 2001 From: Kumar Gauraw Date: Wed, 22 May 2024 12:40:14 +0530 Subject: [PATCH 216/222] Issue #KN-1089 fix: added default value for endpoint variable --- kubernetes/ansible/roles/flink-jobs-deploy/defaults/main.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/kubernetes/ansible/roles/flink-jobs-deploy/defaults/main.yml b/kubernetes/ansible/roles/flink-jobs-deploy/defaults/main.yml index dddf953d4e..7348df1c5b 100644 --- a/kubernetes/ansible/roles/flink-jobs-deploy/defaults/main.yml +++ b/kubernetes/ansible/roles/flink-jobs-deploy/defaults/main.yml @@ -380,6 +380,7 @@ cloud_storage_key: "" cloud_storage_secret: "" cloud_storage_container: "" cloud_storage_endpoint: "" +cloudstorage_sdk_endpoint: "" ### csp-migrator related vars From 9f4c1948b966d0c0771795e156f4a721b4f73b15 Mon Sep 17 00:00:00 2001 From: Kumar Gauraw Date: Wed, 22 May 2024 14:45:23 +0530 Subject: [PATCH 217/222] Issue #KN-1089 fix: added default value for proxy host --- kubernetes/ansible/roles/flink-jobs-deploy/defaults/main.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/kubernetes/ansible/roles/flink-jobs-deploy/defaults/main.yml b/kubernetes/ansible/roles/flink-jobs-deploy/defaults/main.yml index 7348df1c5b..a66e9fba55 100644 --- a/kubernetes/ansible/roles/flink-jobs-deploy/defaults/main.yml +++ b/kubernetes/ansible/roles/flink-jobs-deploy/defaults/main.yml @@ -381,6 +381,7 @@ cloud_storage_secret: "" cloud_storage_container: "" cloud_storage_endpoint: "" cloudstorage_sdk_endpoint: "" +cloud_storage_proxy_host: "" ### csp-migrator related vars From f9a6d8e5b9888bbdb0d23ccad4e4471b1ecfd98b Mon Sep 17 00:00:00 2001 From: Kartheek Palla Date: Wed, 22 May 2024 16:08:12 +0530 Subject: [PATCH 218/222] transaction-event-processor config added --- .../helm_charts/datapipeline_jobs/values.j2 | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/kubernetes/helm_charts/datapipeline_jobs/values.j2 b/kubernetes/helm_charts/datapipeline_jobs/values.j2 index dfede4b5cd..b58b3582b4 100644 --- a/kubernetes/helm_charts/datapipeline_jobs/values.j2 +++ b/kubernetes/helm_charts/datapipeline_jobs/values.j2 @@ -1321,3 +1321,50 @@ cassandra-data-migration: parallelism.default: 1 jobmanager.execution.failover-strategy: region taskmanager.memory.network.fraction: 0.1 + +transaction-event-processor: + transaction-event-processor: |+ + include file("/data/flink/conf/base-config.conf") + job { + env = "{{ env_name }}" + } + + kafka { + input.topic = "{{ env_name }}.learning.graph.events" + output.audit.topic = "{{ env_name }}.telemetry.raw" + output.obsrv.topic = "{{ env_name }}.transaction.meta" + groupId = "{{ env_name }}-transaction-event-processor-group" + } + + task { + consumer.parallelism = {{ audit_event_generator_consumer_parallelism }} + parallelism = {{ audit_event_generator_parallelism }} + producer.parallelism = {{ audit_event_generator_producer_parallelism }} + window.time = 60 + } + + schema { + basePath = "{{ kp_schema_base_path }}" + } + + schema { + basePath = ${schema_base_path} + } + + channel.default = "{{ audit_event_generator_default_channel }}" + + job { + audit-event-generator = true + audit-history-indexer = true + obsrv-metadata-generator = true + } + + flink-conf: |+ + jobmanager.memory.flink.size: {{ flink_job_names['audit-history-indexer'].jobmanager_memory }} + taskmanager.memory.flink.size: {{ flink_job_names['audit-history-indexer'].taskmanager_memory }} + taskmanager.numberOfTaskSlots: {{ flink_job_names['audit-history-indexer'].taskslots }} + parallelism.default: 1 + jobmanager.execution.failover-strategy: region + taskmanager.memory.network.fraction: 0.1 + + job_classname: org.sunbird.job.transaction.task.TransactionEventProcessorStreamTask From f5b3a0518ce9804c8035392a872c8a8b4087bd57 Mon Sep 17 00:00:00 2001 From: Kartheek Palla Date: Wed, 22 May 2024 16:28:42 +0530 Subject: [PATCH 219/222] transaction-event-processor config added --- .../roles/flink-jobs-deploy/defaults/main.yml | 15 ++++++++++ .../helm_charts/datapipeline_jobs/values.j2 | 28 ++++++++----------- 2 files changed, 26 insertions(+), 17 deletions(-) diff --git a/kubernetes/ansible/roles/flink-jobs-deploy/defaults/main.yml b/kubernetes/ansible/roles/flink-jobs-deploy/defaults/main.yml index a66e9fba55..19ab51e0ee 100644 --- a/kubernetes/ansible/roles/flink-jobs-deploy/defaults/main.yml +++ b/kubernetes/ansible/roles/flink-jobs-deploy/defaults/main.yml @@ -126,6 +126,14 @@ collection_certificate_generator_rc_badcharlist: "{{ rc_bad_char_list | default( registry_sunbird_keyspace: "sunbird" cert_registry_table: "cert_registry" +### transaction event processor related vars +transaction_event_processor_consumer_parallelism: 1 +transaction_event_processor_parallelism: 1 +transaction_event_processor_producer_parallelism: 1 +transaction_event_processor_default_channel: "{{ default_channel | default('org.sunbird') }}" +enable_audit_event_generator: true +enable_audit_history_indexer: true +enable_obsrv_metadata_generator: false ### to be removed job_classname: "" @@ -293,6 +301,13 @@ flink_job_names: taskmanager_memory: 1024m taskslots: 1 cpu_requests: 0.3 + transaction-event-processor: + job_class_name: 'org.sunbird.job.transaction.task.TransactionEventProcessorStreamTask' + replica: 1 + jobmanager_memory: 1024m + taskmanager_memory: 1024m + taskslots: 1 + cpu_requests: 0.3 ### Global vars middleware_course_keyspace: "sunbird_courses" diff --git a/kubernetes/helm_charts/datapipeline_jobs/values.j2 b/kubernetes/helm_charts/datapipeline_jobs/values.j2 index b58b3582b4..3482ca1086 100644 --- a/kubernetes/helm_charts/datapipeline_jobs/values.j2 +++ b/kubernetes/helm_charts/datapipeline_jobs/values.j2 @@ -1337,34 +1337,28 @@ transaction-event-processor: } task { - consumer.parallelism = {{ audit_event_generator_consumer_parallelism }} - parallelism = {{ audit_event_generator_parallelism }} - producer.parallelism = {{ audit_event_generator_producer_parallelism }} + consumer.parallelism = {{ transaction_event_processor_consumer_parallelism }} + parallelism = {{ transaction_event_processor_parallelism }} + producer.parallelism = {{ transaction_event_processor_producer_parallelism }} window.time = 60 } schema { - basePath = "{{ kp_schema_base_path }}" - } - - schema { - basePath = ${schema_base_path} + basePath = "{kp_schema_base_path}" } - channel.default = "{{ audit_event_generator_default_channel }}" + channel.default = "{{ transaction_event_processor_default_channel }}" job { - audit-event-generator = true - audit-history-indexer = true - obsrv-metadata-generator = true + audit-event-generator = {{ enable_audit_event_generator }} + audit-history-indexer = {{ enable_audit_history_indexer }} + obsrv-metadata-generator = {{ enable_obsrv_metadata_generator }} } flink-conf: |+ - jobmanager.memory.flink.size: {{ flink_job_names['audit-history-indexer'].jobmanager_memory }} - taskmanager.memory.flink.size: {{ flink_job_names['audit-history-indexer'].taskmanager_memory }} - taskmanager.numberOfTaskSlots: {{ flink_job_names['audit-history-indexer'].taskslots }} + jobmanager.memory.flink.size: {{ flink_job_names['transaction-event-processor'].jobmanager_memory }} + taskmanager.memory.flink.size: {{ flink_job_names['transaction-event-processor'].taskmanager_memory }} + taskmanager.numberOfTaskSlots: {{ flink_job_names['transaction-event-processor'].taskslots }} parallelism.default: 1 jobmanager.execution.failover-strategy: region taskmanager.memory.network.fraction: 0.1 - - job_classname: org.sunbird.job.transaction.task.TransactionEventProcessorStreamTask From e6274c3f3e66ae9e59030546edf6b7216ffe52c3 Mon Sep 17 00:00:00 2001 From: Kartheek Palla Date: Wed, 22 May 2024 16:38:27 +0530 Subject: [PATCH 220/222] transaction-event-processor config added --- .../ansible/roles/flink-jobs-deploy/defaults/main.yml | 6 +++--- kubernetes/helm_charts/datapipeline_jobs/values.j2 | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/kubernetes/ansible/roles/flink-jobs-deploy/defaults/main.yml b/kubernetes/ansible/roles/flink-jobs-deploy/defaults/main.yml index 19ab51e0ee..532c1d34a9 100644 --- a/kubernetes/ansible/roles/flink-jobs-deploy/defaults/main.yml +++ b/kubernetes/ansible/roles/flink-jobs-deploy/defaults/main.yml @@ -131,9 +131,9 @@ transaction_event_processor_consumer_parallelism: 1 transaction_event_processor_parallelism: 1 transaction_event_processor_producer_parallelism: 1 transaction_event_processor_default_channel: "{{ default_channel | default('org.sunbird') }}" -enable_audit_event_generator: true -enable_audit_history_indexer: true -enable_obsrv_metadata_generator: false +enable_audit_event_generator: "true" +enable_audit_history_indexer: "true" +enable_obsrv_metadata_generator: "false" ### to be removed job_classname: "" diff --git a/kubernetes/helm_charts/datapipeline_jobs/values.j2 b/kubernetes/helm_charts/datapipeline_jobs/values.j2 index 3482ca1086..b787bc89b3 100644 --- a/kubernetes/helm_charts/datapipeline_jobs/values.j2 +++ b/kubernetes/helm_charts/datapipeline_jobs/values.j2 @@ -1350,9 +1350,9 @@ transaction-event-processor: channel.default = "{{ transaction_event_processor_default_channel }}" job { - audit-event-generator = {{ enable_audit_event_generator }} - audit-history-indexer = {{ enable_audit_history_indexer }} - obsrv-metadata-generator = {{ enable_obsrv_metadata_generator }} + audit-event-generator = "{{ enable_audit_event_generator }}" + audit-history-indexer = "{{ enable_audit_history_indexer }}" + obsrv-metadata-generator = "{{ enable_obsrv_metadata_generator }}" } flink-conf: |+ From 8fe1c90e0b6f663c50b44e2f76bb1109ebdc9d52 Mon Sep 17 00:00:00 2001 From: Kartheek Palla Date: Tue, 18 Jun 2024 17:27:06 +0530 Subject: [PATCH 221/222] Issue KN-1096 feat: Video streaming job MediaKind configurations added --- .../roles/flink-jobs-deploy/defaults/main.yml | 10 ++++++++++ .../helm_charts/datapipeline_jobs/values.j2 | 19 +++++++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/kubernetes/ansible/roles/flink-jobs-deploy/defaults/main.yml b/kubernetes/ansible/roles/flink-jobs-deploy/defaults/main.yml index dddf953d4e..415c4114b7 100644 --- a/kubernetes/ansible/roles/flink-jobs-deploy/defaults/main.yml +++ b/kubernetes/ansible/roles/flink-jobs-deploy/defaults/main.yml @@ -398,3 +398,13 @@ video_stream_topic_name: "{{ env_name }}.live.video.stream.request" content_hierarchy_keyspace_name: "{{ hierarchy_keyspace_name }}" questionset_hierarchy_keyspace_name: "{{ hierarchy_keyspace_name }}" gdrive_application_name: "drive-download" + +### video-stream-generator azure_mediakind related vars +azure_mediakind_project_name: "{{ azure_mediakind_project_name | default('') }}" +azure_mediakind_auth_token: "{{ azure_mediakind_auth_token | default('') }}" +azure_mediakind_account_name: "{{ azure_mediakind_account_name | default('') }}" +azure_mediakind_transform_default: "media_transform_default" +azure_mediakind_stream_base_url: "{{ azure_mediakind_stream_base_url | default('') }}" +azure_mediakind_stream_endpoint_name: "default" +azure_mediakind_stream_protocol : "Hls" +azure_mediakind_stream_policy_name : "Predefined_ClearStreamingOnly" \ No newline at end of file diff --git a/kubernetes/helm_charts/datapipeline_jobs/values.j2 b/kubernetes/helm_charts/datapipeline_jobs/values.j2 index dad23d5465..91be7d979a 100644 --- a/kubernetes/helm_charts/datapipeline_jobs/values.j2 +++ b/kubernetes/helm_charts/datapipeline_jobs/values.j2 @@ -245,6 +245,25 @@ video-stream-generator: policy_name = "Predefined_ClearStreamingOnly" } } + + azure_mediakind{ + project_name="{{ azure_mediakind_project_name }}" + auth_token="{{ azure_mediakind_auth_token }}" + account_name="{{ azure_mediakind_account_name }}" + api { + endpoint="https://api.mk.io/api" + } + transform { + default = "{{ azure_mediakind_transform_default }}" + } + stream { + base_url = "{{azure_mediakind_stream_base_url}}" + endpoint_name = "{{azure_mediakind_stream_endpoint_name}}" + protocol = "{{azure_mediakind_stream_protocol}}" + policy_name = "{{azure_mediakind_stream_policy_name}}" + } + } + azure_tenant="{{ video_stream_generator_azure_tenant }}" azure_subscription_id="{{ video_stream_generator_azure_subscription_id }}" azure_account_name="{{ video_stream_generator_azure_account_name }}" From 22aeafdc614a9c348e1f65b575129afc42ca9314 Mon Sep 17 00:00:00 2001 From: Kartheek Palla Date: Wed, 19 Jun 2024 14:19:23 +0530 Subject: [PATCH 222/222] Issue KN-1096 feat: Removed unused AMS variables --- .../roles/flink-jobs-deploy/defaults/main.yml | 7 --- .../helm_charts/datapipeline_jobs/values.j2 | 55 +------------------ 2 files changed, 1 insertion(+), 61 deletions(-) diff --git a/kubernetes/ansible/roles/flink-jobs-deploy/defaults/main.yml b/kubernetes/ansible/roles/flink-jobs-deploy/defaults/main.yml index 28bd5030f6..ef3ad36b9c 100644 --- a/kubernetes/ansible/roles/flink-jobs-deploy/defaults/main.yml +++ b/kubernetes/ansible/roles/flink-jobs-deploy/defaults/main.yml @@ -79,13 +79,6 @@ certificate_generator_parallelism: 1 ### Video stream generator related vars ### IMPORTANT: The media-service configuration values should be updated in respective environment. -video_stream_generator_azure_tenant: "{{ media_service_azure_tenant | default('') }}" -video_stream_generator_azure_subscription_id: "{{ media_service_azure_subscription_id | default('') }}" -video_stream_generator_azure_account_name: "{{ media_service_azure_account_name | default('') }}" -video_stream_generator_azure_resource_group_name: "{{ media_service_azure_resource_group_name | default('') }}" -video_stream_generator_azure_token_client_key: "{{ media_service_azure_token_client_key | default('') }}" -video_stream_generator_azure_token_client_secret: "{{ media_service_azure_token_client_secret | default('') }}" -video_stream_generator_azure_stream_base_url: "{{ stream_base_url | default('') }}" video_stream_generator_consumer_parallelism: 1 video_stream_generator_parallelism: 1 video_stream_generator_timer_duration: 1800 diff --git a/kubernetes/helm_charts/datapipeline_jobs/values.j2 b/kubernetes/helm_charts/datapipeline_jobs/values.j2 index 481efe29b7..d3c6b1cd93 100644 --- a/kubernetes/helm_charts/datapipeline_jobs/values.j2 +++ b/kubernetes/helm_charts/datapipeline_jobs/values.j2 @@ -225,27 +225,7 @@ video-stream-generator: table = "job_request" } service.content.basePath="{{ kp_content_service_base_url }}" - azure { - location = "centralindia" - login { - endpoint="https://login.microsoftonline.com" - } - api { - endpoint="https://management.azure.com" - version = "2018-07-01" - } - transform { - default = "media_transform_default" - hls = "media_transform_hls" - } - stream { - base_url="{{ video_stream_generator_azure_stream_base_url }}" - endpoint_name = "default" - protocol = "Hls" - policy_name = "Predefined_ClearStreamingOnly" - } - } - + azure_mediakind{ project_name="{{ azure_mediakind_project_name }}" auth_token="{{ azure_mediakind_auth_token }}" @@ -263,13 +243,6 @@ video-stream-generator: policy_name = "{{azure_mediakind_stream_policy_name}}" } } - - azure_tenant="{{ video_stream_generator_azure_tenant }}" - azure_subscription_id="{{ video_stream_generator_azure_subscription_id }}" - azure_account_name="{{ video_stream_generator_azure_account_name }}" - azure_resource_group_name="{{ video_stream_generator_azure_resource_group_name }}" - azure_token_client_key="{{ video_stream_generator_azure_token_client_key }}" - azure_token_client_secret="{{ video_stream_generator_azure_token_client_secret }}" ## CSP Name. e.g: aws or azure media_service_type="{{ media_service_provider_name }}" ## AWS Elemental Media Convert Config @@ -1129,32 +1102,6 @@ live-video-stream-generator: table = "job_request" } service.content.basePath="{{ kp_content_service_base_url }}" - azure { - location = "centralindia" - login { - endpoint="https://login.microsoftonline.com" - } - api { - endpoint="https://management.azure.com" - version = "2018-07-01" - } - transform { - default = "media_transform_default" - hls = "media_transform_hls" - } - stream { - base_url="{{ video_stream_generator_azure_stream_base_url }}" - endpoint_name = "default" - protocol = "Hls" - policy_name = "Predefined_ClearStreamingOnly" - } - } - azure_tenant="{{ video_stream_generator_azure_tenant }}" - azure_subscription_id="{{ video_stream_generator_azure_subscription_id }}" - azure_account_name="{{ video_stream_generator_azure_account_name }}" - azure_resource_group_name="{{ video_stream_generator_azure_resource_group_name }}" - azure_token_client_key="{{ video_stream_generator_azure_token_client_key }}" - azure_token_client_secret="{{ video_stream_generator_azure_token_client_secret }}" ## CSP Name. e.g: aws or azure media_service_type="{{ media_service_provider_name }}" ## AWS Elemental Media Convert Config