diff --git a/cznicinfo/cache.py b/cznicinfo/cache.py
index d25e1f2b60c10ec20dcf31291dd260240f0ae853..1b95720b91af6db626135f6cf2b9c4fe44c58ac1 100644
--- a/cznicinfo/cache.py
+++ b/cznicinfo/cache.py
@@ -5,6 +5,7 @@ cache_base_path = Path.home() / '.cache' / 'cznicinfo'
 nv_cache_path = cache_base_path / 'nv'
 nv_config_cache_path = nv_cache_path / 'config'
 nv_versions_cache_path = nv_cache_path / 'versions'
+report_cache_path = cache_base_path / 'report'
 
 
 def ensure_cache_dirs():
diff --git a/cznicinfo/commands/report.py b/cznicinfo/commands/report.py
new file mode 100644
index 0000000000000000000000000000000000000000..c436e82977ec29e489fb67cbc4333ff9aefdac53
--- /dev/null
+++ b/cznicinfo/commands/report.py
@@ -0,0 +1,87 @@
+"""
+Generate report of various project(s) versions
+
+Usage: cznicinfo report [-h] [-c] [-f] [-o <dir>] [<project>...]
+
+Arguments:
+    <project>     project(s) to generate report for (default: ALL)
+
+Options:
+    -o <dir>, --outdir <dir>  generate report into this directory
+    -c, --cached              don't check versions, use cached data instead
+    -f, --force               use force - delete outdir if it exists
+    -h, --help                show command help
+
+"""
+
+from docopt import docopt
+import os
+from pathlib import Path
+import shutil
+
+from cznicinfo import cache
+from cznicinfo import exception
+from cznicinfo import infocore
+from cznicinfo import infoquery
+from cznicinfo import log
+from cznicinfo import nv
+from cznicinfo import template
+
+
+def run_command(*args, **kwargs):
+    cargs = docopt(__doc__)
+
+    info = infocore.get_info()
+
+    if cargs['<project>']:
+        projs, not_found = infoquery.get_projects_by_name(
+            info, cargs['<project>'])
+        if not_found:
+            nf_str = ", ".join(not_found)
+            log.warn("following projects were not found: %s", nf_str)
+    else:
+        # all projects by default
+        projs = info.get('projects')
+
+    if not cargs['--cached']:
+        log.info("checking latest versions...")
+        for proj in projs:
+            nv.save_nv_config(proj)
+            nv.run_nv_check(proj)
+
+    if cargs['--outdir']:
+        outdir = Path(cargs['--outdir'])
+        if outdir.exists():
+            if cargs['--force']:
+                log.verbose("removing existing report dir with --force: %s",
+                            outdir)
+                shutil.rmtree(outdir)
+            else:
+                msg = "selected --outdir exists (override with -f/--force)"
+                raise exception.OutputExists(msg=msg)
+    else:
+        outdir = cache.report_cache_path
+        if outdir.exists():
+            # cached report dir can be overwritten without force
+            shutil.rmtree(outdir)
+
+    log.info("generating version report...")
+    os.makedirs(outdir)
+    index = gen_versions_report(projs, outdir)
+
+    print("report index: %s" % index)
+
+    return 0
+
+
+def gen_versions_report(projects, out_dir):
+    out_dir = Path(out_dir)
+    for proj in projects:
+        proj['versions'] = nv.load_versions_by_distro(proj)
+
+    vars = {
+        'projects': projects,
+    }
+    index = template.render_template('report/index.html', out_dir, vars)
+    template.copy_template('report/style.css', out_dir)
+    return index
diff --git a/cznicinfo/exception.py b/cznicinfo/exception.py
index 5f21e2eb350a29efe86f99094f8db3568e5c361e..0db1039415d2111a270bc9237aeab8ea7fa533b2 100644
--- a/cznicinfo/exception.py
+++ b/cznicinfo/exception.py
@@ -63,6 +63,11 @@ class InvalidFormat(CZNICInfoException):
     exit_code = 18
 
 
+class OutputExists(CZNICInfoException):
+    msg_fmt = "Output exists: %(out)s"
+    exit_code = 30
+
+
 class HTTPRequestFailed(CZNICInfoException):
     msg_fmt = "HTTP request failed with code %(code)s: %(request)s"
     exit_code = 44
diff --git a/cznicinfo/nv.py b/cznicinfo/nv.py
index 6c97c687ae1188e2a960e89f62fbf032d7210059..d338f43211704a8cd74dcdc857fc4e717c32df03 100644
--- a/cznicinfo/nv.py
+++ b/cznicinfo/nv.py
@@ -39,12 +39,48 @@ def load_nv_versions(project, postfix='new'):
     return json.load(verfile.open())
 
 
+def load_versions_by_distro(project):
+    versions = load_nv_versions(project)
+    return versions_by_distro(project, versions)
+
+
+def versions_by_distro(project, versions):
+    distros = infocore.get_supported_distros()
+    nv_versions = load_nv_versions(project)
+    versions = []
+    dpkgs = project.get('distro-pkgs', [])
+    for dpkg in dpkgs:
+        dpkg_distro = dpkg.get('distro')
+        if dpkg_distro not in distros:
+            msg = "distro info not available for: %s"
+            log.warn(msg, dpkg_distro)
+            continue
+        distro = distros[dpkg_distro]
+        distro_name = distro['name']
+
+        distro_vers = []
+        for release in distro.get('releases', []):
+            distro_version = release['version']
+            distro_id = nvid(distro_name, distro_version)
+            ver = nv_versions.get(distro_id)
+            dv = {'distro_version': distro_version}
+            dv['version'] = ver or 'N/A'
+            distro_vers.append(dv)
+
+        versions.append({
+            'distro': distro_name,
+            'versions': distro_vers,
+            })
+
+    return versions
+
+
 def get_nv_entry(project, distro, release):
     pkg_name = project['short-name']
     distro_name = distro['name']
     distro_version = str(release['version'])
-    distro_id = nvid("%s-%s" % (distro_name, distro_version))
     nv_source = distro['nv_source']
+    distro_id = nvid(distro_name, distro_version)
     e = "[%s]\n" % distro_id
     e += 'source = "%s"\n' % nv_source
     e += '%s = "%s"\n' % (nv_source, pkg_name)
@@ -62,5 +98,6 @@ def get_nv_head(name):
                 old=old_path, new=new_path))
 
 
-def nvid(s):
-    return re.sub(r'\W+', '-', s.lower())
+def nvid(distro, version):
+    dv = "%s-%s" % (distro, version)
+    return re.sub(r'\W+', '-', dv.lower())
diff --git a/cznicinfo/template.py b/cznicinfo/template.py
new file mode 100644
index 0000000000000000000000000000000000000000..e9b2753b5397502e245304cae13f1ddfbfaa2064
--- /dev/null
+++ b/cznicinfo/template.py
@@ -0,0 +1,30 @@
+import jinja2
+from pathlib import Path
+import shutil
+
+
+def get_template_path(template_fn, module_path=None):
+    if module_path:
+        path = module_path
+    else:
+        import cznicinfo
+        path = Path(cznicinfo.__file__).parent / 'templates'
+
+    return path / template_fn
+
+
+def render_template(template_path, out_dir, vars):
+    src = get_template_path(template_path)
+    dst = out_dir / src.name
+    with src.open('r') as srcf:
+        t = jinja2.Template(srcf.read())
+    with dst.open('w') as dstf:
+        dstf.write(t.render(**vars) + '\n')
+    return dst
+
+
+def copy_template(template_path, out_dir):
+    src = get_template_path(template_path)
+    dst = out_dir / src.name
+    shutil.copyfile(src, dst)
+    return dst
diff --git a/cznicinfo/templates/report/index.html b/cznicinfo/templates/report/index.html
new file mode 100644
index 0000000000000000000000000000000000000000..5fa738b90e64e08cc51c55813ee44e9b811c83c4
--- /dev/null
+++ b/cznicinfo/templates/report/index.html
@@ -0,0 +1,26 @@
+<html lang="en">
+<head>
+<title>CZ.NIC info: packaging versions report</title>
+<meta charset="utf-8"/>
+<link href="style.css" rel="stylesheet" type="text/css"/>
+<link href="https://fonts.googleapis.com/css?family=Roboto" rel="stylesheet"/>
+</head>
+
+<body>
+<h1>cznicinfo report</h1>
+
+{% for proj in projects %}
+  <h2>{{ proj['full-name'] }}</h2>
+  {% for distro in proj['versions'] %}
+  <h3>{{ distro['distro'] }}</h3>
+    {% for ver in distro['versions'] %}
+      <div class="ver">
+          <h4>{{ ver['distro_version'] }}</h4>
+          <div>{{ ver['version'] }}</div>
+      </div>
+    {% endfor %}
+  {% endfor %}
+{% endfor %}
+</tr>
+</table
+</body>
diff --git a/cznicinfo/templates/report/style.css b/cznicinfo/templates/report/style.css
new file mode 100644
index 0000000000000000000000000000000000000000..549babc7f1e7797d9d8abbb304b30eb0c4d6873b
--- /dev/null
+++ b/cznicinfo/templates/report/style.css
@@ -0,0 +1,14 @@
+h1, h2, h3, h4, h5 {
+	clear: both;
+	margin: 1ex 0ex 0ex 0ex;
+}
+
+.ver {
+	float: left;
+	display: block;
+	padding: 0.5ex 1ex;
+}
+
+.ver h4 {
+	margin: 0;
+}
diff --git a/requirements.txt b/requirements.txt
index 1a35129c075b8c32815e1d1b361c8245d0e78316..469f597ef6df42f15a67a6eb40026b96f69baf7c 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,3 +1,4 @@
 docopt
+jinja2
 PyYAML
 requests