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
92 changes: 63 additions & 29 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', 'broadcast']:
env.filters[attr] = partial(prefix_attr, attr)
# Pass the is_multi_npu function as global
env.globals['multi_asic'] = is_multi_npu

return env

def main():
parser=argparse.ArgumentParser(description="Render configuration file from minigraph data and jinja2 template.")
Expand All @@ -221,14 +261,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 @@ -267,9 +308,7 @@ def main():
if brkout_table is not None:
deep_update(data, {'BREAKOUT_CFG': brkout_table})

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 @@ -297,7 +336,7 @@ def main():

if args.from_db:
if args.namespace is None:
configdb = ConfigDBConnector(**db_kwargs)
configdb = ConfigDBConnector(use_unix_socket_path=True, **db_kwargs)
else:
configdb = ConfigDBConnector(use_unix_socket_path=True, namespace=args.namespace, **db_kwargs)

Expand Down Expand Up @@ -328,28 +367,23 @@ def main():
hardware_data['DEVICE_METADATA']['localhost'].update(asic_id=asic_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', 'broadcast']:
env.filters[attr] = partial(prefix_attr, attr)
# Pass the is_multi_npu function as global
env.globals['multi_asic'] = is_multi_npu
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":
Copy link
Collaborator

@qiluo-msft qiluo-msft Aug 12, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"config-db" [](start = 28, length = 11)

This is magic string for a special mode. What if user want to write to a file called 'config-db' ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The user can write and rename the file later. Currently when we save config-db.json, we simply redirect stdout to the file.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe we can use -t template-2.j2,[config-db] to indicate this is config-db, not a config-db file. if user really generate a config-db file, then use -t template-2.j2,\[config-db\]

Copy link
Contributor Author

@tahmed-dev tahmed-dev Aug 12, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the user can use this syntax -t template-2.j2,/config-db or -t template-2.j2,./config-db to write to file named config-db. The check is strictly done against config-db as keyword. The / or ./ would serve as a distinction between the keyword and a filename that happens to be the same as the keyword.

I can changed it to [config-db], however the premise of argument still exist. What if the file name is [config-db]. I tend to think this a user error since there is clear way if config-db is used as a file name.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just tried the ./config-db and it does generate the file as intended:

admin@str-s6000-acs-14:~$ sudo sonic-cfggen -d -t /usr/share/sonic/templates/rsyslog.conf.j2,./config-db
admin@str-s6000-acs-14:~$ ls -alrt
total 85
-rw-r--r-- 1 admin admin   807 Apr 18  2019 .profile
-rw-r--r-- 1 admin admin  3526 Apr 18  2019 .bashrc
-rw-r--r-- 1 admin admin   220 Apr 18  2019 .bash_logout
drwxr-xr-x 1 root  root   4096 Aug 11 03:41 ..
drwxr-xr-x 1 admin admin  4096 Aug 12 21:16 .
-rw-r--r-- 1 root  root   1907 Aug 12 21:16 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 All @@ -363,7 +397,7 @@ def main():

if args.write_to_db:
if args.namespace is None:
configdb = ConfigDBConnector(**db_kwargs)
configdb = ConfigDBConnector(use_unix_socket_path=True, **db_kwargs)
else:
configdb = ConfigDBConnector(use_unix_socket_path=True, namespace=args.namespace, **db_kwargs)

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 @@ -107,6 +108,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