Skip to content

Commit a80a3f9

Browse files
committed
Add support for extra template variables
'netlab config' used to pass the '-e' parameter to the Ansible playbook. That approach no longer works after we started rendering the Jinja2 templates in netlab. This commit parses '-e' parameters in 'netlab config' command and adds extra variables to node data, making them available in custom config templates. Other minor fixes: * Refactor common Ansible args processing into shared functions * Check that no extra vars are specified on config reload * Remove an unneeded call to set_custom_config
1 parent e451a03 commit a80a3f9

File tree

3 files changed

+99
-30
lines changed

3 files changed

+99
-30
lines changed

docs/netlab/config.md

Lines changed: 36 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
11
(netlab-config)=
22
# Deploying Custom Device Configurations
33

4-
**netlab config** uses an internal Ansible playbook (`netsim/ansible/config.ansible`) to deploy custom device configurations generated from the supplied Jinja2 template(s) to lab devices. It takes lab parameters from the _netlab_ snapshot file and Ansible inventory created by the **[netlab create](netlab-create)** or **[netlab up](netlab-up)** command.
4+
**netlab config** is used to deploy custom device configuration templates to lab devices. It reads lab parameters from the _netlab_ snapshot file created by the **[netlab create](netlab-create)** or **[netlab up](netlab-up)** command, renders the supplied Jinja2 template ([limitations](dev-templates)), and uses the internal `config.ansible` Ansible playbook to deploy the rendered configuration snippets.
55

6-
You have to use **netlab config** on a running lab. If you want to try out the configuration templates without starting the lab, add the [**config** attribute](custom-config) to node data and run **netlab create** (to create the Ansible inventory) followed by **[netlab initial -c -o](netlab-initial)** to create the configuration files.
6+
You have to use **netlab config** on a running lab. If you want to try out the configuration templates without starting the lab, add the [**config** attribute](custom-config) to node data and run **netlab create** (to generate the snapshot file), followed by **[netlab initial -c -o](netlab-initial)** to create the configuration files.
77

88
## Usage
99

1010
```text
11-
usage: netlab config [-h] [-r] [-v] [-q] [-i INSTANCE] template
11+
usage: netlab config [-h] [-r] [-l LIMIT] [-e EXTRA_VARS [EXTRA_VARS ...]] [-v] [-q]
12+
[-i INSTANCE]
13+
template
1214
1315
Deploy custom configuration template
1416
@@ -18,10 +20,13 @@ positional arguments:
1820
options:
1921
-h, --help show this help message and exit
2022
-r, --reload Reload saved device configurations
23+
-l, --limit LIMIT Limit the operation to a subset of nodes
24+
-e, --extra-vars EXTRA_VARS [EXTRA_VARS ...]
25+
Specify extra variables for the configuration template
2126
-v, --verbose Verbose logging (add multiple flags for increased verbosity)
2227
-q, --quiet Report only major errors
23-
-i INSTANCE, --instance INSTANCE
24-
Specify lab instance to configure
28+
-i, --instance INSTANCE
29+
Specify the lab instance to configure
2530
2631
All other arguments are passed directly to ansible-playbook
2732
```
@@ -40,19 +45,39 @@ When executed with the `-i` option, **‌netlab config** expects the configurati
4045

4146
## Limiting the Scope of Configuration Deployments
4247

43-
All unrecognized parameters are passed to the internal `config.ansible` Ansible playbook. You can use **ansible-playbook** CLI parameters to modify the configuration deployment, for example:
48+
You can use the `-l` parameter to deploy device configurations on a subset of devices. The parameter value must be a valid _netlab_ node selection expression ([more details](netlab-inspect-node)).
4449

45-
* `-l` parameter to deploy device configurations on a subset of devices.
46-
* `-C` parameter to run the Ansible playbook in dry-run mode.
50+
All unrecognized parameters are passed to the internal `config.ansible` Ansible playbook, allowing you to use the **ansible-playbook** CLI parameters to modify the configuration deployment. For example, you can use the `-C` parameter to run the Ansible playbook in dry-run mode.
51+
52+
## Extra Variables
53+
54+
You can use the `-e` parameter to specify an extra variable value in the `name=value` format (the `-e` parameter can be used multiple times). _netlab_ recognizes only the `name=value` format, not the JSON or filename formats recognized by Ansible.
55+
56+
The extra variables are applied to all nodes and can be used in device configuration templates. For example, the following Jinja2 template uses the `df_state` variable to turn BGP default route advertisements on or off:
57+
58+
```
59+
router bgp {{ bgp.as }}
60+
!
61+
{% for af in ['ipv4','ipv6'] %}
62+
{% for ngb in bgp.neighbors if af in ngb %}
63+
{% if loop.first %}
64+
address-family {{ af }}
65+
{% endif %}
66+
{% if df_state|default('') == 'off' %}no {% endif %}neighbor {{ ngb[af] }} default-originate
67+
{% endfor %}
68+
{% endfor %}
69+
```
70+
71+
After saving the above template into `bgp_default.j2`, you can use `netlab config bgp_default --limit somenode` to enable BGP default route advertisement and `netlab config bgp_default --limit somenode -e df_state=off` to turn it off.
4772

4873
## Restoring Saved Device Configurations
4974

50-
**netlab config --reload** implements the *reload saved device configurations* part of the **netlab initial -r** command. It waits for devices to become ready (because it's used immediately after a lab has been started) and starts the initial configuration process on devices that need more than a replay of saved configuration ([more details](netlab-up-reload)).
75+
**netlab config --reload** implements the *reload saved device configurations* part of the **netlab initial -r** command. It waits for devices to become ready (since it's used immediately after a lab has been started) and starts the initial configuration process on devices that need more than a replay of saved configuration ([more details](netlab-up-reload)).
5176

52-
After that, it treats the saved device configurations as custom templates and uses the same process as the regular **netlab config** command.
77+
After that, it treats the saved device configurations as custom templates (using the same process as the regular **netlab config** command), allowing you to use Jinja2 expressions in saved device configurations.
5378

5479
## Debugging Device Configurations
5580

5681
To display device configurations within the Ansible playbook without deploying them, use `-v --tags test` parameters after the template name.
5782

58-
The `-v` flag triggers a debugging printout, and the bogus `test` flag skips the configuration deployment.
83+
The `-v` flag enables debugging output, and the bogus `test` flag skips configuration deployment.

netsim/cli/config.py

Lines changed: 51 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99

1010
from box import Box
1111

12-
from ..data import get_empty_box
12+
from ..data import get_box
1313
from ..utils import log
1414
from . import (
1515
_nodeset,
@@ -22,6 +22,7 @@
2222
parser_lab_location,
2323
)
2424
from .initial import configs as i_configs
25+
from .initial import utils as i_utils
2526

2627

2728
#
@@ -41,6 +42,10 @@ def custom_config_parse(args: typing.List[str]) -> typing.Tuple[argparse.Namespa
4142
'-l','--limit',
4243
dest='limit', action='store',
4344
help='Limit the operation to a subset of nodes')
45+
parser.add_argument(
46+
'-e','--extra-vars',
47+
dest='extra_vars',action='store',nargs='+',
48+
help='Specify extra variables for the configuration template')
4449
parser.add_argument(
4550
dest='template', action='store',
4651
help='Configuration template or a directory with templates')
@@ -57,14 +62,22 @@ def set_initial_args(args: argparse.Namespace, initial: bool = False) -> None:
5762
setattr(args,'no_refresh',False) # ... mandatory refresh
5863
setattr(args,'generate',None) # ... and internally-generated configs
5964

60-
def set_custom_config(topology: Box, nodeset: list, cfg_name: str) -> None:
65+
def set_custom_config(
66+
topology: Box,
67+
nodeset: list,
68+
cfg_name: str,
69+
extra_vars: dict = {}) -> None:
70+
6171
for n_name in nodeset:
62-
topology.nodes[n_name].config = [ cfg_name ]
72+
n_data = topology.nodes[n_name]
73+
n_data.config = [ cfg_name ]
74+
for k,v in extra_vars.items():
75+
n_data[k] = v
6376

64-
def ansible_extra_vars(topology: Box, reload: bool = False) -> Box:
77+
def ansible_extra_vars(topology: Box, reload: bool = False, extra_vars: dict = {}) -> Box:
6578
cfg_sfx = '.cfg' if reload else ''
6679

67-
ev = get_empty_box()
80+
ev = get_box(extra_vars)
6881
ev.node_files = str(Path("./node_files").resolve().absolute())
6982

7083
ev.paths_t_files.files = "{{ config_module }}" + cfg_sfx # Take only module file from node_files
@@ -76,6 +89,29 @@ def ansible_extra_vars(topology: Box, reload: bool = False) -> Box:
7689
ev.paths_custom.tasks = topology.defaults.paths.custom.tasks
7790
return ev
7891

92+
def get_ansible_args(ans_vars: Box,nodeset: list,cfg_name: str) -> list:
93+
args = i_utils.common_ansible_args()
94+
args += ["-e",ans_vars.to_json(),"-e",f'config={cfg_name}']
95+
args += ["-l",','.join(nodeset)]
96+
return args
97+
98+
def parse_extra_vars(ev_list: typing.Optional[list]) -> dict:
99+
ev: dict = {}
100+
if not ev_list:
101+
return ev
102+
103+
for v_item in ev_list:
104+
if '=' not in v_item:
105+
error_and_exit('Extra variables have to be specified in name=value format')
106+
(n,v) = v_item.split('=',maxsplit=1)
107+
try:
108+
value = eval(v)
109+
except:
110+
value = v
111+
ev[n] = value
112+
113+
return ev
114+
79115
"""
80116
Create the required configs in node_files
81117
"""
@@ -84,11 +120,12 @@ def create_node_files(
84120
nodeset: list,
85121
args: argparse.Namespace,
86122
cfg_name: str,
123+
extra_vars: dict = {},
87124
initial: bool = False,
88125
cfg_suffix: str = 'none') -> None:
89126

90127
set_initial_args(args,initial=initial) # Adjust args for 'netlab initial' processing
91-
set_custom_config(topology,nodeset,cfg_name)
128+
set_custom_config(topology,nodeset,cfg_name,extra_vars)
92129

93130
i_configs.create_node_configs( # Create the necessary files in node_files directory
94131
topology=topology,
@@ -109,6 +146,9 @@ def reload_node_configs(topology: Box,nodeset: list,args: argparse.Namespace, re
109146
if not cfg_path.is_dir(): # Sanity check: are we reloading from a directory?
110147
error_and_exit('The argument specified with the --reload option must be a directory')
111148

149+
if args.extra_vars:
150+
error_and_exit('You cannot specify extra vars while reloading configuration')
151+
112152
no_config = []
113153
for n_name in nodeset: # Identify nodes that have no configs
114154
if not list(cfg_path.glob(n_name+'.*')): # ... in the specified directory
@@ -136,8 +176,7 @@ def reload_node_configs(topology: Box,nodeset: list,args: argparse.Namespace, re
136176
# Run the Ansible playbook with modified path variables and an adjusted nodeset
137177
#
138178
ans_vars = ansible_extra_vars(topology,reload=True)
139-
rest_args = rest + ["-e",ans_vars.to_json(),"-e",f'config={str(cfg_path.name)}']
140-
rest_args += ["-l",','.join(nodeset)]
179+
rest_args = rest + get_ansible_args(ans_vars,nodeset,str(cfg_path.name))
141180
if not ansible.playbook('reload-config.ansible',rest_args,abort_on_error=False):
142181
error_and_exit('Cannot reload initial device configurations')
143182

@@ -153,14 +192,13 @@ def deploy_custom_config(topology: Box,nodeset: list,args: argparse.Namespace, r
153192
if cfg_name.endswith('.j2'):
154193
cfg_name = cfg_name[:-3]
155194

156-
set_custom_config(topology,nodeset,cfg_name)
157-
create_node_files(topology,nodeset,args,cfg_name)
195+
extra_vars=parse_extra_vars(args.extra_vars)
196+
create_node_files(topology,nodeset,args,cfg_name,extra_vars)
158197

159198
# Run the Ansible playbook with modified path variables and an adjusted nodeset
160199
#
161-
ans_vars = ansible_extra_vars(topology,reload=False)
162-
rest_args = rest + ["-e",ans_vars.to_json(),"-e",f'config={cfg_name}']
163-
rest_args += ["-l",','.join(nodeset)]
200+
ans_vars = ansible_extra_vars(topology,reload=False,extra_vars=extra_vars)
201+
rest_args = rest + get_ansible_args(ans_vars,nodeset,cfg_name)
164202
if not ansible.playbook('config.ansible',rest_args,abort_on_error=False):
165203
error_and_exit('Cannot deploy custom configuration template')
166204

netsim/cli/initial/utils.py

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
from box import Box
1010

11+
from ...utils import log
1112
from .. import common_parse_args, parser_lab_location
1213

1314

@@ -73,23 +74,28 @@ def initial_config_parse(args: typing.List[str]) -> typing.Tuple[argparse.Namesp
7374

7475
return parser.parse_known_args(args)
7576

77+
def common_ansible_args() -> list:
78+
rest = []
79+
if log.VERBOSE:
80+
rest += ['-' + 'v' * log.VERBOSE]
81+
82+
if log.QUIET:
83+
os.environ["ANSIBLE_STDOUT_CALLBACK"] = "selective"
84+
85+
return rest
86+
7687
"""
7788
Build Ansible arguments based on 'netlab initial' parameters
7889
"""
7990
def ansible_args(args: argparse.Namespace) -> list:
80-
rest: typing.List[str] = []
81-
if args.verbose:
82-
rest = ['-' + 'v' * args.verbose] + rest
91+
rest = common_ansible_args()
8392

8493
if args.limit:
8594
rest = ['--limit',args.limit] + rest
8695

8796
if args.initial:
8897
rest = ['-t','initial'] + rest
8998

90-
if args.quiet:
91-
os.environ["ANSIBLE_STDOUT_CALLBACK"] = "selective"
92-
9399
if args.module:
94100
if args.module != "*":
95101
rest = ['-e','modlist='+args.module] + rest

0 commit comments

Comments
 (0)