Skip to content

Commit 675fd6c

Browse files
docs: ✨ Add new example script to generate Preconfig YAML via Jinja template
1 parent 4e6e468 commit 675fd6c

6 files changed

Lines changed: 675 additions & 4 deletions

File tree

docs/source/examples/generate_preconfig.rst

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,20 @@ partially or fully deployed in a Zero Touch Provisioning (ZTP) model
1818
where the appliance configuration is staged on Orchestrator prior to the
1919
appliance being approved into the SDWAN environment.
2020

21+
Before running the code, make sure that the Python package ``jinja2`` is
22+
installed in your environment in addition to ``pyedgeconnect``.
23+
24+
These are referenced in the ``requirements.txt`` file in the preconfig
25+
example directory
26+
27+
.. code-block:: python
28+
29+
pip install -r requirements.txt
30+
# OR
31+
pip install pyedgeconnect
32+
pip install jinja2
33+
34+
2135
EdgeConnect YAML Jinja Template
2236
===============================
2337

@@ -175,9 +189,21 @@ template correspond to the headers in the CSV file. If additional
175189
variables are added to the Jinja template, make sure to add appropriate
176190
columns in the CSV file.
177191

178-
When leveraging default values and/or calculated values from source data
179-
you can limit the scope of how many unique values need to be provided
180-
in the CSV file.
192+
.. important::
193+
194+
The included CSV file has headers for all variables referenced in
195+
the included Jinja template, however, due to default values and/or
196+
other conditional logic, it may not be necessary to have columns
197+
for every variable to generate a valid preconfig.
198+
199+
Only a few example values are included in the CSV file in the
200+
repository as a starting point as valid values will vary from each
201+
Orchestrator environment, and many variables have default values that
202+
will be included via the Jinja template.
203+
204+
Always reference the Orchestrator page ``Preconfigure Appliances`` with
205+
the built-in ``new`` preconfig to see acceptable values for specific
206+
preconfig options.
181207

182208
Orchestrator API calls
183209
^^^^^^^^^^^^^^^^^^^^^^^^^^
@@ -219,4 +245,4 @@ To remove the preconfigs generated, the same runtime argument is used of
219245
This will retrieve all configured preconfigs on Orchestrator, find
220246
all preconfigs with a matching name as those in the CSV file, then
221247
prompt the user to confirm that those preconfigs should be removed from
222-
Orchestrator.
248+
Orchestrator.
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
softwareVersion,hostname,group,site,networkRole,region,address,address2,city,state,zipCode,country,latitude,longitude,name,email,phoneNumber,templateGroups,businessIntentOverlays,wan_interface_1_max_outbound_bw,wan_interface_2_max_outbound_bw,wan_interface_1_max_inbound_bw,wan_interface_2_max_inbound_bw,deploymentMode,shapeInboundTraffic,outboundMaxBandwidth,lan_interface_1_name,lan_interface_1_desc,lan_interface_1_ipmask,lan_interface_1_nexthop,lan_interface_1_segment,lan_interface_1_zone,lan_interface_2_name,lan_interface_2_desc,lan_interface_2_ipmask,lan_interface_2_nexthop,lan_interface_2_segment,lan_interface_2_zone,wan_interface_1_name,wan_interface_1_desc,wan_interface_1_label,wan_interface_1_ipmask,wan_interface_1_nexthop,wan_interface_1_firewall_mode,wan_interface_1_behind_nat,wan_interface_1_zone,wan_interface_2_name,wan_interface_2_desc,wan_interface_2_label,wan_interface_2_ipmask,wan_interface_2_nexthop,wan_interface_2_firewall_mode,wan_interface_2_behind_nat,wan_interface_2_zone,useSharedSubnetInfo,advertiseLocalLanSubnets,advertiseLocalWanSubnets,localMetric,localCommunities,redistOspfToSubnetShare,ospfRedistMetric,ospfRedistTag,filterRoutesWithLocalASN,redistToSDwanFabricRouteMap,useDefaultAccount,license_bandwidth,license_boost,bgp_asn,routerId,bgp_peer1_ip,bgp_peer1_asn,bgp_peer1_peer_type,bgp_peer1_inboundRouteMap,bgp_peer1_outboundRouteMap,bgp_peer1_keepAlive,bgp_peer1_holdTime,bgp_peer1_sourceIpInterface,bgp_peer1_asPrependCount,loopback_interfaceId,loopback_ipAddressMask,loopback_zone
2+
,TEST-EDGECONNECT,,,,,,,,,,,,,,,,Default Template Group,"RealTime, CriticalApps, BulkApps, DefaultOverlay",,,,,,,,lan0,,192.0.2.1/24,,,,,,,,,,wan0,,INET1,,,statefulSNAT,auto,,wan1,,INET2,,,statefulSNAT,auto,,,,,,,,,,,,,,100000,,,,,,,,,,,,,,
Lines changed: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,224 @@
1+
import argparse
2+
import csv
3+
import datetime
4+
import getpass
5+
import os
6+
7+
from jinja2 import Environment, FileSystemLoader
8+
from pyedgeconnect import Orchestrator
9+
10+
# Parse runtime arguments
11+
parser = argparse.ArgumentParser()
12+
parser.add_argument(
13+
"-c",
14+
"--csv",
15+
help="Specify source csv file for preconfigs",
16+
type=str,
17+
required=True,
18+
)
19+
parser.add_argument(
20+
"-u",
21+
"--upload",
22+
help="Upload created valid preconfigs to Orchestrator",
23+
type=bool,
24+
default=False,
25+
)
26+
parser.add_argument(
27+
"-aa",
28+
"--autoapply",
29+
help="Mark preconfigs for auto-approve",
30+
type=bool,
31+
default=False,
32+
)
33+
parser.add_argument(
34+
"-j",
35+
"--jinja",
36+
help="specify source jinja2 template",
37+
type=str,
38+
default="ec_preconfig_template.jinja2",
39+
)
40+
parser.add_argument(
41+
"-o",
42+
"--orch",
43+
help="specify Orchestrator URL",
44+
type=str,
45+
)
46+
args = parser.parse_args()
47+
48+
# Set Orchestrator FQDN/IP via arguments, environment variable,
49+
# or user input
50+
if vars(args)["orch"] is not None:
51+
orch_url = vars(args)["orch"]
52+
elif os.getenv("ORCH_URL") is not None:
53+
orch_url = os.getenv("ORCH_URL")
54+
else:
55+
orch_url = input("Orchstrator IP or FQDN: ")
56+
57+
# Set Orchestrator API Key via environment variable or user input
58+
if os.getenv("ORCH_API_KEY") is not None:
59+
orch_api_key = os.getenv("ORCH_API_KEY")
60+
else:
61+
orch_api_key_input = input("Orchstrator API Key (enter to skip): ")
62+
if len(orch_api_key_input) == 0:
63+
orch_api_key = None
64+
# Set user and password if present in environment variable
65+
orch_user = os.getenv("ORCH_USER")
66+
orch_pw = os.getenv("ORCH_PASSWORD")
67+
else:
68+
orch_api_key = orch_api_key_input
69+
70+
# Instantiate Orchestrator with ``log_console`` enabled for
71+
# printing log messages to terminal
72+
orch = Orchestrator(
73+
orch_url,
74+
api_key=orch_api_key,
75+
log_console=True,
76+
verify_ssl=False,
77+
)
78+
79+
# If not using API key, login to Orchestrator with username/password
80+
if orch_api_key is None:
81+
# If username/password not in environment variables, prompt user
82+
if orch_user is None:
83+
orch_user = input("Enter Orchestrator username: ")
84+
orch_pw = getpass.getpass("Enter Orchestrator password: ")
85+
# Check if multi-factor authentication required
86+
mfa_prompt = input("Are you using MFA for this user (y/n)?: ")
87+
if mfa_prompt == "y":
88+
orch.send_mfa(orch_user, orch_pw, temp_code=False)
89+
token = input("Enter MFA token: ")
90+
else:
91+
token = ""
92+
# Login to Orchestrator
93+
confirm_auth = orch.login(orch_user, orch_pw, mfacode=token)
94+
# Check that user/pass authentication works before proceeding
95+
if confirm_auth:
96+
pass
97+
else:
98+
print("Authentication to Orchestrator Failed")
99+
exit()
100+
# If API key specified, check that key is valid before proceeding
101+
else:
102+
confirm_auth = orch.get_orchestrator_hello()
103+
if confirm_auth != "There was an internal server error.":
104+
pass
105+
else:
106+
print("Authentication to Orchestrator Failed")
107+
exit()
108+
109+
# Specify CSV file for generating preconfigs
110+
# This is a mandatory runtime argument
111+
if vars(args)["csv"] is not None:
112+
csv_filename = vars(args)["csv"]
113+
else:
114+
print("Source CSV file not specified, exiting")
115+
exit()
116+
117+
# Setting if configs should be uploaded to Orchestrator, argument
118+
# defaults to False if not specified
119+
upload_to_orch = vars(args)["upload"]
120+
121+
# Setting if discovered appliance with matching serial number or tag
122+
# will be automatically approved and deployed with corresponding
123+
# preconfig. Argument defaults to False if not specified
124+
auto_apply = vars(args)["autoapply"]
125+
126+
# Specify alternate Jinja2 template file for generating preconfig
127+
# in the templates directory. Otherwise use default template.
128+
ec_template_file = vars(args)["jinja"]
129+
130+
131+
# Retrieve Jinja2 template for generating EdgeConnect Preconfig YAML
132+
# Setting ``trim_blocks`` and ``lstrip_blocks`` reduces excessive
133+
# whitepsace from the jinja template conditionals etc.
134+
env = Environment(
135+
loader=FileSystemLoader("templates"),
136+
trim_blocks=True,
137+
lstrip_blocks=True,
138+
)
139+
ec_template = env.get_template(ec_template_file)
140+
141+
# Local directory for configuration outputs
142+
output_directory = "preconfig_outputs/"
143+
if not os.path.exists(output_directory):
144+
os.makedirs(output_directory)
145+
146+
# Open CSV file with configuration data
147+
with open(csv_filename, encoding="utf-8-sig") as csvfile:
148+
csv_dict = csv.DictReader(csvfile)
149+
150+
# Set initial row number for row identification of data
151+
# First row is headers
152+
row_number = 2
153+
154+
# Generate Edge Connect YAML preconfig for each row in data
155+
for row in csv_dict:
156+
157+
# Render CSV values through the Jinja template
158+
yaml_preconfig = ec_template.render(data=row)
159+
160+
# Set value for serial number if provided
161+
appliance_serial = row.get("serial_number")
162+
if appliance_serial is None:
163+
appliance_serial = ""
164+
else:
165+
pass
166+
167+
# Validate preconfig via Orchestrator
168+
validate = orch.validate_preconfig(
169+
hostname=row["hostname"],
170+
yaml_preconfig=yaml_preconfig,
171+
auto_apply=auto_apply,
172+
)
173+
174+
# If the validate function passes on Orchestrator write
175+
# preconfig to local file and check if uploading to Orchestrator
176+
if validate.status_code == 200:
177+
178+
# Write local YAML file
179+
yaml_filename = "{}_preconfig.yml".format(row["hostname"])
180+
with open(output_directory + yaml_filename, "w") as preconfig_file:
181+
write_data = preconfig_file.write(yaml_preconfig)
182+
183+
# If upload option was chosen, upload preconfig to
184+
# Orchestrator with selected auto-apply settings
185+
if upload_to_orch is True:
186+
187+
# In this example the appliance hostname from the CSV
188+
# data (row["hostname"]) is used both for the name of
189+
# the preconfig to appear in Orchestrator, as well as
190+
# the tag on the preconfig that could be used to match
191+
# against a discovered appliance
192+
# Additionally a comment is added with the current
193+
# date
194+
orch.create_preconfig(
195+
hostname=row["hostname"],
196+
yaml_preconfig=yaml_preconfig,
197+
auto_apply=auto_apply,
198+
tag=row["hostname"],
199+
comment="Created/Uploaded @ {}".format(
200+
datetime.date.today().strftime("%d %B %Y")
201+
),
202+
)
203+
print("Posted EC Preconfig {}".format(row["hostname"]))
204+
else:
205+
pass
206+
else:
207+
print(
208+
"Preconfig for {} failed validation | error: {}".format(
209+
row["hostname"], validate.text
210+
)
211+
)
212+
# Write local YAML file of failed config for reference
213+
yaml_filename = "{}_preconfig-FAILED.yml".format(row["hostname"])
214+
with open(output_directory + yaml_filename, "w") as preconfig_file:
215+
write_data = preconfig_file.write(yaml_preconfig)
216+
217+
# Increment row number when iterating to next row in CSV
218+
row_number += 1
219+
220+
# if not using API key, logout from Orchestrator
221+
if orch_api_key is None:
222+
orch.logout()
223+
else:
224+
pass

0 commit comments

Comments
 (0)