From 05f03f1a304a366d7a9b64cecf96b083e9d500e8 Mon Sep 17 00:00:00 2001 From: Vadim Shulkin Date: Tue, 4 Mar 2025 15:35:26 -0500 Subject: [PATCH] Updated --- requirements.txt | 1 + run_python_script.yml | 8 - scripts/__pycache__/utils.cpython-312.pyc | Bin 841 -> 15993 bytes scripts/config.py | 36 ++ scripts/my_script.py | 52 ++- scripts/utils.py | 433 +++++++++++++++++++++- 6 files changed, 509 insertions(+), 21 deletions(-) create mode 100644 scripts/config.py diff --git a/requirements.txt b/requirements.txt index 65a42be..9c405b5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,3 @@ requests pandas +ldap3 diff --git a/run_python_script.yml b/run_python_script.yml index c72fc0e..cb0ceb2 100644 --- a/run_python_script.yml +++ b/run_python_script.yml @@ -18,14 +18,6 @@ virtualenv: /tmp/ansible_venv virtualenv_command: "python3 -m venv" -# - name: Check installed Python packages -# command: bash -c "source /tmp/ansible_venv/bin/activate && pip list" -# register: installed_packages - -# - name: Show installed packages -# debug: -# msg: "{{ installed_packages.stdout }}" - - name: Run the Python script command: /tmp/ansible_venv/bin/python3 scripts/my_script.py register: script_output diff --git a/scripts/__pycache__/utils.cpython-312.pyc b/scripts/__pycache__/utils.cpython-312.pyc index f3a577eb993bbf1fa588c5afb521c7d0fbc92f62..10bff73245547741c1b9e9b7e5347fdc7822ebf2 100644 GIT binary patch literal 15993 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 K=@g8{PJaL-=eFbk diff --git a/scripts/config.py b/scripts/config.py new file mode 100644 index 0000000..fbd3db5 --- /dev/null +++ b/scripts/config.py @@ -0,0 +1,36 @@ +# Credentials +# USERNAME = "vshulkin@gmail.com" +# API_TOKEN = "ATATT3xFfGF0FXkQmlWw8DOdGLHoVc1rm5L9UwF5A3EoQjxkpoWNxUIXpck1eSJgn_u4Wdz8uKrsTDJDxio84WQv3HmsZ9uP7HsBl7vUAYfu0T_YzH9K5BtNyjeTiuA6p1HNYGnmBah-os4pJgE4U_v_MqjH8Bfsc1KcdTVSoRl8F2ZkG1Rs_aI=F1B2E220" +# WORKSPACE_ID = "51f695d7-820b-4515-8957-cb28f8695a35" +# USERNAME = "vadim.shulkin@point72.com" +# API_TOKEN = "ATATT3xFfGF0pbqKnnxf48g_75WnCK-K8Ub-pAg-QuGWk8_58v3Y20_SZMjhzOYxURWF6VSuV2WyxbaDUvf2yYR88mZx-2veYFF0t287L3ANDCRXVwWNvR5jspSurCqG7_0xuxtFEy6GtcfI7-LwCvlMjH5K5D2qIiT93GaGbmn34UqAKFiiMas=D13F4C4D" +# WORKSPACE_ID = "c691994f-ef8f-4597-89e3-b342def65753" + +TOKEN = "ATCTT3xFfGN0oSyQgWdWjc9lNfJLmN7iHyQ_AsVgaFvKFnC2b1mmwCT9_Dg57IknVYMMBTagXKpWEi13_7pNlrZ1GT7Jr4i4414Ws8t_-gdogzUlQ2jHd3W35L01EKtA60rOOODv2T_ZzdSk6CFsU183ID1_WoqqH_HliJ07mCbkCXhFqAK7oMw=2F52B0F1" +objectTypeId = "6" +objectSchemaId = "2" +ad_groups_attributes_id = { + "Name": 80, + "Active": 83 +} +attributesToDisplayIds = [ ad_groups_attributes_id["Name"], ad_groups_attributes_id["Active"] ] + +WORKSPACE_ID = "53cda6da-d40d-42d1-abb8-62dfff88de2a" +API_TOKEN = "ATATT3xFfGF04tMsWI8SR_PdZfj1dsF4pfq2O-Txr6NDmD8NbeNCXrVleJdUiA8FWQPtGO3ueWQSXoLoEi0mo0ptaKMy4GBjWmbyRgRMcsbcy3v7g8fJMCxRvV19x6vPjutgE0saap8lFUpNJgP5ihajMtiIAzwyrS7eVyBHqUlYl6Pl8lwbiqc=445E6B2D" +USERNAME = "bigappledc@gmail.com" +credentials = credentials = f"{USERNAME}:{API_TOKEN}" + +# API Base URL +BASE_URL = "https://api.atlassian.com/jsm/assets/workspace" + +# Ldap info +# ldap_server = 'ldap://ldap.saccap.int' +# ldap_user = 'CN=svcinfautodev, OU=System Accounts,OU=Accounts,DC=saccap,DC=int' +# ldap_password = 'r$t$fHz$4f2k' +# base_dn = 'OU=Groups,DC=saccap,DC=int' + +ldap_server = 'ldap://ldap.nyumc.org' +ldap_user = 'CN=oamstage,OU=ServiceAccounts,OU=NYULMC Non-Users,dc=nyumc,dc=org' +ldap_password = '63dX4@a5' +base_dn = 'OU=NYULMC Groups,DC=nyumc,DC=org' + diff --git a/scripts/my_script.py b/scripts/my_script.py index 9613bfa..b93c3aa 100644 --- a/scripts/my_script.py +++ b/scripts/my_script.py @@ -1,5 +1,4 @@ import sys -import pandas as pd from pathlib import Path @@ -7,13 +6,54 @@ from pathlib import Path sys.path.append(str(Path(__file__).parent)) # Import custom functions -from utils import greet_user, get_ad_groups +from utils import * +from config import * + +def sync_ad_groups(): + # Getting total number of records + url = f"{BASE_URL}/{WORKSPACE_ID}/v1/objecttype/{objectTypeId}" + response = send_request(url, credentials, method="GET") + objectCount = response.get('objectCount') + + # Fetching all records 1000 records per request + url = f"{BASE_URL}/{WORKSPACE_ID}/v1/object/navlist/aql" + #cmdb_df = fetch_all_records(url, credentials, objectCount, page_size=5000) + cmdb_df = pd.read_csv("cmdb_df.csv") + print(cmdb_df) + # Save into csv file + # cmdb_df.to_csv("cmdb_df.csv", index=False) + + # # Fetching ALL Ad Groups + #ad_df = get_ad_groups_entries() + ad_df = pd.read_csv("ad_df.csv") + print(ad_df) + # Save into csv file + # ad_df.to_csv("ad_df.csv", index=False) + + # Get list of entries which should be set Inactive in CMDB + to_be_set_inactive = rows_not_in_df2(cmdb_df, ad_df) + print("Following records no longer exist") + print(to_be_set_inactive) + + set_inactive_in_cmdb(to_be_set_inactive) + + # Get a list of entries to be created in CMDB + to_be_created = rows_new_in_df2(cmdb_df, ad_df) + print("Following records are new") + print(to_be_created) + + create_new_in_cmdb(to_be_created) + def main(): - user = "Ansible AWX" - message = greet_user(user) - print(message) - get_ad_groups() + print("Starting data collection, processing and API calls...") + + # sync_zones() + # sync_zone_groups() + sync_ad_groups() + + print("Process completed successfully.") + if __name__ == "__main__": diff --git a/scripts/utils.py b/scripts/utils.py index 1a9f621..4de3e2c 100644 --- a/scripts/utils.py +++ b/scripts/utils.py @@ -1,14 +1,433 @@ +import requests +import csv +import base64 +from itertools import islice +from ldap3 import Server, Connection, ALL, SUBTREE import pandas as pd +import json +import re + +from config import * -def greet_user(name): - return f"Hello, {name}! Your Python script is running via Ansible AWX." +def replace_in_list(lst, translation_map): + """Replaces values in a list based on a given mapping dictionary.""" + return [translation_map[item] for item in lst if item in translation_map] +def generate_json(value_array, objecttypeid, objectTypeAttributeId): + """Generates a JSON payload for the API request.""" + return { + "objectId": objecttypeid, + "objectTypeAttributeId": objectTypeAttributeId, + "objectAttributeValues": [{"value": v} for v in value_array] + } +def generate_create_zone_json(value_array, objecttypeid, objectTypeAttributeId): + """Generates a JSON payload for the API request.""" + return { + "objectTypeId": objecttypeid, + "attributes": [{ + "objectTypeAttributeId": objectTypeAttributeId, + "objectAttributeValues": [{"value": v} for v in value_array] + }] + } -def get_ad_groups(): - df1 = pd.DataFrame({'A': [1, 2, 3], 'B': ['x', 'y', 'z'], 'C': [10, 20, 30]}) - df2 = pd.DataFrame({'A': [2, 3, 4, 5], 'B': ['y', 'z', 'w', 'q'], 'C': [20, 30, 50, 40]}) +def split_csv(file_path, max_lines, uuid): + """Splits a CSV file into multiple chunks ensuring no chunk exceeds the specified number of lines.""" + chunks = [] + with open(file_path, newline='', encoding='utf-8') as csvfile: + reader = csv.reader(csvfile) + next(reader) # Skip header row + + while True: + chunk = list(islice(reader, max_lines)) + if not chunk: + break + + ad_groups = [{"name": row[0]} for row in chunk] # Assuming first column contains group names + output_data = { + "data": { + "adGroups": ad_groups + }, + "clientGeneratedId": uuid + } + + chunks.append(output_data) + + return chunks +def get_import_info(token): + """Fetches import information from Atlassian API and removes the 'links' branch.""" + url = "https://api.atlassian.com/jsm/assets/v1/imports/info" + headers = { + "Authorization": f"Bearer {token}", + "Content-Type": "application/json" + } - print(df1) - print(df2) + response = requests.get(url, headers=headers) + + if response.status_code == 200: + data = response.json() + return data['links'] + else: + response.raise_for_status() +def initiate_schema_map(url, token): + data_structure = { + "schema": { + "objectSchema": { + "name": "Active Directory Groups", + "description": "Data imported from AD", + "objectTypes": [ + { + "externalId": "object-type/ad-group", + "name": "AD Groups", + "description": "AD Group found during scanning", + "attributes": [ + { + "externalId": "object-type-attribute/adgroup-name", + "name": "Name", + "description": "Ad Group Name", + "type": "text", + "label": True + } + ] + } + ] + } + }, + "mapping": { + "objectTypeMappings": [ + { + "objectTypeExternalId": "object-type/ad-group", + "objectTypeName": "AD Groups", + "selector": "adGroups", + "description": "Mapping for AD Groups", + "attributesMapping": [ + { + "attributeExternalId": "object-type-attribute/adgroup-name", + "attributeName": "Name", + "attributeLocators": ["name"] + } + ] + } + ] + } + } + + headers = { + "Authorization": f"Bearer {token}", + "Content-Type": "application/json" + } + + response = requests.put(url, headers=headers, json=data_structure) + + if response.status_code == 200 or response.status_code == 201: + print("Request successful:", response.json()) + else: + print(f"Error {response.status_code}: {response.text}") + + return response +def start_import(url, token): + headers = { + "Authorization": f"Bearer {token}", + "Content-Type": "application/json" + } + + data_structure={} + response = requests.post(url, headers=headers, json=data_structure) + + if response.status_code == 200: + data = response.json() + return data['links'] + else: + response.raise_for_status() +def send_data(url, token, payload): + headers = { + "Authorization": f"Bearer {token}", + "Content-Type": "application/json" + } + + response = requests.post(url, headers=headers, json=payload) + + if response.status_code == 200 or response.status_code == 201: + print("Request successful:", response.json()) + else: + print(f"Error {response.status_code}: {response.text}") +def get_ad_groups_entries(): + server = Server(ldap_server, get_info=ALL) + conn = Connection(server, ldap_user, ldap_password, auto_bind=True) + + page_size = 1000 + cookie = None + filtered_sAMAccountNames = [] + + try: + while True: + conn.search( + search_base=base_dn, + search_filter='(objectClass=group)', + search_scope=SUBTREE, + attributes=['sAMAccountName'], + paged_size=page_size, + paged_cookie=cookie + ) + + bad_chars = ['!', '@', '#', '$', '%', '^', '&', '*', '(', ')', '_', '-', '=', '+', '[', ']', '{', '}', ';', ':', '"', "'", '<', '>', ',', '.', '/', '?', '|', '\\', ' '] + bad_chars_pattern = f"^[{re.escape(''.join(bad_chars))}]" + + + for entry in conn.entries: + if 'sAMAccountName' in entry: + sAMAccountName = entry.sAMAccountName.value + + if re.match(bad_chars_pattern, sAMAccountName): + continue + if ' ' not in sAMAccountName and '$' not in sAMAccountName: + filtered_sAMAccountNames.append(sAMAccountName) + + cookie = conn.result['controls']['1.2.840.113556.1.4.319']['value']['cookie'] + + # Break the loop if no more pages + if not cookie: + break + + except Exception as e: + print(f"Error during LDAP search: {e}") + finally: + conn.unbind() + + # Remove duplicates and sort the list + unique_sorted_names = sorted(set(filtered_sAMAccountNames)) + + df = pd.DataFrame(unique_sorted_names, columns=["name"]) + + return df + + + + +def rows_not_in_df2(df1: pd.DataFrame, df2: pd.DataFrame, compare_cols=None) -> pd.DataFrame: + """ + Returns all rows in df1 that do not exist in df2, based on specified columns. + + :param df1: First DataFrame (source) + :param df2: Second DataFrame (reference) + :param compare_cols: List of column names to compare (default: all common columns) + :return: DataFrame containing rows from df1 that are not in df2 + """ + + # If no specific columns are given, use all common columns + if compare_cols is None: + compare_cols = df1.columns.intersection(df2.columns).tolist() + + # Ensure specified columns exist in both DataFrames + compare_cols = [col for col in compare_cols if col in df1.columns and col in df2.columns] + + # Convert column types to match in both DataFrames (avoid dtype mismatches) + df1 = df1.copy() + df2 = df2.copy() + for col in compare_cols: + df1[col] = df1[col].astype(str) + df2[col] = df2[col].astype(str) + + # Perform an anti-join using merge with an indicator column + df_merged = df1.merge(df2, on=compare_cols, how='left', indicator=True) + + # Keep rows that exist only in df1 + df1_not_in_df2 = df_merged[df_merged['_merge'] == 'left_only'].drop(columns=['_merge']) + + return df1_not_in_df2.reset_index(drop=True) # Reset index for clean output + +def rows_new_in_df2(df1: pd.DataFrame, df2: pd.DataFrame, compare_cols=None) -> pd.DataFrame: + """ + Returns all rows in df2 that do not exist in df1, based on specified columns. + + :param df1: First DataFrame (previous dataset) + :param df2: Second DataFrame (new dataset) + :param compare_cols: List of column names to compare (default: all common columns) + :return: DataFrame containing rows from df2 that are new (not in df1) + """ + + # If no specific columns are given, use all common columns + if compare_cols is None: + compare_cols = df1.columns.intersection(df2.columns).tolist() + + # Ensure specified columns exist in both DataFrames + compare_cols = [col for col in compare_cols if col in df1.columns and col in df2.columns] + + # Convert column types to match in both DataFrames (to avoid dtype mismatches) + df1 = df1.copy() + df2 = df2.copy() + for col in compare_cols: + df1[col] = df1[col].astype(str) + df2[col] = df2[col].astype(str) + + # Perform an anti-join using merge with an indicator column + df_merged = df2.merge(df1, on=compare_cols, how='left', indicator=True) + + # Keep rows that exist only in df2 (newly added rows) + df2_new_rows = df_merged[df_merged['_merge'] == 'left_only'].drop(columns=['_merge']) + + return df2_new_rows.reset_index(drop=True) # Reset index for clean output + +def send_request(url, credentials, payload=None, method="POST", headers=None): + """ + Sends an HTTP request with credentials and a data payload. + + Parameters: + url (str): The endpoint URL. + credentials (str): The username:api_token + payload (dict): The data payload to send. + method (str): The HTTP method (GET, POST, PUT, DELETE). Default is POST. + headers (dict, optional): Additional headers for the request. + + Returns: + response (Response): The response object from the request. + """ + + + encoded_credentials = base64.b64encode(credentials.encode()).decode() + + # Default headers if none are provided + if headers is None: + headers = { + "Accept": "application/json", + "Content-Type": "application/json", + "Authorization": f"Basic {encoded_credentials}" + } + else: + headers["Authorization"] = f"Basic {encoded_credentials}" + + request_params = { + "method": method, + "url": url, + "headers": headers + } + + if method.upper() != "GET" and payload: + request_params["data"] = payload # Add payload only for non-GET requests + + # Send the request with basic authentication + try: + response = requests.request(**request_params) + + # Check response status codes and return appropriate messages + if response.status_code == 201: + return {"message": "Created Successfully", "data": response.json()} + elif response.status_code == 400: + return {"error": "Bad Request", "details": response.text} + elif response.status_code == 401: + return {"error": "Unauthorized Access", "details": response.text} + elif response.status_code == 500: + return {"error": "Internal Server Error", "details": response.text} + + else: + response.raise_for_status() # Raise an error for other HTTP errors + return response.json() # Return successful response data + + except requests.exceptions.RequestException as e: + return {"error": str(e)} +def fetch_all_records(url, credentials, object_count=None, page_size=100): + """ + Fetch all objects from JSM Assets API in paginated requests. + + :param object_count: Total number of objects to retrieve. + :param page_size: Number of objects per request (default: 100). + :return: List of all retrieved objects. + """ + encoded_credentials = base64.b64encode(credentials.encode()).decode() + + # Default headers if none are provided + headers = { + "Accept": "application/json", + "Content-Type": "application/json", + "Authorization": f"Basic {encoded_credentials}" + } + + all_objects = [] + total_pages = (object_count // page_size) + (1 if object_count % page_size else 0) + + for page in range(total_pages): + params = { + "objectTypeId": objectTypeId, + "objectSchemaId": objectSchemaId, + "page": page + 1, + "attributesToDisplay": { + "attributesToDisplayIds": attributesToDisplayIds + }, + "asc": 1, + "resultsPerPage": page_size, + "qlQuery": "" + } + + # print(json.dumps(params, indent=2)) + response = requests.post(url, headers=headers, json=params) + + if response.status_code == 200: + data = response.json() + all_objects.extend(data.get("objectEntries", [])) # Extract objects from response + else: + print(f"Error on page {page + 1}: {response.status_code} - {response.text}") + break # Stop fetching if an error occurs + + columns = {"name": [], "id": []} + + for entry in all_objects: + columns["name"].append(entry["name"]) + columns["id"].append(entry["id"]) + + df = pd.DataFrame(columns) + + return df + +def get_cmdb_zones(): + return + +def get_centrify_zones(): + return + +def set_inactive_in_cmdb(df1: pd.DataFrame): + for index, row in df1.iterrows(): + objectid = row['id'] + name = row['name'] + attributes_id = ad_groups_attributes_id["Active"] + url = f"{BASE_URL}/{WORKSPACE_ID}/v1/objectattribute/{objectid}/attribute/{attributes_id}" + + payload = { + "objectAttributeValues":[{"value":"false"}] + } + + response = send_request(url, credentials, payload=json.dumps(payload), method="PUT", headers=None) + print("{} {}".format(name, response)) + + +def create_new_in_cmdb(df1: pd.DataFrame): + url = f"{BASE_URL}/{WORKSPACE_ID}/v1/object/create" + + for index, row in df1.iterrows(): + name = row['name'] + payload = { + "objectTypeId": objectTypeId, + "attributes": [ + { + "objectTypeAttributeId": ad_groups_attributes_id["Name"], + "objectAttributeValues": [ + { + "value": name + } + ] + }, + { + "objectTypeAttributeId": ad_groups_attributes_id["Active"], + "objectAttributeValues": [ + { + "value": "true" + } + ] + } + ], + "hasAvatar": False, + "avatarUUID": "" + } + + response = send_request(url, credentials, payload=json.dumps(payload), method="POST", headers=None) + print("{} {}".format(name, response))