Skip to content

Commit ebc8c60

Browse files
author
Shiva Shankar Vaddepally
committed
migrationtool for citrix.adc to netscaler.adc
Signed-off-by: Shiva Shankar Vaddepally <shivashankar.vaddepally@cloud.com>
1 parent f12bda0 commit ebc8c60

5 files changed

Lines changed: 357 additions & 0 deletions

File tree

migrationtool/README.md

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
# NetScaler ADC Ansible Collection Migration Tool
2+
3+
Migrates Ansible playbooks from legacy `citrix.adc` collection to new `netscaler.adc` collection format.
4+
5+
## Usage
6+
7+
```bash
8+
python3 convert_yaml.py -i input_playbook.yaml -o output_playbook.yaml
9+
```
10+
11+
**Arguments:**
12+
- `-i, --input`: (Required) Input YAML playbook
13+
- `-o, --output`: (Optional) Output file (defaults to `output.yaml`)
14+
- `-v, --verbose`: (Optional) Enable verbose output
15+
16+
## What it converts
17+
18+
1. **Legacy modules**: `citrix.adc.lbvserver``netscaler.adc.lbvserver`
19+
2. **NITRO requests**: `citrix_adc_nitro_request` → specific resource modules
20+
21+
### Example Conversion
22+
23+
**Before:**
24+
```yaml
25+
- name: Configure LB vserver
26+
citrix_adc_nitro_request:
27+
operation: present
28+
resource: lbvserver
29+
name: my_lb_vserver
30+
attributes:
31+
servicetype: HTTP
32+
port: 80
33+
```
34+
35+
**After:**
36+
```yaml
37+
- name: Configure LB vserver
38+
netscaler.adc.lbvserver:
39+
state: present
40+
name: my_lb_vserver
41+
servicetype: HTTP
42+
port: 80
43+
```
44+
45+
## Requirements
46+
47+
```bash
48+
pip install pyyaml jinja2
49+
```
50+
51+
## Files
52+
53+
- `convert_yaml.py`: Main conversion script
54+
- `resourcelist.py`: Module and state mappings
55+
nitro_pass: "{{ nitro_pass }}"
56+
operation: present
57+
resource: lbvserver
58+
name: my_lb_vserver
59+
attributes:
60+
servicetype: HTTP
61+
ipv46: 10.10.10.10
62+
port: 80
63+
```
64+
65+
**After:**
66+
```yaml
67+
- name: Configure LB vserver
68+
netscaler.adc.lbvserver:
69+
nsip: "{{ nsip }}"
70+
nitro_user: "{{ nitro_user }}"
71+
nitro_pass: "{{ nitro_pass }}"
72+
state: present
73+
name: my_lb_vserver
74+
servicetype: HTTP
75+
ipv46: 10.10.10.10
76+
port: 80
77+
```
78+
79+
### State Mapping
80+
- `add``present`
81+
- `update``present`
82+
- `delete``absent`
83+
- `present``present`
84+
- `absent``absent`
85+
- `action` → Uses the action value from the task
86+
87+
## Files
88+
89+
- `convert_yaml.py`: Main conversion script
90+
- `resourcelist.py`: Contains `resource_map` and `state_map` mappings
91+
- `template.j2`: Jinja2 template (if used)
92+
93+
## Requirements
94+
95+
- Python 3.x
96+
- PyYAML
97+
- Jinja2
98+
99+
## Installation
100+
101+
```bash
102+
pip install pyyaml jinja2
103+
```
104+
105+
## Input Format Support
106+
107+
The tool supports various YAML input formats:
108+
- Single playbook dictionary
109+
- List of plays
110+
- List of tasks (automatically wrapped in a play structure)
111+
112+
## Output
113+
114+
The tool generates a properly formatted YAML playbook with:
115+
- Converted module names
116+
- Updated authentication parameters
117+
- Preserved task names and structure
118+
- Proper indentation and formatting
119+
120+
## Troubleshooting
121+
122+
### Common Issues
123+
124+
1. **Resource not found**: Check if the resource type exists in `resource_map`
125+
2. **Missing name field**: Ensure the original task has a `name` parameter for NITRO requests
126+
3. **Authentication errors**: Verify credential parameters are correctly set
127+
128+
### Debug Output
129+
130+
The tool provides console output showing:
131+
- Module mappings being applied
132+
- NITRO request processing details
133+
- Tasks being converted
134+
135+
## Contributing
136+
137+
To add support for new modules:
138+
1. Update `resource_map` in `resourcelist.py`
139+
2. Add appropriate state mappings if needed
140+
3. Test with sample playbooks

migrationtool/__init__.py

Whitespace-only changes.

migrationtool/convert_yaml.py

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
import yaml
2+
import argparse
3+
import jinja2
4+
from resourcelist import (
5+
resource_map,
6+
state_map
7+
)
8+
9+
from collections import OrderedDict
10+
11+
class CustomDumper(yaml.SafeDumper):
12+
def ignore_aliases(self, data):
13+
return True
14+
15+
def increase_indent(self, flow=False, indentless=False):
16+
return super(CustomDumper, self).increase_indent(flow, False)
17+
18+
def represent_custom_object(dumper, data):
19+
try:
20+
return dumper.represent_dict(data.__dict__)
21+
except AttributeError:
22+
raise yaml.representer.RepresenterError("cannot represent an object", data)
23+
24+
# Register the custom representer
25+
CustomDumper.add_representer(object, represent_custom_object)
26+
27+
def get_state_attributes(plugin_key):
28+
logincreds ={}
29+
logincreds["nsip"] = plugin_key.get("nsip", None)
30+
logincreds["nitro_user"] = plugin_key.get("nitro_user", None)
31+
logincreds["nitro_pass"] = plugin_key.get("nitro_pass", None)
32+
logincreds["validate_certs"] = plugin_key.get("validate_certs", "no")
33+
logincreds["nitro_protocol"] = plugin_key.get("nitro_protocol", "http")
34+
35+
return logincreds
36+
37+
def convert_yaml_file(input_file, output_file, template_file, verbose):
38+
39+
# Convert input file (citrix.adc) to output file (citrix.adc.yaml) using template
40+
with open(input_file, 'r') as infile:
41+
data = yaml.safe_load(infile)
42+
43+
# Handle both list and dict formats
44+
if isinstance(data, list):
45+
# If data is a list, assume it's a list of plays/tasks
46+
if len(data) > 0 and isinstance(data[0], dict):
47+
# Take the first item if it's a dictionary
48+
play_data = data[0]
49+
else:
50+
# If it's a list of tasks, wrap it in a play structure
51+
play_data = {"tasks": data}
52+
elif isinstance(data, dict):
53+
play_data = data
54+
else:
55+
raise ValueError(f"Unsupported YAML structure. Expected dict or list, got {type(data)}")
56+
57+
hosts = play_data.get("hosts", "localhost")
58+
vars_data = play_data.get("vars", {})
59+
tasks = play_data.get("tasks", [])
60+
name = play_data.get("name", "sample converted playbook")
61+
gather_facts = play_data.get("gather_facts", False)
62+
if verbose:
63+
print("tasks:", tasks)
64+
new_tasks = []
65+
for task in tasks:
66+
taskname = task.get("name", "")
67+
delegate_to = task.get("delegate_to", "localhost")
68+
register = task.get("register", None)
69+
if isinstance(task, dict):
70+
for pluginkey, pluginvalue in task.items():
71+
# Skip non-module keys like 'name' and 'delegate_to'
72+
if pluginkey in ['name', 'delegate_to']:
73+
continue
74+
if verbose:
75+
print(f"Module: {pluginkey}, Parameters: {pluginvalue}")
76+
if pluginkey.split('.')[-1] in resource_map:
77+
new_resource_name = resource_map[pluginkey.split('.')[-1]]
78+
if verbose:
79+
print(f"Remapped {pluginkey} to {new_resource_name}")
80+
new_task = {
81+
"name": taskname,
82+
"delegate_to": delegate_to,
83+
new_resource_name: pluginvalue,
84+
}
85+
elif pluginkey == "citrix_adc_nitro_request":
86+
newplugin = {}
87+
if verbose:
88+
print(f"Processing citrix_adc_nitro_request for {taskname}")
89+
operation = pluginvalue.get("operation", "present")
90+
if operation == "action":
91+
operation = pluginkey.get("action", "")
92+
state = state_map[operation]
93+
resource = pluginvalue.get("resource", None)
94+
entityname = pluginvalue.get("name", "")
95+
if resource is None:
96+
print(f"Resource not found for {pluginkey}, skipping")
97+
continue
98+
attributeslist = pluginvalue.get("attributes", [])
99+
100+
login_attributes = get_state_attributes(pluginvalue)
101+
newplugin["state"] = state
102+
newplugin.update(login_attributes)
103+
104+
# Handle attributes first
105+
if attributeslist != []:
106+
if verbose:
107+
print(f'attribute list: {attributeslist}')
108+
newplugin.update(attributeslist)
109+
else:
110+
newplugin["name"] = entityname
111+
112+
new_task = {
113+
"name": taskname,
114+
"delegate_to": delegate_to,
115+
f'netscaler.adc.{resource}': newplugin,
116+
}
117+
118+
else:
119+
# Keep original name if no mapping found
120+
new_resource_name = pluginkey
121+
print(f"No mapping found for {pluginkey}, keeping original name")
122+
123+
124+
new_tasks.append(new_task)
125+
126+
# Playbook struct
127+
playbook = [{
128+
"name": name,
129+
"hosts": hosts,
130+
"gather_facts": gather_facts,
131+
"vars": vars_data,
132+
"tasks": new_tasks
133+
}]
134+
135+
# Write the playbook directly as YAML without template
136+
with open(output_file, 'w') as outfile:
137+
yaml.dump(playbook, outfile, default_flow_style=False, sort_keys=False, Dumper=CustomDumper, indent=2)
138+
139+
print(f"Output written to: {output_file}")
140+
141+
def main():
142+
parser = argparse.ArgumentParser(description="Convert YAML files for migration")
143+
parser.add_argument("-i", "--input", required=True, help="Input YAML file")
144+
parser.add_argument("-o", "--output", required=False, help="Output YAML file")
145+
parser.add_argument("-v", "--verbose", required=False, help="verbose mode")
146+
args = parser.parse_args()
147+
148+
input_file = args.input
149+
print(f"Input file: {input_file}")
150+
output_file = args.output if args.output else "output.yaml"
151+
template_file = "./template.j2"
152+
verbose = args.verbose
153+
154+
print("Starting YAML conversion process...")
155+
convert_yaml_file(input_file, output_file, template_file, verbose)
156+
print("Conversion completed successfully!")
157+
158+
if __name__ == "__main__":
159+
main()

migrationtool/resourcelist.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
resource_map = {
2+
"citrix_adc_appfw_confidfield" : "netscaler.adc.appfwconfidfield",
3+
"citrix_adc_appfw_fieldtype" : "netscaler.adc.appfwfieldtype",
4+
"citrix_adc_appfw_global_bindings" : "netscaler.adc.appfwglobal_bindings",
5+
"citrix_adc_appfw_htmlerrorpage" : "netscaler.adc.appfwhtmlerrorpage",
6+
"citrix_adc_appfw_jsoncontenttype" : "netscaler.adc.appfwjsoncontenttype",
7+
"citrix_adc_appfw_learningsettings" : "netscaler.adc.appfwlearningsettings",
8+
"citrix_adc_appfw_policylabel": "netscaler.adc.appfwpolicylabel",
9+
"citrix_adc_appfw_policy": "netscaler.adc.appfwpolicy",
10+
"citrix_adc_appfw_profile": "netscaler.adc.appfwprofile",
11+
"citrix_adc_appfw_settings": "netscaler.adc.appfwsettings",
12+
"citrix_adc_appfw_signatures": "netscaler.adc.appfwsignatures",
13+
"citrix_adc_appfw_wsdl": "netscaler.adc.appfwwsdl",
14+
"citrix_adc_appfw_xmlcontenttype": "netscaler.adc.appfwxmlcontenttype",
15+
"citrix_adc_appfw_xmlerrorpage": "netscaler.adc.appfwxmlerrorpage",
16+
"citrix_adc_appfw_xmlschema": "netscaler.adc.appfwxmlschema",
17+
"citrix_adc_cs_action": "netscaler.adc.csaction",
18+
"citrix_adc_cs_policy": "netscaler.adc.cspolicy",
19+
"citrix_adc_cs_vserver": "netscaler.adc.csvserver",
20+
"citrix_adc_dnsnsrec": "netscaler.adc.dnsnsrec",
21+
"citrix_adc_get_bearer_token": "netscaler.adc.get_bearer_token",
22+
"citrix_adc_gslb_service": "netscaler.adc.gslbservice",
23+
"citrix_adc_gslb_site": "netscaler.adc.gslbsite",
24+
"citrix_adc_gslb_vserver": "netscaler.adc.gslbvserver",
25+
"citrix_adc_lb_monitor": "netscaler.adc.lbmonitor",
26+
"citrix_adc_lb_vserver": "netscaler.adc.lbvserver",
27+
"citrix_adc_nsip": "netscaler.adc.nsip",
28+
"citrix_adc_nspartition": "netscaler.adc.nspartition",
29+
"citrix_adc_password_reset": "netscaler.adc.password_reset",
30+
"citrix_adc_save_config": "netscaler.adc.save_config",
31+
"citrix_adc_server": "netscaler.adc.server",
32+
"citrix_adc_servicegroup": "netscaler.adc.servicegroup",
33+
"citrix_adc_service": "netscaler.adc.service",
34+
"citrix_adc_ssl_certkey": "netscaler.adc.sslcertkey",
35+
"citrix_adc_sslcipher": "netscaler.adc.sslcipher",
36+
"citrix_adc_sslcipher_sslciphersuite_binding": "netscaler.adc.sslcipher_sslciphersuite_binding",
37+
"citrix_adc_sslprofile_sslcipher_binding": "netscaler.adc.sslprofile_sslcipher_binding",
38+
"citrix_adc_system_file": "netscaler.adc.systemfile",
39+
}
40+
41+
state_map = {
42+
"add": "present",
43+
"update": "present",
44+
"delete": "absent",
45+
"enable": "enable",
46+
"disable": "disable",
47+
"present": "present",
48+
"absent": "absent",
49+
}
50+
if __name__ == "__main__":
51+
pass

migrationtool/template.j2

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
- name: Sample converted playbook
3+
hosts: {{ hosts }}
4+
gather_facts: {{ gather_facts | default('no') }}
5+
vars: {{ vars }}
6+
tasks:
7+
{{ tasks }}

0 commit comments

Comments
 (0)