commit 24ff9fd2fc7b0507d1c9dd3ac5dba887d8b34edd Author: fultonbr Date: Thu Sep 18 09:40:08 2025 -0500 On branch main Initial commit Changes to be committed: new file: README.md new file: abx/apply_nsx_tags_for_tiers/README.md new file: abx/apply_nsx_tags_for_tiers/action.py new file: abx/list_vcenter_vms/README.md new file: abx/list_vcenter_vms/action.py new file: abx/send_email/README.md new file: abx/send_email/action.py new file: blueprints/forms/vdefend-form.json new file: blueprints/vdefend-form-driven.yaml diff --git a/README.md b/README.md new file mode 100644 index 0000000..ed22717 --- /dev/null +++ b/README.md @@ -0,0 +1,52 @@ +# vDefend Form-Driven Blueprint (VCF Automation 9) + +This package contains: +- `blueprints/vdefend-form-driven.yaml` — Cloud Template that calls Terraform to create vDefend groups/services/rules. +- `blueprints/forms/vdefend-form.json` — Custom Form draft that pulls vCenter VMs via ABX and parses CSV ports. +- ABX actions: + - `list_vcenter_vms` — exposes vCenter inventory for the form. + - `apply_nsx_tags_for_tiers` — tags selected VMs with `tier` and `env`. + - `send_email` — generic SMTP mailer. + +## Wiring overview + +1) **Create ABX actions** (Python 3): + - `list_vcenter_vms`: set constants `VCENTER_SERVER`, `VCENTER_USERNAME`, `VCENTER_PASSWORD`. + - `apply_nsx_tags_for_tiers`: none (reads blueprint inputs). + - `send_email`: set constants `SMTP_HOST` (and optionally user/pass). + +2) **Import the Terraform module** (re-use the `vdefend_baseline_module` from the previous kit, or point the blueprint to your Git path). + +3) **Create the Cloud Template** from `vdefend-form-driven.yaml`. Map `nsx_*` inputs to **Project Secrets**. + +4) **Attach Custom Form**: + - Import `vdefend-form.json` into the Form Designer for this template. + - Change the data source `actionId` on `vm_web`, `vm_app`, `vm_db` to the actual ABX ID of `list_vcenter_vms`. + +5) **Event Subscriptions**: + - Create a subscription: **Event = Deployment Completed**, **Filter by blueprint name = vdefend-form-driven**. + - Add two actions in order: + 1. `apply_nsx_tags_for_tiers` — Map inputs from the deployment inputs (`vm_web`, `vm_app`, `vm_db`, `env_value`, `nsx_manager_url`, `nsx_username`, `nsx_password`). + 2. `send_email` — Build `body` and `to_email` using deployment inputs/outputs: + - `to_email = requester_email` + - `subject = "vDefend policy created: " + app_name` + - `body` example: + ``` + Application: ${app_name} + Environment: ${env_value} + + NSX Section: ${outputs.sectionPath} + Groups: + Web: ${outputs.groups.web} + App: ${outputs.groups.app} + DB : ${outputs.groups.db} + + Ports: + Web->App: ${inputs.ports_web_to_app} + App->DB : ${inputs.ports_app_to_db} + ``` + +## Notes +- The policy is tag-driven. After deployment, the ABX action tags your selected VMs: `{tier:web|app|db}` and `{env:}`. The groups in NSX will immediately include them. +- To extend rules, add more arrays (e.g., `ports_web_to_db`) and mirror them in the Terraform module. +- For strict change control, protect the section with a **lock** or maintain via GitOps-only. diff --git a/abx/apply_nsx_tags_for_tiers/README.md b/abx/apply_nsx_tags_for_tiers/README.md new file mode 100644 index 0000000..0664ec8 --- /dev/null +++ b/abx/apply_nsx_tags_for_tiers/README.md @@ -0,0 +1,14 @@ +# ABX: apply_nsx_tags_for_tiers (Python 3) + +Applies NSX tags to selected VMs for tier and environment. Use as a **Deployment Completed** subscription so it runs after the Terraform policy is created. + +## Inputs +- nsx_manager_url, nsx_username, nsx_password +- env_value: prod|test|dev (or any string) +- vm_web: array of VM names +- vm_app: array of VM names +- vm_db: array of VM names + +## Behavior +- Looks up each VM by display_name in NSX Fabric VMs. +- Adds tags: `{scope: 'tier', tag: 'web|app|db'}` and `{scope: 'env', tag: env_value}`. diff --git a/abx/apply_nsx_tags_for_tiers/action.py b/abx/apply_nsx_tags_for_tiers/action.py new file mode 100644 index 0000000..d60dc6a --- /dev/null +++ b/abx/apply_nsx_tags_for_tiers/action.py @@ -0,0 +1,50 @@ +import requests, json + +def _session(nm, user, pwd): + s = requests.Session() + s.verify = False + s.auth = (user, pwd) + s.headers.update({"Content-Type": "application/json"}) + return s + +def _find_by_name(s, nm, name): + # A simple scan; for large estates consider filtering parameters & paging + r = s.get(f"{nm}/api/v1/fabric/virtual-machines") + r.raise_for_status() + for vm in r.json().get("results", []): + if vm.get("display_name") == name or vm.get("vm_name") == name: + return vm.get("external_id") + return None + +def _apply_tags(s, nm, external_id, tags): + body = {"external_id": external_id, "tags": tags} + r = s.post(f"{nm}/api/v1/fabric/virtual-machines", params={"action": "add_tags"}, data=json.dumps(body)) + if r.status_code not in (200,204): + raise Exception(f"Tagging failed: {r.status_code} {r.text}") + +def handler(context, inputs): + nm = inputs["nsx_manager_url"] + user = inputs["nsx_username"] + pwd = inputs["nsx_password"] + env_value = inputs["env_value"] + + tiers = { + "web": inputs.get("vm_web") or [], + "app": inputs.get("vm_app") or [], + "db": inputs.get("vm_db") or [], + } + + s = _session(nm, user, pwd) + applied = [] + + for tier, names in tiers.items(): + for name in names: + extid = _find_by_name(s, nm, name) + if not extid: + applied.append({"name": name, "tier": tier, "status": "not_found"}) + continue + tags = [{"scope":"tier","tag":tier},{"scope":"env","tag":env_value}] + _apply_tags(s, nm, extid, tags) + applied.append({"name": name, "tier": tier, "status": "tagged"}) + + return {"result": applied} diff --git a/abx/list_vcenter_vms/README.md b/abx/list_vcenter_vms/README.md new file mode 100644 index 0000000..aac2688 --- /dev/null +++ b/abx/list_vcenter_vms/README.md @@ -0,0 +1,11 @@ +# ABX: list_vcenter_vms (Python 3) + +Returns an array of VM names from vCenter for use in the Custom Form. + +## Constants (recommended) +- VCENTER_SERVER (e.g., https://vcsa.lab.legionitgroup.com) +- VCENTER_USERNAME +- VCENTER_PASSWORD + +## Output format +Return a JSON array of strings (VM names) or objects `{ "text": "vm-name", "value": "vm-name" }`. diff --git a/abx/list_vcenter_vms/action.py b/abx/list_vcenter_vms/action.py new file mode 100644 index 0000000..c5913d8 --- /dev/null +++ b/abx/list_vcenter_vms/action.py @@ -0,0 +1,33 @@ +import os, requests + +def _login(session, base): + # Try modern API session (vSphere 8/9) + r = session.post(f"{base}/api/session") + if r.status_code in (200, 204): + return + # Fallback to legacy vAPI + r = session.post(f"{base}/rest/com/vmware/cis/session") + if r.status_code not in (200, 201): + raise Exception(f"vCenter login failed: {r.status_code} {r.text}") + +def handler(context, inputs): + base = os.getenv("VCENTER_SERVER") or inputs.get("vcenter_server") + user = os.getenv("VCENTER_USERNAME") or inputs.get("vcenter_username") + pwd = os.getenv("VCENTER_PASSWORD") or inputs.get("vcenter_password") + if not (base and user and pwd): + raise Exception("Missing vCenter credentials/server. Set VCENTER_* constants or pass in inputs.") + s = requests.Session() + s.verify = False + s.auth = (user, pwd) + _login(s, base) + # Try modern inventory endpoint + r = s.get(f"{base}/api/vcenter/vm") + if r.status_code == 200: + vms = [vm.get("name") for vm in r.json() if vm.get("name")] + else: + # Fallback legacy + r = s.get(f"{base}/rest/vcenter/vm") + r.raise_for_status() + vms = [vm.get("name") for vm in r.json().get("value", []) if vm.get("name")] + # Return array for multi-select + return vms diff --git a/abx/send_email/README.md b/abx/send_email/README.md new file mode 100644 index 0000000..eb6ce6e --- /dev/null +++ b/abx/send_email/README.md @@ -0,0 +1,9 @@ +# ABX: send_email (Python 3) + +Sends a summary email to the requester with the created section/groups and the chosen ports. + +## Inputs +- smtp_host, smtp_port (25 default), smtp_user/smtp_pass (optional), use_tls (bool) +- from_email, to_email, subject, body (text) + +This action is generic; blueprint post-processing can construct a friendly body. diff --git a/abx/send_email/action.py b/abx/send_email/action.py new file mode 100644 index 0000000..bb5af66 --- /dev/null +++ b/abx/send_email/action.py @@ -0,0 +1,34 @@ +import smtplib, ssl, os +from email.mime.text import MIMEText + +def handler(context, inputs): + smtp_host = inputs.get("smtp_host") or os.getenv("SMTP_HOST") + smtp_port = int(inputs.get("smtp_port", 25)) + smtp_user = inputs.get("smtp_user") or os.getenv("SMTP_USER") + smtp_pass = inputs.get("smtp_pass") or os.getenv("SMTP_PASS") + use_tls = bool(inputs.get("use_tls", False)) + + from_email = inputs["from_email"] + to_email = inputs["to_email"] + subject = inputs.get("subject", "vDefend policy created") + body = inputs.get("body", "") + + msg = MIMEText(body, "plain", "utf-8") + msg["Subject"] = subject + msg["From"] = from_email + msg["To"] = to_email + + if use_tls: + context = ssl.create_default_context() + with smtplib.SMTP(smtp_host, smtp_port) as server: + server.starttls(context=context) + if smtp_user and smtp_pass: + server.login(smtp_user, smtp_pass) + server.send_message(msg) + else: + with smtplib.SMTP(smtp_host, smtp_port) as server: + if smtp_user and smtp_pass: + server.login(smtp_user, smtp_pass) + server.send_message(msg) + + return {"status": "sent", "to": to_email} diff --git a/blueprints/forms/vdefend-form.json b/blueprints/forms/vdefend-form.json new file mode 100644 index 0000000..507a8b3 --- /dev/null +++ b/blueprints/forms/vdefend-form.json @@ -0,0 +1,149 @@ +{ + "schemaVersion": "1", + "layout": { + "pages": [ + { + "id": "page1", + "title": "vDefend Policy Builder", + "sections": [ + { + "id": "sec-app", + "label": "Application", + "fields": [ + "app_name", + "env_value", + "requester_email" + ] + }, + { + "id": "sec-vms", + "label": "Select VMs", + "fields": [ + "vm_web", + "vm_app", + "vm_db" + ] + }, + { + "id": "sec-ports", + "label": "Ports", + "fields": [ + "ports_web_to_app_csv", + "ports_app_to_db_csv" + ] + }, + { + "id": "sec-endpoints", + "label": "Endpoints", + "fields": [ + "nsx_manager_url", + "nsx_username", + "nsx_password" + ] + } + ] + } + ] + }, + "fields": { + "app_name": { + "type": "string", + "label": "Application Name" + }, + "env_value": { + "type": "string", + "label": "Environment", + "enum": [ + "prod", + "test", + "dev" + ] + }, + "requester_email": { + "type": "string", + "label": "Requester Email" + }, + "ports_web_to_app": { + "type": "array", + "label": "Ports (Web->App)", + "hidden": true + }, + "ports_app_to_db": { + "type": "array", + "label": "Ports (App->DB)", + "hidden": true + }, + "ports_web_to_app_csv": { + "type": "string", + "label": "Ports (Web->App) CSV", + "default": "80,443", + "computeScript": "return form.getValue('ports_web_to_app_csv').split(',').map(s=>Number(s.trim())).filter(n=>!isNaN(n));", + "onChangeScript": "form.setValue('ports_web_to_app', eval(field.computeScript));" + }, + "ports_app_to_db_csv": { + "type": "string", + "label": "Ports (App->DB) CSV", + "default": "5432", + "computeScript": "return form.getValue('ports_app_to_db_csv').split(',').map(s=>Number(s.trim())).filter(n=>!isNaN(n));", + "onChangeScript": "form.setValue('ports_app_to_db', eval(field.computeScript));" + }, + "vm_web": { + "type": "array", + "label": "Web Tier VMs", + "dataSource": { + "type": "action", + "actionId": "list_vcenter_vms", + "parameters": {} + }, + "multiSelect": true + }, + "vm_app": { + "type": "array", + "label": "App Tier VMs", + "dataSource": { + "type": "action", + "actionId": "list_vcenter_vms", + "parameters": {} + }, + "multiSelect": true + }, + "vm_db": { + "type": "array", + "label": "DB Tier VMs", + "dataSource": { + "type": "action", + "actionId": "list_vcenter_vms", + "parameters": {} + }, + "multiSelect": true + }, + "nsx_manager_url": { + "type": "string", + "label": "NSX Manager URL" + }, + "nsx_username": { + "type": "string", + "label": "NSX Username" + }, + "nsx_password": { + "type": "string", + "label": "NSX Password", + "encrypted": true + } + }, + "options": { + "fieldOrder": [ + "app_name", + "env_value", + "requester_email", + "vm_web", + "vm_app", + "vm_db", + "ports_web_to_app_csv", + "ports_app_to_db_csv", + "nsx_manager_url", + "nsx_username", + "nsx_password" + ] + } +} \ No newline at end of file diff --git a/blueprints/vdefend-form-driven.yaml b/blueprints/vdefend-form-driven.yaml new file mode 100644 index 0000000..043910d --- /dev/null +++ b/blueprints/vdefend-form-driven.yaml @@ -0,0 +1,94 @@ +formatVersion: 1 +name: vdefend-form-driven +version: 1 +inputs: + app_name: + type: string + title: Application Name + description: Logical name used to prefix NSX groups and section. + default: vdefend-app + env_value: + type: string + title: Environment + enum: + - prod + - test + - dev + default: prod + requester_email: + type: string + title: Requester Email + # vCenter inventory selection (populated via Custom Form using ABX data source) + vm_web: + type: array + title: Web Tier VMs + description: Select one or more VMs for the Web tier + items: + type: string + vm_app: + type: array + title: App Tier VMs + items: + type: string + vm_db: + type: array + title: DB Tier VMs + items: + type: string + # Port lists (array of numbers; Custom Form will parse CSV input into arrays) + ports_web_to_app: + type: array + title: Ports (Web -> App) + items: + type: number + default: + - 80 + - 443 + ports_app_to_db: + type: array + title: Ports (App -> DB) + items: + type: number + default: + - 5432 + # Endpoints / credentials (map these to Project Secrets in production) + nsx_manager_url: + type: string + title: NSX Manager URL + nsx_username: + type: string + encrypted: true + nsx_password: + type: string + encrypted: true + +resources: + vdefendPolicy: + type: Cloud.Terraform + properties: + providers: + - name: nsxt + source: vmware/nsxt + version: ">= 3.9.0" + module: + # point to your Git content source that contains the module path below + source: git::https://your.git/VCFA_Avi_vDefend_kit.git//terraform/vdefend_baseline_module + variables: + nsx_manager_url: ${input.nsx_manager_url} + nsx_username: ${input.nsx_username} + nsx_password: ${input.nsx_password} + domain: "default" + app_name: ${input.app_name} + env_value: ${input.env_value} + services_web_to_app: ${input.ports_web_to_app} + services_app_to_db: ${input.ports_app_to_db} + create_drop_others_rule: false + +outputs: + sectionPath: + value: ${resource.vdefendPolicy.outputs.section} + groups: + value: + web: ${resource.vdefendPolicy.outputs.group_web} + app: ${resource.vdefendPolicy.outputs.group_app} + db: ${resource.vdefendPolicy.outputs.group_db}