diff --git a/README.md b/README.md index a043abb..ef67a8a 100644 --- a/README.md +++ b/README.md @@ -357,11 +357,17 @@ instance = j1.create_integration_instance( ```python # Start sync job for an integration instance -sync_job = j1.start_sync_job(instance_id='') -print(f"Started sync job: {sync_job['job']['_id']}") +sync_job = j1.start_sync_job( + instance_id=instance_id, + sync_mode="PATCH", + source="integration-external" +) + +sync_job_id = sync_job['job'].get('id') +print(f"Started sync job: {sync_job_id}") # The returned job ID is used for subsequent operations -job_id = sync_job['job']['_id'] +job_id = sync_job_id ``` ##### Upload Batch of Entities @@ -514,7 +520,7 @@ print(f"Uploaded {len(combined_payload['entities'])} entities and {len(combined_ ```python # Finalize the sync job result = j1.finalize_sync_job(instance_job_id='') -print(f"Finalized sync job: {result['job']['_id']}") +print(f"Finalized sync job: {result['job'].get('id')}") # Check job status if result['job']['status'] == 'COMPLETED': diff --git a/examples/04_integration_management.py b/examples/04_integration_management.py index d35c751..8e96f19 100644 --- a/examples/04_integration_management.py +++ b/examples/04_integration_management.py @@ -149,7 +149,7 @@ def sync_job_examples(j1, instance_id): try: sync_job = j1.start_sync_job( instance_id=instance_id, - sync_mode="CREATE_OR_UPDATE", + sync_mode="PATCH", source="api" ) job_id = sync_job['job']['_id'] diff --git a/examples/bulk_upload.py b/examples/bulk_upload.py new file mode 100644 index 0000000..a8ac5ea --- /dev/null +++ b/examples/bulk_upload.py @@ -0,0 +1,69 @@ +from jupiterone.client import JupiterOneClient +import os + +account = os.environ.get("JUPITERONE_ACCOUNT") +token = os.environ.get("JUPITERONE_TOKEN") +url = "https://graphql.us.jupiterone.io" + +j1 = JupiterOneClient(account=account, token=token, url=url) + +instance_id = "e7113c37-1ea8-4d00-9b82-c24952e70916" + +sync_job = j1.start_sync_job( + instance_id=instance_id, + sync_mode="PATCH", + source="integration-external" + ) + +print(sync_job) +sync_job_id = sync_job['job'].get('id') + +# Prepare entities payload +entities_payload = [ + { + "_key": "server-001", + "_type": "aws_ec2_instance", + "_class": "Host", + "displayName": "web-server-001", + "instanceId": "i-1234567890abcdef0", + "instanceType": "t3.micro", + "state": "running", + "tag.Environment": "production", + "tag.Team": "engineering" + }, + { + "_key": "server-002", + "_type": "aws_ec2_instance", + "_class": "Host", + "displayName": "web-server-002", + "instanceId": "i-0987654321fedcba0", + "instanceType": "t3.small", + "state": "running", + "tag.Environment": "staging", + "tag.Team": "engineering" + }, + { + "_key": "database-001", + "_type": "aws_rds_instance", + "_class": "Database", + "displayName": "prod-database", + "dbInstanceIdentifier": "prod-db", + "engine": "postgres", + "dbInstanceClass": "db.t3.micro", + "tag.Environment": "production", + "tag.Team": "data" + } +] + +# Upload entities batch +result = j1.upload_entities_batch_json( + instance_job_id=sync_job_id, + entities_list=entities_payload +) +print(f"Uploaded {len(entities_payload)} entities") +print(result) + +# Finalize the sync job +result = j1.finalize_sync_job(instance_job_id=sync_job_id) +print(f"Finalized sync job: {result['job']['id']}") + diff --git a/examples/examples.py b/examples/examples.py index 7e31763..061e509 100644 --- a/examples/examples.py +++ b/examples/examples.py @@ -124,9 +124,9 @@ integration_instance_id = "" # start_sync_job -# sync_mode can be "DIFF", "CREATE_OR_UPDATE", or "PATCH" +# sync_mode can be "DIFF" or "PATCH" start_sync_job_r = j1.start_sync_job(instance_id=integration_instance_id, - sync_mode='CREATE_OR_UPDATE', + sync_mode='PATCH', source='integration-external') print("start_sync_job()") print(start_sync_job_r) diff --git a/examples/sync_job_workflow.py b/examples/sync_job_workflow.py new file mode 100644 index 0000000..a487583 --- /dev/null +++ b/examples/sync_job_workflow.py @@ -0,0 +1,141 @@ +#!/usr/bin/env python3 +""" +JupiterOne Sync Job Workflow Example + +This example demonstrates the core synchronization job workflow: +1. Start a sync job +2. Upload entities batch +3. Finalize the sync job + +This is a standalone script that can be run independently to test +the sync job functionality. + +Note: This example uses PATCH sync mode, which is suitable for entity-only +uploads. If you need to upload relationships, use DIFF sync mode instead. +See: https://docs.jupiterone.io/reference/pipeline-upgrade#patch-sync-jobs-cannot-target-relationships +""" + +import os +import sys +from jupiterone.client import JupiterOneClient + +def main(): + """Main function to demonstrate sync job workflow.""" + + # Initialize JupiterOne client + # You can set these as environment variables or replace with your values + api_token = os.getenv('JUPITERONE_API_TOKEN') + account_id = os.getenv('JUPITERONE_ACCOUNT_ID') + + if not api_token or not account_id: + print("Error: Please set JUPITERONE_API_TOKEN and JUPITERONE_ACCOUNT_ID environment variables") + print("Example:") + print(" export JUPITERONE_API_TOKEN='your-api-token'") + print(" export JUPITERONE_ACCOUNT_ID='your-account-id'") + sys.exit(1) + + # Create JupiterOne client + j1 = JupiterOneClient(token=api_token, account=account_id) + + print("=== JupiterOne Sync Job Workflow Example ===\n") + + # You'll need to replace this with an actual integration instance ID + instance_id = os.getenv('JUPITERONE_INSTANCE_ID') + if not instance_id: + print("Error: Please set JUPITERONE_INSTANCE_ID environment variable") + print("Example:") + print(" export JUPITERONE_INSTANCE_ID='your-integration-instance-id'") + sys.exit(1) + + try: + # Step 1: Start sync job + print("1. Starting synchronization job...") + print(" Note: Using PATCH mode (entities only). Use DIFF mode if uploading relationships.") + sync_job = j1.start_sync_job( + instance_id=instance_id, + sync_mode="PATCH", + source="integration-external" + ) + + sync_job_id = sync_job['job'].get('id') + print(f"✓ Started sync job: {sync_job_id}") + print(f" Status: {sync_job['job']['status']}") + print() + + # Step 2: Upload entities batch + print("2. Uploading entities batch...") + + # Sample entities payload + entities_payload = [ + { + "_key": "example-server-001", + "_type": "example_server", + "_class": "Host", + "displayName": "Example Server 001", + "hostname": "server-001.example.com", + "ipAddress": "192.168.1.100", + "operatingSystem": "Linux", + "tag.Environment": "development", + "tag.Team": "engineering", + "tag.Purpose": "web_server" + }, + { + "_key": "example-server-002", + "_type": "example_server", + "_class": "Host", + "displayName": "Example Server 002", + "hostname": "server-002.example.com", + "ipAddress": "192.168.1.101", + "operatingSystem": "Linux", + "tag.Environment": "staging", + "tag.Team": "engineering", + "tag.Purpose": "database_server" + }, + { + "_key": "example-database-001", + "_type": "example_database", + "_class": "Database", + "displayName": "Example Database 001", + "databaseName": "app_db", + "engine": "postgresql", + "version": "13.4", + "tag.Environment": "development", + "tag.Team": "data" + } + ] + + # Upload entities + upload_result = j1.upload_entities_batch_json( + instance_job_id=sync_job_id, + entities_list=entities_payload + ) + print(f"✓ Uploaded {len(entities_payload)} entities successfully") + print(f" Upload result: {upload_result}") + print() + + # Step 3: Finalize sync job + print("3. Finalizing synchronization job...") + finalize_result = j1.finalize_sync_job(instance_job_id=sync_job_id) + + finalize_job_id = finalize_result['job'].get('id') + print(f"✓ Finalized sync job: {finalize_job_id}") + print(f" Status: {finalize_result['job']['status']}") + + # Check final status + if finalize_result['job']['status'] == 'COMPLETED': + print("✓ Sync job completed successfully!") + elif finalize_result['job']['status'] == 'FAILED': + error_msg = finalize_result['job'].get('error', 'Unknown error') + print(f"✗ Sync job failed: {error_msg}") + else: + print(f"ℹ Sync job status: {finalize_result['job']['status']}") + + print("\n=== Sync Job Workflow Complete ===") + + except Exception as e: + print(f"✗ Error during sync job workflow: {e}") + sys.exit(1) + +if __name__ == "__main__": + main() + diff --git a/jupiterone/client.py b/jupiterone/client.py index 721c843..076fd04 100644 --- a/jupiterone/client.py +++ b/jupiterone/client.py @@ -662,8 +662,15 @@ def start_sync_job( args: instance_id (str): The "integrationInstanceId" request param for synchronization job - sync_mode (str): The "syncMode" request body property for synchronization job. "DIFF", "CREATE_OR_UPDATE", or "PATCH" + sync_mode (str): The "syncMode" request body property for synchronization job. "DIFF" or "PATCH" source (str): The "source" request body property for synchronization job. "api" or "integration-external" + + Note: + IMPORTANT: PATCH sync jobs cannot target relationships. If your sync job involves creating + or updating relationships, you must use "DIFF" sync_mode instead of "PATCH". This is due + to the JupiterOne data pipeline upgrade. + + For more information, see: https://docs.jupiterone.io/reference/pipeline-upgrade#patch-sync-jobs-cannot-target-relationships """ endpoint = "/persister/synchronization/jobs"