diff --git a/tests/j2lint.py b/tests/j2lint.py
new file mode 100755
index 0000000000..65189c38f9
--- /dev/null
+++ b/tests/j2lint.py
@@ -0,0 +1,79 @@
+#!/usr/bin/env python3
+
+# Copyright 2020 StackHPC Ltd.
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""
+Original license:
+@author Gerard van Helden <drm@melp.nl>
+@license DBAD, see <http://www.dbad-license.org/>
+@url https://github.com/drm/jinja2-lint
+Simple j2 linter, useful for checking jinja2 template syntax
+
+Adapted for OpenStack Kolla/Kolla-Ansible purposes
+"""
+
+from ansible.plugins.filter.core import to_json
+from functools import reduce
+from jinja2 import BaseLoader
+from jinja2 import Environment
+from jinja2 import exceptions
+from jinja2 import TemplateNotFound
+from kolla_ansible import kolla_address
+from kolla_ansible import put_address_in_context
+import os.path
+
+
+class AbsolutePathLoader(BaseLoader):
+    def get_source(self, environment, template):
+        if not os.path.exists(template):
+            raise TemplateNotFound(template)
+        mtime = os.path.getmtime(template)
+        with open(template) as file:
+            source = file.read()
+        return source, template, lambda: mtime == os.path.getmtime(template)
+
+
+def check(template, out, err, env=Environment(loader=AbsolutePathLoader(),
+          autoescape=True)):
+    try:
+        env.filters['basename'] = os.path.basename
+        env.filters['bool'] = bool
+        env.filters['hash'] = hash
+        env.filters['to_json'] = to_json
+        env.filters['kolla_address'] = kolla_address
+        env.filters['put_address_in_context'] = put_address_in_context
+        env.get_template(template)
+        out.write("%s: Syntax OK\n" % template)
+        return 0
+    except TemplateNotFound:
+        err.write("%s: File not found\n" % template)
+        return 2
+    except exceptions.TemplateSyntaxError as ex:
+        err.write("%s: Syntax check failed: %s in %s at %d\n"
+                  % (template, ex.message, ex.filename, ex.lineno))
+        return 1
+
+
+def main(**kwargs):
+    import sys
+    try:
+        sys.exit(reduce(lambda r, fn: r +
+                        check(fn, sys.stdout, sys.stderr, **kwargs),
+                        sys.argv[1:], 0))
+    except IndexError:
+        sys.stdout.write("Usage: j2lint.py filename [filename ...]\n")
+
+
+if __name__ == "__main__":
+    main()
diff --git a/tox.ini b/tox.ini
index d64225677d..0b1932289a 100644
--- a/tox.ini
+++ b/tox.ini
@@ -87,6 +87,7 @@ commands =
   {[testenv:doc8]commands}
   {[testenv:bandit]commands}
   {[testenv:bashate]commands}
+  {[testenv:j2lint]commands}
   {[testenv:yamllint]commands}
   {[testenv:ansible-lint]commands}
 
@@ -121,6 +122,11 @@ commands =
 deps = {[testenv:linters]deps}
 commands = bandit --skip B303 -r ansible kolla_ansible tests tools
 
+[testenv:j2lint]
+deps = {[testenv:linters]deps}
+commands =
+  find {toxinidir} -type f -name "*.j2" -not -path "*/.tox/*" -exec {toxinidir}/tests/j2lint.py \{\} +
+
 [testenv:ansible-lint]
 # Lint only code in ansible/* - ignore tests/ and roles/ used by CI
 setenv = {[testenv:linters]setenv}