Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 61 additions & 27 deletions src/sonic-config-engine/sonic-cfggen
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import yaml
import jinja2
import netaddr
import json
import contextlib
from functools import partial
from minigraph import minigraph_encoder
from minigraph import parse_xml
Expand Down Expand Up @@ -204,6 +205,45 @@ def sort_data(data):
data[table] = OrderedDict(natsorted(data[table].items()))
return data

@contextlib.contextmanager
def smart_open(filename=None, mode=None):
"""
Provide contextual file descriptor of filename if it is not a file descriptor
"""
smart_file = open(filename, mode) if not isinstance(filename, file) else filename
try:
yield smart_file
finally:
if not isinstance(filename, file):
smart_file.close()

def _process_json(args, data):
"""
Process JSON file and update switch configuration data
"""
for json_file in args.json:
with open(json_file, 'r') as stream:
deep_update(data, FormatConverter.to_deserialized(json.load(stream)))

def _get_jinja2_env(paths):
"""
Retreive Jinj2 env used to render configuration templates
"""
loader = jinja2.FileSystemLoader(paths)
redis_bcc = RedisBytecodeCache(SonicV2Connector(host='127.0.0.1'))
env = jinja2.Environment(loader=loader, trim_blocks=True, bytecode_cache=redis_bcc)
env.filters['sort_by_port_index'] = sort_by_port_index
env.filters['ipv4'] = is_ipv4
env.filters['ipv6'] = is_ipv6
env.filters['unique_name'] = unique_name
env.filters['pfx_filter'] = pfx_filter
env.filters['ip_network'] = ip_network
for attr in ['ip', 'network', 'prefixlen', 'netmask']:
env.filters[attr] = partial(prefix_attr, attr)
# Pass the is_multi_asic function as global
env.globals['multi_asic'] = is_multi_asic

return env

def main():
parser=argparse.ArgumentParser(description="Render configuration file from minigraph data and jinja2 template.")
Expand All @@ -220,14 +260,15 @@ def main():
parser.add_argument("-H", "--platform-info", help="read platform and hardware info", action='store_true')
parser.add_argument("-s", "--redis-unix-sock-file", help="unix sock file for redis connection")
group = parser.add_mutually_exclusive_group()
group.add_argument("-t", "--template", help="render the data with the template file")
group.add_argument("-t", "--template", help="render the data with the template file", action="append", default=[],
type=lambda opt_value: tuple(opt_value.split(',')) if ',' in opt_value else (opt_value, sys.stdout))
parser.add_argument("-T", "--template_dir", help="search base for the template files", action='store')
group.add_argument("-v", "--var", help="print the value of a variable, support jinja2 expression")
group.add_argument("--var-json", help="print the value of a variable, in json format")
group.add_argument("-w", "--write-to-db", help="write config into configdb", action='store_true')
group.add_argument("--print-data", help="print all data", action='store_true')
group.add_argument("--preset", help="generate sample configuration from a preset template", choices=get_available_config())
group = parser.add_mutually_exclusive_group()
group.add_argument("--print-data", help="print all data", action='store_true')
group.add_argument("-K", "--key", help="Lookup for a specific key")
args = parser.parse_args()

Expand Down Expand Up @@ -260,9 +301,7 @@ def main():
sys.exit(1)
deep_update(data, {'PORT': ports})

for json_file in args.json:
with open(json_file, 'r') as stream:
deep_update(data, FormatConverter.to_deserialized(json.load(stream)))
_process_json(args, data)

if args.minigraph != None:
minigraph = args.minigraph
Expand Down Expand Up @@ -326,28 +365,23 @@ def main():
hardware_data['DEVICE_METADATA']['localhost'].update(asic_id=device_id)
deep_update(data, hardware_data)

if args.template is not None:
template_file = os.path.abspath(args.template)
paths = ['/', '/usr/share/sonic/templates', os.path.dirname(template_file)]
if args.template_dir is not None:
template_dir = os.path.abspath(args.template_dir)
paths.append(template_dir)
loader = jinja2.FileSystemLoader(paths)

redis_bcc = RedisBytecodeCache(SonicV2Connector(host='127.0.0.1'))
env = jinja2.Environment(loader=loader, trim_blocks=True, bytecode_cache=redis_bcc)
env.filters['sort_by_port_index'] = sort_by_port_index
env.filters['ipv4'] = is_ipv4
env.filters['ipv6'] = is_ipv6
env.filters['unique_name'] = unique_name
env.filters['pfx_filter'] = pfx_filter
env.filters['ip_network'] = ip_network
for attr in ['ip', 'network', 'prefixlen', 'netmask']:
env.filters[attr] = partial(prefix_attr, attr)
# Pass the is_multi_asic function as global
env.globals['multi_asic'] = is_multi_asic
template = env.get_template(template_file)
print(template.render(sort_data(data)))
paths = ['/', '/usr/share/sonic/templates']
if args.template_dir:
paths.append(os.path.abspath(args.template_dir))

if args.template:
for template_file, _ in args.template:
paths.append(os.path.dirname(os.path.abspath(template_file)))
env = _get_jinja2_env(paths)
sorted_data = sort_data(data)
for template_file, dest_file in args.template:
template = env.get_template(os.path.basename(template_file))
template_data = template.render(sorted_data)
if dest_file == "config-db":
deep_update(data, FormatConverter.to_deserialized(json.loads(template_data)))
else:
with smart_open(dest_file, 'w') as df:
print(template_data, file=df)

if args.var != None:
template = jinja2.Template('{{' + args.var + '}}')
Expand Down
4 changes: 4 additions & 0 deletions src/sonic-config-engine/tests/sample-template-1.json.j2
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"jk1_1": "{{ key1_1 }}",
"jk1_2": "{{ key1_2 }}"
}
4 changes: 4 additions & 0 deletions src/sonic-config-engine/tests/sample-template-2.json.j2
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"jk2_1": "{{ key2_1 }}",
"jk2_2": "{{ key2_2 }}"
}
1 change: 1 addition & 0 deletions src/sonic-config-engine/tests/test2.j2
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{{ key1 }}
25 changes: 25 additions & 0 deletions src/sonic-config-engine/tests/test_cfggen.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from unittest import TestCase
import json
import subprocess
import os

Expand Down Expand Up @@ -106,6 +107,30 @@ def test_render_template(self):
output = self.run_script(argument)
self.assertEqual(output.strip(), 'value1\nvalue2')

def test_template_batch_mode(self):
argument = '-y ' + os.path.join(self.test_dir, 'test.yml')
argument += ' -a \'{"key1":"value"}\''
argument += ' -t ' + os.path.join(self.test_dir, 'test.j2') + ',' + os.path.join(self.test_dir, 'test.txt')
argument += ' -t ' + os.path.join(self.test_dir, 'test2.j2') + ',' + os.path.join(self.test_dir, 'test2.txt')
output = self.run_script(argument)
assert(os.path.exists(os.path.join(self.test_dir, 'test.txt')))
assert(os.path.exists(os.path.join(self.test_dir, 'test2.txt')))
with open(os.path.join(self.test_dir, 'test.txt')) as tf:
self.assertEqual(tf.read().strip(), 'value1\nvalue2')
with open(os.path.join(self.test_dir, 'test2.txt')) as tf:
self.assertEqual(tf.read().strip(), 'value')

def test_template_json_batch_mode(self):
data = {"key1_1":"value1_1", "key1_2":"value1_2", "key2_1":"value2_1", "key2_2":"value2_2"}
argument = " -a '{0}'".format(repr(data).replace('\'', '"'))
argument += ' -t ' + os.path.join(self.test_dir, 'sample-template-1.json.j2') + ",config-db"
argument += ' -t ' + os.path.join(self.test_dir, 'sample-template-2.json.j2') + ",config-db"
argument += ' --print-data'
output = self.run_script(argument)
output_data = json.loads(output)
for key, value in data.items():
self.assertEqual(output_data[key.replace("key", "jk")], value)

# FIXME: This test depends heavily on the ordering of the interfaces and
# it is not at all intuitive what that ordering should be. Could make it
# more robust by adding better parsing logic.
Expand Down