Initial Commit of NSX Migration Script
This commit is contained in:
commit
1068b2997d
2 files changed
+1049
No files matched your search
@@ -0,0 +1,358 @@
|
|||||||
|
# NSX Migration Tool
|
||||||
|
|
||||||
|
A Python script for exporting and importing NSX Distributed Firewall (DFW) rules, Security Groups, and VM Tags between NSX Manager instances running version 3.x or 4.x.
|
||||||
|
|
||||||
|
No third-party libraries required — uses Python standard library only.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
- Python 3.6 or later
|
||||||
|
- Network access to the NSX Manager API (port 443)
|
||||||
|
- NSX credentials with at least **Enterprise Admin** or **Security Admin** role
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Supported NSX Versions
|
||||||
|
|
||||||
|
- NSX-T 3.x
|
||||||
|
- NSX-T 4.x
|
||||||
|
- NSX 9.x (including VCF-mode deployments)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## What Gets Exported / Imported
|
||||||
|
|
||||||
|
| Resource | Export Source | Import Target | Notes |
|
||||||
|
|---|---|---|---|
|
||||||
|
| **Tags** | `/api/v1/fabric/virtual-machines` | `?action=update_tags` | Stored as VM tag assignments, not standalone objects |
|
||||||
|
| **Security Groups** | `/policy/api/v1/infra/domains/default/groups` | Same path via PATCH | Full group definitions including membership criteria |
|
||||||
|
| **DFW Rules** | `/policy/api/v1/infra/domains/default/security-policies` | Same path via PATCH | All policies and their rules, preserving order |
|
||||||
|
|
||||||
|
### Output Files (Export)
|
||||||
|
|
||||||
|
| File | Default Name | Contents |
|
||||||
|
|---|---|---|
|
||||||
|
| Tags | `tags.json` | Unique tag list + per-VM assignment records |
|
||||||
|
| Security Groups | `sec_groups.json` | Full group definitions |
|
||||||
|
| DFW Rules | `dfw_rules.json` | Policies with embedded `_rules` arrays |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```
|
||||||
|
python nsx_migrate.py <action> --host <NSX_IP> -u <username> -p <password> [options]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Actions
|
||||||
|
|
||||||
|
| Action | Description |
|
||||||
|
|---|---|
|
||||||
|
| `export` | Export all resources to JSON files |
|
||||||
|
| `import` | Import all resources in order: Tags → Security Groups → DFW Rules |
|
||||||
|
| `import-tags` | Import only VM tag assignments |
|
||||||
|
| `import-groups` | Import only security groups |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Arguments
|
||||||
|
|
||||||
|
### Connection
|
||||||
|
|
||||||
|
| Argument | Short | Required | Default | Description |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| `--host` | | Yes | | NSX Manager hostname or IP address |
|
||||||
|
| `--username` | `-u` | Yes | | NSX Manager username |
|
||||||
|
| `--password` | `-p` | Yes | | NSX Manager password |
|
||||||
|
| `--no-verify-ssl` | | No | `False` | Disable SSL certificate verification (for self-signed certs) |
|
||||||
|
|
||||||
|
### Source context (export)
|
||||||
|
|
||||||
|
| Argument | Required | Default | Description |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `--src-org` | No | `default` | Source VCF org name |
|
||||||
|
| `--src-project` | No | _(none)_ | Source VCF project name — omit to use the default infra space |
|
||||||
|
| `--src-vpc` | No | _(none)_ | Source VPC name within the project — requires `--src-project` |
|
||||||
|
|
||||||
|
### Destination context (import)
|
||||||
|
|
||||||
|
| Argument | Required | Default | Description |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `--dst-org` | No | `default` | Destination VCF org name |
|
||||||
|
| `--dst-project` | No | _(none)_ | Destination VCF project name — omit to use the default infra space |
|
||||||
|
| `--dst-vpc` | No | _(none)_ | Destination VPC name within the project — requires `--dst-project` |
|
||||||
|
|
||||||
|
### Import behaviour
|
||||||
|
|
||||||
|
| Argument | Required | Default | Description |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `--conflict` | No | `overwrite` | Conflict handling mode — see below |
|
||||||
|
| `--merge-tags` | No | `False` | Merge imported tags with existing VM tags instead of replacing |
|
||||||
|
|
||||||
|
### File paths
|
||||||
|
|
||||||
|
| Argument | Required | Default | Description |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `--dfw-rules-file` | No | `dfw_rules.json` | Path to DFW rules JSON file |
|
||||||
|
| `--sec-groups-file` | No | `sec_groups.json` | Path to security groups JSON file |
|
||||||
|
| `--tags-file` | No | `tags.json` | Path to tags JSON file |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Project and VPC Context
|
||||||
|
|
||||||
|
NSX 9.x running in VCF mode supports multi-tenancy through **Projects** and **VPCs**. The script resolves API paths based on the context arguments provided.
|
||||||
|
|
||||||
|
| Context | API path used |
|
||||||
|
|---|---|
|
||||||
|
| No project/VPC (default) | `/policy/api/v1/infra/domains/default/...` |
|
||||||
|
| Project only | `/policy/api/v1/orgs/{org}/projects/{project}/infra/domains/default/...` |
|
||||||
|
| Project + VPC | `/policy/api/v1/orgs/{org}/projects/{project}/vpcs/{vpc}/...` |
|
||||||
|
|
||||||
|
**Source and destination contexts are independent.** You can export from a project and import to a different project, to a VPC, or to the default space — in any combination.
|
||||||
|
|
||||||
|
> **Note:** VM tag lookups always use the global fabric API (`/api/v1/fabric/virtual-machines`) regardless of project or VPC context. Tags are a fabric-level concept and are not scoped to projects or VPCs.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Conflict Handling (`--conflict`)
|
||||||
|
|
||||||
|
Controls what happens when an object already exists on the destination NSX.
|
||||||
|
|
||||||
|
| Mode | Behavior |
|
||||||
|
|---|---|
|
||||||
|
| `overwrite` | Always write the object, replacing any existing version **(default)** |
|
||||||
|
| `skip` | Leave existing objects untouched; only import objects that do not exist |
|
||||||
|
| `prompt` | Ask interactively for each conflict |
|
||||||
|
|
||||||
|
### Prompt Mode Keys
|
||||||
|
|
||||||
|
When `--conflict prompt` is used, the following keys are accepted per conflict:
|
||||||
|
|
||||||
|
| Key | Action |
|
||||||
|
|---|---|
|
||||||
|
| `y` | Yes — overwrite this object |
|
||||||
|
| `n` | No — skip this object |
|
||||||
|
| `a` | Yes to all — overwrite this and all remaining conflicts without prompting |
|
||||||
|
|
||||||
|
> **Note:** In non-interactive environments (piped input, CI/CD), `prompt` mode defaults to **skip** when stdin is unavailable.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Tag Merge (`--merge-tags`)
|
||||||
|
|
||||||
|
Applies to `import` and `import-tags` actions only.
|
||||||
|
|
||||||
|
| Mode | Behavior |
|
||||||
|
|---|---|
|
||||||
|
| Without flag | Replace all tags on the VM with the imported set (existing tags are removed) |
|
||||||
|
| `--merge-tags` | Union of imported tags and existing tags — no tags are removed, only new ones added |
|
||||||
|
|
||||||
|
Tag deduplication is based on the `(scope, tag)` pair.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## VM Resolution (Cross-vCenter)
|
||||||
|
|
||||||
|
When importing tags, the script resolves VMs on the destination using a two-step lookup:
|
||||||
|
|
||||||
|
1. **Primary — `external_id`**: Matches the vCenter MoRef ID. Works when both environments use the same vCenter or the IDs happen to align.
|
||||||
|
2. **Fallback — `display_name`**: Matches by VM name. Used automatically when the `external_id` lookup returns no results.
|
||||||
|
|
||||||
|
If multiple VMs share the same display name, the first result is used and a warning is printed. If neither lookup finds the VM, a warning is logged and the assignment is skipped — the import continues.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
### Export from source NSX
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python nsx_migrate.py export \
|
||||||
|
--host 192.168.1.10 \
|
||||||
|
--username admin \
|
||||||
|
--password VMware1! \
|
||||||
|
--no-verify-ssl
|
||||||
|
```
|
||||||
|
|
||||||
|
Produces `tags.json`, `sec_groups.json`, and `dfw_rules.json` in the current directory.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Full import to destination NSX (overwrite existing)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python nsx_migrate.py import \
|
||||||
|
--host 192.168.2.10 \
|
||||||
|
--username admin \
|
||||||
|
--password VMware1! \
|
||||||
|
--no-verify-ssl
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Full import — skip objects that already exist
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python nsx_migrate.py import \
|
||||||
|
--host 192.168.2.10 \
|
||||||
|
--username admin \
|
||||||
|
--password VMware1! \
|
||||||
|
--no-verify-ssl \
|
||||||
|
--conflict skip
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Full import — prompt on each conflict
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python nsx_migrate.py import \
|
||||||
|
--host 192.168.2.10 \
|
||||||
|
--username admin \
|
||||||
|
--password VMware1! \
|
||||||
|
--no-verify-ssl \
|
||||||
|
--conflict prompt
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Import only tags, merging with existing
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python nsx_migrate.py import-tags \
|
||||||
|
--host 192.168.2.10 \
|
||||||
|
--username admin \
|
||||||
|
--password VMware1! \
|
||||||
|
--no-verify-ssl \
|
||||||
|
--merge-tags
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Import only security groups, skip existing
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python nsx_migrate.py import-groups \
|
||||||
|
--host 192.168.2.10 \
|
||||||
|
--username admin \
|
||||||
|
--password VMware1! \
|
||||||
|
--no-verify-ssl \
|
||||||
|
--conflict skip
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Export from a VCF project
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python nsx_migrate.py export \
|
||||||
|
--host 192.168.1.10 -u admin -p VMware1! --no-verify-ssl \
|
||||||
|
--src-project my-project
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Export from a VPC inside a project
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python nsx_migrate.py export \
|
||||||
|
--host 192.168.1.10 -u admin -p VMware1! --no-verify-ssl \
|
||||||
|
--src-project my-project --src-vpc my-vpc
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Import into a specific project
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python nsx_migrate.py import \
|
||||||
|
--host 192.168.2.10 -u admin -p VMware1! --no-verify-ssl \
|
||||||
|
--dst-project my-project --conflict skip
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Import into a VPC
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python nsx_migrate.py import \
|
||||||
|
--host 192.168.2.10 -u admin -p VMware1! --no-verify-ssl \
|
||||||
|
--dst-project my-project --dst-vpc my-vpc
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Cross-project migration on the same NSX host
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Step 1 — export from project-a
|
||||||
|
python nsx_migrate.py export \
|
||||||
|
--host 192.168.1.10 -u admin -p VMware1! --no-verify-ssl \
|
||||||
|
--src-project project-a
|
||||||
|
|
||||||
|
# Step 2 — import to project-b
|
||||||
|
python nsx_migrate.py import \
|
||||||
|
--host 192.168.1.10 -u admin -p VMware1! --no-verify-ssl \
|
||||||
|
--dst-project project-b --conflict skip
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Use custom file names
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Export to custom paths
|
||||||
|
python nsx_migrate.py export \
|
||||||
|
--host 192.168.1.10 -u admin -p VMware1! \
|
||||||
|
--dfw-rules-file backup_dfw.json \
|
||||||
|
--sec-groups-file backup_groups.json \
|
||||||
|
--tags-file backup_tags.json
|
||||||
|
|
||||||
|
# Import from custom paths
|
||||||
|
python nsx_migrate.py import \
|
||||||
|
--host 192.168.2.10 -u admin -p VMware1! \
|
||||||
|
--dfw-rules-file backup_dfw.json \
|
||||||
|
--sec-groups-file backup_groups.json \
|
||||||
|
--tags-file backup_tags.json
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Import Order
|
||||||
|
|
||||||
|
When running a full `import`, resources are always applied in the following order to ensure dependencies are satisfied:
|
||||||
|
|
||||||
|
```
|
||||||
|
1. Tags — VMs must be present; no dependency on groups or rules
|
||||||
|
2. Security Groups — may reference tags in dynamic membership criteria
|
||||||
|
3. DFW Rules — policies reference security groups as sources/destinations
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Output / Status Codes
|
||||||
|
|
||||||
|
Each resource line during import prints one of the following prefixes:
|
||||||
|
|
||||||
|
| Prefix | Meaning |
|
||||||
|
|---|---|
|
||||||
|
| `OK` | Successfully written to destination |
|
||||||
|
| `SKIP` | Object already exists and was left untouched (`skip` or `prompt`=no) |
|
||||||
|
| `WARNING` | Non-fatal issue (e.g. VM not found for tag assignment) |
|
||||||
|
| `FAIL` | API call failed — error detail printed on the same line |
|
||||||
|
|
||||||
|
A summary line is printed after each resource type showing total counts.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Notes and Limitations
|
||||||
|
|
||||||
|
- **Tags are not standalone objects in NSX.** They exist only as attributes on VMs. The export captures tag assignments per VM; the import re-applies them. VMs must already exist and be visible to NSX on the destination.
|
||||||
|
- **Security group membership expressions** are exported as-is. If expressions reference objects that do not exist on the destination (e.g. segments, tags not yet applied), the group will be created but membership may be empty until those dependencies exist.
|
||||||
|
- **DFW rule references** (source/destination groups, services) are exported by path/ID. If referenced groups or services do not exist on the destination, rules will be created but may not match traffic as expected. Importing groups before rules (the default order) mitigates this for group references.
|
||||||
|
- **Read-only fields** (`_revision`, `_create_time`, `_last_modified_*`, etc.) are stripped before import so PATCH requests do not fail due to server-managed metadata.
|
||||||
|
- **VCF deployments (NSX 9.x):** VCF-mode NSX adds extra fields to objects (`remote_path`, `unique_id`, `realization_id`, `owner_id`) that reference the source environment's org/project context. These are automatically stripped before import so they are not carried across to the destination.
|
||||||
|
- The script operates on the **default domain** only (`/infra/domains/default`).
|
||||||
+691
@@ -0,0 +1,691 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
NSX Migration Tool
|
||||||
|
Exports and imports DFW rules, Security Groups, and Tags from/to NSX Manager (3.x/4.x/9.x)
|
||||||
|
Supports default infra, VCF Projects, and VPC contexts on both source and destination.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
import urllib.request
|
||||||
|
import urllib.error
|
||||||
|
import urllib.parse
|
||||||
|
import base64
|
||||||
|
import ssl
|
||||||
|
import os
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# HTTP helpers
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def build_headers(username, password):
|
||||||
|
credentials = base64.b64encode(f"{username}:{password}".encode()).decode()
|
||||||
|
return {
|
||||||
|
"Authorization": f"Basic {credentials}",
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Accept": "application/json",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def make_request(url, headers, method="GET", data=None, verify_ssl=True):
|
||||||
|
ctx = ssl.create_default_context() if verify_ssl else ssl._create_unverified_context()
|
||||||
|
body = json.dumps(data).encode() if data else None
|
||||||
|
req = urllib.request.Request(url, data=body, headers=headers, method=method)
|
||||||
|
try:
|
||||||
|
with urllib.request.urlopen(req, context=ctx) as resp:
|
||||||
|
raw = resp.read()
|
||||||
|
return json.loads(raw) if raw else {}
|
||||||
|
except urllib.error.HTTPError as e:
|
||||||
|
body = e.read().decode(errors="replace")
|
||||||
|
raise RuntimeError(f"HTTP {e.code} {e.reason} — {url}\n{body}")
|
||||||
|
|
||||||
|
|
||||||
|
def paginate(base_url, headers, verify_ssl):
|
||||||
|
"""Collect all results from a paginated NSX endpoint."""
|
||||||
|
results = []
|
||||||
|
cursor = None
|
||||||
|
while True:
|
||||||
|
url = base_url + (f"&cursor={cursor}" if cursor else "")
|
||||||
|
resp = make_request(url, headers, verify_ssl=verify_ssl)
|
||||||
|
results.extend(resp.get("results", []))
|
||||||
|
cursor = resp.get("cursor")
|
||||||
|
if not cursor:
|
||||||
|
break
|
||||||
|
return results
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Context / path builder
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def build_api_paths(base_url, org="default", project=None, vpc=None):
|
||||||
|
"""
|
||||||
|
Build a dict of endpoint base paths for the given NSX context.
|
||||||
|
|
||||||
|
Three contexts are supported:
|
||||||
|
1. Default (no project/VPC):
|
||||||
|
Policy base → /policy/api/v1/infra/domains/default
|
||||||
|
2. Project (VCF multi-tenancy, no VPC):
|
||||||
|
Policy base → /policy/api/v1/orgs/{org}/projects/{project}/infra/domains/default
|
||||||
|
3. VPC (isolated network within a project):
|
||||||
|
Policy base → /policy/api/v1/orgs/{org}/projects/{project}/vpcs/{vpc}
|
||||||
|
(VPCs have groups/security-policies directly; no domain level)
|
||||||
|
|
||||||
|
The fabric VM API (/api/v1/fabric/...) is always global — it is not
|
||||||
|
scoped to a project or VPC.
|
||||||
|
"""
|
||||||
|
fabric_base = f"{base_url}/api/v1/fabric"
|
||||||
|
|
||||||
|
if vpc:
|
||||||
|
if not project:
|
||||||
|
raise ValueError("--vpc requires --project to be set")
|
||||||
|
policy_base = (
|
||||||
|
f"{base_url}/policy/api/v1/orgs/{org}/projects/{project}/vpcs/{vpc}"
|
||||||
|
)
|
||||||
|
context_label = f"org={org}, project={project}, vpc={vpc}"
|
||||||
|
elif project:
|
||||||
|
policy_base = (
|
||||||
|
f"{base_url}/policy/api/v1/orgs/{org}/projects/{project}/infra/domains/default"
|
||||||
|
)
|
||||||
|
context_label = f"org={org}, project={project}"
|
||||||
|
else:
|
||||||
|
policy_base = f"{base_url}/policy/api/v1/infra/domains/default"
|
||||||
|
context_label = "default"
|
||||||
|
|
||||||
|
return {
|
||||||
|
"context_label": context_label,
|
||||||
|
"fabric_vms": f"{fabric_base}/virtual-machines",
|
||||||
|
"fabric_vm_tags": f"{fabric_base}/virtual-machines?action=update_tags",
|
||||||
|
"groups": f"{policy_base}/groups",
|
||||||
|
"security_policies": f"{policy_base}/security-policies",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def paths_from_args(base_url, org, project, vpc):
|
||||||
|
"""Validate and build API paths, exiting on misconfiguration."""
|
||||||
|
try:
|
||||||
|
return build_api_paths(base_url, org=org, project=project, vpc=vpc)
|
||||||
|
except ValueError as e:
|
||||||
|
print(f"ERROR: {e}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# EXPORT
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def export_tags(paths, headers, verify_ssl):
|
||||||
|
"""
|
||||||
|
NSX tags live on VMs — not as standalone objects.
|
||||||
|
Collect all unique tags and their per-VM assignments.
|
||||||
|
The fabric VM API is always global (not project/VPC scoped).
|
||||||
|
"""
|
||||||
|
print(" Collecting tags from virtual machines...")
|
||||||
|
vms = paginate(f"{paths['fabric_vms']}?page_size=1000", headers, verify_ssl)
|
||||||
|
|
||||||
|
tag_assignments = []
|
||||||
|
seen_tags = set()
|
||||||
|
all_tags = []
|
||||||
|
|
||||||
|
for vm in vms:
|
||||||
|
vm_tags = vm.get("tags", [])
|
||||||
|
if vm_tags:
|
||||||
|
tag_assignments.append({
|
||||||
|
"resource_type": "VirtualMachine",
|
||||||
|
"external_id": vm.get("external_id"),
|
||||||
|
"display_name": vm.get("display_name"),
|
||||||
|
"tags": vm_tags,
|
||||||
|
})
|
||||||
|
for t in vm_tags:
|
||||||
|
key = (t.get("scope", ""), t.get("tag", ""))
|
||||||
|
if key not in seen_tags:
|
||||||
|
seen_tags.add(key)
|
||||||
|
all_tags.append({"scope": t.get("scope", ""), "tag": t.get("tag", "")})
|
||||||
|
|
||||||
|
print(f" Found {len(all_tags)} unique tags across {len(vms)} VMs.")
|
||||||
|
return {"tags": all_tags, "assignments": tag_assignments}
|
||||||
|
|
||||||
|
|
||||||
|
def export_security_groups(paths, headers, verify_ssl):
|
||||||
|
"""Export NSX Groups (security groups) from the resolved Policy API path."""
|
||||||
|
print(" Collecting security groups...")
|
||||||
|
groups = paginate(f"{paths['groups']}?page_size=1000", headers, verify_ssl)
|
||||||
|
print(f" Found {len(groups)} security groups.")
|
||||||
|
return groups
|
||||||
|
|
||||||
|
|
||||||
|
def export_dfw_rules(paths, headers, verify_ssl):
|
||||||
|
"""Export all DFW security policies and their rules."""
|
||||||
|
print(" Collecting DFW security policies...")
|
||||||
|
policies = paginate(f"{paths['security_policies']}?page_size=1000", headers, verify_ssl)
|
||||||
|
|
||||||
|
exported = []
|
||||||
|
for policy in policies:
|
||||||
|
policy_id = policy.get("id")
|
||||||
|
print(f" Fetching rules for policy: {policy.get('display_name', policy_id)}")
|
||||||
|
rules = paginate(
|
||||||
|
f"{paths['security_policies']}/{policy_id}/rules?page_size=1000",
|
||||||
|
headers, verify_ssl,
|
||||||
|
)
|
||||||
|
exported.append({**policy, "_rules": rules})
|
||||||
|
|
||||||
|
total_rules = sum(len(p["_rules"]) for p in exported)
|
||||||
|
print(f" Found {len(exported)} policies with {total_rules} total rules.")
|
||||||
|
return exported
|
||||||
|
|
||||||
|
|
||||||
|
def run_export(args):
|
||||||
|
base_url = f"https://{args.host}"
|
||||||
|
headers = build_headers(args.username, args.password)
|
||||||
|
verify_ssl = not args.no_verify_ssl
|
||||||
|
|
||||||
|
paths = paths_from_args(base_url, args.src_org, args.src_project, args.src_vpc)
|
||||||
|
|
||||||
|
print(f"\n[Export] Connecting to {args.host}")
|
||||||
|
print(f" Context : {paths['context_label']}")
|
||||||
|
|
||||||
|
# Tags
|
||||||
|
print("\n[1/3] Exporting tags...")
|
||||||
|
tags_data = export_tags(paths, headers, verify_ssl)
|
||||||
|
with open(args.tags_file, "w") as f:
|
||||||
|
json.dump(tags_data, f, indent=2)
|
||||||
|
print(f" Saved -> {args.tags_file}")
|
||||||
|
|
||||||
|
# Security Groups
|
||||||
|
print("\n[2/3] Exporting security groups...")
|
||||||
|
groups_data = export_security_groups(paths, headers, verify_ssl)
|
||||||
|
with open(args.sec_groups_file, "w") as f:
|
||||||
|
json.dump(groups_data, f, indent=2)
|
||||||
|
print(f" Saved -> {args.sec_groups_file}")
|
||||||
|
|
||||||
|
# DFW Rules
|
||||||
|
print("\n[3/3] Exporting DFW rules...")
|
||||||
|
dfw_data = export_dfw_rules(paths, headers, verify_ssl)
|
||||||
|
with open(args.dfw_rules_file, "w") as f:
|
||||||
|
json.dump(dfw_data, f, indent=2)
|
||||||
|
print(f" Saved -> {args.dfw_rules_file}")
|
||||||
|
|
||||||
|
print("\n[Export complete]")
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# IMPORT — helpers
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
IMPORT_STRIP_KEYS = {
|
||||||
|
# Standard read-only metadata
|
||||||
|
"_create_time", "_last_modified_time", "_create_user", "_last_modified_user",
|
||||||
|
"_system_owned", "_protection", "_revision", "_self",
|
||||||
|
# Server-assigned identity fields (also present in nested objects like rule conditions)
|
||||||
|
"unique_id", "realization_id", "owner_id",
|
||||||
|
# VCF/multi-tenancy path — environment-specific, must not carry across to a different instance
|
||||||
|
"remote_path",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def clean_for_import(obj):
|
||||||
|
"""Recursively remove read-only/server-managed fields before PUT/PATCH."""
|
||||||
|
if isinstance(obj, dict):
|
||||||
|
return {k: clean_for_import(v) for k, v in obj.items() if k not in IMPORT_STRIP_KEYS}
|
||||||
|
if isinstance(obj, list):
|
||||||
|
return [clean_for_import(i) for i in obj]
|
||||||
|
return obj
|
||||||
|
|
||||||
|
|
||||||
|
def object_exists(url, headers, verify_ssl):
|
||||||
|
"""Return True if the object exists (HTTP 200), False on 404, raise on other errors."""
|
||||||
|
try:
|
||||||
|
make_request(url, headers, verify_ssl=verify_ssl)
|
||||||
|
return True
|
||||||
|
except RuntimeError as e:
|
||||||
|
if "HTTP 404" in str(e):
|
||||||
|
return False
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
def prompt_overwrite(name, resource_type, yes_to_all):
|
||||||
|
"""
|
||||||
|
Ask the user whether to overwrite an existing object.
|
||||||
|
Returns (should_write: bool, updated_yes_to_all: bool).
|
||||||
|
Accepts: y=yes, n=no, a=yes to all remaining.
|
||||||
|
"""
|
||||||
|
if yes_to_all:
|
||||||
|
return True, True
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
answer = input(
|
||||||
|
f" {resource_type} '{name}' already exists. Overwrite? [y=yes / n=no / a=yes to all]: "
|
||||||
|
).strip().lower()
|
||||||
|
except EOFError:
|
||||||
|
print(" (non-interactive, defaulting to skip)")
|
||||||
|
return False, False
|
||||||
|
if answer == "y":
|
||||||
|
return True, False
|
||||||
|
if answer == "n":
|
||||||
|
return False, False
|
||||||
|
if answer == "a":
|
||||||
|
return True, True
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_vm(paths, headers, verify_ssl, ext_id, display_name):
|
||||||
|
"""
|
||||||
|
Resolve a VM on the destination NSX by external_id first, then fall back
|
||||||
|
to display_name. The fabric VM API is always global (not project/VPC scoped).
|
||||||
|
Returns (external_id, existing_tags) or (None, []).
|
||||||
|
"""
|
||||||
|
# Primary: match by external_id (same vCenter MoRef)
|
||||||
|
try:
|
||||||
|
vms = paginate(
|
||||||
|
f"{paths['fabric_vms']}?external_id={ext_id}&page_size=10",
|
||||||
|
headers, verify_ssl,
|
||||||
|
)
|
||||||
|
if vms:
|
||||||
|
return vms[0]["external_id"], vms[0].get("tags", [])
|
||||||
|
except RuntimeError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Fallback: match by display_name (cross-vCenter)
|
||||||
|
if display_name:
|
||||||
|
try:
|
||||||
|
vms = paginate(
|
||||||
|
f"{paths['fabric_vms']}?display_name={urllib.parse.quote(display_name)}&page_size=10",
|
||||||
|
headers, verify_ssl,
|
||||||
|
)
|
||||||
|
if vms:
|
||||||
|
if len(vms) > 1:
|
||||||
|
print(f" WARNING {display_name}: {len(vms)} VMs matched by name — using first result")
|
||||||
|
return vms[0]["external_id"], vms[0].get("tags", [])
|
||||||
|
except RuntimeError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return None, []
|
||||||
|
|
||||||
|
|
||||||
|
def merge_tag_lists(existing, incoming):
|
||||||
|
"""Union of two tag lists, deduplicated by (scope, tag) pair."""
|
||||||
|
seen = {(t.get("scope", ""), t.get("tag", "")) for t in existing}
|
||||||
|
merged = list(existing)
|
||||||
|
for t in incoming:
|
||||||
|
key = (t.get("scope", ""), t.get("tag", ""))
|
||||||
|
if key not in seen:
|
||||||
|
merged.append(t)
|
||||||
|
seen.add(key)
|
||||||
|
return merged
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# IMPORT — resources
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def import_tags(paths, headers, verify_ssl, tags_file, conflict, merge_tags, step_label="[1/3]"):
|
||||||
|
"""
|
||||||
|
Import VM tag assignments.
|
||||||
|
|
||||||
|
conflict: "overwrite" | "skip" | "prompt"
|
||||||
|
merge_tags: if True, union imported tags with existing instead of replacing.
|
||||||
|
Ignored when conflict=skip (existing tags are left untouched entirely).
|
||||||
|
Note: the fabric VM API used for tag updates is always global, not scoped to
|
||||||
|
the destination project/VPC.
|
||||||
|
"""
|
||||||
|
print(f"\n{step_label} Importing tags from {tags_file} ...")
|
||||||
|
print(f" Conflict mode : {conflict}" + (" + merge" if merge_tags else ""))
|
||||||
|
with open(tags_file) as f:
|
||||||
|
data = json.load(f)
|
||||||
|
|
||||||
|
assignments = data.get("assignments", [])
|
||||||
|
ok, skipped, warn, fail = 0, 0, 0, 0
|
||||||
|
yes_to_all = False
|
||||||
|
|
||||||
|
for entry in assignments:
|
||||||
|
ext_id = entry.get("external_id")
|
||||||
|
incoming_tags = entry.get("tags", [])
|
||||||
|
name = entry.get("display_name", ext_id)
|
||||||
|
|
||||||
|
resolved_id, existing_tags = resolve_vm(paths, headers, verify_ssl, ext_id, name)
|
||||||
|
|
||||||
|
if not resolved_id:
|
||||||
|
print(f" WARNING {name}: VM not found by external_id or display_name — skipping")
|
||||||
|
warn += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
has_existing_tags = bool(existing_tags)
|
||||||
|
|
||||||
|
if has_existing_tags:
|
||||||
|
if conflict == "skip":
|
||||||
|
print(f" SKIP {name}: VM already has {len(existing_tags)} tag(s)")
|
||||||
|
skipped += 1
|
||||||
|
continue
|
||||||
|
elif conflict == "prompt":
|
||||||
|
do_write, yes_to_all = prompt_overwrite(name, "VM tags", yes_to_all)
|
||||||
|
if not do_write:
|
||||||
|
skipped += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
if has_existing_tags and merge_tags:
|
||||||
|
final_tags = merge_tag_lists(existing_tags, incoming_tags)
|
||||||
|
tag_note = (
|
||||||
|
f"{len(final_tags)} tags "
|
||||||
|
f"({len(incoming_tags)} imported + {len(existing_tags)} existing, merged)"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
final_tags = incoming_tags
|
||||||
|
tag_note = f"{len(final_tags)} tags"
|
||||||
|
|
||||||
|
payload = {"external_id": resolved_id, "tags": final_tags}
|
||||||
|
try:
|
||||||
|
make_request(paths["fabric_vm_tags"], headers, method="POST", data=payload, verify_ssl=verify_ssl)
|
||||||
|
print(f" OK {name} ({tag_note})")
|
||||||
|
ok += 1
|
||||||
|
except RuntimeError as e:
|
||||||
|
print(f" FAIL {name}: {e}")
|
||||||
|
fail += 1
|
||||||
|
|
||||||
|
print(f" Tags: {ok} ok, {skipped} skipped, {warn} warnings, {fail} failed.")
|
||||||
|
|
||||||
|
|
||||||
|
def import_security_groups(paths, headers, verify_ssl, sec_groups_file, conflict, step_label="[2/3]"):
|
||||||
|
"""
|
||||||
|
Import security groups into the resolved destination context.
|
||||||
|
|
||||||
|
conflict: "overwrite" | "skip" | "prompt"
|
||||||
|
"""
|
||||||
|
print(f"\n{step_label} Importing security groups from {sec_groups_file} ...")
|
||||||
|
print(f" Conflict mode : {conflict}")
|
||||||
|
print(f" Target : {paths['groups']}")
|
||||||
|
with open(sec_groups_file) as f:
|
||||||
|
groups = json.load(f)
|
||||||
|
|
||||||
|
ok, skipped, fail = 0, 0, 0
|
||||||
|
yes_to_all = False
|
||||||
|
|
||||||
|
for group in groups:
|
||||||
|
group_id = group.get("id")
|
||||||
|
name = group.get("display_name", group_id)
|
||||||
|
group_url = f"{paths['groups']}/{group_id}"
|
||||||
|
|
||||||
|
if conflict != "overwrite":
|
||||||
|
exists = object_exists(group_url, headers, verify_ssl)
|
||||||
|
if exists:
|
||||||
|
if conflict == "skip":
|
||||||
|
print(f" SKIP {name}: group already exists")
|
||||||
|
skipped += 1
|
||||||
|
continue
|
||||||
|
elif conflict == "prompt":
|
||||||
|
do_write, yes_to_all = prompt_overwrite(name, "Security Group", yes_to_all)
|
||||||
|
if not do_write:
|
||||||
|
skipped += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
payload = clean_for_import(group)
|
||||||
|
try:
|
||||||
|
make_request(group_url, headers, method="PATCH", data=payload, verify_ssl=verify_ssl)
|
||||||
|
print(f" OK {name}")
|
||||||
|
ok += 1
|
||||||
|
except RuntimeError as e:
|
||||||
|
print(f" FAIL {name}: {e}")
|
||||||
|
fail += 1
|
||||||
|
|
||||||
|
print(f" Security groups: {ok} ok, {skipped} skipped, {fail} failed.")
|
||||||
|
|
||||||
|
|
||||||
|
def import_dfw_rules(paths, headers, verify_ssl, dfw_rules_file, conflict, step_label="[3/3]"):
|
||||||
|
"""
|
||||||
|
Import DFW security policies and their rules into the resolved destination context.
|
||||||
|
|
||||||
|
conflict: "overwrite" | "skip" | "prompt"
|
||||||
|
Rules inside a brand-new policy are always written without individual checks.
|
||||||
|
Rules inside an existing policy are individually checked against the conflict mode.
|
||||||
|
"""
|
||||||
|
print(f"\n{step_label} Importing DFW rules from {dfw_rules_file} ...")
|
||||||
|
print(f" Conflict mode : {conflict}")
|
||||||
|
print(f" Target : {paths['security_policies']}")
|
||||||
|
with open(dfw_rules_file) as f:
|
||||||
|
policies = json.load(f)
|
||||||
|
|
||||||
|
ok_policies, skip_policies, fail_policies = 0, 0, 0
|
||||||
|
ok_rules, skip_rules, fail_rules = 0, 0, 0
|
||||||
|
yes_to_all = False
|
||||||
|
|
||||||
|
for policy in policies:
|
||||||
|
policy_id = policy.get("id")
|
||||||
|
policy_name = policy.get("display_name", policy_id)
|
||||||
|
rules = policy.pop("_rules", [])
|
||||||
|
policy_url = f"{paths['security_policies']}/{policy_id}"
|
||||||
|
|
||||||
|
policy_existed = False
|
||||||
|
if conflict != "overwrite":
|
||||||
|
policy_existed = object_exists(policy_url, headers, verify_ssl)
|
||||||
|
if policy_existed:
|
||||||
|
if conflict == "skip":
|
||||||
|
print(f" SKIP policy: {policy_name} (and its {len(rules)} rule(s))")
|
||||||
|
skip_policies += 1
|
||||||
|
skip_rules += len(rules)
|
||||||
|
continue
|
||||||
|
elif conflict == "prompt":
|
||||||
|
do_write, yes_to_all = prompt_overwrite(policy_name, "DFW Policy", yes_to_all)
|
||||||
|
if not do_write:
|
||||||
|
skip_policies += 1
|
||||||
|
skip_rules += len(rules)
|
||||||
|
continue
|
||||||
|
|
||||||
|
payload = clean_for_import(policy)
|
||||||
|
try:
|
||||||
|
make_request(policy_url, headers, method="PATCH", data=payload, verify_ssl=verify_ssl)
|
||||||
|
print(f" OK policy: {policy_name}")
|
||||||
|
ok_policies += 1
|
||||||
|
except RuntimeError as e:
|
||||||
|
print(f" FAIL policy: {policy_name}: {e}")
|
||||||
|
fail_policies += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
for rule in rules:
|
||||||
|
rule_id = rule.get("id")
|
||||||
|
rule_name = rule.get("display_name", rule_id)
|
||||||
|
rule_url = f"{policy_url}/rules/{rule_id}"
|
||||||
|
|
||||||
|
if conflict != "overwrite" and policy_existed:
|
||||||
|
rule_exists = object_exists(rule_url, headers, verify_ssl)
|
||||||
|
if rule_exists:
|
||||||
|
if conflict == "skip":
|
||||||
|
print(f" SKIP rule: {rule_name}")
|
||||||
|
skip_rules += 1
|
||||||
|
continue
|
||||||
|
elif conflict == "prompt":
|
||||||
|
do_write, yes_to_all = prompt_overwrite(rule_name, "DFW Rule", yes_to_all)
|
||||||
|
if not do_write:
|
||||||
|
skip_rules += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
rule_payload = clean_for_import(rule)
|
||||||
|
try:
|
||||||
|
make_request(rule_url, headers, method="PATCH", data=rule_payload, verify_ssl=verify_ssl)
|
||||||
|
ok_rules += 1
|
||||||
|
except RuntimeError as e:
|
||||||
|
print(f" FAIL rule: {rule_name}: {e}")
|
||||||
|
fail_rules += 1
|
||||||
|
|
||||||
|
print(f" Policies: {ok_policies} ok, {skip_policies} skipped, {fail_policies} failed.")
|
||||||
|
print(f" Rules: {ok_rules} ok, {skip_rules} skipped, {fail_rules} failed.")
|
||||||
|
|
||||||
|
|
||||||
|
def run_import(args):
|
||||||
|
base_url = f"https://{args.host}"
|
||||||
|
headers = build_headers(args.username, args.password)
|
||||||
|
verify_ssl = not args.no_verify_ssl
|
||||||
|
conflict = args.conflict
|
||||||
|
merge_tags = args.merge_tags
|
||||||
|
|
||||||
|
paths = paths_from_args(base_url, args.dst_org, args.dst_project, args.dst_vpc)
|
||||||
|
|
||||||
|
print(f"\n[Import] Connecting to {args.host}")
|
||||||
|
print(f" Context : {paths['context_label']}")
|
||||||
|
|
||||||
|
action = args.action
|
||||||
|
|
||||||
|
# Validate required input files before starting
|
||||||
|
if action in ("import", "import-tags"):
|
||||||
|
if not os.path.exists(args.tags_file):
|
||||||
|
print(f"ERROR: Tags file not found: {args.tags_file}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
if action in ("import", "import-groups"):
|
||||||
|
if not os.path.exists(args.sec_groups_file):
|
||||||
|
print(f"ERROR: Security groups file not found: {args.sec_groups_file}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
if action == "import":
|
||||||
|
if not os.path.exists(args.dfw_rules_file):
|
||||||
|
print(f"ERROR: DFW rules file not found: {args.dfw_rules_file}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
if action == "import-tags":
|
||||||
|
import_tags(paths, headers, verify_ssl, args.tags_file, conflict, merge_tags, step_label="[1/1]")
|
||||||
|
|
||||||
|
elif action == "import-groups":
|
||||||
|
import_security_groups(paths, headers, verify_ssl, args.sec_groups_file, conflict, step_label="[1/1]")
|
||||||
|
|
||||||
|
else:
|
||||||
|
# Full import — order matters: tags → groups → rules
|
||||||
|
import_tags(paths, headers, verify_ssl, args.tags_file, conflict, merge_tags, step_label="[1/3]")
|
||||||
|
import_security_groups(paths, headers, verify_ssl, args.sec_groups_file, conflict, step_label="[2/3]")
|
||||||
|
import_dfw_rules(paths, headers, verify_ssl, args.dfw_rules_file, conflict, step_label="[3/3]")
|
||||||
|
|
||||||
|
print("\n[Import complete]")
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# CLI
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description="NSX Manager export/import tool for DFW rules, security groups, and tags.",
|
||||||
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||||
|
epilog="""
|
||||||
|
Actions:
|
||||||
|
export Export all resources (tags, security groups, DFW rules) to JSON files
|
||||||
|
import Import all resources in order: tags → security groups → DFW rules
|
||||||
|
import-tags Import only tags (applies VM tag assignments)
|
||||||
|
import-groups Import only security groups
|
||||||
|
|
||||||
|
Context (project / VPC):
|
||||||
|
Use --src-* args for export, --dst-* args for import.
|
||||||
|
If no project is specified the default infra space is used.
|
||||||
|
--dst-vpc / --src-vpc requires the corresponding --*-project to also be set.
|
||||||
|
|
||||||
|
Context resolution:
|
||||||
|
No project/VPC → /policy/api/v1/infra/domains/default/...
|
||||||
|
Project only → /policy/api/v1/orgs/{org}/projects/{project}/infra/domains/default/...
|
||||||
|
Project + VPC → /policy/api/v1/orgs/{org}/projects/{project}/vpcs/{vpc}/...
|
||||||
|
|
||||||
|
Note: VM tag lookups always use the global fabric API regardless of project/VPC context.
|
||||||
|
|
||||||
|
Conflict modes (--conflict):
|
||||||
|
overwrite Always write, replacing any existing object [default]
|
||||||
|
skip Leave existing objects untouched, import only new ones
|
||||||
|
prompt Ask interactively for each conflict (y / n / a=yes-to-all)
|
||||||
|
|
||||||
|
Tag merge (--merge-tags, import/import-tags only):
|
||||||
|
Without flag Replace all tags on the VM with the imported set
|
||||||
|
With flag Union imported tags with existing VM tags (no removals)
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
# Export from default space
|
||||||
|
python nsx_migrate.py export --host 192.168.1.10 -u admin -p VMware1! --no-verify-ssl
|
||||||
|
|
||||||
|
# Export from a VCF project
|
||||||
|
python nsx_migrate.py export --host 192.168.1.10 -u admin -p VMware1! --no-verify-ssl \\
|
||||||
|
--src-project my-project
|
||||||
|
|
||||||
|
# Export from a VPC inside a project
|
||||||
|
python nsx_migrate.py export --host 192.168.1.10 -u admin -p VMware1! --no-verify-ssl \\
|
||||||
|
--src-project my-project --src-vpc my-vpc
|
||||||
|
|
||||||
|
# Import to a specific project on the destination
|
||||||
|
python nsx_migrate.py import --host 192.168.2.10 -u admin -p VMware1! --no-verify-ssl \\
|
||||||
|
--dst-project my-project --conflict skip
|
||||||
|
|
||||||
|
# Import to a VPC on the destination
|
||||||
|
python nsx_migrate.py import --host 192.168.2.10 -u admin -p VMware1! --no-verify-ssl \\
|
||||||
|
--dst-project my-project --dst-vpc my-vpc
|
||||||
|
|
||||||
|
# Cross-project migration: export from project-a, import to project-b (same host)
|
||||||
|
python nsx_migrate.py export --host 192.168.1.10 -u admin -p VMware1! --no-verify-ssl \\
|
||||||
|
--src-project project-a
|
||||||
|
python nsx_migrate.py import --host 192.168.1.10 -u admin -p VMware1! --no-verify-ssl \\
|
||||||
|
--dst-project project-b --conflict skip
|
||||||
|
|
||||||
|
# Import only tags with merge, into a specific project
|
||||||
|
python nsx_migrate.py import-tags --host 192.168.2.10 -u admin -p VMware1! --no-verify-ssl \\
|
||||||
|
--dst-project my-project --merge-tags
|
||||||
|
|
||||||
|
# Import only security groups, skip existing, into a VPC
|
||||||
|
python nsx_migrate.py import-groups --host 192.168.2.10 -u admin -p VMware1! --no-verify-ssl \\
|
||||||
|
--dst-project my-project --dst-vpc my-vpc --conflict skip
|
||||||
|
""",
|
||||||
|
)
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
"action",
|
||||||
|
choices=["export", "import", "import-tags", "import-groups"],
|
||||||
|
help="Action to perform",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Connection
|
||||||
|
conn = parser.add_argument_group("Connection")
|
||||||
|
conn.add_argument("--host", required=True, help="NSX Manager hostname or IP")
|
||||||
|
conn.add_argument("--username", "-u", required=True, help="NSX Manager username")
|
||||||
|
conn.add_argument("--password", "-p", required=True, help="NSX Manager password")
|
||||||
|
conn.add_argument(
|
||||||
|
"--no-verify-ssl", action="store_true", default=False,
|
||||||
|
help="Disable SSL certificate verification (for self-signed certs)",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Source context (used by export)
|
||||||
|
src = parser.add_argument_group("Source context (export)")
|
||||||
|
src.add_argument("--src-org", default="default", metavar="ORG",
|
||||||
|
help="Source VCF org (default: 'default')")
|
||||||
|
src.add_argument("--src-project", default=None, metavar="PROJECT",
|
||||||
|
help="Source VCF project name (omit for default infra space)")
|
||||||
|
src.add_argument("--src-vpc", default=None, metavar="VPC",
|
||||||
|
help="Source VPC name within the project (requires --src-project)")
|
||||||
|
|
||||||
|
# Destination context (used by import)
|
||||||
|
dst = parser.add_argument_group("Destination context (import)")
|
||||||
|
dst.add_argument("--dst-org", default="default", metavar="ORG",
|
||||||
|
help="Destination VCF org (default: 'default')")
|
||||||
|
dst.add_argument("--dst-project", default=None, metavar="PROJECT",
|
||||||
|
help="Destination VCF project name (omit for default infra space)")
|
||||||
|
dst.add_argument("--dst-vpc", default=None, metavar="VPC",
|
||||||
|
help="Destination VPC name within the project (requires --dst-project)")
|
||||||
|
|
||||||
|
# Import behaviour
|
||||||
|
imp = parser.add_argument_group("Import behaviour")
|
||||||
|
imp.add_argument(
|
||||||
|
"--conflict", choices=["overwrite", "skip", "prompt"], default="overwrite",
|
||||||
|
help="How to handle objects that already exist on the destination (default: overwrite)",
|
||||||
|
)
|
||||||
|
imp.add_argument(
|
||||||
|
"--merge-tags", action="store_true", default=False,
|
||||||
|
help="Merge imported tags with existing VM tags instead of replacing (import/import-tags only)",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Files
|
||||||
|
files = parser.add_argument_group("File paths")
|
||||||
|
files.add_argument("--dfw-rules-file", default="dfw_rules.json",
|
||||||
|
help="DFW rules file (default: dfw_rules.json)")
|
||||||
|
files.add_argument("--sec-groups-file", default="sec_groups.json",
|
||||||
|
help="Security groups file (default: sec_groups.json)")
|
||||||
|
files.add_argument("--tags-file", default="tags.json",
|
||||||
|
help="Tags file (default: tags.json)")
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
if args.action == "export":
|
||||||
|
run_export(args)
|
||||||
|
else:
|
||||||
|
run_import(args)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Reference in new issue
Block a user