summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--devel/management/commands/import_signatures.py104
-rw-r--r--media/archweb.css12
-rw-r--r--media/visualize.js130
-rw-r--r--templates/visualize/index.html13
-rw-r--r--visualize/urls.py1
-rw-r--r--visualize/views.py48
6 files changed, 300 insertions, 8 deletions
diff --git a/devel/management/commands/import_signatures.py b/devel/management/commands/import_signatures.py
new file mode 100644
index 00000000..8a4ce873
--- /dev/null
+++ b/devel/management/commands/import_signatures.py
@@ -0,0 +1,104 @@
+# -*- coding: utf-8 -*-
+"""
+import_signatures command
+
+Import signatures from a given GPG keyring.
+
+Usage: ./manage.py generate_keyring <keyring_path>
+"""
+
+from datetime import datetime
+import logging
+import subprocess
+import sys
+
+from django.core.management.base import BaseCommand, CommandError
+from django.db import transaction
+
+from devel.models import PGPSignature
+
+logging.basicConfig(
+ level=logging.INFO,
+ format='%(asctime)s -> %(levelname)s: %(message)s',
+ datefmt='%Y-%m-%d %H:%M:%S',
+ stream=sys.stderr)
+logger = logging.getLogger()
+
+class Command(BaseCommand):
+ args = "<keyring_path>"
+ help = "Import signatures from a given GPG keyring."
+
+ def handle(self, *args, **options):
+ v = int(options.get('verbosity', None))
+ if v == 0:
+ logger.level = logging.ERROR
+ elif v == 1:
+ logger.level = logging.INFO
+ elif v == 2:
+ logger.level = logging.DEBUG
+
+ if len(args) < 1:
+ raise CommandError("keyring_path must be provided")
+
+ import_signatures(args[0])
+
+def parse_sigdata(data):
+ nodes = {}
+ edges = []
+ current_pubkey = None
+
+ # parse all of the output from our successful GPG command
+ logger.info("parsing command output")
+ for line in data.split('\n'):
+ parts = line.split(':')
+ if parts[0] == 'pub':
+ current_pubkey = parts[4]
+ nodes[current_pubkey] = None
+ if parts[0] == 'uid':
+ uid = parts[9]
+ # only set uid if this is the first one encountered
+ if nodes[current_pubkey] is None:
+ nodes[current_pubkey] = uid
+ if parts[0] == 'sig':
+ created = datetime.utcfromtimestamp(int(parts[5]))
+ expires = None
+ if parts[6]:
+ expires = datetime.utcfromtimestamp(int(parts[6]))
+ valid = parts[1] != '-'
+ edge = (parts[4], current_pubkey, created, expires, valid)
+ edges.append(edge)
+
+ return nodes, edges
+
+
+def import_signatures(keyring):
+ gpg_cmd = ["gpg", "--no-default-keyring", "--keyring", keyring,
+ "--list-sigs", "--with-colons", "--fixed-list-mode"]
+ logger.info("running command: %r", gpg_cmd)
+ proc = subprocess.Popen(gpg_cmd, stdout=subprocess.PIPE)
+ outdata, errdata = proc.communicate()
+ if proc.returncode != 0:
+ logger.error(errdata)
+ raise subprocess.CalledProcessError(proc.returncode, gpg_cmd)
+
+ nodes, edges = parse_sigdata(outdata)
+
+ # now prune the data down to what we actually want.
+ # prune edges not in nodes, remove duplicates, and self-sigs
+ pruned_edges = set(edge for edge in edges
+ if edge[0] in nodes and edge[0] != edge[1])
+
+ logger.info("creating or finding %d signatures", len(pruned_edges))
+ created_ct = 0
+ with transaction.commit_on_success():
+ for edge in pruned_edges:
+ _, created = PGPSignature.objects.get_or_create(
+ signer=edge[0], signee=edge[1],
+ created=edge[2], expires=edge[3],
+ defaults={ 'valid': edge[4] })
+ if created:
+ created_ct += 1
+
+ logger.info("created %d signatures", created_ct)
+
+# vim: set ts=4 sw=4 et:
diff --git a/media/archweb.css b/media/archweb.css
index bfb712f8..2cba2864 100644
--- a/media/archweb.css
+++ b/media/archweb.css
@@ -986,3 +986,15 @@ ul.signoff-list {
font-size: 0.85em;
line-height: 1em;
}
+
+#visualize-keys svg {
+ width: 100%;
+}
+
+ #visualize-keys circle {
+ stroke-width: 1.5px;
+ }
+
+ #visualize-keys line {
+ stroke: #888;
+ }
diff --git a/media/visualize.js b/media/visualize.js
index fa89a613..bd7b6679 100644
--- a/media/visualize.js
+++ b/media/visualize.js
@@ -19,7 +19,7 @@
*/
function packages_treemap(chart_id, orderings, default_order) {
- var jq_div = $(chart_id),
+ var jq_div = jQuery(chart_id),
color = d3.scale.category20();
var key_func = function(d) { return d.key; };
var value_package_count = function(d) { return d.count; },
@@ -129,7 +129,7 @@ function packages_treemap(chart_id, orderings, default_order) {
});
};
- $.each(orderings, function(k, v) {
+ jQuery.each(orderings, function(k, v) {
make_group_button(k, v);
});
@@ -140,7 +140,131 @@ function packages_treemap(chart_id, orderings, default_order) {
.data(treemap.size([jq_div.width(), jq_div.height()]), key_func)
.call(cell);
};
- $(window).resize(function() {
+ jQuery(window).resize(function() {
+ if (resize_timeout) {
+ clearTimeout(resize_timeout);
+ }
+ resize_timeout = setTimeout(real_resize, 200);
+ });
+}
+
+function developer_keys(chart_id, data_url) {
+ var jq_div = jQuery(chart_id),
+ r = 10;
+
+ var force = d3.layout.force()
+ .gravity(0.1)
+ .charge(-200)
+ .linkStrength(0.2)
+ .size([jq_div.width(), jq_div.height()]);
+
+ var svg = d3.select(chart_id)
+ .append("svg");
+
+ d3.json(data_url, function(json) {
+ var fill = d3.scale.category20();
+
+ var index_for_key = function(key) {
+ var i;
+ key = key.slice(-8);
+ for (i = 0; i < json.nodes.length; i++) {
+ var node_key = json.nodes[i].key;
+ if (node_key && node_key.slice(-8) === key) {
+ return i;
+ }
+ }
+ };
+
+ /* filter edges to only include those that we have two nodes for */
+ var edges = jQuery.grep(json.edges, function(d, i) {
+ d.source = index_for_key(d.signer);
+ d.target = index_for_key(d.signee);
+ return d.source >= 0 && d.target >= 0;
+ });
+
+ jQuery.map(json.nodes, function(d, i) { d.master_sigs = 0; });
+ jQuery.map(edges, function(d, i) {
+ if (json.nodes[d.source].group === "master") {
+ json.nodes[d.target].master_sigs += 1;
+ }
+ });
+ jQuery.map(json.nodes, function(d, i) {
+ if (d.group === "dev" || d.group === "tu") {
+ d.approved = d.master_sigs >= 3;
+ } else {
+ d.approved = null;
+ }
+ });
+
+ var link = svg.selectAll("line")
+ .data(edges)
+ .enter()
+ .append("line");
+
+ var node = svg.selectAll("circle")
+ .data(json.nodes)
+ .enter().append("circle")
+ .attr("r", function(d) {
+ switch (d.group) {
+ case "master":
+ return r * 1.6 - 0.75;
+ case "cacert":
+ return r * 1.4 - 0.75;
+ case "dev":
+ case "tu":
+ default:
+ return r - 0.75;
+ }
+ })
+ .style("fill", function(d) { return fill(d.group); })
+ .style("stroke", function(d) {
+ if (d.approved === null) {
+ return d3.rgb(fill(d.group)).darker();
+ } else if (d.approved) {
+ return "green";
+ } else {
+ return "red";
+ }
+ })
+ .call(force.drag);
+ node.append("title").text(function(d) { return d.name; });
+
+ var distance = function(d, i) {
+ /* place a long line between all master keys and other keys.
+ * however, other connected clusters should be close together. */
+ if (d.source.group === "master" || d.target.group === "master") {
+ return 200;
+ } else {
+ return 50;
+ }
+ };
+
+ var tick = function() {
+ var offset = r * 2,
+ w = jq_div.width(),
+ h = jq_div.height();
+ node.attr("cx", function(d) { return (d.x = Math.max(offset, Math.min(w - offset, d.x))); })
+ .attr("cy", function(d) { return (d.y = Math.max(offset, Math.min(h - offset, d.y))); });
+
+ link.attr("x1", function(d) { return d.source.x; })
+ .attr("y1", function(d) { return d.source.y; })
+ .attr("x2", function(d) { return d.target.x; })
+ .attr("y2", function(d) { return d.target.y; });
+ };
+
+ force.nodes(json.nodes)
+ .links(edges)
+ .linkDistance(distance)
+ .on("tick", tick)
+ .start();
+ });
+
+ var resize_timeout = null;
+ var real_resize = function() {
+ resize_timeout = null;
+ force.size([jq_div.width(), jq_div.height()]);
+ };
+ jQuery(window).resize(function() {
if (resize_timeout) {
clearTimeout(resize_timeout);
}
diff --git a/templates/visualize/index.html b/templates/visualize/index.html
index cc4593d2..1854eb58 100644
--- a/templates/visualize/index.html
+++ b/templates/visualize/index.html
@@ -4,10 +4,7 @@
{% block content %}
<div class="box">
-
- <h2>Visualizations of Packaging Data</h2>
-
- <h3>Package Treemap</h3>
+ <h2>Visualization of Package Data</h2>
<div class="visualize-buttons">
<div>
@@ -25,9 +22,16 @@
</div>
<div id="visualize-archrepo" class="visualize-chart"></div>
</div>
+{% comment %}
+<div class="box">
+ <h2>Visualization of PGP Master and Signing Keys</h2>
+ <div id="visualize-keys" class="visualize-chart"></div>
+</div>
+{% endcomment %}
{% load cdn %}{% jquery %}
<script type="text/javascript" src="/media/d3.min.js"></script>
+<script type="text/javascript" src="/media/d3.geom.min.js"></script>
<script type="text/javascript" src="/media/d3.layout.min.js"></script>
<script type="text/javascript" src="/media/archweb.js"></script>
<script type="text/javascript" src="/media/visualize.js"></script>
@@ -38,6 +42,7 @@ $(document).ready(function() {
"arch": { url: "{% url visualize-byarch %}", color_attr: "arch" },
};
packages_treemap("#visualize-archrepo", orderings, "repo");
+ /*developer_keys("#visualize-keys", "{% url visualize-pgp_keys %}");*/
});
</script>
{% endblock %}
diff --git a/visualize/urls.py b/visualize/urls.py
index 57ee0626..02d83bec 100644
--- a/visualize/urls.py
+++ b/visualize/urls.py
@@ -4,6 +4,7 @@ urlpatterns = patterns('visualize.views',
(r'^$', 'index', {}, 'visualize-index'),
(r'^by_arch/$', 'by_arch', {}, 'visualize-byarch'),
(r'^by_repo/$', 'by_repo', {}, 'visualize-byrepo'),
+ (r'^pgp_keys/$', 'pgp_keys', {}, 'visualize-pgp_keys'),
)
# vim: set ts=4 sw=4 et:
diff --git a/visualize/views.py b/visualize/views.py
index f2b1d63b..be6057b2 100644
--- a/visualize/views.py
+++ b/visualize/views.py
@@ -1,10 +1,14 @@
-from django.db.models import Count, Sum
+from datetime import datetime
+
+from django.contrib.auth.models import User
+from django.db.models import Count, Sum, Q
from django.http import HttpResponse
from django.utils import simplejson
from django.views.decorators.cache import cache_page
from django.views.generic.simple import direct_to_template
from main.models import Package, Arch, Repo
+from devel.models import MasterKey, PGPSignature
def index(request):
return direct_to_template(request, 'visualize/index.html', {})
@@ -66,4 +70,46 @@ def by_repo(request):
to_json = simplejson.dumps(data['by_repo'], ensure_ascii=False)
return HttpResponse(to_json, mimetype='application/json')
+
+@cache_page(1800)
+def pgp_keys(request):
+ node_list = []
+
+ users = User.objects.filter(is_active=True).select_related('userprofile')
+ node_list.extend({
+ 'name': dev.get_full_name(),
+ 'key': dev.userprofile.pgp_key,
+ 'group': 'dev'
+ } for dev in users.filter(groups__name='Developers'))
+ node_list.extend({
+ 'name': tu.get_full_name(),
+ 'key': tu.userprofile.pgp_key,
+ 'group': 'tu'
+ } for tu in users.filter(groups__name='Trusted Users').exclude(
+ groups__name='Developers'))
+
+ master_keys = MasterKey.objects.select_related('owner').filter(
+ revoked__isnull=True)
+ node_list.extend({
+ 'name': 'Master Key (%s)' % key.owner.get_full_name(),
+ 'key': key.pgp_key,
+ 'group': 'master'
+ } for key in master_keys)
+
+ node_list.append({
+ 'name': 'CA Cert Signing Authority',
+ 'key': 'A31D4F81EF4EBD07B456FA04D2BB0D0165D0FD58',
+ 'group': 'cacert',
+ })
+
+ not_expired = Q(expires__gt=datetime.now) | Q(expires__isnull=True)
+ signatures = PGPSignature.objects.filter(not_expired, valid=True)
+ edge_list = [{ 'signee': sig.signee, 'signer': sig.signer }
+ for sig in signatures]
+
+ data = { 'nodes': node_list, 'edges': edge_list }
+
+ to_json = simplejson.dumps(data, ensure_ascii=False)
+ return HttpResponse(to_json, mimetype='application/json')
+
# vim: set ts=4 sw=4 et: