Created
February 12, 2026 06:22
-
-
Save RajChowdhury240/635920e1e45dc56455686101d60f9cd3 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env python3 | |
| """ | |
| Generate README.md with all active AWS accounts info. | |
| Pulls account details from AWS Organizations and tags, | |
| then renders a Bitbucket-compatible Markdown table. | |
| """ | |
| import boto3 | |
| from datetime import datetime, timezone | |
| # ────────────────────────────────────────────────────────────────────── | |
| # CONFIGURATION | |
| # ────────────────────────────────────────────────────────────────────── | |
| # Map SSO usernames → display names. Add/remove as needed. | |
| SSO_MAPPING = { | |
| # "john.doe": "John Doe", | |
| # "jane.smith": "Jane Smith", | |
| } | |
| # IAM switch-role names (the two buttons rendered per account) | |
| SWITCH_ROLES = ["ca-iam-cie-engineer", "ca-iam-cie-a"] | |
| # AWS Switch Role console URL template | |
| SWITCH_ROLE_URL = ( | |
| "https://signin.aws.amazon.com/switchrole" | |
| "?roleName={role}&account={account_id}&displayName={display}" | |
| ) | |
| # ────────────────────────────────────────────────────────────────────── | |
| # VERSATILE ACCOUNTS (not part of our Org – maintained manually) | |
| # Add rows here; they appear in a separate table at the bottom. | |
| # ────────────────────────────────────────────────────────────────────── | |
| VERSATILE_ACCOUNTS = [ | |
| { | |
| "metadata_name": "versatile-test", | |
| "account_id": "021891588050", | |
| "account_name": "test", | |
| "versatile_ou": "Root/Workloads", | |
| }, | |
| # { | |
| # "metadata_name": "versatile-network", | |
| # "account_id": "XXXXXXXXXXXX", | |
| # "account_name": "network", | |
| # "versatile_ou": "Root/Workloads", | |
| # }, | |
| ] | |
| # ────────────────────────────────────────────────────────────────────── | |
| # TAG KEYS | |
| # ────────────────────────────────────────────────────────────────────── | |
| TAG_DESCRIPTION = "syf:aws:account_description" | |
| TAG_PRIMARY_OWNER = "syf:aws:account.primary_owner" | |
| TAG_SECONDARY_OWNER = "syf:aws:account.secondary_owner" | |
| TAG_ALLOWED_CIS = "syf:aws:account.allowed_cis" | |
| TAG_AVM_RESOURCE = "syf:aws:avm_resource" | |
| # ────────────────────────────────────────────────────────────────────── | |
| # HELPERS | |
| # ────────────────────────────────────────────────────────────────────── | |
| def get_all_active_accounts(org_client): | |
| """Return all ACTIVE accounts in the organization.""" | |
| accounts = [] | |
| paginator = org_client.get_paginator("list_accounts") | |
| for page in paginator.paginate(): | |
| for acct in page["Accounts"]: | |
| if acct["Status"] == "ACTIVE": | |
| accounts.append(acct) | |
| return sorted(accounts, key=lambda a: a["Name"].lower()) | |
| def get_account_tags(org_client, account_id): | |
| """Return {tag_key: tag_value} for an account.""" | |
| tags = {} | |
| paginator = org_client.get_paginator("list_tags_for_resource") | |
| for page in paginator.paginate(ResourceId=account_id): | |
| for tag in page["Tags"]: | |
| tags[tag["Key"]] = tag["Value"] | |
| return tags | |
| def get_parent_ous(org_client, account_id): | |
| """ | |
| Walk from the account up to Root and return: | |
| - ou_name : the immediate parent OU name (e.g. "prod") | |
| - ou_path : full slash-separated path (e.g. "Root/Workloads/prod") | |
| """ | |
| parts = [] | |
| child_id = account_id | |
| while True: | |
| resp = org_client.list_parents(ChildId=child_id) | |
| parent = resp["Parents"][0] | |
| parent_id = parent["Id"] | |
| parent_type = parent["Type"] | |
| if parent_type == "ROOT": | |
| parts.append("Root") | |
| break | |
| else: | |
| ou = org_client.describe_organizational_unit( | |
| OrganizationalUnitId=parent_id | |
| )["OrganizationalUnit"] | |
| parts.append(ou["Name"]) | |
| child_id = parent_id | |
| parts.reverse() | |
| ou_path = "/".join(parts) | |
| # The immediate OU is the last element before reversal → first before Root | |
| ou_name = parts[-1] if len(parts) > 1 else "Root" | |
| return ou_name, ou_path | |
| def format_owner(sso_value): | |
| """Return 'sso (Full Name)' if mapping exists, else just sso.""" | |
| if not sso_value or sso_value.strip() == "": | |
| return "N/A" | |
| sso = sso_value.strip() | |
| name = SSO_MAPPING.get(sso) | |
| return f"{sso} ({name})" if name else sso | |
| def switch_role_buttons(account_id, account_name): | |
| """Return Markdown links styled as buttons for each switch role.""" | |
| buttons = [] | |
| for role in SWITCH_ROLES: | |
| display = f"{account_name}+{role}" | |
| url = SWITCH_ROLE_URL.format( | |
| role=role, | |
| account_id=account_id, | |
| display=display, | |
| ) | |
| buttons.append(f"[`{role}`]({url})") | |
| return " | ".join(buttons) | |
| def ou_color(ou_path): | |
| """ | |
| Return an environment marker with color hint. | |
| Bitbucket Markdown doesn't support HTML colors in all views, | |
| so we use emoji + bold markers for visibility: | |
| 🔴 prod | 🟢 sandbox | 🔵 dev/lab/other | |
| """ | |
| path_lower = ou_path.lower() | |
| if "prod" in path_lower: | |
| return "🔴" | |
| if "sandbox" in path_lower: | |
| return "🟢" | |
| return "🔵" | |
| def build_ou_tree(accounts_with_ou): | |
| """ | |
| Build a text-based OU tree from all collected OU paths. | |
| Returns a string like: | |
| Root | |
| ├── Workloads | |
| │ ├── prod | |
| │ ├── dev | |
| │ └── sandbox | |
| └── Security | |
| """ | |
| tree = {} | |
| for item in accounts_with_ou: | |
| parts = item["ou_path"].split("/") | |
| node = tree | |
| for p in parts: | |
| node = node.setdefault(p, {}) | |
| lines = [] | |
| def _render(node, prefix=""): | |
| keys = sorted(node.keys()) | |
| for i, key in enumerate(keys): | |
| is_last = i == len(keys) - 1 | |
| connector = "└── " if is_last else "├── " | |
| lines.append(f"{prefix}{connector}{key}") | |
| extension = " " if is_last else "│ " | |
| _render(node[key], prefix + extension) | |
| if tree: | |
| root_keys = sorted(tree.keys()) | |
| for rk in root_keys: | |
| lines.append(rk) | |
| _render(tree[rk]) | |
| return "\n".join(lines) | |
| # ────────────────────────────────────────────────────────────────────── | |
| # MAIN | |
| # ────────────────────────────────────────────────────────────────────── | |
| def main(): | |
| org_client = boto3.client("organizations") | |
| print("Fetching active accounts …") | |
| accounts = get_all_active_accounts(org_client) | |
| print(f" Found {len(accounts)} active accounts.") | |
| rows = [] | |
| for acct in accounts: | |
| aid = acct["Id"] | |
| name = acct["Name"] | |
| print(f" Processing {name} ({aid}) …") | |
| tags = get_account_tags(org_client, aid) | |
| ou_name, ou_path = get_parent_ous(org_client, aid) | |
| avm_tag = tags.get(TAG_AVM_RESOURCE, "").strip().lower() | |
| is_avm = avm_tag not in ("", "false") | |
| rows.append( | |
| { | |
| "name": name, | |
| "id": aid, | |
| "switch_role": switch_role_buttons(aid, name), | |
| "ou_name": ou_name, | |
| "ou_path": ou_path, | |
| "avm_legacy": "AVM" if is_avm else "Legacy", | |
| "allowed_cis": tags.get(TAG_ALLOWED_CIS, "N/A").strip() or "N/A", | |
| "primary_owner": format_owner(tags.get(TAG_PRIMARY_OWNER, "")), | |
| "secondary_owner": format_owner(tags.get(TAG_SECONDARY_OWNER, "")), | |
| "description": tags.get(TAG_DESCRIPTION, "N/A").strip() or "N/A", | |
| "color": ou_color(ou_path), | |
| } | |
| ) | |
| # ── Build OU tree ──────────────────────────────────────────────── | |
| ou_tree = build_ou_tree(rows) | |
| # ── Render Markdown ────────────────────────────────────────────── | |
| now = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M UTC") | |
| md_lines = [ | |
| "# AWS Accounts Directory", | |
| "", | |
| f"> **Last generated:** {now}", | |
| "", | |
| "## Color Legend", | |
| "", | |
| "| Marker | Environment |", | |
| "|--------|-------------|", | |
| "| 🔴 | **Production** |", | |
| "| 🟢 | **Sandbox** |", | |
| "| 🔵 | **Dev / Lab / Other** |", | |
| "", | |
| "---", | |
| "", | |
| "## Active Accounts", | |
| "", | |
| "| # | Env | Account Name | Account ID | Switch Role | OU Name | OU Path | AVM/Legacy | Allowed CIs | Primary Owner | Secondary Owner | Description |", | |
| "|---|-----|-------------|------------|-------------|---------|---------|------------|-------------|---------------|-----------------|-------------|", | |
| ] | |
| for idx, r in enumerate(rows, start=1): | |
| md_lines.append( | |
| f"| {idx} " | |
| f"| {r['color']} " | |
| f"| **{r['name']}** " | |
| f"| `{r['id']}` " | |
| f"| {r['switch_role']} " | |
| f"| {r['ou_name']} " | |
| f"| `{r['ou_path']}` " | |
| f"| {r['avm_legacy']} " | |
| f"| {r['allowed_cis']} " | |
| f"| {r['primary_owner']} " | |
| f"| {r['secondary_owner']} " | |
| f"| {r['description']} |" | |
| ) | |
| # ── Versatile Accounts ─────────────────────────────────────────── | |
| if VERSATILE_ACCOUNTS: | |
| md_lines += [ | |
| "", | |
| "---", | |
| "", | |
| "## Versatile Accounts (External Org)", | |
| "", | |
| "> These accounts are **not** part of our AWS Organization but we have switch-role access.", | |
| "", | |
| "| # | Metadata Name | Account ID | Account Name | Switch Role | Versatile OU |", | |
| "|---|--------------|------------|-------------|-------------|-------------|", | |
| ] | |
| for idx, v in enumerate(VERSATILE_ACCOUNTS, start=1): | |
| sr = switch_role_buttons(v["account_id"], v["account_name"]) | |
| md_lines.append( | |
| f"| {idx} " | |
| f"| {v['metadata_name']} " | |
| f"| `{v['account_id']}` " | |
| f"| **{v['account_name']}** " | |
| f"| {sr} " | |
| f"| `{v['versatile_ou']}` |" | |
| ) | |
| # ── OU Tree ────────────────────────────────────────────────────── | |
| md_lines += [ | |
| "", | |
| "---", | |
| "", | |
| "## Organization OU Tree", | |
| "", | |
| "```", | |
| ou_tree, | |
| "```", | |
| "", | |
| "---", | |
| "", | |
| f"*Auto-generated by `generate_readme.py` — {now}*", | |
| "", | |
| ] | |
| readme_content = "\n".join(md_lines) | |
| with open("README.md", "w") as f: | |
| f.write(readme_content) | |
| print(f"\nREADME.md generated successfully with {len(rows)} accounts.") | |
| print(f"Versatile accounts added: {len(VERSATILE_ACCOUNTS)}") | |
| if __name__ == "__main__": | |
| main() |
Author
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
{tree_text}