Implementation of blueprint stackalytics-core
* Data updater is implemented * Completed implementation of commit processor * Logging is added into commit processor and runtime storage * Commit processor is fixed * Domain-company map is inverted * Extracted get update count into separate function * Fixed regex that matches diff statistics (lines inserted, lines deleted and files changed) * Implemented caching of unknown users * Replaced dictionaries by sets for pids and branches * Vcs is responsible for module and branches fields of commit record * Added release tags support * Implemented statistics by company * Added config for releases * Implemented front-end for companies details * Implemented front-end for modules details * Fixed metric switch * Implemented timeline rendering * Release selector is fixed * Chdir is needed after cloning a new repo * Company details screen is implemented * Fixed invalid emails processing by Launchpad * Fixed parsing of 0 files changed case * Module details screen implemented * Commit message is cleared and links are inserted * Engineer details screen is implemented * Fixed mapping from company to email for subdomains of 3rd level * Fixed wrong user structure for users not found by LP * Also coverage for commit processor * Fixed company matching algorithm * The company was not matched when user email had more domains than company's one * Add option to enforce sync with default data * Default data is added. Old confs removed * Add *.local into gitignore Scripts cleanup Moved from pylibmc to python-memcached Library pylibmc depends on libmemcached and doesn't work on CentOS (version conflict bw lib requirement and memcached). Change-Id: I0cc61c6d344ba24442ec954635010b518c0efa95
This commit is contained in:
parent
a5f1411218
commit
b7f19335f6
1
.gitignore
vendored
1
.gitignore
vendored
@ -1,5 +1,6 @@
|
|||||||
*~
|
*~
|
||||||
*.pyc
|
*.pyc
|
||||||
|
*.local
|
||||||
AUTHORS
|
AUTHORS
|
||||||
ChangeLog
|
ChangeLog
|
||||||
MANIFEST
|
MANIFEST
|
||||||
|
@ -5,7 +5,7 @@ import os
|
|||||||
import sys
|
import sys
|
||||||
sys.path.insert(0, os.getcwd())
|
sys.path.insert(0, os.getcwd())
|
||||||
|
|
||||||
from dashboard.dashboard import app
|
from dashboard.web import app
|
||||||
|
|
||||||
|
|
||||||
app.run()
|
app.run()
|
@ -1,31 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
if [[ -z $STACKALYTICS_HOME ]]; then
|
|
||||||
echo "Variable STACKALYTICS_HOME must be specified"
|
|
||||||
exit
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "Analytics home is $STACKALYTICS_HOME"
|
|
||||||
|
|
||||||
DASHBOARD_CONF='$STACKALYTICS_HOME/conf/dashboard.conf'
|
|
||||||
|
|
||||||
TOP_DIR=$(cd $(dirname "$0") && pwd)
|
|
||||||
|
|
||||||
DB_FILE=`mktemp -u --tmpdir=$STACKALYTICS_HOME/data stackalytics-XXXXXXXXXXXX.sqlite`
|
|
||||||
TEMP_CONF=`mktemp -u`
|
|
||||||
|
|
||||||
cd $TOP_DIR/../scripts/
|
|
||||||
./pull-repos.sh
|
|
||||||
|
|
||||||
cd $TOP_DIR/../
|
|
||||||
./bin/stackalytics --config-file $STACKALYTICS_HOME/conf/analytics.conf --db-database $DB_FILE --verbose
|
|
||||||
|
|
||||||
DATE=`date -u +'%d-%b-%y %H:%M %Z'`
|
|
||||||
|
|
||||||
echo DATABASE = \'$DB_FILE\' >> $TEMP_CONF
|
|
||||||
echo LAST_UPDATE = \'$DATE\' >> $TEMP_CONF
|
|
||||||
|
|
||||||
#rm $DASHBOARD_CONF
|
|
||||||
#mv $TEMP_CONF $DASHBOARD_CONF
|
|
||||||
|
|
||||||
echo "Data is refreshed, please restart service"
|
|
@ -1,5 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
TOP_DIR=$(cd $(dirname "$0") && pwd)
|
|
||||||
|
|
||||||
./tools/with_venv.sh python scripts/launchpad/grab-unmapped-launchpad-ids.py
|
|
@ -5,7 +5,7 @@ import os
|
|||||||
import sys
|
import sys
|
||||||
sys.path.insert(0, os.getcwd())
|
sys.path.insert(0, os.getcwd())
|
||||||
|
|
||||||
from pycvsanaly2.main import main
|
from stackalytics.processor.main import main
|
||||||
|
|
||||||
|
|
||||||
main()
|
main()
|
18
bin/update
18
bin/update
@ -1,18 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
if [[ -z $STACKALYTICS_HOME ]]; then
|
|
||||||
echo "Variable STACKALYTICS_HOME must be specified"
|
|
||||||
exit
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "Analytics home is $STACKALYTICS_HOME"
|
|
||||||
|
|
||||||
CONF="$STACKALYTICS_HOME/conf/analytics.conf"
|
|
||||||
|
|
||||||
TOP_DIR=$(cd $(dirname "$0") && pwd)
|
|
||||||
cd $TOP_DIR/../scripts/
|
|
||||||
./pull-repos.sh
|
|
||||||
|
|
||||||
echo "Updating data"
|
|
||||||
cd $TOP_DIR/../
|
|
||||||
./bin/stackalytics --config-file $CONF --db-database $STACKALYTICS_HOME/data/stackalyticss.sqlite --verbose
|
|
100
dashboard/memory_storage.py
Normal file
100
dashboard/memory_storage.py
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
from stackalytics.processor import user_utils
|
||||||
|
|
||||||
|
MEMORY_STORAGE_CACHED = 0
|
||||||
|
|
||||||
|
|
||||||
|
class MemoryStorage(object):
|
||||||
|
def __init__(self, records):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class CachedMemoryStorage(MemoryStorage):
|
||||||
|
def __init__(self, records):
|
||||||
|
super(CachedMemoryStorage, self).__init__(records)
|
||||||
|
|
||||||
|
self.records = {}
|
||||||
|
self.company_index = {}
|
||||||
|
self.date_index = {}
|
||||||
|
self.module_index = {}
|
||||||
|
self.launchpad_id_index = {}
|
||||||
|
self.release_index = {}
|
||||||
|
self.dates = []
|
||||||
|
for record in records:
|
||||||
|
self.records[record['record_id']] = record
|
||||||
|
self.index(record)
|
||||||
|
self.dates = sorted(self.date_index)
|
||||||
|
self.company_name_mapping = dict((c.lower(), c)
|
||||||
|
for c in self.company_index.keys())
|
||||||
|
|
||||||
|
def index(self, record):
|
||||||
|
|
||||||
|
self._add_to_index(self.company_index, record, 'company_name')
|
||||||
|
self._add_to_index(self.module_index, record, 'module')
|
||||||
|
self._add_to_index(self.launchpad_id_index, record, 'launchpad_id')
|
||||||
|
self._add_to_index(self.release_index, record, 'release')
|
||||||
|
self._add_to_index(self.date_index, record, 'date')
|
||||||
|
|
||||||
|
record['week'] = user_utils.timestamp_to_week(record['date'])
|
||||||
|
record['loc'] = record['lines_added'] + record['lines_deleted']
|
||||||
|
|
||||||
|
def _add_to_index(self, record_index, record, key):
|
||||||
|
record_key = record[key]
|
||||||
|
if record_key in record_index:
|
||||||
|
record_index[record_key].add(record['record_id'])
|
||||||
|
else:
|
||||||
|
record_index[record_key] = set([record['record_id']])
|
||||||
|
|
||||||
|
def _get_record_ids_from_index(self, items, index):
|
||||||
|
record_ids = set()
|
||||||
|
for item in items:
|
||||||
|
if item not in index:
|
||||||
|
raise Exception('Parameter %s not valid' % item)
|
||||||
|
record_ids |= index[item]
|
||||||
|
return record_ids
|
||||||
|
|
||||||
|
def get_record_ids_by_modules(self, modules):
|
||||||
|
return self._get_record_ids_from_index(modules, self.module_index)
|
||||||
|
|
||||||
|
def get_record_ids_by_companies(self, companies):
|
||||||
|
return self._get_record_ids_from_index(
|
||||||
|
map(self._get_company_name, companies),
|
||||||
|
self.company_index)
|
||||||
|
|
||||||
|
def get_record_ids_by_launchpad_ids(self, launchpad_ids):
|
||||||
|
return self._get_record_ids_from_index(launchpad_ids,
|
||||||
|
self.launchpad_id_index)
|
||||||
|
|
||||||
|
def get_record_ids_by_releases(self, releases):
|
||||||
|
return self._get_record_ids_from_index(releases, self.release_index)
|
||||||
|
|
||||||
|
def get_record_ids(self):
|
||||||
|
return set(self.records.keys())
|
||||||
|
|
||||||
|
def get_records(self, record_ids):
|
||||||
|
for i in record_ids:
|
||||||
|
yield self.records[i]
|
||||||
|
|
||||||
|
def _get_company_name(self, company_name):
|
||||||
|
normalized = company_name.lower()
|
||||||
|
if normalized not in self.company_name_mapping:
|
||||||
|
raise Exception('Unknown company name %s' % company_name)
|
||||||
|
return self.company_name_mapping[normalized]
|
||||||
|
|
||||||
|
def get_companies(self):
|
||||||
|
return self.company_index.keys()
|
||||||
|
|
||||||
|
def get_modules(self):
|
||||||
|
return self.module_index.keys()
|
||||||
|
|
||||||
|
def get_launchpad_ids(self):
|
||||||
|
return self.launchpad_id_index.keys()
|
||||||
|
|
||||||
|
|
||||||
|
class MemoryStorageFactory(object):
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_storage(memory_storage_type, records):
|
||||||
|
if memory_storage_type == MEMORY_STORAGE_CACHED:
|
||||||
|
return CachedMemoryStorage(records)
|
||||||
|
else:
|
||||||
|
raise Exception('Unknown memory storage type')
|
@ -94,7 +94,7 @@ div.drops label {
|
|||||||
color: #909cb5;
|
color: #909cb5;
|
||||||
}
|
}
|
||||||
|
|
||||||
span.drop_period {
|
span.drop_release {
|
||||||
margin-top: 6px;
|
margin-top: 6px;
|
||||||
display: block;
|
display: block;
|
||||||
height: 30px;
|
height: 30px;
|
||||||
|
@ -34,9 +34,9 @@
|
|||||||
<link rel='archives' title='July 2011' href='http://www.mirantis.com/2011/07/' />
|
<link rel='archives' title='July 2011' href='http://www.mirantis.com/2011/07/' />
|
||||||
<link rel='archives' title='June 2011' href='http://www.mirantis.com/2011/06/' />
|
<link rel='archives' title='June 2011' href='http://www.mirantis.com/2011/06/' />
|
||||||
<link rel='archives' title='May 2011' href='http://www.mirantis.com/2011/05/' />
|
<link rel='archives' title='May 2011' href='http://www.mirantis.com/2011/05/' />
|
||||||
<link href='http://fonts.googleapis.com/css?family=PT+Sans:400,700,400italic&subset=latin,cyrillic' rel='stylesheet' type='text/css'>
|
<link href='http://fonts.googleapis.com/css?family=PT+Sans:400,700,400italic&subset=latin,cyrillic' rel='stylesheet' type='text/css' />
|
||||||
<link href='http://fonts.googleapis.com/css?family=PT+Sans+Caption&subset=latin,cyrillic' rel='stylesheet' type='text/css'>
|
<link href='http://fonts.googleapis.com/css?family=PT+Sans+Caption&subset=latin,cyrillic' rel='stylesheet' type='text/css' />
|
||||||
<link href='http://fonts.googleapis.com/css?family=PT+Sans+Narrow:400,700&subset=latin,cyrillic' rel='stylesheet' type='text/css'>
|
<link href='http://fonts.googleapis.com/css?family=PT+Sans+Narrow:400,700&subset=latin,cyrillic' rel='stylesheet' type='text/css' />
|
||||||
|
|
||||||
|
|
||||||
{# <script type="text/javascript" src="{{ url_for('static', filename='js/jquery-1.9.1.min.js') }}"></script>#}
|
{# <script type="text/javascript" src="{{ url_for('static', filename='js/jquery-1.9.1.min.js') }}"></script>#}
|
||||||
@ -49,21 +49,21 @@
|
|||||||
{% block head %}{% endblock %}
|
{% block head %}{% endblock %}
|
||||||
|
|
||||||
<!-- Google Analytics -->
|
<!-- Google Analytics -->
|
||||||
<script type="text/javascript">
|
{# <script type="text/javascript">#}
|
||||||
var _gaq = _gaq || [];
|
{# var _gaq = _gaq || [];#}
|
||||||
_gaq.push(['_setAccount', 'UA-8933515-2']);
|
{# _gaq.push(['_setAccount', 'UA-8933515-2']);#}
|
||||||
_gaq.push(['_setDomainName', 'stackalytics.com']);
|
{# _gaq.push(['_setDomainName', 'stackalytics.com']);#}
|
||||||
_gaq.push(['_setAllowLinker', true]);
|
{# _gaq.push(['_setAllowLinker', true]);#}
|
||||||
_gaq.push(['_trackPageview']);
|
{# _gaq.push(['_trackPageview']);#}
|
||||||
(function () {
|
{# (function () {#}
|
||||||
var ga = document.createElement('script');
|
{# var ga = document.createElement('script');#}
|
||||||
ga.type = 'text/javascript';
|
{# ga.type = 'text/javascript';#}
|
||||||
ga.async = true;
|
{# ga.async = true;#}
|
||||||
ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js';
|
{# ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js';#}
|
||||||
var s = document.getElementsByTagName('script')[0];
|
{# var s = document.getElementsByTagName('script')[0];#}
|
||||||
s.parentNode.insertBefore(ga, s);
|
{# s.parentNode.insertBefore(ga, s);#}
|
||||||
})();
|
{# })();#}
|
||||||
</script>
|
{# </script>#}
|
||||||
|
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
|
@ -4,16 +4,19 @@
|
|||||||
{{ company }}
|
{{ company }}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block scripts %}
|
||||||
|
<script type="text/javascript">
|
||||||
|
chartAndTableRenderer("/data/engineers", "left_list", "left_chart", "/engineers/", {company: "{{ company|encode }}" });
|
||||||
|
chartAndTableRenderer("/data/modules", "right_list", "right_chart", "/modules/", {company: "{{ company|encode }}" });
|
||||||
|
timelineRenderer({company: "{{ company|encode }}" })
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
|
||||||
{% block left_frame %}
|
{% block left_frame %}
|
||||||
|
|
||||||
<h2>Contribution by engineers</h2>
|
<h2>Contribution by engineers</h2>
|
||||||
|
|
||||||
<script type="text/javascript">
|
|
||||||
loadTable("left_list", "/data/companies/{{ company|encode }}");
|
|
||||||
loadChart("left_chart", "/data/companies/{{ company|encode }}", {limit: 10});
|
|
||||||
loadTimeline({company: '{{ company }}'})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div id="left_chart" style="width: 100%; height: 350px;"></div>
|
<div id="left_chart" style="width: 100%; height: 350px;"></div>
|
||||||
|
|
||||||
<table id="left_list" class="display">
|
<table id="left_list" class="display">
|
||||||
@ -35,11 +38,6 @@
|
|||||||
|
|
||||||
<h2>Contribution by modules</h2>
|
<h2>Contribution by modules</h2>
|
||||||
|
|
||||||
<script type="text/javascript">
|
|
||||||
loadTable("right_list", "/data/modules", {company: "{{ company|encode }}" });
|
|
||||||
loadChart("right_chart", "/data/modules", {limit: 10, company: "{{ company|encode }}" });
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div id="right_chart" style="width: 100%; height: 350px;"></div>
|
<div id="right_chart" style="width: 100%; height: 350px;"></div>
|
||||||
|
|
||||||
<table id="right_list" class="display">
|
<table id="right_list" class="display">
|
||||||
@ -59,10 +57,10 @@
|
|||||||
{% if blueprints %}
|
{% if blueprints %}
|
||||||
<div>Blueprints:
|
<div>Blueprints:
|
||||||
<ol>
|
<ol>
|
||||||
{% for one in blueprints %}
|
{% for rec in blueprints %}
|
||||||
<li>
|
<li>
|
||||||
<a href="https://blueprints.launchpad.net/{{ one[1] }}/+spec/{{ one[0] }}">{{ one[0] }}</a>
|
<a href="https://blueprints.launchpad.net/{{ rec['module'] }}/+spec/{{ rec['id'] }}">{{ rec['id'] }}</a>
|
||||||
<small>{{ one[1] }}</small>
|
<small>{{ rec['module'] }}</small>
|
||||||
</li>
|
</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ol>
|
</ol>
|
||||||
@ -70,12 +68,18 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if bugs %}
|
{% if bugs %}
|
||||||
<div>Bug fixes: <b>{{ bugs|length }}</b>
|
<div>Bugs:
|
||||||
|
<ol>
|
||||||
|
{% for rec in bugs %}
|
||||||
|
<li>
|
||||||
|
<a href="https://bugs.launchpad.net/bugs/{{ rec['id'] }}">{{ rec['id'] }}</a>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ol>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<div>Total commits: <b>{{ commits|length }}</b>, among them <b>{{ code_commits }}</b> commits in code,
|
<div>Total commits: <b>{{ commits|length }}</b></div>
|
||||||
among them <b>{{ test_only_commits }}</b> test only commits</div>
|
|
||||||
<div>Total LOC: <b>{{ loc }}</b></div>
|
<div>Total LOC: <b>{{ loc }}</b></div>
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -1,25 +1,26 @@
|
|||||||
{% extends "layout.html" %}
|
{% extends "layout.html" %}
|
||||||
|
|
||||||
{% block title %}
|
{% block title %}
|
||||||
{{ details.name }}
|
{{ user.user_name }}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block scripts %}
|
||||||
|
<script type="text/javascript">
|
||||||
|
chartAndTableRenderer("/data/modules", "right_list", "right_chart", "/modules/", {launchpad_id: "{{ launchpad_id }}" });
|
||||||
|
timelineRenderer({launchpad_id: "{{ launchpad_id }}" })
|
||||||
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block left_frame %}
|
{% block left_frame %}
|
||||||
|
|
||||||
<script type="text/javascript">
|
|
||||||
loadTimeline({engineer: '{{engineer}}'})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div style='float: left;'>
|
<div style='float: left;'>
|
||||||
<img src="{{ details.email|gravatar(size=64) }}">
|
<img src="{{ user.emails[0]|gravatar(size=64) }}">
|
||||||
</div>
|
</div>
|
||||||
<div style='margin-left: 90px;'>
|
<div style='margin-left: 90px;'>
|
||||||
<h2 style='margin-bottom: 0.5em;'>{{ details.name }}</h2>
|
<h2 style='margin-bottom: 0.5em;'>{{ user.user_name }}</h2>
|
||||||
{% if details.company %}
|
{% if user.companies %}
|
||||||
<div>Company: {{ link('/companies/' + details.company, details.company) }}</div>
|
<div>Company: {{ user.companies[-1].company_name|link('/companies/' + user.companies[-1].company_name)|safe }}</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<div>Launchpad: <a href="https://launchpad.net/~{{ details.launchpad_id }}">{{ details.launchpad_id }}</a></div>
|
<div>Launchpad: <a href="https://launchpad.net/~{{ launchpad_id }}">{{ launchpad_id }}</a></div>
|
||||||
{# <div>Email: {{ details.email }}</div>#}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h3>Commits history</h3>
|
<h3>Commits history</h3>
|
||||||
@ -28,15 +29,13 @@
|
|||||||
<div>There are no commits for selected period or project type.</div>
|
<div>There are no commits for selected period or project type.</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% for message in commits %}
|
{% for rec in commits %}
|
||||||
<div>
|
<div>
|
||||||
<h4>{{ message.date|datetimeformat }} to <a href="https://launchpad.net/{{ message.module }}">{{ message.module }}</a>
|
<h4>{{ rec.date|datetimeformat }} to <a href="https://launchpad.net/{{ rec.module }}">{{ rec.module }}</a>
|
||||||
{% if message.is_code %} <span style="color: royalblue">code</span> {% endif %}
|
|
||||||
{% if message.is_test %} <span style="color: magenta">test</span> {% endif %}
|
|
||||||
</h4>
|
</h4>
|
||||||
<div style='white-space: pre-wrap; padding-left: 2em;'>{{ message.message|safe }}</div>
|
<div style='white-space: pre-wrap; padding-left: 2em;'>{{ rec|commit_message|safe }}</div>
|
||||||
<div style="padding-left: 2em;"><span style="color: green">+ {{ message.added_loc }}</span>
|
<div style="padding-left: 2em;"><span style="color: green">+ {{ rec.lines_added }}</span>
|
||||||
<span style="color: red">- {{ message.removed_loc }}</span></div>
|
<span style="color: red">- {{ rec.lines_deleted }}</span></div>
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
||||||
@ -47,11 +46,6 @@
|
|||||||
{% if commits %}
|
{% if commits %}
|
||||||
<h2>Contribution by modules</h2>
|
<h2>Contribution by modules</h2>
|
||||||
|
|
||||||
<script type="text/javascript">
|
|
||||||
loadTable("right_list", "/data/modules", {engineer: "{{ engineer }}" });
|
|
||||||
loadChart("right_chart", "/data/modules", {limit: 10, engineer: "{{ engineer }}" });
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div id="right_chart" style="width: 100%; height: 350px;"></div>
|
<div id="right_chart" style="width: 100%; height: 350px;"></div>
|
||||||
|
|
||||||
<table id="right_list" class="display">
|
<table id="right_list" class="display">
|
||||||
@ -71,10 +65,10 @@
|
|||||||
{% if blueprints %}
|
{% if blueprints %}
|
||||||
<div>Blueprints:
|
<div>Blueprints:
|
||||||
<ol>
|
<ol>
|
||||||
{% for one in blueprints %}
|
{% for rec in blueprints %}
|
||||||
<li>
|
<li>
|
||||||
<a href="https://blueprints.launchpad.net/{{ one[1] }}/+spec/{{ one[0] }}">{{ one[0] }}</a>
|
<a href="https://blueprints.launchpad.net/{{ rec['module'] }}/+spec/{{ rec['id'] }}">{{ rec['id'] }}</a>
|
||||||
<small>{{ one[1] }}</small>
|
<small>{{ rec['module'] }}</small>
|
||||||
</li>
|
</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ol>
|
</ol>
|
||||||
@ -82,23 +76,18 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if bugs %}
|
{% if bugs %}
|
||||||
<div>Bug fixes:
|
<div>Bugs:
|
||||||
<ol>
|
<ol>
|
||||||
{% for one in bugs %}
|
{% for rec in bugs %}
|
||||||
<li>
|
<li>
|
||||||
<a href="https://bugs.launchpad.net/bugs/{{ one[0] }}">{{ one[0] }}</a>
|
<a href="https://bugs.launchpad.net/bugs/{{ rec['id'] }}">{{ rec['id'] }}</a>
|
||||||
<small>
|
|
||||||
{% if one[1] %} <span style="color: royalblue">C</span> {% endif %}
|
|
||||||
{% if one[2] %} <span style="color: magenta">T</span> {% endif %}
|
|
||||||
</small>
|
|
||||||
</li>
|
</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ol>
|
</ol>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<div>Total commits: <b>{{ commits|length }}</b>, among them <b>{{ code_commits }}</b> commits in code,
|
<div>Total commits: <b>{{ commits|length }}</b></div>
|
||||||
among them <b>{{ test_only_commits }}</b> test only commits</div>
|
|
||||||
<div>Total LOC: <b>{{ loc }}</b></div>
|
<div>Total LOC: <b>{{ loc }}</b></div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
@ -18,159 +18,173 @@
|
|||||||
<script type="text/javascript" src="{{ url_for('static', filename='js/jqplot.highlighter.min.js') }}"></script>
|
<script type="text/javascript" src="{{ url_for('static', filename='js/jqplot.highlighter.min.js') }}"></script>
|
||||||
|
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
// load table data
|
|
||||||
function loadTable(table_id, source, options) {
|
function showTimeline(data) {
|
||||||
|
var plot = $.jqplot('timeline', data, {
|
||||||
|
gridPadding: {
|
||||||
|
right: 35
|
||||||
|
},
|
||||||
|
cursor: {
|
||||||
|
show: false
|
||||||
|
},
|
||||||
|
highlighter: {
|
||||||
|
show: true,
|
||||||
|
sizeAdjust: 6
|
||||||
|
},
|
||||||
|
axes: {
|
||||||
|
xaxis: {
|
||||||
|
tickRenderer: $.jqplot.CanvasAxisTickRenderer,
|
||||||
|
tickOptions: {
|
||||||
|
fontSize: '8pt',
|
||||||
|
angle: -90,
|
||||||
|
formatString: '%b \'%y'
|
||||||
|
},
|
||||||
|
renderer: $.jqplot.DateAxisRenderer,
|
||||||
|
tickInterval: '1 month'
|
||||||
|
},
|
||||||
|
yaxis: {
|
||||||
|
min: 0,
|
||||||
|
label: ''
|
||||||
|
},
|
||||||
|
y2axis: {
|
||||||
|
min: 0,
|
||||||
|
label: ''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
series: [
|
||||||
|
{
|
||||||
|
shadow: false,
|
||||||
|
fill: true,
|
||||||
|
fillColor: '#4bb2c5',
|
||||||
|
fillAlpha: 0.3
|
||||||
|
},
|
||||||
|
{
|
||||||
|
shadow: false,
|
||||||
|
fill: true,
|
||||||
|
color: '#4bb2c5',
|
||||||
|
fillColor: '#4bb2c5'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
shadow: false,
|
||||||
|
lineWidth: 1.5,
|
||||||
|
showMarker: true,
|
||||||
|
markerOptions: { size: 5 },
|
||||||
|
yaxis: 'y2axis'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function timelineRenderer(options) {
|
||||||
$(document).ready(function () {
|
$(document).ready(function () {
|
||||||
$("#"+table_id).dataTable({
|
$.ajax({
|
||||||
"aLengthMenu": [[25, 50, -1], [25, 50, "All"]],
|
url: make_uri("/data/timeline", options),
|
||||||
"aaSorting": [[ 2, "desc" ]],
|
dataType: "json",
|
||||||
"bProcessing": true,
|
success: function (data) {
|
||||||
"sAjaxSource": make_uri(source, options),
|
showTimeline(data);
|
||||||
"sPaginationType": "full_numbers",
|
}
|
||||||
"iDisplayLength": 25,
|
|
||||||
"aoColumns": [
|
|
||||||
{ "mData": "index" },
|
|
||||||
{ "mData": "link" },
|
|
||||||
{ "mData": "rank" }
|
|
||||||
]
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// load chart
|
function chartAndTableRenderer(url, table_id, chart_id, link_prefix, options) {
|
||||||
function loadChart(chart_id, source, options) {
|
|
||||||
$(document).ready(function () {
|
$(document).ready(function () {
|
||||||
// Our ajax data renderer which here retrieves a text file.
|
|
||||||
// it could contact any source and pull data, however.
|
$.ajax({
|
||||||
// The options argument isn't used in this renderer.
|
url: make_uri(url, options),
|
||||||
var ajaxDataRenderer = function (url, plot, options) {
|
dataType: "json",
|
||||||
var ret = null;
|
success: function (data) {
|
||||||
$.ajax({
|
|
||||||
// have to use synchronous here, else the function
|
var tableData = [];
|
||||||
// will return before the data is fetched
|
var chartData = [];
|
||||||
async: false,
|
|
||||||
url: url,
|
var limit = 10;
|
||||||
dataType: "json",
|
var aggregate = 0;
|
||||||
success: function (data) {
|
var index = 1;
|
||||||
var array = [];
|
var i;
|
||||||
for(i = 0; i < data['aaData'].length; i++) {
|
|
||||||
array.push([data['aaData'][i].name, data['aaData'][i].rank]);
|
for (i = 0; i < data.length; i++) {
|
||||||
|
if (i < limit - 1) {
|
||||||
|
chartData.push([data[i].name, data[i].metric]);
|
||||||
|
} else {
|
||||||
|
aggregate += data[i].metric;
|
||||||
}
|
}
|
||||||
ret = [array]
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return ret;
|
|
||||||
};
|
|
||||||
|
|
||||||
// passing in the url string as the jqPlot data argument is a handy
|
var index_label = index;
|
||||||
// shortcut for our renderer. You could also have used the
|
if (data[i].name == "*independent") {
|
||||||
// "dataRendererOptions" option to pass in the url.
|
index_label = "";
|
||||||
var plot = $.jqplot(chart_id, make_uri(source, options), {
|
} else {
|
||||||
dataRenderer: ajaxDataRenderer,
|
index++;
|
||||||
seriesDefaults: {
|
}
|
||||||
// Make this a pie chart.
|
var link = make_link(link_prefix, data[i].id, data[i].name);
|
||||||
renderer: jQuery.jqplot.PieRenderer,
|
tableData.push({"index": index_label, "link": link, "metric": data[i].metric});
|
||||||
rendererOptions: {
|
|
||||||
// Put data labels on the pie slices.
|
|
||||||
// By default, labels show the percentage of the slice.
|
|
||||||
showDataLabels: true
|
|
||||||
}
|
}
|
||||||
},
|
|
||||||
legend: { show: true, location: 'e' }
|
if (i == limit) {
|
||||||
|
chartData.push([data[i-1].name, data[i-1].metric]);
|
||||||
|
} else if (i > limit) {
|
||||||
|
chartData.push(["others", aggregate]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$("#" + table_id).dataTable({
|
||||||
|
"aLengthMenu": [
|
||||||
|
[25, 50, -1],
|
||||||
|
[25, 50, "All"]
|
||||||
|
],
|
||||||
|
"aaSorting": [
|
||||||
|
[ 2, "desc" ]
|
||||||
|
],
|
||||||
|
"sPaginationType": "full_numbers",
|
||||||
|
"iDisplayLength": 25,
|
||||||
|
"aaData": tableData,
|
||||||
|
"aoColumns": [
|
||||||
|
{ "mData": "index" },
|
||||||
|
{ "mData": "link" },
|
||||||
|
{ "mData": "metric" }
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
var plot = $.jqplot(chart_id, [chartData], {
|
||||||
|
seriesDefaults: {
|
||||||
|
renderer: jQuery.jqplot.PieRenderer,
|
||||||
|
rendererOptions: {
|
||||||
|
showDataLabels: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
legend: { show: true, location: 'e' }
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// load timeline
|
function getUrlVars() {
|
||||||
function loadTimeline(options) {
|
var vars = {};
|
||||||
$(document).ready(function () {
|
var parts = window.location.href.replace(/[?&]+([^=&]+)=([^&]*)/gi, function (m, key, value) {
|
||||||
var ajaxDataRenderer = function (url, plot, options) {
|
vars[key] = value;
|
||||||
var ret = null;
|
|
||||||
$.ajax({
|
|
||||||
// have to use synchronous here, else the function
|
|
||||||
// will return before the data is fetched
|
|
||||||
async: false,
|
|
||||||
url: url,
|
|
||||||
dataType: "json",
|
|
||||||
success: function (data) {
|
|
||||||
ret = data;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return ret;
|
|
||||||
};
|
|
||||||
|
|
||||||
var jsonurl = make_uri("/data/timeline", options);
|
|
||||||
|
|
||||||
var plot = $.jqplot('timeline', jsonurl, {
|
|
||||||
dataRenderer: ajaxDataRenderer,
|
|
||||||
dataRendererOptions: {
|
|
||||||
unusedOptionalUrl: jsonurl
|
|
||||||
},
|
|
||||||
gridPadding: {right: 35},
|
|
||||||
cursor: {
|
|
||||||
show: false
|
|
||||||
},
|
|
||||||
highlighter: {
|
|
||||||
show: true,
|
|
||||||
sizeAdjust: 6
|
|
||||||
},
|
|
||||||
axes: {
|
|
||||||
xaxis: {
|
|
||||||
tickRenderer: $.jqplot.CanvasAxisTickRenderer,
|
|
||||||
tickOptions: {
|
|
||||||
fontSize: '8pt',
|
|
||||||
angle: -90,
|
|
||||||
formatString: '%b \'%y'
|
|
||||||
},
|
|
||||||
renderer: $.jqplot.DateAxisRenderer,
|
|
||||||
tickInterval: '1 month'
|
|
||||||
},
|
|
||||||
yaxis: {
|
|
||||||
min: 0,
|
|
||||||
label: ''
|
|
||||||
},
|
|
||||||
y2axis: {
|
|
||||||
min: 0,
|
|
||||||
label: ''
|
|
||||||
}
|
|
||||||
},
|
|
||||||
series: [
|
|
||||||
{
|
|
||||||
shadow: false,
|
|
||||||
fill: true,
|
|
||||||
fillColor: '#4bb2c5',
|
|
||||||
fillAlpha: 0.3
|
|
||||||
},
|
|
||||||
{
|
|
||||||
shadow: false,
|
|
||||||
fill: true,
|
|
||||||
color: '#4bb2c5',
|
|
||||||
fillColor: '#4bb2c5'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
shadow: false,
|
|
||||||
lineWidth: 1.5,
|
|
||||||
showMarker: true,
|
|
||||||
markerOptions: { size: 5 },
|
|
||||||
yaxis: 'y2axis'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
return vars;
|
||||||
}
|
}
|
||||||
|
|
||||||
$(document).ready(function () {
|
$(document).ready(function () {
|
||||||
$('#metric').val('{{ metric }}');
|
$('#metric').val('{{ metric }}');
|
||||||
$('#period').val('{{ period }}');
|
$('#release').val('{{ release }}');
|
||||||
$('#project_type').val('{{ project_type }}');
|
$('#project_type').val('{{ project_type }}');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function make_link(uri_prefix, id, title, options) {
|
||||||
|
var link = make_uri(uri_prefix + encodeURIComponent(id).toLowerCase(), options);
|
||||||
|
return "<a href=\"" + link + "\">" + title + "</a>"
|
||||||
|
}
|
||||||
|
|
||||||
function make_uri(uri, options) {
|
function make_uri(uri, options) {
|
||||||
var ops = {};
|
var ops = {};
|
||||||
if (options != null) {
|
if (options != null) {
|
||||||
$.extend(ops, options);
|
$.extend(ops, options);
|
||||||
}
|
}
|
||||||
$.extend(ops, make_std_options());
|
$.extend(ops, getUrlVars());
|
||||||
var str = $.map(ops,function (val, index) {
|
var str = $.map(ops,function (val, index) {
|
||||||
return index + "=" + val;
|
return index + "=" + val;
|
||||||
}).join("&");
|
}).join("&");
|
||||||
@ -180,9 +194,9 @@
|
|||||||
|
|
||||||
function make_std_options() {
|
function make_std_options() {
|
||||||
var options = {};
|
var options = {};
|
||||||
if (getPeriod() != 'havana') {
|
{# if (getRelease() != 'havana') {#}
|
||||||
options['period'] = getPeriod();
|
options['release'] = getRelease();
|
||||||
}
|
{# }#}
|
||||||
if (getMetric() != 'loc') {
|
if (getMetric() != 'loc') {
|
||||||
options['metric'] = getMetric();
|
options['metric'] = getMetric();
|
||||||
}
|
}
|
||||||
@ -203,7 +217,7 @@
|
|||||||
reload();
|
reload();
|
||||||
});
|
});
|
||||||
|
|
||||||
$(document).on('change', '#period', function (evt) {
|
$(document).on('change', '#release', function (evt) {
|
||||||
reload();
|
reload();
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -211,8 +225,8 @@
|
|||||||
reload();
|
reload();
|
||||||
});
|
});
|
||||||
|
|
||||||
function getPeriod() {
|
function getRelease() {
|
||||||
return $('#period').val()
|
return $('#release').val()
|
||||||
}
|
}
|
||||||
|
|
||||||
function getMetric() {
|
function getMetric() {
|
||||||
@ -225,6 +239,8 @@
|
|||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
{% block scripts %}{% endblock %}
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block body %}
|
{% block body %}
|
||||||
@ -244,10 +260,9 @@
|
|||||||
<option value="loc">Lines of code</option>
|
<option value="loc">Lines of code</option>
|
||||||
</select>
|
</select>
|
||||||
</span>
|
</span>
|
||||||
<span class="drop_period" style="float: right;">
|
<span class="drop_release" style="float: right;">
|
||||||
<label for="period">Period </label><select id="period" name="period">
|
<label for="release">Release </label><select id="release" name="release">
|
||||||
<option value="all">All times</option>
|
<option value="all">All times</option>
|
||||||
<option value="six_months">Last 6 months</option>
|
|
||||||
<option value="havana">Havana</option>
|
<option value="havana">Havana</option>
|
||||||
<option value="grizzly">Grizzly</option>
|
<option value="grizzly">Grizzly</option>
|
||||||
<option value="folsom">Folsom</option>
|
<option value="folsom">Folsom</option>
|
||||||
@ -278,7 +293,3 @@
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% macro link(base, title) -%}
|
|
||||||
<a href="{{ base }}?metric={{ metric }}&period={{ period }}&project_type={{ project_type }}">{{ title }}</a>
|
|
||||||
{%- endmacro %}
|
|
||||||
|
@ -4,16 +4,17 @@
|
|||||||
{{ module }}
|
{{ module }}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block scripts %}
|
||||||
|
<script type="text/javascript">
|
||||||
|
chartAndTableRenderer("/data/companies", "left_list", "left_chart", "/companies/", {company: "{{ company|encode }}" });
|
||||||
|
timelineRenderer({company: "{{ company|encode }}" })
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
{% block left_frame %}
|
{% block left_frame %}
|
||||||
|
|
||||||
<h2>Contribution by companies</h2>
|
<h2>Contribution by companies</h2>
|
||||||
|
|
||||||
<script type="text/javascript">
|
|
||||||
loadTable("left_list", "/data/companies", { module: "{{ module }}" });
|
|
||||||
loadChart("left_chart", "/data/companies", { module: "{{ module }}", limit: 10 });
|
|
||||||
loadTimeline({module: '{{ module }}'})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div id="left_chart" style="width: 100%; height: 350px;"></div>
|
<div id="left_chart" style="width: 100%; height: 350px;"></div>
|
||||||
|
|
||||||
<table id="left_list" class="display">
|
<table id="left_list" class="display">
|
||||||
@ -38,30 +39,25 @@
|
|||||||
{% for rec in commits %}
|
{% for rec in commits %}
|
||||||
<div style="padding-bottom: 1em;">
|
<div style="padding-bottom: 1em;">
|
||||||
<div style='float: left; '>
|
<div style='float: left; '>
|
||||||
<img src="{{ rec.email|gravatar(size=32) }}">
|
<img src="{{ rec.author_email|gravatar(size=32) }}">
|
||||||
</div>
|
</div>
|
||||||
<div style="margin-left: 4em;">
|
<div style="margin-left: 4em;">
|
||||||
<div>
|
<div>
|
||||||
{{ link('/engineers/' + rec.launchpad_id, rec.name) }}
|
{% if rec.launchpad_id %}
|
||||||
{# <a href="/engineers/{{ rec.launchpad_id }}?metric={{ metric }}&period={{ period }}&project_type={{ project_type }}">{{ rec.name }}</a>#}
|
{{ rec.author|link('/engineers/' + rec.launchpad_id)|safe }}
|
||||||
|
{% else %}
|
||||||
|
{{ rec.author }}
|
||||||
|
{% endif %}
|
||||||
{% if rec.company %}
|
{% if rec.company %}
|
||||||
(
|
(
|
||||||
{{ link('/companies/' + rec.company, rec.company) }}
|
{{ rec.company|link('/companies/' + rec.company)|safe }}
|
||||||
{# <a href="/companies/{{ rec.company }}?metric={{ metric }}&period={{ period }}&project_type={{ project_type }}">{{ rec.company }}</a>#}
|
|
||||||
)
|
)
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<em>{{ rec.date|datetimeformat }}</em>
|
<em>{{ rec.date|datetimeformat }}</em>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if rec.ref %}
|
<div><b>{{ rec.subject }}</b></div>
|
||||||
<div>{{ rec.ref|safe }}</div>
|
<div style="white-space: pre-wrap;">{{ rec|commit_message|safe }}</div>
|
||||||
{% endif %}
|
|
||||||
<div>{{ rec.text }}</div>
|
|
||||||
{% if rec.change_id %}
|
|
||||||
<div>Change-Id: <a
|
|
||||||
href="https://review.openstack.org/#q,{{ rec.change_id }},n,z">{{ rec.change_id }}</a>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
@ -4,16 +4,18 @@
|
|||||||
Overview
|
Overview
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block scripts %}
|
||||||
|
<script type="text/javascript">
|
||||||
|
chartAndTableRenderer("/data/companies", "left_list", "left_chart", "/companies/");
|
||||||
|
chartAndTableRenderer("/data/modules", "right_list", "right_chart", "/modules/");
|
||||||
|
timelineRenderer()
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
{% block left_frame %}
|
{% block left_frame %}
|
||||||
|
|
||||||
<h2>Contribution by companies</h2>
|
<h2>Contribution by companies</h2>
|
||||||
|
|
||||||
<script type="text/javascript">
|
|
||||||
loadTable("left_list", "/data/companies");
|
|
||||||
loadChart("left_chart", "/data/companies", {limit: 10});
|
|
||||||
loadTimeline('')
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div id="left_chart" style="width: 100%; height: 350px;"></div>
|
<div id="left_chart" style="width: 100%; height: 350px;"></div>
|
||||||
|
|
||||||
<table id="left_list" class="display">
|
<table id="left_list" class="display">
|
||||||
@ -35,11 +37,6 @@
|
|||||||
|
|
||||||
<h2>Contribution by modules</h2>
|
<h2>Contribution by modules</h2>
|
||||||
|
|
||||||
<script type="text/javascript">
|
|
||||||
loadTable("right_list", "/data/modules");
|
|
||||||
loadChart("right_chart", "/data/modules", {limit: 10});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div id="right_chart" style="width: 100%; height: 350px;"></div>
|
<div id="right_chart" style="width: 100%; height: 350px;"></div>
|
||||||
|
|
||||||
<table id="right_list" class="display">
|
<table id="right_list" class="display">
|
452
dashboard/web.py
Normal file
452
dashboard/web.py
Normal file
@ -0,0 +1,452 @@
|
|||||||
|
import cgi
|
||||||
|
import datetime
|
||||||
|
import functools
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import urllib
|
||||||
|
|
||||||
|
import flask
|
||||||
|
from flask.ext import gravatar as gravatar_ext
|
||||||
|
import time
|
||||||
|
|
||||||
|
from dashboard import memory_storage
|
||||||
|
from stackalytics.processor.persistent_storage import PersistentStorageFactory
|
||||||
|
from stackalytics.processor.runtime_storage import RuntimeStorageFactory
|
||||||
|
from stackalytics.processor import user_utils
|
||||||
|
|
||||||
|
DEBUG = True
|
||||||
|
RUNTIME_STORAGE_URI = 'memcached://127.0.0.1:11211'
|
||||||
|
PERSISTENT_STORAGE_URI = 'mongodb://localhost'
|
||||||
|
|
||||||
|
# create our little application :)
|
||||||
|
app = flask.Flask(__name__)
|
||||||
|
app.config.from_object(__name__)
|
||||||
|
app.config.from_envvar('DASHBOARD_CONF', silent=True)
|
||||||
|
|
||||||
|
|
||||||
|
def get_vault():
|
||||||
|
vault = getattr(app, 'stackalytics_vault', None)
|
||||||
|
if not vault:
|
||||||
|
vault = {}
|
||||||
|
vault['runtime_storage'] = RuntimeStorageFactory.get_storage(
|
||||||
|
RUNTIME_STORAGE_URI)
|
||||||
|
vault['persistent_storage'] = PersistentStorageFactory.get_storage(
|
||||||
|
PERSISTENT_STORAGE_URI)
|
||||||
|
vault['memory_storage'] = (
|
||||||
|
memory_storage.MemoryStorageFactory.get_storage(
|
||||||
|
memory_storage.MEMORY_STORAGE_CACHED,
|
||||||
|
vault['runtime_storage'].get_update(os.getpid())))
|
||||||
|
|
||||||
|
releases = vault['persistent_storage'].get_releases()
|
||||||
|
vault['releases'] = dict((r['release_name'].lower(), r)
|
||||||
|
for r in releases)
|
||||||
|
app.stackalytics_vault = vault
|
||||||
|
return vault
|
||||||
|
|
||||||
|
|
||||||
|
def get_memory_storage():
|
||||||
|
return get_vault()['memory_storage']
|
||||||
|
|
||||||
|
|
||||||
|
def record_filter(parameter_getter=lambda x: flask.request.args.get(x)):
|
||||||
|
def decorator(f):
|
||||||
|
@functools.wraps(f)
|
||||||
|
def decorated_function(*args, **kwargs):
|
||||||
|
|
||||||
|
vault = get_vault()
|
||||||
|
memory_storage = vault['memory_storage']
|
||||||
|
record_ids = memory_storage.get_record_ids()
|
||||||
|
|
||||||
|
param = parameter_getter('modules')
|
||||||
|
if param:
|
||||||
|
record_ids &= memory_storage.get_record_ids_by_modules(
|
||||||
|
param.split(','))
|
||||||
|
|
||||||
|
if 'launchpad_id' in kwargs:
|
||||||
|
param = kwargs['launchpad_id']
|
||||||
|
else:
|
||||||
|
param = (parameter_getter('launchpad_id') or
|
||||||
|
parameter_getter('launchpad_ids'))
|
||||||
|
if param:
|
||||||
|
record_ids &= memory_storage.get_record_ids_by_launchpad_ids(
|
||||||
|
param.split(','))
|
||||||
|
|
||||||
|
if 'company' in kwargs:
|
||||||
|
param = kwargs['company']
|
||||||
|
else:
|
||||||
|
param = (parameter_getter('company') or
|
||||||
|
parameter_getter('companies'))
|
||||||
|
if param:
|
||||||
|
record_ids &= memory_storage.get_record_ids_by_companies(
|
||||||
|
param.split(','))
|
||||||
|
|
||||||
|
param = parameter_getter('release') or parameter_getter('releases')
|
||||||
|
if param:
|
||||||
|
if param != 'all':
|
||||||
|
record_ids &= memory_storage.get_record_ids_by_releases(
|
||||||
|
c.lower() for c in param.split(','))
|
||||||
|
|
||||||
|
kwargs['records'] = memory_storage.get_records(record_ids)
|
||||||
|
return f(*args, **kwargs)
|
||||||
|
|
||||||
|
return decorated_function
|
||||||
|
|
||||||
|
return decorator
|
||||||
|
|
||||||
|
|
||||||
|
def aggregate_filter():
|
||||||
|
def decorator(f):
|
||||||
|
@functools.wraps(f)
|
||||||
|
def decorated_function(*args, **kwargs):
|
||||||
|
|
||||||
|
metric_filter = lambda r: r['loc']
|
||||||
|
metric_param = flask.request.args.get('metric')
|
||||||
|
if metric_param:
|
||||||
|
metric = metric_param.lower()
|
||||||
|
if metric == 'commits':
|
||||||
|
metric_filter = lambda r: 1
|
||||||
|
elif metric != 'loc':
|
||||||
|
raise Exception('Invalid metric %s' % metric)
|
||||||
|
|
||||||
|
kwargs['metric_filter'] = metric_filter
|
||||||
|
return f(*args, **kwargs)
|
||||||
|
|
||||||
|
return decorated_function
|
||||||
|
|
||||||
|
return decorator
|
||||||
|
|
||||||
|
|
||||||
|
def exception_handler():
|
||||||
|
def decorator(f):
|
||||||
|
@functools.wraps(f)
|
||||||
|
def decorated_function(*args, **kwargs):
|
||||||
|
try:
|
||||||
|
return f(*args, **kwargs)
|
||||||
|
except Exception as e:
|
||||||
|
print e
|
||||||
|
flask.abort(404)
|
||||||
|
|
||||||
|
return decorated_function
|
||||||
|
|
||||||
|
return decorator
|
||||||
|
|
||||||
|
|
||||||
|
DEFAULT_METRIC = 'loc'
|
||||||
|
DEFAULT_RELEASE = 'havana'
|
||||||
|
DEFAULT_PROJECT_TYPE = 'incubation'
|
||||||
|
|
||||||
|
INDEPENDENT = '*independent'
|
||||||
|
|
||||||
|
METRIC_LABELS = {
|
||||||
|
'loc': 'Lines of code',
|
||||||
|
'commits': 'Commits',
|
||||||
|
}
|
||||||
|
|
||||||
|
PROJECT_TYPES = {
|
||||||
|
'core': ['core'],
|
||||||
|
'incubation': ['core', 'incubation'],
|
||||||
|
'all': ['core', 'incubation', 'dev'],
|
||||||
|
}
|
||||||
|
|
||||||
|
ISSUE_TYPES = ['bug', 'blueprint']
|
||||||
|
|
||||||
|
DEFAULT_RECORDS_LIMIT = 10
|
||||||
|
|
||||||
|
|
||||||
|
def templated(template=None):
|
||||||
|
def decorator(f):
|
||||||
|
@functools.wraps(f)
|
||||||
|
def decorated_function(*args, **kwargs):
|
||||||
|
|
||||||
|
vault = get_vault()
|
||||||
|
template_name = template
|
||||||
|
if template_name is None:
|
||||||
|
template_name = (flask.request.endpoint.replace('.', '/') +
|
||||||
|
'.html')
|
||||||
|
ctx = f(*args, **kwargs)
|
||||||
|
if ctx is None:
|
||||||
|
ctx = {}
|
||||||
|
|
||||||
|
# put parameters into template
|
||||||
|
metric = flask.request.args.get('metric')
|
||||||
|
if metric not in METRIC_LABELS:
|
||||||
|
metric = None
|
||||||
|
ctx['metric'] = metric or DEFAULT_METRIC
|
||||||
|
ctx['metric_label'] = METRIC_LABELS[ctx['metric']]
|
||||||
|
|
||||||
|
release = flask.request.args.get('release')
|
||||||
|
releases = vault['releases']
|
||||||
|
if release:
|
||||||
|
release = release.lower()
|
||||||
|
if release not in releases:
|
||||||
|
release = None
|
||||||
|
else:
|
||||||
|
release = releases[release]['release_name']
|
||||||
|
ctx['release'] = (release or DEFAULT_RELEASE).lower()
|
||||||
|
|
||||||
|
return flask.render_template(template_name, **ctx)
|
||||||
|
|
||||||
|
return decorated_function
|
||||||
|
|
||||||
|
return decorator
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/')
|
||||||
|
@templated()
|
||||||
|
def overview():
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def contribution_details(records, limit=DEFAULT_RECORDS_LIMIT):
|
||||||
|
blueprints_map = {}
|
||||||
|
bugs_map = {}
|
||||||
|
commits = []
|
||||||
|
loc = 0
|
||||||
|
|
||||||
|
for record in records:
|
||||||
|
loc += record['loc']
|
||||||
|
commits.append(record)
|
||||||
|
blueprint = record['blueprint_id']
|
||||||
|
if blueprint:
|
||||||
|
if blueprint in blueprints_map:
|
||||||
|
blueprints_map[blueprint].append(record)
|
||||||
|
else:
|
||||||
|
blueprints_map[blueprint] = [record]
|
||||||
|
|
||||||
|
bug = record['bug_id']
|
||||||
|
if bug:
|
||||||
|
if bug in bugs_map:
|
||||||
|
bugs_map[bug].append(record)
|
||||||
|
else:
|
||||||
|
bugs_map[bug] = [record]
|
||||||
|
|
||||||
|
blueprints = sorted([{'id': key,
|
||||||
|
'module': value[0]['module'],
|
||||||
|
'records': value}
|
||||||
|
for key, value in blueprints_map.iteritems()],
|
||||||
|
key=lambda x: x['id'])
|
||||||
|
bugs = sorted([{'id': key, 'records': value}
|
||||||
|
for key, value in bugs_map.iteritems()],
|
||||||
|
key=lambda x: x['id'])
|
||||||
|
commits.sort(key=lambda x: x['date'], reverse=True)
|
||||||
|
|
||||||
|
result = {
|
||||||
|
'blueprints': blueprints,
|
||||||
|
'bugs': bugs,
|
||||||
|
'commits': commits[0:limit],
|
||||||
|
'loc': loc,
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/companies/<company>')
|
||||||
|
@exception_handler()
|
||||||
|
@templated()
|
||||||
|
@record_filter()
|
||||||
|
def company_details(company, records):
|
||||||
|
details = contribution_details(records)
|
||||||
|
details['company'] = company
|
||||||
|
return details
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/modules/<module>')
|
||||||
|
@exception_handler()
|
||||||
|
@templated()
|
||||||
|
@record_filter()
|
||||||
|
def module_details(module, records):
|
||||||
|
details = contribution_details(records)
|
||||||
|
details['module'] = module
|
||||||
|
return details
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/engineers/<launchpad_id>')
|
||||||
|
@exception_handler()
|
||||||
|
@templated()
|
||||||
|
@record_filter()
|
||||||
|
def engineer_details(launchpad_id, records):
|
||||||
|
persistent_storage = get_vault()['persistent_storage']
|
||||||
|
user = list(persistent_storage.get_users(launchpad_id=launchpad_id))[0]
|
||||||
|
|
||||||
|
details = contribution_details(records)
|
||||||
|
details['launchpad_id'] = launchpad_id
|
||||||
|
details['user'] = user
|
||||||
|
return details
|
||||||
|
|
||||||
|
|
||||||
|
@app.errorhandler(404)
|
||||||
|
def page_not_found(e):
|
||||||
|
return flask.render_template('404.html'), 404
|
||||||
|
|
||||||
|
|
||||||
|
def _get_aggregated_stats(records, metric_filter, keys, param_id,
|
||||||
|
param_title=None):
|
||||||
|
param_title = param_title or param_id
|
||||||
|
result = dict((c, 0) for c in keys)
|
||||||
|
titles = {}
|
||||||
|
for record in records:
|
||||||
|
result[record[param_id]] += metric_filter(record)
|
||||||
|
titles[record[param_id]] = record[param_title]
|
||||||
|
|
||||||
|
response = [{'id': r, 'metric': result[r], 'name': titles[r]}
|
||||||
|
for r in result if result[r]]
|
||||||
|
response.sort(key=lambda x: x['metric'], reverse=True)
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/data/companies')
|
||||||
|
@exception_handler()
|
||||||
|
@record_filter()
|
||||||
|
@aggregate_filter()
|
||||||
|
def get_companies(records, metric_filter):
|
||||||
|
response = _get_aggregated_stats(records, metric_filter,
|
||||||
|
get_memory_storage().get_companies(),
|
||||||
|
'company_name')
|
||||||
|
return json.dumps(response)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/data/modules')
|
||||||
|
@exception_handler()
|
||||||
|
@record_filter()
|
||||||
|
@aggregate_filter()
|
||||||
|
def get_modules(records, metric_filter):
|
||||||
|
response = _get_aggregated_stats(records, metric_filter,
|
||||||
|
get_memory_storage().get_modules(),
|
||||||
|
'module')
|
||||||
|
return json.dumps(response)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/data/engineers')
|
||||||
|
@exception_handler()
|
||||||
|
@record_filter()
|
||||||
|
@aggregate_filter()
|
||||||
|
def get_engineers(records, metric_filter):
|
||||||
|
response = _get_aggregated_stats(records, metric_filter,
|
||||||
|
get_memory_storage().get_launchpad_ids(),
|
||||||
|
'launchpad_id', 'author')
|
||||||
|
return json.dumps(response)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/data/timeline')
|
||||||
|
@exception_handler()
|
||||||
|
@record_filter(parameter_getter=lambda x: flask.request.args.get(x)
|
||||||
|
if (x != "release") and (x != "releases") else None)
|
||||||
|
def timeline(records):
|
||||||
|
|
||||||
|
# find start and end dates
|
||||||
|
release_name = flask.request.args.get('release')
|
||||||
|
|
||||||
|
if not release_name:
|
||||||
|
release_name = DEFAULT_RELEASE
|
||||||
|
else:
|
||||||
|
release_name = release_name.lower()
|
||||||
|
|
||||||
|
releases = get_vault()['releases']
|
||||||
|
if release_name not in releases:
|
||||||
|
flask.abort(404)
|
||||||
|
release = releases[release_name]
|
||||||
|
|
||||||
|
start_date = release_start_date = user_utils.timestamp_to_week(
|
||||||
|
user_utils.date_to_timestamp(release['start_date']))
|
||||||
|
end_date = release_end_date = user_utils.timestamp_to_week(
|
||||||
|
user_utils.date_to_timestamp(release['end_date']))
|
||||||
|
now = user_utils.timestamp_to_week(int(time.time()))
|
||||||
|
|
||||||
|
# expand start-end to year if needed
|
||||||
|
if release_end_date - release_start_date < 52:
|
||||||
|
expansion = (52 - (release_end_date - release_start_date)) // 2
|
||||||
|
if release_end_date + expansion < now:
|
||||||
|
end_date += expansion
|
||||||
|
else:
|
||||||
|
end_date = now
|
||||||
|
start_date = end_date - 52
|
||||||
|
|
||||||
|
# empty stats for all weeks in range
|
||||||
|
weeks = range(start_date, end_date)
|
||||||
|
week_stat_loc = dict((c, 0) for c in weeks)
|
||||||
|
week_stat_commits = dict((c, 0) for c in weeks)
|
||||||
|
|
||||||
|
# fill stats with the data
|
||||||
|
for record in records:
|
||||||
|
week = record['week']
|
||||||
|
if week in weeks:
|
||||||
|
week_stat_loc[week] += record['loc']
|
||||||
|
week_stat_commits[week] += 1
|
||||||
|
|
||||||
|
# form arrays in format acceptable to timeline plugin
|
||||||
|
array_loc = []
|
||||||
|
array_commits = []
|
||||||
|
array_commits_hl = []
|
||||||
|
|
||||||
|
for week in weeks:
|
||||||
|
week_str = user_utils.week_to_date(week)
|
||||||
|
array_loc.append([week_str, week_stat_loc[week]])
|
||||||
|
if release_start_date <= week <= release_end_date:
|
||||||
|
array_commits_hl.append([week_str, week_stat_commits[week]])
|
||||||
|
array_commits.append([week_str, week_stat_commits[week]])
|
||||||
|
|
||||||
|
return json.dumps([array_commits, array_commits_hl, array_loc])
|
||||||
|
|
||||||
|
|
||||||
|
# Jinja Filters
|
||||||
|
|
||||||
|
@app.template_filter('datetimeformat')
|
||||||
|
def format_datetime(timestamp):
|
||||||
|
return datetime.datetime.utcfromtimestamp(
|
||||||
|
timestamp).strftime('%d %b %Y @ %H:%M')
|
||||||
|
|
||||||
|
|
||||||
|
@app.template_filter('launchpadmodule')
|
||||||
|
def format_launchpad_module_link(module):
|
||||||
|
return '<a href="https://launchpad.net/%s">%s</a>' % (module, module)
|
||||||
|
|
||||||
|
|
||||||
|
@app.template_filter('encode')
|
||||||
|
def safe_encode(s):
|
||||||
|
return urllib.quote_plus(s)
|
||||||
|
|
||||||
|
|
||||||
|
@app.template_filter('link')
|
||||||
|
def make_link(title, uri=None):
|
||||||
|
return '<a href="%(uri)s">%(title)s</a>' % {'uri': uri, 'title': title}
|
||||||
|
|
||||||
|
|
||||||
|
def clear_text(s):
|
||||||
|
return cgi.escape(re.sub(r'\n{2,}', '\n', s, flags=re.MULTILINE))
|
||||||
|
|
||||||
|
|
||||||
|
def link_blueprint(s, module):
|
||||||
|
return re.sub(r'(blueprint\s+)([\w-]+)',
|
||||||
|
r'\1<a href="https://blueprints.launchpad.net/' +
|
||||||
|
module + r'/+spec/\2">\2</a>',
|
||||||
|
s, flags=re.IGNORECASE)
|
||||||
|
|
||||||
|
|
||||||
|
def link_bug(s):
|
||||||
|
return re.sub(r'(bug\s+)#?([\d]{5,7})',
|
||||||
|
r'\1<a href="https://bugs.launchpad.net/bugs/\2">\2</a>',
|
||||||
|
s, flags=re.IGNORECASE)
|
||||||
|
|
||||||
|
|
||||||
|
def link_change_id(s):
|
||||||
|
return re.sub(r'\s+(I[0-9a-f]{40})',
|
||||||
|
r' <a href="https://review.openstack.org/#q,\1,n,z">\1</a>',
|
||||||
|
s)
|
||||||
|
|
||||||
|
|
||||||
|
@app.template_filter('commit_message')
|
||||||
|
def make_commit_message(record):
|
||||||
|
|
||||||
|
return link_change_id(link_bug(link_blueprint(clear_text(
|
||||||
|
record['message']), record['module'])))
|
||||||
|
|
||||||
|
|
||||||
|
gravatar = gravatar_ext.Gravatar(app,
|
||||||
|
size=100,
|
||||||
|
rating='g',
|
||||||
|
default='wavatar',
|
||||||
|
force_default=False,
|
||||||
|
force_lower=False)
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
app.run('0.0.0.0')
|
13549
etc/default_data.json
Normal file
13549
etc/default_data.json
Normal file
File diff suppressed because it is too large
Load Diff
@ -1,94 +0,0 @@
|
|||||||
# domain employer [< yyyy-mm-dd]
|
|
||||||
3ds.com Dassault Systèmes
|
|
||||||
99cloud.net 99cloud
|
|
||||||
alyseo.com Alyseo
|
|
||||||
ansolabs.com Rackspace < 2012-07-20
|
|
||||||
ansolabs.com Nebula
|
|
||||||
atomia.com Atomia
|
|
||||||
att.com AT&T
|
|
||||||
attinteractive.com AT&T
|
|
||||||
bigswitch.com Big Switch Networks
|
|
||||||
b1-systems.de BL Systems
|
|
||||||
brocade.com Brocade
|
|
||||||
bull.net Bull
|
|
||||||
canonical.com Canonical
|
|
||||||
cern.ch CERN
|
|
||||||
cisco.com Cisco Systems
|
|
||||||
citrix.com Citrix
|
|
||||||
cloud.com Citrix Systems
|
|
||||||
cloudbau.de Cloudbau
|
|
||||||
cloudscaling.com Cloudscaling
|
|
||||||
dell.com Dell
|
|
||||||
Dell.com Dell
|
|
||||||
denali-systems.com Denali Systems
|
|
||||||
dreamhost.com DreamHost
|
|
||||||
emc.com EMC
|
|
||||||
enovance.com eNovance
|
|
||||||
fathomdb.com FathomDB
|
|
||||||
gluster.com Red Hat
|
|
||||||
griddynamics.com Grid Dynamics
|
|
||||||
guardian.co.uk The Guardian
|
|
||||||
hds.com Hitachi
|
|
||||||
hp.com HP
|
|
||||||
huawei.com Huawei
|
|
||||||
ibm.com IBM
|
|
||||||
inktank.com Inktank
|
|
||||||
intel.com Intel
|
|
||||||
internap.com Internap
|
|
||||||
isi.edu University of Southern Carolina
|
|
||||||
ispras.ru ISP RAS
|
|
||||||
kt.com KT Corporation
|
|
||||||
kth.se Kungliga Tekniska högskolan
|
|
||||||
linux.vnet.ibm.com IBM
|
|
||||||
locaweb.com.br Locaweb
|
|
||||||
lahondaresearch.org La Honda Research
|
|
||||||
managedit.ie Managed IT
|
|
||||||
mellanox.com Mellanox
|
|
||||||
memset.com Memset
|
|
||||||
metacloud.com Metacloud
|
|
||||||
midokura.com Midokura
|
|
||||||
midokura.jp Midokura
|
|
||||||
mirantis.com Mirantis
|
|
||||||
mirantis.ru Mirantis
|
|
||||||
nasa.gov NASA
|
|
||||||
nebula.com Nebula
|
|
||||||
nexenta.com Nexenta
|
|
||||||
nec.co.jp NEC
|
|
||||||
cq.jp.nec.com NEC
|
|
||||||
da.jp.nec.com NEC
|
|
||||||
mxw.nes.nec.co.jp NEC
|
|
||||||
mxd.nes.nec.co.jp NEC
|
|
||||||
netapp.com NetApp
|
|
||||||
nicira.com Nicira
|
|
||||||
nimbisservices.com Nimbis Services
|
|
||||||
ntt.co.jp NTT
|
|
||||||
ntt.com NTT
|
|
||||||
nttdata.com NTT
|
|
||||||
nttdata.co.jp NTT
|
|
||||||
nttmcl.com NTT
|
|
||||||
vertex.co.in NTT
|
|
||||||
pednape StackOps
|
|
||||||
pistoncloud.com Piston Cloud
|
|
||||||
rackspace.co.uk Rackspace
|
|
||||||
rackspace.com Rackspace
|
|
||||||
radware.com Radware
|
|
||||||
redhat.com Red Hat
|
|
||||||
scality.com Scality
|
|
||||||
sdsc.edu San Diego Supercomputer Center
|
|
||||||
sina.com SINA
|
|
||||||
software.dell.com Dell
|
|
||||||
solidfire.com SolidFire
|
|
||||||
suse.de SUSE
|
|
||||||
suse.com SUSE
|
|
||||||
suse.cz SUSE
|
|
||||||
swiftstack.com SwiftStack
|
|
||||||
thoughtworks.com ThoughtWorks
|
|
||||||
umd.edu University of Maryland
|
|
||||||
unimelb.edu.au University of Melbourne
|
|
||||||
valinux.co.jp VA Linux
|
|
||||||
vexxhost.com VexxHost
|
|
||||||
virtualtech.jp Virtualtech
|
|
||||||
vmware.com VMware
|
|
||||||
wikimedia.org Wikimedia Foundation
|
|
||||||
yahoo-inc.com Yahoo!
|
|
||||||
zadarastorage.com Zadara Storage
|
|
@ -1,257 +0,0 @@
|
|||||||
armamig@gmail.com Armando.Migliaccio@eu.citrix.com
|
|
||||||
yogesh.srikrishnan@rackspace.com yogesh.srikrishnan@rackspace.com
|
|
||||||
emagana@gmail.com eperdomo@cisco.com eperdomo@dhcp-171-71-119-164.cisco.com
|
|
||||||
jeblair@hp.com jeblair@openstack.org
|
|
||||||
rohitgarwalla@gmail.com roagarwa@cisco.com
|
|
||||||
shweta.ap05@gmail.com shpadubi@cisco.com
|
|
||||||
e0ne@e0ne.info ikolodyazhny@mirantis.com
|
|
||||||
santhosh.m@thoughtworks.com santhom@thoughtworks.com
|
|
||||||
john.garbutt@citrix.com john.garbutt@rackspace.com
|
|
||||||
thingee@gmail.com mike.perez@dreamhost.com
|
|
||||||
duncan.thomas@gmail.com duncan.thomas@hp.com
|
|
||||||
adamg@canonical.com adam.gandelman@canonical.com
|
|
||||||
admin@jakedahn.com jake@ansolabs.com
|
|
||||||
amesserl@rackspace.com ant@openstack.org
|
|
||||||
amigliaccio@internap.com Armando.Migliaccio@eu.citrix.com
|
|
||||||
andrew@cloudscaling.com acs@parvuscaptus.com
|
|
||||||
anne.gentle@rackspace.com anne@openstack.org
|
|
||||||
anne@openstack.org anne@openstack.org
|
|
||||||
armando.migliaccio@citrix.com Armando.Migliaccio@eu.citrix.com
|
|
||||||
asomya@cisco.com asomya@cisco.com
|
|
||||||
bcwaldon@gmail.com brian.waldon@rackspace.com
|
|
||||||
bfschott@gmail.com bschott@isi.edu
|
|
||||||
Bogott abogott@wikimedia.org
|
|
||||||
brian.lamar@gmail.com brian.lamar@rackspace.com
|
|
||||||
brian.lamar@rackspace.com brian.lamar@rackspace.com
|
|
||||||
cbehrens@codestud.com cbehrens+github@codestud.com
|
|
||||||
cbehrens+github@codestud.com cbehrens+github@codestud.com
|
|
||||||
chiradeep@chiradeep-lt2 chiradeep@cloud.com
|
|
||||||
chmouel@chmouel.com chmouel.boudjnah@rackspace.co.uk
|
|
||||||
chmouel@enovance.com chmouel@chmouel.com
|
|
||||||
chris.behrens@rackspace.com cbehrens@codestud.com
|
|
||||||
chris@slicehost.com chris@pistoncloud.com
|
|
||||||
chuck.short@canonical.com zulcss@ubuntu.com
|
|
||||||
clayg@clayg-desktop clay.gerrard@gmail.com
|
|
||||||
clay.gerrard@gmail.com clay.gerrard@gmail.com
|
|
||||||
clay.gerrard@rackspace.com clay.gerrard@gmail.com
|
|
||||||
code@term.ie code@term.ie
|
|
||||||
corvus@gnu.org jeblair@hp.com
|
|
||||||
corvus@inaugust.com jeblair@hp.com
|
|
||||||
corywright@gmail.com cory.wright@rackspace.com
|
|
||||||
cory.wright@rackspace.com corywright@gmail.com
|
|
||||||
dan@nicira.com dan@nicira.com
|
|
||||||
dan.prince@rackspace.com dprince@redhat.com
|
|
||||||
danwent@dan-xs3-cs dan@nicira.com
|
|
||||||
danwent@gmail.com dan@nicira.com
|
|
||||||
Dave.Walker@Canonical.com dave.walker@canonical.com
|
|
||||||
DaveWalker@ubuntu.com dave.walker@canonical.com
|
|
||||||
DaveWalker@ubuntu.com Dave.Walker@canonical.com
|
|
||||||
david.goetz@gmail.com david.goetz@rackspace.com
|
|
||||||
david.hadas@gmail.com davidh@il.ibm.com
|
|
||||||
devcamcar@illian.local devin.carlen@gmail.com
|
|
||||||
devnull@brim.net gholt@rackspace.com
|
|
||||||
Dietz matt.dietz@rackspace.com
|
|
||||||
dolph.mathews@gmail.com dolph.mathews@rackspace.com
|
|
||||||
doug.hellmann@gmail.com doug.hellmann@dreamhost.com
|
|
||||||
dougw@sdsc.edu dweimer@gmail.com
|
|
||||||
dpgoetz@gmail.com david.goetz@rackspace.com
|
|
||||||
dt-github@xr7.org dtroyer@gmail.com
|
|
||||||
Édouard edouard.thuleau@orange.com
|
|
||||||
emellor@silver ewan.mellor@citrix.com
|
|
||||||
enugaev@griddynamics.com reldan@oscloud.ru
|
|
||||||
florian.hines@gmail.com syn@ronin.io
|
|
||||||
gaurav@gluster.com gaurav@gluster.com
|
|
||||||
ghe.rivero@gmail.com ghe@debian.org
|
|
||||||
ghe.rivero@stackops.com ghe@debian.org
|
|
||||||
gholt@brim.net gholt@rackspace.com
|
|
||||||
gihub@highbridgellc.com github@highbridgellc.com
|
|
||||||
github@anarkystic.com code@term.ie
|
|
||||||
github@anarkystic.com github@anarkystic.com
|
|
||||||
glange@rackspace.com greglange@gmail.com
|
|
||||||
greglange+launchpad@gmail.com greglange@gmail.com
|
|
||||||
heut2008@gmail.com yaguang.tang@canonical.com
|
|
||||||
higginsd@gmail.com derekh@redhat.com
|
|
||||||
ialekseev@griddynamics.com ilyaalekseyev@acm.org
|
|
||||||
ilya@oscloud.ru ilyaalekseyev@acm.org
|
|
||||||
itoumsn@shayol itoumsn@nttdata.co.jp
|
|
||||||
jake@ansolabs.com jake@ansolabs.com
|
|
||||||
jake@markupisart.com jake@ansolabs.com
|
|
||||||
james.blair@rackspace.com jeblair@hp.com
|
|
||||||
jaypipes@gmail.com jaypipes@gmail.com
|
|
||||||
jesse@aire.local anotherjesse@gmail.com
|
|
||||||
jesse@dancelamb anotherjesse@gmail.com
|
|
||||||
jesse@gigantor.local anotherjesse@gmail.com
|
|
||||||
jesse@ubuntu anotherjesse@gmail.com
|
|
||||||
jian.wen@ubuntu.com jian.wen@canonical.com
|
|
||||||
jkearney@nova.(none) josh@jk0.org
|
|
||||||
jkearney@nova.(none) josh.kearney@pistoncloud.com
|
|
||||||
jmckenty@joshua-mckentys-macbook-pro.local jmckenty@gmail.com
|
|
||||||
jmckenty@yyj-dhcp171.corp.flock.com jmckenty@gmail.com
|
|
||||||
joe@cloudscaling.com joe@swiftstack.com
|
|
||||||
johannes@compute3.221.st johannes.erdfelt@rackspace.com
|
|
||||||
johannes@erdfelt.com johannes.erdfelt@rackspace.com
|
|
||||||
john.dickinson@rackspace.com me@not.mn
|
|
||||||
john.eo@gmail.com john.eo@rackspace.com
|
|
||||||
john.griffith8@gmail.com john.griffith@solidfire.com
|
|
||||||
john.griffith@solidfire.com john.griffith@solidfire.com
|
|
||||||
josh@jk0.org josh.kearney@pistoncloud.com
|
|
||||||
josh.kearney@rackspace.com josh@jk0.org
|
|
||||||
josh.kearney@rackspace.com josh.kearney@pistoncloud.com
|
|
||||||
joshua.mckenty@nasa.gov jmckenty@gmail.com
|
|
||||||
jpipes@serialcoder jaypipes@gmail.com
|
|
||||||
jpipes@uberbox.gateway.2wire.net jaypipes@gmail.com
|
|
||||||
jsuh@bespin jsuh@isi.edu
|
|
||||||
jtran@attinteractive.com jhtran@att.com
|
|
||||||
julien.danjou@enovance.com julien@danjou.info
|
|
||||||
justin@fathomdb.com justin@fathomdb.com
|
|
||||||
justinsb@justinsb-desktop justin@fathomdb.com
|
|
||||||
kapil.foss@gmail.com kapil.foss@gmail.com
|
|
||||||
ken.pepple@rabbityard.com ken.pepple@gmail.com
|
|
||||||
ke.wu@nebula.com ke.wu@ibeca.me
|
|
||||||
khaled.hussein@gmail.com khaled.hussein@rackspace.com
|
|
||||||
Knouff philip.knouff@mailtrust.com
|
|
||||||
Kölker jason@koelker.net
|
|
||||||
kshileev@griddynamics.com kshileev@gmail.com
|
|
||||||
laner@controller rlane@wikimedia.org
|
|
||||||
letterj@racklabs.com letterj@gmail.com
|
|
||||||
liem.m.nguyen@gmail.com liem_m_nguyen@hp.com
|
|
||||||
liem.m.nguyen@hp.com liem_m_nguyen@hp.com
|
|
||||||
Lopez aloga@ifca.unican.es
|
|
||||||
lorin@isi.edu lorin@nimbisservices.com
|
|
||||||
lrqrun@gmail.com lrqrun@gmail.com
|
|
||||||
lzyeval@gmail.com zhongyue.nah@intel.com
|
|
||||||
marcelo.martins@rackspace.com btorch@gmail.com
|
|
||||||
masumotok@nttdata.co.jp masumotok@nttdata.co.jp
|
|
||||||
masumoto masumotok@nttdata.co.jp
|
|
||||||
matt.dietz@rackspace.com matt.dietz@rackspace.com
|
|
||||||
matthew.dietz@gmail.com matt.dietz@rackspace.com
|
|
||||||
matthewdietz@Matthew-Dietzs-MacBook-Pro.local matt.dietz@rackspace.com
|
|
||||||
McConnell bmcconne@rackspace.com
|
|
||||||
mdietz@openstack matt.dietz@rackspace.com
|
|
||||||
mgius7096@gmail.com launchpad@markgius.com
|
|
||||||
michael.barton@rackspace.com mike@weirdlooking.com
|
|
||||||
michael.still@canonical.com mikal@stillhq.com
|
|
||||||
mike-launchpad@weirdlooking.com mike@weirdlooking.com
|
|
||||||
Moore joelbm24@gmail.com
|
|
||||||
mordred@hudson mordred@inaugust.com
|
|
||||||
nati.ueno@gmail.com ueno.nachi@lab.ntt.co.jp
|
|
||||||
naveed.massjouni@rackspace.com naveedm9@gmail.com
|
|
||||||
nelson@nelson-laptop russ@crynwr.com
|
|
||||||
nirmal.ranganathan@rackspace.com rnirmal@gmail.com
|
|
||||||
nirmal.ranganathan@rackspace.coom rnirmal@gmail.com
|
|
||||||
nova@u4 ueno.nachi@lab.ntt.co.jp
|
|
||||||
nsokolov@griddynamics.net nsokolov@griddynamics.com
|
|
||||||
openstack@lab.ntt.co.jp ueno.nachi@lab.ntt.co.jp
|
|
||||||
paul@openstack.org paul@openstack.org
|
|
||||||
paul@substation9.com paul@openstack.org
|
|
||||||
paul.voccio@rackspace.com paul@openstack.org
|
|
||||||
pvoccio@castor.local paul@openstack.org
|
|
||||||
ramana@venus.lekha.org rjuvvadi@hcl.com
|
|
||||||
rclark@chat-blanc rick@openstack.org
|
|
||||||
renuka.apte@citrix.com renuka.apte@citrix.com
|
|
||||||
rick.harris@rackspace.com rconradharris@gmail.com
|
|
||||||
rick@quasar.racklabs.com rconradharris@gmail.com
|
|
||||||
root@bsirish.(none) sirish.bitra@gmail.com
|
|
||||||
root@debian.ohthree.com amesserl@rackspace.com
|
|
||||||
root@mirror.nasanebula.net vishvananda@gmail.com
|
|
||||||
root@openstack2-api masumotok@nttdata.co.jp
|
|
||||||
root@tonbuntu sleepsonthefloor@gmail.com
|
|
||||||
root@ubuntu vishvananda@gmail.com
|
|
||||||
rrjuvvadi@gmail.com rjuvvadi@hcl.com
|
|
||||||
salv.orlando@gmail.com salvatore.orlando@eu.citrix.com
|
|
||||||
sandy@sandywalsh.com sandy@darksecretsoftware.com
|
|
||||||
sandy@sandywalsh.com sandy.walsh@rackspace.com
|
|
||||||
sandy.walsh@rackspace.com sandy@darksecretsoftware.com
|
|
||||||
sandy.walsh@rackspace.com sandy.walsh@rackspace.com
|
|
||||||
sateesh.chodapuneedi@citrix.com sateesh.chodapuneedi@citrix.com
|
|
||||||
SB justin@fathomdb.com
|
|
||||||
sirish.bitra@gmail.com sirish.bitra@gmail.com
|
|
||||||
sleepsonthefloor@gmail.com sleepsonthefloor@gmail.com
|
|
||||||
Smith code@term.ie
|
|
||||||
Sokolov nsokolov@griddynamics.com
|
|
||||||
Somya asomya@cisco.com
|
|
||||||
soren.hansen@rackspace.com soren@linux2go.dk
|
|
||||||
soren@linux2go.dk soren.hansen@rackspace.com
|
|
||||||
soren@openstack.org soren.hansen@rackspace.com
|
|
||||||
sorhanse@cisco.com sorenhansen@rackspace.com
|
|
||||||
sorlando@nicira.com salvatore.orlando@eu.citrix.com
|
|
||||||
spam@andcheese.org sam@swiftstack.com
|
|
||||||
superstack@superstack.org justin@fathomdb.com
|
|
||||||
termie@preciousroy.local code@term.ie
|
|
||||||
thuleau@gmail.com edouard1.thuleau@orange.com
|
|
||||||
thuleau@gmail.com edouard.thuleau@orange.com
|
|
||||||
tim.simpson4@gmail.com tim.simpson@rackspace.com
|
|
||||||
todd@lapex todd@ansolabs.com
|
|
||||||
todd@rubidine.com todd@ansolabs.com
|
|
||||||
todd@rubidine.com xtoddx@gmail.com
|
|
||||||
tpatil@vertex.co.in tushar.vitthal.patil@gmail.com
|
|
||||||
Tran jtran@attinteractive.com
|
|
||||||
treyemorris@gmail.com trey.morris@rackspace.com
|
|
||||||
ttcl@mac.com troy.toman@rackspace.com
|
|
||||||
Ueno ueno.nachi@lab.ntt.co.jp
|
|
||||||
vishvananda@yahoo.com vishvananda@gmail.com
|
|
||||||
vito.ordaz@gmail.com victor.rodionov@nexenta.com
|
|
||||||
wenjianhn@gmail.com jian.wen@canonical.com
|
|
||||||
will.wolf@rackspace.com throughnothing@gmail.com
|
|
||||||
wwkeyboard@gmail.com aaron.lee@rackspace.com
|
|
||||||
xchu@redhat.com xychu2008@gmail.com
|
|
||||||
xtoddx@gmail.com todd@ansolabs.com
|
|
||||||
xyj.asmy@gmail.com xyj.asmy@gmail.com
|
|
||||||
yorik@ytaraday yorik.sar@gmail.com
|
|
||||||
YS vivek.ys@gmail.com
|
|
||||||
z-github@brim.net gholt@rackspace.com
|
|
||||||
ziad.sawalha@rackspace.com github@highbridgellc.com
|
|
||||||
z-launchpad@brim.net gholt@rackspace.com
|
|
||||||
derek.morton25@gmail.com derek@networkwhisperer.com
|
|
||||||
bartosz.gorski@ntti3.com bartosz.gorski@nttmcl.com
|
|
||||||
launchpad@chmouel.com chmouel@chmouel.com
|
|
||||||
launchpad@chmouel.com chmouel@enovance.com
|
|
||||||
launchpad@chmouel.com chmouel@openstack.org
|
|
||||||
imsplitbit@gmail.com dsalinas@rackspace.com
|
|
||||||
clint@fewbar.com clint.byrum@hp.com
|
|
||||||
clint@fewbar.com clint@ubuntu.com
|
|
||||||
sbaker@redhat.com steve@stevebaker.org
|
|
||||||
asalkeld@redhat.com angus@salkeld.id.au
|
|
||||||
evgeniy@afonichev.com eafonichev@mirantis.com
|
|
||||||
smoser@ubuntu.com scott.moser@canonical.com
|
|
||||||
smoser@ubuntu.com smoser@brickies.net
|
|
||||||
smoser@ubuntu.com smoser@canonical.com
|
|
||||||
smoser@ubuntu.com ssmoser2@gmail.com
|
|
||||||
jason@koelker.net jkoelker@rackspace.com
|
|
||||||
john.garbutt@rackspace.com john@johngarbutt.com
|
|
||||||
zhongyue.nah@intel.com lzyeval@gmail.com
|
|
||||||
jiajun@unitedstack.com iamljj@gmail.com
|
|
||||||
christophe.sauthier@ubuntu.com christophe.sauthier@gmail.com
|
|
||||||
christophe.sauthier@ubuntu.com christophe@sauthier.com
|
|
||||||
christophe.sauthier@objectif-libre.com christophe@sauthier.com
|
|
||||||
aababilov@griddynamics.com ilovegnulinux@gmail.com
|
|
||||||
yportnova@griddynamics.com yportnov@yahoo-inc.com
|
|
||||||
mkislins@yahoo-inc.com mkislinska@griddynamics.com
|
|
||||||
ryan.moore@hp.com rmoore08@gmail.com
|
|
||||||
starodubcevna@gmail.com nstarodubtsev@mirantis.com
|
|
||||||
lakhinder.walia@hds.com lakhindr@hotmail.com
|
|
||||||
kanzhe@gmail.com kanzhe.jiang@bigswitch.com
|
|
||||||
anita.kuno@enovance.com akuno@lavabit.com
|
|
||||||
me@frostman.ru slukjanov@mirantis.com
|
|
||||||
alexei.kornienko@gmail.com akornienko@mirantis.com
|
|
||||||
nicolas@barcet.com nick.barcet@canonical.com
|
|
||||||
nicolas@barcet.com nick@enovance.com
|
|
||||||
nicolas@barcet.com nijaba@ubuntu.com
|
|
||||||
graham.binns@canonical.com gmb@canonical.com
|
|
||||||
graham.binns@canonical.com gmb@grahambinns.com
|
|
||||||
graham.binns@canonical.com graham.binns@gmail.com
|
|
||||||
graham.binns@canonical.com graham@canonical.com
|
|
||||||
graham.binns@canonical.com graham@grahambinns.com
|
|
||||||
emilien.macchi@stackops.com emilien.macchi@enovance.com
|
|
||||||
emilien.macchi@stackops.com emilien@enovance.com
|
|
||||||
swann.croiset@bull.net swann@oopss.org
|
|
||||||
soulascedric@gmail.com cedric.soulas@cloudwatt.com
|
|
||||||
simon.pasquier@bull.net pasquier.simon+launchpad@gmail.com
|
|
||||||
simon.pasquier@bull.net pasquier.simon@gmail.com
|
|
||||||
bogorodskiy@gmail.com novel@FreeBSD.org
|
|
||||||
bogorodskiy@gmail.com rbogorodskiy@mirantis.com
|
|
||||||
svilgelm@mirantis.com sergey.vilgelm@gmail.com
|
|
||||||
robert.myers@rackspace.com robert_myers@earthlink.net
|
|
||||||
raymond_pekowski@dell.com pekowski@gmail.com
|
|
||||||
agorodnev@mirantis.com a.gorodnev@gmail.com
|
|
||||||
rprikhodchenko@mirantis.com me@romcheg.me
|
|
@ -1,17 +0,0 @@
|
|||||||
# user@domain employer
|
|
||||||
anotherjesse@gmail.com Nebula
|
|
||||||
bcwaldon@gmail.com Nebula
|
|
||||||
code@term.ie Nebula
|
|
||||||
dprince@redhat.com Red Hat
|
|
||||||
github@anarkystic.com Nebula
|
|
||||||
jake@ansolabs.com Nebula
|
|
||||||
jaypipes@gmail.com AT&T
|
|
||||||
jeblair@hp.com HP
|
|
||||||
lzyeval@gmail.com Intel
|
|
||||||
me@not.mn SwiftStack
|
|
||||||
mordred@inaugust.com HP
|
|
||||||
sleepsonthefloor@gmail.com Nebula
|
|
||||||
soren@linux2go.dk Cisco
|
|
||||||
vishvananda@gmail.com Nebula
|
|
||||||
dtroyer@gmail.com Nebula
|
|
||||||
|
|
@ -1,36 +0,0 @@
|
|||||||
tatyana-leontovich Grid Dynamics
|
|
||||||
vkhomenko Grid Dynamics
|
|
||||||
cthiel-suse DE Telekom
|
|
||||||
yorik-sar Mirantis
|
|
||||||
gelbuhos Mirantis
|
|
||||||
aababilov Grid Dynamics
|
|
||||||
alexpilotti Cloudbase Solutions
|
|
||||||
devananda HP
|
|
||||||
heckj Nebula
|
|
||||||
matt-sherborne Rackspace
|
|
||||||
michael-ogorman Cisco Systems
|
|
||||||
boris-42 Mirantis
|
|
||||||
boris-42 *independent < 2013-04-10
|
|
||||||
victor-r-howard Comcast
|
|
||||||
amitry Comcast
|
|
||||||
scollins Comcast
|
|
||||||
w-emailme Comcast
|
|
||||||
jasondunsmore Rackspace
|
|
||||||
kannan Rightscale
|
|
||||||
bob-melander Cisco Systems
|
|
||||||
gabriel-hurley Nebula
|
|
||||||
mathrock National Security Agency
|
|
||||||
yosshy NEC
|
|
||||||
johngarbutt Rackspace
|
|
||||||
johngarbutt Citrix < 2013-02-01
|
|
||||||
jean-baptiste-ransy Alyseo
|
|
||||||
darren-birkett Rackspace
|
|
||||||
lucasagomes Red Hat
|
|
||||||
nobodycam HP
|
|
||||||
cboylan HP
|
|
||||||
dmllr SUSE
|
|
||||||
therve HP
|
|
||||||
hughsaunders Rackspace
|
|
||||||
bruno-semperlotti Dassault Systèmes
|
|
||||||
james-slagle Red Hat
|
|
||||||
openstack *robots
|
|
1128
etc/launchpad-ids
1128
etc/launchpad-ids
File diff suppressed because it is too large
Load Diff
@ -1,27 +0,0 @@
|
|||||||
[DEFAULT]
|
|
||||||
# Run in debug mode?
|
|
||||||
# debug = False
|
|
||||||
|
|
||||||
# Database parameters
|
|
||||||
# db_driver = sqlite
|
|
||||||
# db_user = operator
|
|
||||||
# db_password = None
|
|
||||||
# db_database = /opt/stack/data/stackalytics.sqlite
|
|
||||||
# db_hostname = localhost
|
|
||||||
|
|
||||||
# Extensions
|
|
||||||
# extensions = CommitsLOC,MessageDetails
|
|
||||||
|
|
||||||
# Root for all project sources. The tool will iterate over its contents
|
|
||||||
# sources_root = /opt/stack/repos
|
|
||||||
|
|
||||||
# email mappings (e.g. collected from .mailmap files)
|
|
||||||
# email_aliases = etc/email-aliases
|
|
||||||
# mappings from domains to company names
|
|
||||||
# domain2company = etc/domain-company
|
|
||||||
# mappings from emails to company names
|
|
||||||
# email2company = etc/email-company
|
|
||||||
# mappings from launchpad ids to emails and user names
|
|
||||||
# launchpad2email = etc/launchpad-ids
|
|
||||||
# mappings from launchpad id to company name
|
|
||||||
# launchpad2company = etc/launchpad-company
|
|
24
etc/stackalytics.conf
Normal file
24
etc/stackalytics.conf
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
[DEFAULT]
|
||||||
|
# Run in debug mode?
|
||||||
|
# debug = False
|
||||||
|
|
||||||
|
# Default data
|
||||||
|
# default-data = etc/default_data.json
|
||||||
|
|
||||||
|
# The folder that holds all project sources to analyze
|
||||||
|
# sources_root = ../metric-root-tmp
|
||||||
|
|
||||||
|
# Runtime storage URI
|
||||||
|
# runtime_storage_uri = memcached://127.0.0.1:11211
|
||||||
|
|
||||||
|
# URI of persistent storage
|
||||||
|
# persistent_storage_uri = mongodb://localhost
|
||||||
|
|
||||||
|
# Update persistent storage with default data
|
||||||
|
# read-default-data = False
|
||||||
|
|
||||||
|
# Repo poll period in seconds
|
||||||
|
# repo_poll_period = 300
|
||||||
|
|
||||||
|
# Address of update handler
|
||||||
|
# frontend_update_address = http://user:user@localhost/update/%s
|
121
etc/test_default_data.json
Normal file
121
etc/test_default_data.json
Normal file
@ -0,0 +1,121 @@
|
|||||||
|
{
|
||||||
|
"users": [
|
||||||
|
{
|
||||||
|
"launchpad_id": "foo",
|
||||||
|
"user_name": "Pupkin",
|
||||||
|
"emails": ["a@a"],
|
||||||
|
"companies": [
|
||||||
|
{
|
||||||
|
"company_name": "Uno",
|
||||||
|
"end_date": "2013-Jan-01"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"company_name": "Duo",
|
||||||
|
"end_date": null
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
"companies": [
|
||||||
|
{
|
||||||
|
"company_name": "Mirantis",
|
||||||
|
"domains": ["mirantis.com"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"company_name": "*independent",
|
||||||
|
"domains": [""]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"company_name": "Hewlett-Packard",
|
||||||
|
"domains": ["hp.com"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"company_name": "Intel",
|
||||||
|
"domains": ["intel.com"]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
"repos": [
|
||||||
|
{
|
||||||
|
"branches": ["master"],
|
||||||
|
"name": "Quantum Client",
|
||||||
|
"type": "core",
|
||||||
|
"uri": "git://github.com/openstack/python-quantumclient.git",
|
||||||
|
"releases": [
|
||||||
|
{
|
||||||
|
"release_name": "Folsom",
|
||||||
|
"tag_from": "folsom-1",
|
||||||
|
"tag_to": "2.1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"release_name": "Grizzly",
|
||||||
|
"tag_from": "2.1",
|
||||||
|
"tag_to": "2.2.1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"release_name": "Havana",
|
||||||
|
"tag_from": "2.2.1",
|
||||||
|
"tag_to": "HEAD"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"branches": ["master"],
|
||||||
|
"name": "Keystone",
|
||||||
|
"type": "core",
|
||||||
|
"uri": "git://github.com/openstack/keystone.git",
|
||||||
|
"releases": [
|
||||||
|
{
|
||||||
|
"release_name": "Essex",
|
||||||
|
"tag_from": "2011.3",
|
||||||
|
"tag_to": "2012.1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"release_name": "Folsom",
|
||||||
|
"tag_from": "2012.1",
|
||||||
|
"tag_to": "2012.2"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"release_name": "Grizzly",
|
||||||
|
"tag_from": "2012.2",
|
||||||
|
"tag_to": "2013.1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"release_name": "Havana",
|
||||||
|
"tag_from": "2013.1",
|
||||||
|
"tag_to": "HEAD"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
"releases": [
|
||||||
|
{
|
||||||
|
"release_name": "ALL",
|
||||||
|
"start_date": "2010-May-01",
|
||||||
|
"end_date": "now"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"release_name": "Essex",
|
||||||
|
"start_date": "2011-Oct-01",
|
||||||
|
"end_date": "2012-Apr-01"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"release_name": "Folsom",
|
||||||
|
"start_date": "2012-Apr-01",
|
||||||
|
"end_date": "2012-Oct-01"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"release_name": "Grizzly",
|
||||||
|
"start_date": "2012-Oct-01",
|
||||||
|
"end_date": "2013-Apr-01"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"release_name": "Havana",
|
||||||
|
"start_date": "2013-Apr-01",
|
||||||
|
"end_date": "now"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
}
|
@ -1,11 +1,12 @@
|
|||||||
d2to1>=0.2.10,<0.3
|
d2to1>=0.2.10,<0.3
|
||||||
pbr>=0.5.16,<0.6
|
|
||||||
#MySQL-python
|
|
||||||
#pysqlite
|
|
||||||
#git+git://github.com/MetricsGrimoire/RepositoryHandler.git#egg=repositoryhandler-0.5
|
|
||||||
#git+git://github.com/SoftwareIntrospectionLab/guilty.git#egg=guilty-2.1
|
|
||||||
launchpadlib
|
|
||||||
Flask>=0.9
|
Flask>=0.9
|
||||||
Flask-Gravatar
|
Flask-Gravatar
|
||||||
oslo.config
|
iso8601
|
||||||
pylibmc
|
launchpadlib
|
||||||
|
http://tarballs.openstack.org/oslo.config/oslo.config-1.2.0a2.tar.gz#egg=oslo.config-1.2.0a2
|
||||||
|
pbr>=0.5.16,<0.6
|
||||||
|
psutil
|
||||||
|
python-memcached
|
||||||
|
pymongo
|
||||||
|
sh
|
||||||
|
six
|
||||||
|
1
stackalytics/__init__.py
Normal file
1
stackalytics/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
__author__ = 'ishakhat'
|
1
stackalytics/openstack/__init__.py
Normal file
1
stackalytics/openstack/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
__author__ = 'ishakhat'
|
0
stackalytics/openstack/common/__init__.py
Normal file
0
stackalytics/openstack/common/__init__.py
Normal file
68
stackalytics/openstack/common/importutils.py
Normal file
68
stackalytics/openstack/common/importutils.py
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||||
|
|
||||||
|
# Copyright 2011 OpenStack Foundation.
|
||||||
|
# All Rights Reserved.
|
||||||
|
#
|
||||||
|
# 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.
|
||||||
|
|
||||||
|
"""
|
||||||
|
Import related utilities and helper functions.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import traceback
|
||||||
|
|
||||||
|
|
||||||
|
def import_class(import_str):
|
||||||
|
"""Returns a class from a string including module and class."""
|
||||||
|
mod_str, _sep, class_str = import_str.rpartition('.')
|
||||||
|
try:
|
||||||
|
__import__(mod_str)
|
||||||
|
return getattr(sys.modules[mod_str], class_str)
|
||||||
|
except (ValueError, AttributeError):
|
||||||
|
raise ImportError('Class %s cannot be found (%s)' %
|
||||||
|
(class_str,
|
||||||
|
traceback.format_exception(*sys.exc_info())))
|
||||||
|
|
||||||
|
|
||||||
|
def import_object(import_str, *args, **kwargs):
|
||||||
|
"""Import a class and return an instance of it."""
|
||||||
|
return import_class(import_str)(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
def import_object_ns(name_space, import_str, *args, **kwargs):
|
||||||
|
"""Tries to import object from default namespace.
|
||||||
|
|
||||||
|
Imports a class and return an instance of it, first by trying
|
||||||
|
to find the class in a default namespace, then failing back to
|
||||||
|
a full path if not found in the default namespace.
|
||||||
|
"""
|
||||||
|
import_value = "%s.%s" % (name_space, import_str)
|
||||||
|
try:
|
||||||
|
return import_class(import_value)(*args, **kwargs)
|
||||||
|
except ImportError:
|
||||||
|
return import_class(import_str)(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
def import_module(import_str):
|
||||||
|
"""Import a module."""
|
||||||
|
__import__(import_str)
|
||||||
|
return sys.modules[import_str]
|
||||||
|
|
||||||
|
|
||||||
|
def try_import(import_str, default=None):
|
||||||
|
"""Try to import a module and if it fails return default."""
|
||||||
|
try:
|
||||||
|
return import_module(import_str)
|
||||||
|
except ImportError:
|
||||||
|
return default
|
169
stackalytics/openstack/common/jsonutils.py
Normal file
169
stackalytics/openstack/common/jsonutils.py
Normal file
@ -0,0 +1,169 @@
|
|||||||
|
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||||
|
|
||||||
|
# Copyright 2010 United States Government as represented by the
|
||||||
|
# Administrator of the National Aeronautics and Space Administration.
|
||||||
|
# Copyright 2011 Justin Santa Barbara
|
||||||
|
# All Rights Reserved.
|
||||||
|
#
|
||||||
|
# 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.
|
||||||
|
|
||||||
|
'''
|
||||||
|
JSON related utilities.
|
||||||
|
|
||||||
|
This module provides a few things:
|
||||||
|
|
||||||
|
1) A handy function for getting an object down to something that can be
|
||||||
|
JSON serialized. See to_primitive().
|
||||||
|
|
||||||
|
2) Wrappers around loads() and dumps(). The dumps() wrapper will
|
||||||
|
automatically use to_primitive() for you if needed.
|
||||||
|
|
||||||
|
3) This sets up anyjson to use the loads() and dumps() wrappers if anyjson
|
||||||
|
is available.
|
||||||
|
'''
|
||||||
|
|
||||||
|
|
||||||
|
import datetime
|
||||||
|
import functools
|
||||||
|
import inspect
|
||||||
|
import itertools
|
||||||
|
import json
|
||||||
|
import types
|
||||||
|
import xmlrpclib
|
||||||
|
|
||||||
|
import six
|
||||||
|
|
||||||
|
from stackalytics.openstack.common import timeutils
|
||||||
|
|
||||||
|
|
||||||
|
_nasty_type_tests = [inspect.ismodule, inspect.isclass, inspect.ismethod,
|
||||||
|
inspect.isfunction, inspect.isgeneratorfunction,
|
||||||
|
inspect.isgenerator, inspect.istraceback, inspect.isframe,
|
||||||
|
inspect.iscode, inspect.isbuiltin, inspect.isroutine,
|
||||||
|
inspect.isabstract]
|
||||||
|
|
||||||
|
_simple_types = (types.NoneType, int, basestring, bool, float, long)
|
||||||
|
|
||||||
|
|
||||||
|
def to_primitive(value, convert_instances=False, convert_datetime=True,
|
||||||
|
level=0, max_depth=3):
|
||||||
|
"""Convert a complex object into primitives.
|
||||||
|
|
||||||
|
Handy for JSON serialization. We can optionally handle instances,
|
||||||
|
but since this is a recursive function, we could have cyclical
|
||||||
|
data structures.
|
||||||
|
|
||||||
|
To handle cyclical data structures we could track the actual objects
|
||||||
|
visited in a set, but not all objects are hashable. Instead we just
|
||||||
|
track the depth of the object inspections and don't go too deep.
|
||||||
|
|
||||||
|
Therefore, convert_instances=True is lossy ... be aware.
|
||||||
|
|
||||||
|
"""
|
||||||
|
# handle obvious types first - order of basic types determined by running
|
||||||
|
# full tests on nova project, resulting in the following counts:
|
||||||
|
# 572754 <type 'NoneType'>
|
||||||
|
# 460353 <type 'int'>
|
||||||
|
# 379632 <type 'unicode'>
|
||||||
|
# 274610 <type 'str'>
|
||||||
|
# 199918 <type 'dict'>
|
||||||
|
# 114200 <type 'datetime.datetime'>
|
||||||
|
# 51817 <type 'bool'>
|
||||||
|
# 26164 <type 'list'>
|
||||||
|
# 6491 <type 'float'>
|
||||||
|
# 283 <type 'tuple'>
|
||||||
|
# 19 <type 'long'>
|
||||||
|
if isinstance(value, _simple_types):
|
||||||
|
return value
|
||||||
|
|
||||||
|
if isinstance(value, datetime.datetime):
|
||||||
|
if convert_datetime:
|
||||||
|
return timeutils.strtime(value)
|
||||||
|
else:
|
||||||
|
return value
|
||||||
|
|
||||||
|
# value of itertools.count doesn't get caught by nasty_type_tests
|
||||||
|
# and results in infinite loop when list(value) is called.
|
||||||
|
if type(value) == itertools.count:
|
||||||
|
return six.text_type(value)
|
||||||
|
|
||||||
|
# FIXME(vish): Workaround for LP bug 852095. Without this workaround,
|
||||||
|
# tests that raise an exception in a mocked method that
|
||||||
|
# has a @wrap_exception with a notifier will fail. If
|
||||||
|
# we up the dependency to 0.5.4 (when it is released) we
|
||||||
|
# can remove this workaround.
|
||||||
|
if getattr(value, '__module__', None) == 'mox':
|
||||||
|
return 'mock'
|
||||||
|
|
||||||
|
if level > max_depth:
|
||||||
|
return '?'
|
||||||
|
|
||||||
|
# The try block may not be necessary after the class check above,
|
||||||
|
# but just in case ...
|
||||||
|
try:
|
||||||
|
recursive = functools.partial(to_primitive,
|
||||||
|
convert_instances=convert_instances,
|
||||||
|
convert_datetime=convert_datetime,
|
||||||
|
level=level,
|
||||||
|
max_depth=max_depth)
|
||||||
|
if isinstance(value, dict):
|
||||||
|
return dict((k, recursive(v)) for k, v in value.iteritems())
|
||||||
|
elif isinstance(value, (list, tuple)):
|
||||||
|
return [recursive(lv) for lv in value]
|
||||||
|
|
||||||
|
# It's not clear why xmlrpclib created their own DateTime type, but
|
||||||
|
# for our purposes, make it a datetime type which is explicitly
|
||||||
|
# handled
|
||||||
|
if isinstance(value, xmlrpclib.DateTime):
|
||||||
|
value = datetime.datetime(*tuple(value.timetuple())[:6])
|
||||||
|
|
||||||
|
if convert_datetime and isinstance(value, datetime.datetime):
|
||||||
|
return timeutils.strtime(value)
|
||||||
|
elif hasattr(value, 'iteritems'):
|
||||||
|
return recursive(dict(value.iteritems()), level=level + 1)
|
||||||
|
elif hasattr(value, '__iter__'):
|
||||||
|
return recursive(list(value))
|
||||||
|
elif convert_instances and hasattr(value, '__dict__'):
|
||||||
|
# Likely an instance of something. Watch for cycles.
|
||||||
|
# Ignore class member vars.
|
||||||
|
return recursive(value.__dict__, level=level + 1)
|
||||||
|
else:
|
||||||
|
if any(test(value) for test in _nasty_type_tests):
|
||||||
|
return six.text_type(value)
|
||||||
|
return value
|
||||||
|
except TypeError:
|
||||||
|
# Class objects are tricky since they may define something like
|
||||||
|
# __iter__ defined but it isn't callable as list().
|
||||||
|
return six.text_type(value)
|
||||||
|
|
||||||
|
|
||||||
|
def dumps(value, default=to_primitive, **kwargs):
|
||||||
|
return json.dumps(value, default=default, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
def loads(s):
|
||||||
|
return json.loads(s)
|
||||||
|
|
||||||
|
|
||||||
|
def load(s):
|
||||||
|
return json.load(s)
|
||||||
|
|
||||||
|
|
||||||
|
try:
|
||||||
|
import anyjson
|
||||||
|
except ImportError:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
anyjson._modules.append((__name__, 'dumps', TypeError,
|
||||||
|
'loads', ValueError, 'load'))
|
||||||
|
anyjson.force_implementation(__name__)
|
559
stackalytics/openstack/common/log.py
Normal file
559
stackalytics/openstack/common/log.py
Normal file
@ -0,0 +1,559 @@
|
|||||||
|
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||||
|
|
||||||
|
# Copyright 2011 OpenStack Foundation.
|
||||||
|
# Copyright 2010 United States Government as represented by the
|
||||||
|
# Administrator of the National Aeronautics and Space Administration.
|
||||||
|
# All Rights Reserved.
|
||||||
|
#
|
||||||
|
# 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.
|
||||||
|
|
||||||
|
"""Openstack logging handler.
|
||||||
|
|
||||||
|
This module adds to logging functionality by adding the option to specify
|
||||||
|
a context object when calling the various log methods. If the context object
|
||||||
|
is not specified, default formatting is used. Additionally, an instance uuid
|
||||||
|
may be passed as part of the log message, which is intended to make it easier
|
||||||
|
for admins to find messages related to a specific instance.
|
||||||
|
|
||||||
|
It also allows setting of formatting information through conf.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
import ConfigParser
|
||||||
|
import cStringIO
|
||||||
|
import inspect
|
||||||
|
import itertools
|
||||||
|
import logging
|
||||||
|
import logging.config
|
||||||
|
import logging.handlers
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import traceback
|
||||||
|
|
||||||
|
from oslo.config import cfg
|
||||||
|
|
||||||
|
# from quantum.openstack.common.gettextutils import _
|
||||||
|
from stackalytics.openstack.common import importutils
|
||||||
|
from stackalytics.openstack.common import jsonutils
|
||||||
|
# from quantum.openstack.common import local
|
||||||
|
|
||||||
|
|
||||||
|
_DEFAULT_LOG_DATE_FORMAT = "%Y-%m-%d %H:%M:%S"
|
||||||
|
|
||||||
|
common_cli_opts = [
|
||||||
|
cfg.BoolOpt('debug',
|
||||||
|
short='d',
|
||||||
|
default=False,
|
||||||
|
help='Print debugging output (set logging level to '
|
||||||
|
'DEBUG instead of default WARNING level).'),
|
||||||
|
cfg.BoolOpt('verbose',
|
||||||
|
short='v',
|
||||||
|
default=False,
|
||||||
|
help='Print more verbose output (set logging level to '
|
||||||
|
'INFO instead of default WARNING level).'),
|
||||||
|
]
|
||||||
|
|
||||||
|
logging_cli_opts = [
|
||||||
|
cfg.StrOpt('log-config',
|
||||||
|
metavar='PATH',
|
||||||
|
help='If this option is specified, the logging configuration '
|
||||||
|
'file specified is used and overrides any other logging '
|
||||||
|
'options specified. Please see the Python logging module '
|
||||||
|
'documentation for details on logging configuration '
|
||||||
|
'files.'),
|
||||||
|
cfg.StrOpt('log-format',
|
||||||
|
default=None,
|
||||||
|
metavar='FORMAT',
|
||||||
|
help='A logging.Formatter log message format string which may '
|
||||||
|
'use any of the available logging.LogRecord attributes. '
|
||||||
|
'This option is deprecated. Please use '
|
||||||
|
'logging_context_format_string and '
|
||||||
|
'logging_default_format_string instead.'),
|
||||||
|
cfg.StrOpt('log-date-format',
|
||||||
|
default=_DEFAULT_LOG_DATE_FORMAT,
|
||||||
|
metavar='DATE_FORMAT',
|
||||||
|
help='Format string for %%(asctime)s in log records. '
|
||||||
|
'Default: %(default)s'),
|
||||||
|
cfg.StrOpt('log-file',
|
||||||
|
metavar='PATH',
|
||||||
|
deprecated_name='logfile',
|
||||||
|
help='(Optional) Name of log file to output to. '
|
||||||
|
'If no default is set, logging will go to stdout.'),
|
||||||
|
cfg.StrOpt('log-dir',
|
||||||
|
deprecated_name='logdir',
|
||||||
|
help='(Optional) The base directory used for relative '
|
||||||
|
'--log-file paths'),
|
||||||
|
cfg.BoolOpt('use-syslog',
|
||||||
|
default=False,
|
||||||
|
help='Use syslog for logging.'),
|
||||||
|
cfg.StrOpt('syslog-log-facility',
|
||||||
|
default='LOG_USER',
|
||||||
|
help='syslog facility to receive log lines')
|
||||||
|
]
|
||||||
|
|
||||||
|
generic_log_opts = [
|
||||||
|
cfg.BoolOpt('use_stderr',
|
||||||
|
default=True,
|
||||||
|
help='Log output to standard error')
|
||||||
|
]
|
||||||
|
|
||||||
|
log_opts = [
|
||||||
|
cfg.StrOpt('logging_context_format_string',
|
||||||
|
default='%(asctime)s.%(msecs)03d %(process)d %(levelname)s '
|
||||||
|
'%(name)s [%(request_id)s %(user)s %(tenant)s] '
|
||||||
|
'%(instance)s%(message)s',
|
||||||
|
help='format string to use for log messages with context'),
|
||||||
|
cfg.StrOpt('logging_default_format_string',
|
||||||
|
default='%(asctime)s.%(msecs)03d %(process)d %(levelname)s '
|
||||||
|
'%(name)s [-] %(instance)s%(message)s',
|
||||||
|
help='format string to use for log messages without context'),
|
||||||
|
cfg.StrOpt('logging_debug_format_suffix',
|
||||||
|
default='%(funcName)s %(pathname)s:%(lineno)d',
|
||||||
|
help='data to append to log format when level is DEBUG'),
|
||||||
|
cfg.StrOpt('logging_exception_prefix',
|
||||||
|
default='%(asctime)s.%(msecs)03d %(process)d TRACE %(name)s '
|
||||||
|
'%(instance)s',
|
||||||
|
help='prefix each line of exception output with this format'),
|
||||||
|
cfg.ListOpt('default_log_levels',
|
||||||
|
default=[
|
||||||
|
'amqplib=WARN',
|
||||||
|
'sqlalchemy=WARN',
|
||||||
|
'boto=WARN',
|
||||||
|
'suds=INFO',
|
||||||
|
'keystone=INFO',
|
||||||
|
'eventlet.wsgi.server=WARN'
|
||||||
|
],
|
||||||
|
help='list of logger=LEVEL pairs'),
|
||||||
|
cfg.BoolOpt('publish_errors',
|
||||||
|
default=False,
|
||||||
|
help='publish error events'),
|
||||||
|
cfg.BoolOpt('fatal_deprecations',
|
||||||
|
default=False,
|
||||||
|
help='make deprecations fatal'),
|
||||||
|
|
||||||
|
# NOTE(mikal): there are two options here because sometimes we are handed
|
||||||
|
# a full instance (and could include more information), and other times we
|
||||||
|
# are just handed a UUID for the instance.
|
||||||
|
cfg.StrOpt('instance_format',
|
||||||
|
default='[instance: %(uuid)s] ',
|
||||||
|
help='If an instance is passed with the log message, format '
|
||||||
|
'it like this'),
|
||||||
|
cfg.StrOpt('instance_uuid_format',
|
||||||
|
default='[instance: %(uuid)s] ',
|
||||||
|
help='If an instance UUID is passed with the log message, '
|
||||||
|
'format it like this'),
|
||||||
|
]
|
||||||
|
|
||||||
|
CONF = cfg.CONF
|
||||||
|
CONF.register_cli_opts(common_cli_opts)
|
||||||
|
CONF.register_cli_opts(logging_cli_opts)
|
||||||
|
CONF.register_opts(generic_log_opts)
|
||||||
|
CONF.register_opts(log_opts)
|
||||||
|
|
||||||
|
# our new audit level
|
||||||
|
# NOTE(jkoelker) Since we synthesized an audit level, make the logging
|
||||||
|
# module aware of it so it acts like other levels.
|
||||||
|
logging.AUDIT = logging.INFO + 1
|
||||||
|
logging.addLevelName(logging.AUDIT, 'AUDIT')
|
||||||
|
|
||||||
|
|
||||||
|
try:
|
||||||
|
NullHandler = logging.NullHandler
|
||||||
|
except AttributeError: # NOTE(jkoelker) NullHandler added in Python 2.7
|
||||||
|
class NullHandler(logging.Handler):
|
||||||
|
def handle(self, record):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def emit(self, record):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def createLock(self):
|
||||||
|
self.lock = None
|
||||||
|
|
||||||
|
|
||||||
|
def _dictify_context(context):
|
||||||
|
if context is None:
|
||||||
|
return None
|
||||||
|
if not isinstance(context, dict) and getattr(context, 'to_dict', None):
|
||||||
|
context = context.to_dict()
|
||||||
|
return context
|
||||||
|
|
||||||
|
|
||||||
|
def _get_binary_name():
|
||||||
|
return os.path.basename(inspect.stack()[-1][1])
|
||||||
|
|
||||||
|
|
||||||
|
def _get_log_file_path(binary=None):
|
||||||
|
logfile = CONF.log_file
|
||||||
|
logdir = CONF.log_dir
|
||||||
|
|
||||||
|
if logfile and not logdir:
|
||||||
|
return logfile
|
||||||
|
|
||||||
|
if logfile and logdir:
|
||||||
|
return os.path.join(logdir, logfile)
|
||||||
|
|
||||||
|
if logdir:
|
||||||
|
binary = binary or _get_binary_name()
|
||||||
|
return '%s.log' % (os.path.join(logdir, binary),)
|
||||||
|
|
||||||
|
|
||||||
|
class BaseLoggerAdapter(logging.LoggerAdapter):
|
||||||
|
|
||||||
|
def audit(self, msg, *args, **kwargs):
|
||||||
|
self.log(logging.AUDIT, msg, *args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class LazyAdapter(BaseLoggerAdapter):
|
||||||
|
def __init__(self, name='unknown', version='unknown'):
|
||||||
|
self._logger = None
|
||||||
|
self.extra = {}
|
||||||
|
self.name = name
|
||||||
|
self.version = version
|
||||||
|
|
||||||
|
@property
|
||||||
|
def logger(self):
|
||||||
|
if not self._logger:
|
||||||
|
self._logger = getLogger(self.name, self.version)
|
||||||
|
return self._logger
|
||||||
|
|
||||||
|
|
||||||
|
class ContextAdapter(BaseLoggerAdapter):
|
||||||
|
warn = logging.LoggerAdapter.warning
|
||||||
|
|
||||||
|
def __init__(self, logger, project_name, version_string):
|
||||||
|
self.logger = logger
|
||||||
|
self.project = project_name
|
||||||
|
self.version = version_string
|
||||||
|
|
||||||
|
@property
|
||||||
|
def handlers(self):
|
||||||
|
return self.logger.handlers
|
||||||
|
|
||||||
|
def deprecated(self, msg, *args, **kwargs):
|
||||||
|
stdmsg = _("Deprecated: %s") % msg
|
||||||
|
if CONF.fatal_deprecations:
|
||||||
|
self.critical(stdmsg, *args, **kwargs)
|
||||||
|
raise DeprecatedConfig(msg=stdmsg)
|
||||||
|
else:
|
||||||
|
self.warn(stdmsg, *args, **kwargs)
|
||||||
|
|
||||||
|
def process(self, msg, kwargs):
|
||||||
|
if 'extra' not in kwargs:
|
||||||
|
kwargs['extra'] = {}
|
||||||
|
extra = kwargs['extra']
|
||||||
|
|
||||||
|
context = kwargs.pop('context', None)
|
||||||
|
# if not context:
|
||||||
|
# context = getattr(local.store, 'context', None)
|
||||||
|
if context:
|
||||||
|
extra.update(_dictify_context(context))
|
||||||
|
|
||||||
|
instance = kwargs.pop('instance', None)
|
||||||
|
instance_extra = ''
|
||||||
|
if instance:
|
||||||
|
instance_extra = CONF.instance_format % instance
|
||||||
|
else:
|
||||||
|
instance_uuid = kwargs.pop('instance_uuid', None)
|
||||||
|
if instance_uuid:
|
||||||
|
instance_extra = (CONF.instance_uuid_format
|
||||||
|
% {'uuid': instance_uuid})
|
||||||
|
extra.update({'instance': instance_extra})
|
||||||
|
|
||||||
|
extra.update({"project": self.project})
|
||||||
|
extra.update({"version": self.version})
|
||||||
|
extra['extra'] = extra.copy()
|
||||||
|
return msg, kwargs
|
||||||
|
|
||||||
|
|
||||||
|
class JSONFormatter(logging.Formatter):
|
||||||
|
def __init__(self, fmt=None, datefmt=None):
|
||||||
|
# NOTE(jkoelker) we ignore the fmt argument, but its still there
|
||||||
|
# since logging.config.fileConfig passes it.
|
||||||
|
self.datefmt = datefmt
|
||||||
|
|
||||||
|
def formatException(self, ei, strip_newlines=True):
|
||||||
|
lines = traceback.format_exception(*ei)
|
||||||
|
if strip_newlines:
|
||||||
|
lines = [itertools.ifilter(
|
||||||
|
lambda x: x,
|
||||||
|
line.rstrip().splitlines()) for line in lines]
|
||||||
|
lines = list(itertools.chain(*lines))
|
||||||
|
return lines
|
||||||
|
|
||||||
|
def format(self, record):
|
||||||
|
message = {'message': record.getMessage(),
|
||||||
|
'asctime': self.formatTime(record, self.datefmt),
|
||||||
|
'name': record.name,
|
||||||
|
'msg': record.msg,
|
||||||
|
'args': record.args,
|
||||||
|
'levelname': record.levelname,
|
||||||
|
'levelno': record.levelno,
|
||||||
|
'pathname': record.pathname,
|
||||||
|
'filename': record.filename,
|
||||||
|
'module': record.module,
|
||||||
|
'lineno': record.lineno,
|
||||||
|
'funcname': record.funcName,
|
||||||
|
'created': record.created,
|
||||||
|
'msecs': record.msecs,
|
||||||
|
'relative_created': record.relativeCreated,
|
||||||
|
'thread': record.thread,
|
||||||
|
'thread_name': record.threadName,
|
||||||
|
'process_name': record.processName,
|
||||||
|
'process': record.process,
|
||||||
|
'traceback': None}
|
||||||
|
|
||||||
|
if hasattr(record, 'extra'):
|
||||||
|
message['extra'] = record.extra
|
||||||
|
|
||||||
|
if record.exc_info:
|
||||||
|
message['traceback'] = self.formatException(record.exc_info)
|
||||||
|
|
||||||
|
return jsonutils.dumps(message)
|
||||||
|
|
||||||
|
|
||||||
|
def _create_logging_excepthook(product_name):
|
||||||
|
def logging_excepthook(type, value, tb):
|
||||||
|
extra = {}
|
||||||
|
if CONF.verbose:
|
||||||
|
extra['exc_info'] = (type, value, tb)
|
||||||
|
getLogger(product_name).critical(str(value), **extra)
|
||||||
|
return logging_excepthook
|
||||||
|
|
||||||
|
|
||||||
|
class LogConfigError(Exception):
|
||||||
|
|
||||||
|
message = ('Error loading logging config %(log_config)s: %(err_msg)s')
|
||||||
|
|
||||||
|
def __init__(self, log_config, err_msg):
|
||||||
|
self.log_config = log_config
|
||||||
|
self.err_msg = err_msg
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.message % dict(log_config=self.log_config,
|
||||||
|
err_msg=self.err_msg)
|
||||||
|
|
||||||
|
|
||||||
|
def _load_log_config(log_config):
|
||||||
|
try:
|
||||||
|
logging.config.fileConfig(log_config)
|
||||||
|
except ConfigParser.Error as exc:
|
||||||
|
raise LogConfigError(log_config, str(exc))
|
||||||
|
|
||||||
|
|
||||||
|
def setup(product_name):
|
||||||
|
"""Setup logging."""
|
||||||
|
if CONF.log_config:
|
||||||
|
_load_log_config(CONF.log_config)
|
||||||
|
else:
|
||||||
|
_setup_logging_from_conf()
|
||||||
|
sys.excepthook = _create_logging_excepthook(product_name)
|
||||||
|
|
||||||
|
|
||||||
|
def set_defaults(logging_context_format_string):
|
||||||
|
cfg.set_defaults(log_opts,
|
||||||
|
logging_context_format_string=
|
||||||
|
logging_context_format_string)
|
||||||
|
|
||||||
|
|
||||||
|
def _find_facility_from_conf():
|
||||||
|
facility_names = logging.handlers.SysLogHandler.facility_names
|
||||||
|
facility = getattr(logging.handlers.SysLogHandler,
|
||||||
|
CONF.syslog_log_facility,
|
||||||
|
None)
|
||||||
|
|
||||||
|
if facility is None and CONF.syslog_log_facility in facility_names:
|
||||||
|
facility = facility_names.get(CONF.syslog_log_facility)
|
||||||
|
|
||||||
|
if facility is None:
|
||||||
|
valid_facilities = facility_names.keys()
|
||||||
|
consts = ['LOG_AUTH', 'LOG_AUTHPRIV', 'LOG_CRON', 'LOG_DAEMON',
|
||||||
|
'LOG_FTP', 'LOG_KERN', 'LOG_LPR', 'LOG_MAIL', 'LOG_NEWS',
|
||||||
|
'LOG_AUTH', 'LOG_SYSLOG', 'LOG_USER', 'LOG_UUCP',
|
||||||
|
'LOG_LOCAL0', 'LOG_LOCAL1', 'LOG_LOCAL2', 'LOG_LOCAL3',
|
||||||
|
'LOG_LOCAL4', 'LOG_LOCAL5', 'LOG_LOCAL6', 'LOG_LOCAL7']
|
||||||
|
valid_facilities.extend(consts)
|
||||||
|
raise TypeError(('syslog facility must be one of: %s') %
|
||||||
|
', '.join("'%s'" % fac
|
||||||
|
for fac in valid_facilities))
|
||||||
|
|
||||||
|
return facility
|
||||||
|
|
||||||
|
|
||||||
|
def _setup_logging_from_conf():
|
||||||
|
log_root = getLogger(None).logger
|
||||||
|
for handler in log_root.handlers:
|
||||||
|
log_root.removeHandler(handler)
|
||||||
|
|
||||||
|
if CONF.use_syslog:
|
||||||
|
facility = _find_facility_from_conf()
|
||||||
|
syslog = logging.handlers.SysLogHandler(address='/dev/log',
|
||||||
|
facility=facility)
|
||||||
|
log_root.addHandler(syslog)
|
||||||
|
|
||||||
|
logpath = _get_log_file_path()
|
||||||
|
if logpath:
|
||||||
|
filelog = logging.handlers.WatchedFileHandler(logpath)
|
||||||
|
log_root.addHandler(filelog)
|
||||||
|
|
||||||
|
if CONF.use_stderr:
|
||||||
|
streamlog = ColorHandler()
|
||||||
|
log_root.addHandler(streamlog)
|
||||||
|
|
||||||
|
elif not CONF.log_file:
|
||||||
|
# pass sys.stdout as a positional argument
|
||||||
|
# python2.6 calls the argument strm, in 2.7 it's stream
|
||||||
|
streamlog = logging.StreamHandler(sys.stdout)
|
||||||
|
log_root.addHandler(streamlog)
|
||||||
|
|
||||||
|
if CONF.publish_errors:
|
||||||
|
handler = importutils.import_object(
|
||||||
|
"quantum.openstack.common.log_handler.PublishErrorsHandler",
|
||||||
|
logging.ERROR)
|
||||||
|
log_root.addHandler(handler)
|
||||||
|
|
||||||
|
datefmt = CONF.log_date_format
|
||||||
|
for handler in log_root.handlers:
|
||||||
|
# NOTE(alaski): CONF.log_format overrides everything currently. This
|
||||||
|
# should be deprecated in favor of context aware formatting.
|
||||||
|
if CONF.log_format:
|
||||||
|
handler.setFormatter(logging.Formatter(fmt=CONF.log_format,
|
||||||
|
datefmt=datefmt))
|
||||||
|
log_root.info('Deprecated: log_format is now deprecated and will '
|
||||||
|
'be removed in the next release')
|
||||||
|
else:
|
||||||
|
handler.setFormatter(ContextFormatter(datefmt=datefmt))
|
||||||
|
|
||||||
|
if CONF.debug:
|
||||||
|
log_root.setLevel(logging.DEBUG)
|
||||||
|
elif CONF.verbose:
|
||||||
|
log_root.setLevel(logging.INFO)
|
||||||
|
else:
|
||||||
|
log_root.setLevel(logging.WARNING)
|
||||||
|
|
||||||
|
for pair in CONF.default_log_levels:
|
||||||
|
mod, _sep, level_name = pair.partition('=')
|
||||||
|
level = logging.getLevelName(level_name)
|
||||||
|
logger = logging.getLogger(mod)
|
||||||
|
logger.setLevel(level)
|
||||||
|
|
||||||
|
_loggers = {}
|
||||||
|
|
||||||
|
|
||||||
|
def getLogger(name='unknown', version='unknown'):
|
||||||
|
if name not in _loggers:
|
||||||
|
_loggers[name] = ContextAdapter(logging.getLogger(name),
|
||||||
|
name,
|
||||||
|
version)
|
||||||
|
return _loggers[name]
|
||||||
|
|
||||||
|
|
||||||
|
def getLazyLogger(name='unknown', version='unknown'):
|
||||||
|
"""Returns lazy logger.
|
||||||
|
|
||||||
|
Creates a pass-through logger that does not create the real logger
|
||||||
|
until it is really needed and delegates all calls to the real logger
|
||||||
|
once it is created.
|
||||||
|
"""
|
||||||
|
return LazyAdapter(name, version)
|
||||||
|
|
||||||
|
|
||||||
|
class WritableLogger(object):
|
||||||
|
"""A thin wrapper that responds to `write` and logs."""
|
||||||
|
|
||||||
|
def __init__(self, logger, level=logging.INFO):
|
||||||
|
self.logger = logger
|
||||||
|
self.level = level
|
||||||
|
|
||||||
|
def write(self, msg):
|
||||||
|
self.logger.log(self.level, msg)
|
||||||
|
|
||||||
|
|
||||||
|
class ContextFormatter(logging.Formatter):
|
||||||
|
"""A context.RequestContext aware formatter configured through flags.
|
||||||
|
|
||||||
|
The flags used to set format strings are: logging_context_format_string
|
||||||
|
and logging_default_format_string. You can also specify
|
||||||
|
logging_debug_format_suffix to append extra formatting if the log level is
|
||||||
|
debug.
|
||||||
|
|
||||||
|
For information about what variables are available for the formatter see:
|
||||||
|
http://docs.python.org/library/logging.html#formatter
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
def format(self, record):
|
||||||
|
"""Uses contextstring if request_id is set, otherwise default."""
|
||||||
|
# NOTE(sdague): default the fancier formating params
|
||||||
|
# to an empty string so we don't throw an exception if
|
||||||
|
# they get used
|
||||||
|
for key in ('instance', 'color'):
|
||||||
|
if key not in record.__dict__:
|
||||||
|
record.__dict__[key] = ''
|
||||||
|
|
||||||
|
if record.__dict__.get('request_id', None):
|
||||||
|
self._fmt = CONF.logging_context_format_string
|
||||||
|
else:
|
||||||
|
self._fmt = CONF.logging_default_format_string
|
||||||
|
|
||||||
|
if (record.levelno == logging.DEBUG and
|
||||||
|
CONF.logging_debug_format_suffix):
|
||||||
|
self._fmt += " " + CONF.logging_debug_format_suffix
|
||||||
|
|
||||||
|
# Cache this on the record, Logger will respect our formated copy
|
||||||
|
if record.exc_info:
|
||||||
|
record.exc_text = self.formatException(record.exc_info, record)
|
||||||
|
return logging.Formatter.format(self, record)
|
||||||
|
|
||||||
|
def formatException(self, exc_info, record=None):
|
||||||
|
"""Format exception output with CONF.logging_exception_prefix."""
|
||||||
|
if not record:
|
||||||
|
return logging.Formatter.formatException(self, exc_info)
|
||||||
|
|
||||||
|
stringbuffer = cStringIO.StringIO()
|
||||||
|
traceback.print_exception(exc_info[0], exc_info[1], exc_info[2],
|
||||||
|
None, stringbuffer)
|
||||||
|
lines = stringbuffer.getvalue().split('\n')
|
||||||
|
stringbuffer.close()
|
||||||
|
|
||||||
|
if CONF.logging_exception_prefix.find('%(asctime)') != -1:
|
||||||
|
record.asctime = self.formatTime(record, self.datefmt)
|
||||||
|
|
||||||
|
formatted_lines = []
|
||||||
|
for line in lines:
|
||||||
|
pl = CONF.logging_exception_prefix % record.__dict__
|
||||||
|
fl = '%s%s' % (pl, line)
|
||||||
|
formatted_lines.append(fl)
|
||||||
|
return '\n'.join(formatted_lines)
|
||||||
|
|
||||||
|
|
||||||
|
class ColorHandler(logging.StreamHandler):
|
||||||
|
LEVEL_COLORS = {
|
||||||
|
logging.DEBUG: '\033[00;32m', # GREEN
|
||||||
|
logging.INFO: '\033[00;36m', # CYAN
|
||||||
|
logging.AUDIT: '\033[01;36m', # BOLD CYAN
|
||||||
|
logging.WARN: '\033[01;33m', # BOLD YELLOW
|
||||||
|
logging.ERROR: '\033[01;31m', # BOLD RED
|
||||||
|
logging.CRITICAL: '\033[01;31m', # BOLD RED
|
||||||
|
}
|
||||||
|
|
||||||
|
def format(self, record):
|
||||||
|
record.color = self.LEVEL_COLORS[record.levelno]
|
||||||
|
return logging.StreamHandler.format(self, record)
|
||||||
|
|
||||||
|
|
||||||
|
class DeprecatedConfig(Exception):
|
||||||
|
message = ("Fatal call to deprecated config: %(msg)s")
|
||||||
|
|
||||||
|
def __init__(self, msg):
|
||||||
|
super(Exception, self).__init__(self.message % dict(msg=msg))
|
187
stackalytics/openstack/common/timeutils.py
Normal file
187
stackalytics/openstack/common/timeutils.py
Normal file
@ -0,0 +1,187 @@
|
|||||||
|
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||||
|
|
||||||
|
# Copyright 2011 OpenStack Foundation.
|
||||||
|
# All Rights Reserved.
|
||||||
|
#
|
||||||
|
# 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.
|
||||||
|
|
||||||
|
"""
|
||||||
|
Time related utilities and helper functions.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import calendar
|
||||||
|
import datetime
|
||||||
|
|
||||||
|
import iso8601
|
||||||
|
|
||||||
|
|
||||||
|
# ISO 8601 extended time format with microseconds
|
||||||
|
_ISO8601_TIME_FORMAT_SUBSECOND = '%Y-%m-%dT%H:%M:%S.%f'
|
||||||
|
_ISO8601_TIME_FORMAT = '%Y-%m-%dT%H:%M:%S'
|
||||||
|
PERFECT_TIME_FORMAT = _ISO8601_TIME_FORMAT_SUBSECOND
|
||||||
|
|
||||||
|
|
||||||
|
def isotime(at=None, subsecond=False):
|
||||||
|
"""Stringify time in ISO 8601 format."""
|
||||||
|
if not at:
|
||||||
|
at = utcnow()
|
||||||
|
st = at.strftime(_ISO8601_TIME_FORMAT
|
||||||
|
if not subsecond
|
||||||
|
else _ISO8601_TIME_FORMAT_SUBSECOND)
|
||||||
|
tz = at.tzinfo.tzname(None) if at.tzinfo else 'UTC'
|
||||||
|
st += ('Z' if tz == 'UTC' else tz)
|
||||||
|
return st
|
||||||
|
|
||||||
|
|
||||||
|
def parse_isotime(timestr):
|
||||||
|
"""Parse time from ISO 8601 format."""
|
||||||
|
try:
|
||||||
|
return iso8601.parse_date(timestr)
|
||||||
|
except iso8601.ParseError as e:
|
||||||
|
raise ValueError(e.message)
|
||||||
|
except TypeError as e:
|
||||||
|
raise ValueError(e.message)
|
||||||
|
|
||||||
|
|
||||||
|
def strtime(at=None, fmt=PERFECT_TIME_FORMAT):
|
||||||
|
"""Returns formatted utcnow."""
|
||||||
|
if not at:
|
||||||
|
at = utcnow()
|
||||||
|
return at.strftime(fmt)
|
||||||
|
|
||||||
|
|
||||||
|
def parse_strtime(timestr, fmt=PERFECT_TIME_FORMAT):
|
||||||
|
"""Turn a formatted time back into a datetime."""
|
||||||
|
return datetime.datetime.strptime(timestr, fmt)
|
||||||
|
|
||||||
|
|
||||||
|
def normalize_time(timestamp):
|
||||||
|
"""Normalize time in arbitrary timezone to UTC naive object."""
|
||||||
|
offset = timestamp.utcoffset()
|
||||||
|
if offset is None:
|
||||||
|
return timestamp
|
||||||
|
return timestamp.replace(tzinfo=None) - offset
|
||||||
|
|
||||||
|
|
||||||
|
def is_older_than(before, seconds):
|
||||||
|
"""Return True if before is older than seconds."""
|
||||||
|
if isinstance(before, basestring):
|
||||||
|
before = parse_strtime(before).replace(tzinfo=None)
|
||||||
|
return utcnow() - before > datetime.timedelta(seconds=seconds)
|
||||||
|
|
||||||
|
|
||||||
|
def is_newer_than(after, seconds):
|
||||||
|
"""Return True if after is newer than seconds."""
|
||||||
|
if isinstance(after, basestring):
|
||||||
|
after = parse_strtime(after).replace(tzinfo=None)
|
||||||
|
return after - utcnow() > datetime.timedelta(seconds=seconds)
|
||||||
|
|
||||||
|
|
||||||
|
def utcnow_ts():
|
||||||
|
"""Timestamp version of our utcnow function."""
|
||||||
|
return calendar.timegm(utcnow().timetuple())
|
||||||
|
|
||||||
|
|
||||||
|
def utcnow():
|
||||||
|
"""Overridable version of utils.utcnow."""
|
||||||
|
if utcnow.override_time:
|
||||||
|
try:
|
||||||
|
return utcnow.override_time.pop(0)
|
||||||
|
except AttributeError:
|
||||||
|
return utcnow.override_time
|
||||||
|
return datetime.datetime.utcnow()
|
||||||
|
|
||||||
|
|
||||||
|
def iso8601_from_timestamp(timestamp):
|
||||||
|
"""Returns a iso8601 formated date from timestamp."""
|
||||||
|
return isotime(datetime.datetime.utcfromtimestamp(timestamp))
|
||||||
|
|
||||||
|
|
||||||
|
utcnow.override_time = None
|
||||||
|
|
||||||
|
|
||||||
|
def set_time_override(override_time=datetime.datetime.utcnow()):
|
||||||
|
"""Overrides utils.utcnow.
|
||||||
|
|
||||||
|
Make it return a constant time or a list thereof, one at a time.
|
||||||
|
"""
|
||||||
|
utcnow.override_time = override_time
|
||||||
|
|
||||||
|
|
||||||
|
def advance_time_delta(timedelta):
|
||||||
|
"""Advance overridden time using a datetime.timedelta."""
|
||||||
|
assert(not utcnow.override_time is None)
|
||||||
|
try:
|
||||||
|
for dt in utcnow.override_time:
|
||||||
|
dt += timedelta
|
||||||
|
except TypeError:
|
||||||
|
utcnow.override_time += timedelta
|
||||||
|
|
||||||
|
|
||||||
|
def advance_time_seconds(seconds):
|
||||||
|
"""Advance overridden time by seconds."""
|
||||||
|
advance_time_delta(datetime.timedelta(0, seconds))
|
||||||
|
|
||||||
|
|
||||||
|
def clear_time_override():
|
||||||
|
"""Remove the overridden time."""
|
||||||
|
utcnow.override_time = None
|
||||||
|
|
||||||
|
|
||||||
|
def marshall_now(now=None):
|
||||||
|
"""Make an rpc-safe datetime with microseconds.
|
||||||
|
|
||||||
|
Note: tzinfo is stripped, but not required for relative times.
|
||||||
|
"""
|
||||||
|
if not now:
|
||||||
|
now = utcnow()
|
||||||
|
return dict(day=now.day, month=now.month, year=now.year, hour=now.hour,
|
||||||
|
minute=now.minute, second=now.second,
|
||||||
|
microsecond=now.microsecond)
|
||||||
|
|
||||||
|
|
||||||
|
def unmarshall_time(tyme):
|
||||||
|
"""Unmarshall a datetime dict."""
|
||||||
|
return datetime.datetime(day=tyme['day'],
|
||||||
|
month=tyme['month'],
|
||||||
|
year=tyme['year'],
|
||||||
|
hour=tyme['hour'],
|
||||||
|
minute=tyme['minute'],
|
||||||
|
second=tyme['second'],
|
||||||
|
microsecond=tyme['microsecond'])
|
||||||
|
|
||||||
|
|
||||||
|
def delta_seconds(before, after):
|
||||||
|
"""Return the difference between two timing objects.
|
||||||
|
|
||||||
|
Compute the difference in seconds between two date, time, or
|
||||||
|
datetime objects (as a float, to microsecond resolution).
|
||||||
|
"""
|
||||||
|
delta = after - before
|
||||||
|
try:
|
||||||
|
return delta.total_seconds()
|
||||||
|
except AttributeError:
|
||||||
|
return ((delta.days * 24 * 3600) + delta.seconds +
|
||||||
|
float(delta.microseconds) / (10 ** 6))
|
||||||
|
|
||||||
|
|
||||||
|
def is_soon(dt, window):
|
||||||
|
"""Determines if time is going to happen in the next window seconds.
|
||||||
|
|
||||||
|
:params dt: the time
|
||||||
|
:params window: minimum seconds to remain to consider the time not soon
|
||||||
|
|
||||||
|
:return: True if expiration is within the given duration
|
||||||
|
"""
|
||||||
|
soon = (utcnow() + datetime.timedelta(seconds=window))
|
||||||
|
return normalize_time(dt) <= soon
|
1
stackalytics/processor/__init__.py
Normal file
1
stackalytics/processor/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
__author__ = 'ishakhat'
|
177
stackalytics/processor/commit_processor.py
Normal file
177
stackalytics/processor/commit_processor.py
Normal file
@ -0,0 +1,177 @@
|
|||||||
|
# Copyright (c) 2013 Mirantis Inc.
|
||||||
|
#
|
||||||
|
# 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.
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import re
|
||||||
|
|
||||||
|
from launchpadlib import launchpad
|
||||||
|
from oslo.config import cfg
|
||||||
|
|
||||||
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
COMMIT_PROCESSOR_DUMMY = 0
|
||||||
|
COMMIT_PROCESSOR_CACHED = 1
|
||||||
|
|
||||||
|
|
||||||
|
class CommitProcessor(object):
|
||||||
|
def __init__(self, persistent_storage):
|
||||||
|
self.persistent_storage = persistent_storage
|
||||||
|
|
||||||
|
def process(self, commit_iterator):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class DummyProcessor(CommitProcessor):
|
||||||
|
def __init__(self, persistent_storage):
|
||||||
|
super(DummyProcessor, self).__init__(persistent_storage)
|
||||||
|
|
||||||
|
def process(self, commit_iterator):
|
||||||
|
return commit_iterator
|
||||||
|
|
||||||
|
|
||||||
|
class CachedProcessor(CommitProcessor):
|
||||||
|
def __init__(self, persistent_storage):
|
||||||
|
super(CachedProcessor, self).__init__(persistent_storage)
|
||||||
|
|
||||||
|
companies = persistent_storage.get_companies()
|
||||||
|
self.domains_index = {}
|
||||||
|
for company in companies:
|
||||||
|
for domain in company['domains']:
|
||||||
|
self.domains_index[domain] = company['company_name']
|
||||||
|
|
||||||
|
users = persistent_storage.get_users()
|
||||||
|
self.users_index = {}
|
||||||
|
for user in users:
|
||||||
|
for email in user['emails']:
|
||||||
|
self.users_index[email] = user
|
||||||
|
|
||||||
|
LOG.debug('Cached commit processor is instantiated')
|
||||||
|
|
||||||
|
def _find_company(self, companies, date):
|
||||||
|
for r in companies:
|
||||||
|
if date < r['end_date']:
|
||||||
|
return r['company_name']
|
||||||
|
return companies[-1]['company_name']
|
||||||
|
|
||||||
|
def _get_company_by_email(self, email):
|
||||||
|
name, at, domain = email.partition('@')
|
||||||
|
if domain:
|
||||||
|
parts = domain.split('.')
|
||||||
|
for i in range(len(parts), 1, -1):
|
||||||
|
m = '.'.join(parts[len(parts) - i:])
|
||||||
|
if m in self.domains_index:
|
||||||
|
return self.domains_index[m]
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _unknown_user_email(self, email):
|
||||||
|
|
||||||
|
lp_profile = None
|
||||||
|
if not re.match(r'[^@]+@[^@]+\.[^@]+', email):
|
||||||
|
LOG.debug('User email is not valid %s' % email)
|
||||||
|
else:
|
||||||
|
LOG.debug('Lookup user email %s at Launchpad' % email)
|
||||||
|
lp = launchpad.Launchpad.login_anonymously(cfg.CONF.launchpad_user)
|
||||||
|
try:
|
||||||
|
lp_profile = lp.people.getByEmail(email=email)
|
||||||
|
except Exception as error:
|
||||||
|
LOG.warn('Lookup of email %s failed %s' %
|
||||||
|
(email, error.message))
|
||||||
|
if not lp_profile:
|
||||||
|
# user is not found in Launchpad, create dummy record for commit
|
||||||
|
# update
|
||||||
|
LOG.debug('Email is not found at Launchpad, mapping to nobody')
|
||||||
|
user = {
|
||||||
|
'launchpad_id': None,
|
||||||
|
'companies': [{
|
||||||
|
'company_name': self.domains_index[''],
|
||||||
|
'end_date': 0
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
# get user's launchpad id from his profile
|
||||||
|
launchpad_id = lp_profile.name
|
||||||
|
LOG.debug('Found user %s' % launchpad_id)
|
||||||
|
|
||||||
|
# check if user with launchpad_id exists in persistent storage
|
||||||
|
persistent_user_iterator = self.persistent_storage.get_users(
|
||||||
|
launchpad_id=launchpad_id)
|
||||||
|
|
||||||
|
for persistent_user in persistent_user_iterator:
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
persistent_user = None
|
||||||
|
|
||||||
|
if persistent_user:
|
||||||
|
# user already exist, merge
|
||||||
|
LOG.debug('User exists in persistent storage, add new email')
|
||||||
|
persistent_user_email = persistent_user['emails'][0]
|
||||||
|
if persistent_user_email not in self.users_index:
|
||||||
|
raise Exception('User index is not valid')
|
||||||
|
user = self.users_index[persistent_user_email]
|
||||||
|
user['emails'].append(email)
|
||||||
|
self.persistent_storage.update_user(user)
|
||||||
|
else:
|
||||||
|
# add new user
|
||||||
|
LOG.debug('Add new user into persistent storage')
|
||||||
|
company = (self._get_company_by_email(email) or
|
||||||
|
self.domains_index[''])
|
||||||
|
user = {
|
||||||
|
'launchpad_id': launchpad_id,
|
||||||
|
'user_name': lp_profile.display_name,
|
||||||
|
'emails': [email],
|
||||||
|
'companies': [{
|
||||||
|
'company_name': company,
|
||||||
|
'end_date': 0,
|
||||||
|
}],
|
||||||
|
}
|
||||||
|
self.persistent_storage.insert_user(user)
|
||||||
|
|
||||||
|
# update local index
|
||||||
|
self.users_index[email] = user
|
||||||
|
return user
|
||||||
|
|
||||||
|
def _update_commit_with_user_data(self, commit):
|
||||||
|
email = commit['author_email'].lower()
|
||||||
|
if email in self.users_index:
|
||||||
|
user = self.users_index[email]
|
||||||
|
else:
|
||||||
|
user = self._unknown_user_email(email)
|
||||||
|
commit['launchpad_id'] = user['launchpad_id']
|
||||||
|
company = self._get_company_by_email(email)
|
||||||
|
if not company:
|
||||||
|
company = self._find_company(user['companies'], commit['date'])
|
||||||
|
commit['company_name'] = company
|
||||||
|
|
||||||
|
def process(self, commit_iterator):
|
||||||
|
|
||||||
|
for commit in commit_iterator:
|
||||||
|
self._update_commit_with_user_data(commit)
|
||||||
|
|
||||||
|
yield commit
|
||||||
|
|
||||||
|
|
||||||
|
class CommitProcessorFactory(object):
|
||||||
|
@staticmethod
|
||||||
|
def get_processor(commit_processor_type, persistent_storage):
|
||||||
|
LOG.debug('Factory is asked for commit processor type %s' %
|
||||||
|
commit_processor_type)
|
||||||
|
if commit_processor_type == COMMIT_PROCESSOR_DUMMY:
|
||||||
|
return DummyProcessor(persistent_storage)
|
||||||
|
elif commit_processor_type == COMMIT_PROCESSOR_CACHED:
|
||||||
|
return CachedProcessor(persistent_storage)
|
||||||
|
else:
|
||||||
|
raise Exception('Unknown commit processor type %s' %
|
||||||
|
commit_processor_type)
|
164
stackalytics/processor/main.py
Normal file
164
stackalytics/processor/main.py
Normal file
@ -0,0 +1,164 @@
|
|||||||
|
# Copyright (c) 2013 Mirantis Inc.
|
||||||
|
#
|
||||||
|
# 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.
|
||||||
|
|
||||||
|
import time
|
||||||
|
|
||||||
|
from oslo.config import cfg
|
||||||
|
import psutil
|
||||||
|
from psutil import _error
|
||||||
|
import sh
|
||||||
|
|
||||||
|
from stackalytics.openstack.common import log as logging
|
||||||
|
from stackalytics.openstack.common.timeutils import iso8601_from_timestamp
|
||||||
|
from stackalytics.processor import commit_processor
|
||||||
|
from stackalytics.processor.persistent_storage import PersistentStorageFactory
|
||||||
|
from stackalytics.processor.runtime_storage import RuntimeStorageFactory
|
||||||
|
from stackalytics.processor.vcs import VcsFactory
|
||||||
|
|
||||||
|
|
||||||
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
OPTS = [
|
||||||
|
cfg.StrOpt('default-data', default='etc/default_data.json',
|
||||||
|
help='Default data'),
|
||||||
|
cfg.StrOpt('sources-root', default=None, required=True,
|
||||||
|
help='The folder that holds all project sources to analyze'),
|
||||||
|
cfg.StrOpt('runtime-storage-uri', default='memcached://127.0.0.1:11211',
|
||||||
|
help='Storage URI'),
|
||||||
|
cfg.StrOpt('frontend-update-address',
|
||||||
|
default='http://user:user@localhost/update/%s',
|
||||||
|
help='Address of update handler'),
|
||||||
|
cfg.StrOpt('repo-poll-period', default='300',
|
||||||
|
help='Repo poll period in seconds'),
|
||||||
|
cfg.StrOpt('persistent-storage-uri', default='mongodb://localhost',
|
||||||
|
help='URI of persistent storage'),
|
||||||
|
cfg.BoolOpt('sync-default-data', default=False,
|
||||||
|
help='Update persistent storage with default data. '
|
||||||
|
'Existing data is not overwritten'),
|
||||||
|
cfg.BoolOpt('force-sync-default-data', default=False,
|
||||||
|
help='Completely overwrite persistent storage with the '
|
||||||
|
'default data'),
|
||||||
|
cfg.StrOpt('launchpad-user', default='stackalytics-bot',
|
||||||
|
help='User to access Launchpad'),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def get_pids():
|
||||||
|
uwsgi_dict = {}
|
||||||
|
for pid in psutil.get_pid_list():
|
||||||
|
try:
|
||||||
|
p = psutil.Process(pid)
|
||||||
|
if p.cmdline and p.cmdline[0].find('/uwsgi '):
|
||||||
|
uwsgi_dict[p.pid] = p.parent
|
||||||
|
except _error.NoSuchProcess:
|
||||||
|
# the process may disappear after get_pid_list call, ignore it
|
||||||
|
pass
|
||||||
|
|
||||||
|
result = set()
|
||||||
|
for pid in uwsgi_dict:
|
||||||
|
if uwsgi_dict[pid] in uwsgi_dict:
|
||||||
|
result.add(pid)
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def update_pid(pid):
|
||||||
|
url = cfg.CONF.frontend_update_address % pid
|
||||||
|
sh.curl(url)
|
||||||
|
|
||||||
|
|
||||||
|
def update_pids(runtime_storage):
|
||||||
|
pids = get_pids()
|
||||||
|
if not pids:
|
||||||
|
return
|
||||||
|
runtime_storage.active_pids(pids)
|
||||||
|
current_time = time.time()
|
||||||
|
for pid in pids:
|
||||||
|
if current_time > runtime_storage.get_pid_update_time(pid):
|
||||||
|
update_pid(pid)
|
||||||
|
return current_time
|
||||||
|
|
||||||
|
|
||||||
|
def process_repo(repo, runtime_storage, processor):
|
||||||
|
uri = repo['uri']
|
||||||
|
LOG.debug('Processing repo uri %s' % uri)
|
||||||
|
|
||||||
|
vcs = VcsFactory.get_vcs(repo)
|
||||||
|
vcs.fetch()
|
||||||
|
|
||||||
|
for branch in repo['branches']:
|
||||||
|
LOG.debug('Processing repo %s, branch %s' % (uri, branch))
|
||||||
|
|
||||||
|
head_commit_id = runtime_storage.get_head_commit_id(uri, branch)
|
||||||
|
|
||||||
|
commit_iterator = vcs.log(branch, head_commit_id)
|
||||||
|
processed_commit_iterator = processor.process(commit_iterator)
|
||||||
|
runtime_storage.set_records(processed_commit_iterator)
|
||||||
|
|
||||||
|
head_commit_id = vcs.get_head_commit_id(branch)
|
||||||
|
runtime_storage.set_head_commit_id(uri, branch, head_commit_id)
|
||||||
|
|
||||||
|
|
||||||
|
def update_repos(runtime_storage, persistent_storage):
|
||||||
|
current_time = time.time()
|
||||||
|
repo_update_time = runtime_storage.get_repo_update_time()
|
||||||
|
|
||||||
|
if current_time < repo_update_time:
|
||||||
|
LOG.info('The next update is scheduled at %s. Skipping' %
|
||||||
|
iso8601_from_timestamp(repo_update_time))
|
||||||
|
return
|
||||||
|
|
||||||
|
repos = persistent_storage.get_repos()
|
||||||
|
processor = commit_processor.CommitProcessorFactory.get_processor(
|
||||||
|
commit_processor.COMMIT_PROCESSOR_CACHED,
|
||||||
|
persistent_storage)
|
||||||
|
|
||||||
|
for repo in repos:
|
||||||
|
process_repo(repo, runtime_storage, processor)
|
||||||
|
|
||||||
|
runtime_storage.set_repo_update_time(time.time() +
|
||||||
|
int(cfg.CONF.repo_poll_period))
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
# init conf and logging
|
||||||
|
conf = cfg.CONF
|
||||||
|
conf.register_cli_opts(OPTS)
|
||||||
|
conf.register_opts(OPTS)
|
||||||
|
conf()
|
||||||
|
|
||||||
|
logging.setup('stackalytics')
|
||||||
|
LOG.info('Logging enabled')
|
||||||
|
|
||||||
|
persistent_storage = PersistentStorageFactory.get_storage(
|
||||||
|
cfg.CONF.persistent_storage_uri)
|
||||||
|
|
||||||
|
if conf.sync_default_data or conf.force_sync_default_data:
|
||||||
|
LOG.info('Going to synchronize persistent storage with default data '
|
||||||
|
'from file %s' % cfg.CONF.default_data)
|
||||||
|
persistent_storage.sync(cfg.CONF.default_data,
|
||||||
|
force=conf.force_sync_default_data)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
runtime_storage = RuntimeStorageFactory.get_storage(
|
||||||
|
cfg.CONF.runtime_storage_uri)
|
||||||
|
|
||||||
|
update_pids(runtime_storage)
|
||||||
|
|
||||||
|
update_repos(runtime_storage, persistent_storage)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
150
stackalytics/processor/persistent_storage.py
Normal file
150
stackalytics/processor/persistent_storage.py
Normal file
@ -0,0 +1,150 @@
|
|||||||
|
# Copyright (c) 2013 Mirantis Inc.
|
||||||
|
#
|
||||||
|
# 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.
|
||||||
|
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import re
|
||||||
|
|
||||||
|
import pymongo
|
||||||
|
|
||||||
|
from stackalytics.processor.user_utils import normalize_user
|
||||||
|
|
||||||
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class PersistentStorage(object):
|
||||||
|
def __init__(self, uri):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def sync(self, default_data_file_name, force=False):
|
||||||
|
if force:
|
||||||
|
self.clean_all()
|
||||||
|
|
||||||
|
default_data = self._read_default_persistent_storage(
|
||||||
|
default_data_file_name)
|
||||||
|
|
||||||
|
self._build_index(default_data['repos'], 'uri',
|
||||||
|
self.get_repos, self.insert_repo)
|
||||||
|
self._build_index(default_data['companies'], 'company_name',
|
||||||
|
self.get_companies, self.insert_company)
|
||||||
|
self._build_index(default_data['users'], 'launchpad_id',
|
||||||
|
self.get_users, self.insert_user)
|
||||||
|
self._build_index(default_data['releases'], 'release_name',
|
||||||
|
self.get_releases, self.insert_release)
|
||||||
|
|
||||||
|
def _build_index(self, default_data, primary_key, getter, inserter):
|
||||||
|
# loads all items from persistent storage
|
||||||
|
existing_items = set([item[primary_key] for item in getter()])
|
||||||
|
# inserts items from default storage that are not in persistent storage
|
||||||
|
map(inserter, [item for item in default_data
|
||||||
|
if item[primary_key] not in existing_items])
|
||||||
|
|
||||||
|
def get_companies(self, **criteria):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def insert_company(self, company):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def get_repos(self, **criteria):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def insert_repo(self, repo):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def get_users(self, **criteria):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def insert_user(self, user):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def update_user(self, user):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def get_releases(self, **criteria):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def insert_release(self, release):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def clean_all(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _read_default_persistent_storage(self, file_name):
|
||||||
|
try:
|
||||||
|
with open(file_name, 'r') as content_file:
|
||||||
|
content = content_file.read()
|
||||||
|
return json.loads(content)
|
||||||
|
except Exception as e:
|
||||||
|
LOG.error('Error while reading config: %s' % e)
|
||||||
|
|
||||||
|
|
||||||
|
class MongodbStorage(PersistentStorage):
|
||||||
|
def __init__(self, uri):
|
||||||
|
super(MongodbStorage, self).__init__(uri)
|
||||||
|
|
||||||
|
self.client = pymongo.MongoClient(uri)
|
||||||
|
self.mongo = self.client.stackalytics
|
||||||
|
|
||||||
|
self.mongo.companies.create_index([("company", pymongo.ASCENDING)])
|
||||||
|
self.mongo.repos.create_index([("uri", pymongo.ASCENDING)])
|
||||||
|
self.mongo.users.create_index([("launchpad_id", pymongo.ASCENDING)])
|
||||||
|
self.mongo.releases.create_index([("releases", pymongo.ASCENDING)])
|
||||||
|
|
||||||
|
LOG.debug('Mongodb storage is created')
|
||||||
|
|
||||||
|
def clean_all(self):
|
||||||
|
LOG.debug('Clear all tables')
|
||||||
|
self.mongo.companies.remove()
|
||||||
|
self.mongo.repos.remove()
|
||||||
|
self.mongo.users.remove()
|
||||||
|
self.mongo.releases.remove()
|
||||||
|
|
||||||
|
def get_companies(self, **criteria):
|
||||||
|
return self.mongo.companies.find(criteria)
|
||||||
|
|
||||||
|
def insert_company(self, company):
|
||||||
|
self.mongo.companies.insert(company)
|
||||||
|
|
||||||
|
def get_repos(self, **criteria):
|
||||||
|
return self.mongo.repos.find(criteria)
|
||||||
|
|
||||||
|
def insert_repo(self, repo):
|
||||||
|
self.mongo.repos.insert(repo)
|
||||||
|
|
||||||
|
def get_users(self, **criteria):
|
||||||
|
return self.mongo.users.find(criteria)
|
||||||
|
|
||||||
|
def insert_user(self, user):
|
||||||
|
self.mongo.users.insert(normalize_user(user))
|
||||||
|
|
||||||
|
def update_user(self, user):
|
||||||
|
normalize_user(user)
|
||||||
|
launchpad_id = user['launchpad_id']
|
||||||
|
self.mongo.users.update({'launchpad_id': launchpad_id}, user)
|
||||||
|
|
||||||
|
def get_releases(self, **criteria):
|
||||||
|
return self.mongo.releases.find(criteria)
|
||||||
|
|
||||||
|
def insert_release(self, release):
|
||||||
|
self.mongo.releases.insert(release)
|
||||||
|
|
||||||
|
|
||||||
|
class PersistentStorageFactory(object):
|
||||||
|
@staticmethod
|
||||||
|
def get_storage(uri):
|
||||||
|
LOG.debug('Persistent storage is requested for uri %s' % uri)
|
||||||
|
match = re.search(r'^mongodb:\/\/', uri)
|
||||||
|
if match:
|
||||||
|
return MongodbStorage(uri)
|
196
stackalytics/processor/runtime_storage.py
Normal file
196
stackalytics/processor/runtime_storage.py
Normal file
@ -0,0 +1,196 @@
|
|||||||
|
# Copyright (c) 2013 Mirantis Inc.
|
||||||
|
#
|
||||||
|
# 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.
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
|
import re
|
||||||
|
import urllib
|
||||||
|
|
||||||
|
import memcache
|
||||||
|
from oslo.config import cfg
|
||||||
|
|
||||||
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class RuntimeStorage(object):
|
||||||
|
def __init__(self, uri):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def set_records(self, records_iterator):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def get_head_commit_id(self, uri, branch):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def set_head_commit_id(self, uri, branch, head_commit_id):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def get_update(self, pid):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def active_pids(self, pids):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def get_pid_update_time(self, pid):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def set_pid_update_time(self, pid, time):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def get_repo_update_time(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def set_repo_update_time(self, time):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class MemcachedStorage(RuntimeStorage):
|
||||||
|
|
||||||
|
def __init__(self, uri):
|
||||||
|
super(MemcachedStorage, self).__init__(uri)
|
||||||
|
|
||||||
|
stripped = re.sub(r'memcached:\/\/', '', uri)
|
||||||
|
if stripped:
|
||||||
|
storage_uri = stripped.split(',')
|
||||||
|
self.memcached = memcache.Client(storage_uri)
|
||||||
|
self._build_index()
|
||||||
|
else:
|
||||||
|
raise Exception('Invalid storage uri %s' % cfg.CONF.storage_uri)
|
||||||
|
|
||||||
|
def set_records(self, records_iterator):
|
||||||
|
for record in records_iterator:
|
||||||
|
if record['commit_id'] in self.commit_id_index:
|
||||||
|
# update
|
||||||
|
record_id = self.commit_id_index[record['commit_id']]
|
||||||
|
old_record = self.memcached.get(
|
||||||
|
self._get_record_name(record_id))
|
||||||
|
old_record['branches'] |= record['branches']
|
||||||
|
LOG.debug('Update record %s' % record)
|
||||||
|
self.memcached.set(self._get_record_name(record_id),
|
||||||
|
old_record)
|
||||||
|
else:
|
||||||
|
# insert record
|
||||||
|
record_id = self._get_record_count()
|
||||||
|
record['record_id'] = record_id
|
||||||
|
LOG.debug('Insert new record %s' % record)
|
||||||
|
self.memcached.set(self._get_record_name(record_id), record)
|
||||||
|
self._set_record_count(record_id + 1)
|
||||||
|
|
||||||
|
self._commit_update(record_id)
|
||||||
|
|
||||||
|
def get_head_commit_id(self, uri, branch):
|
||||||
|
key = str(urllib.quote_plus(uri) + ':' + branch)
|
||||||
|
return self.memcached.get(key)
|
||||||
|
|
||||||
|
def set_head_commit_id(self, uri, branch, head_commit_id):
|
||||||
|
key = str(urllib.quote_plus(uri) + ':' + branch)
|
||||||
|
self.memcached.set(key, head_commit_id)
|
||||||
|
|
||||||
|
def get_update(self, pid):
|
||||||
|
last_update = self.memcached.get('pid:%s' % pid)
|
||||||
|
update_count = self._get_update_count()
|
||||||
|
|
||||||
|
self.memcached.set('pid:%s' % pid, update_count)
|
||||||
|
self._set_pids(pid)
|
||||||
|
|
||||||
|
if not last_update:
|
||||||
|
for record_id in range(0, self._get_record_count()):
|
||||||
|
yield self.memcached.get(self._get_record_name(record_id))
|
||||||
|
else:
|
||||||
|
for update_id in range(last_update, update_count):
|
||||||
|
yield self.memcached.get(self._get_record_name(
|
||||||
|
self.memcached.get('update:%s' % update_id)))
|
||||||
|
|
||||||
|
def active_pids(self, pids):
|
||||||
|
stored_pids = self.memcached.get('pids') or set()
|
||||||
|
for pid in stored_pids:
|
||||||
|
if pid not in pids:
|
||||||
|
self.memcached.delete('pid:%s' % pid)
|
||||||
|
self.memcached.delete('pid_update_time:%s' % pid)
|
||||||
|
|
||||||
|
self.memcached.set('pids', pids)
|
||||||
|
|
||||||
|
# remove unneeded updates
|
||||||
|
min_update = self._get_update_count()
|
||||||
|
for pid in pids:
|
||||||
|
n = self.memcached.get('pid:%s' % pid)
|
||||||
|
if n:
|
||||||
|
if n < min_update:
|
||||||
|
min_update = n
|
||||||
|
|
||||||
|
first_valid_update_id = self.memcached.get('first_valid_update_id')
|
||||||
|
if not first_valid_update_id:
|
||||||
|
first_valid_update_id = 0
|
||||||
|
|
||||||
|
for i in range(first_valid_update_id, min_update):
|
||||||
|
self.memcached.delete('update:%s' % i)
|
||||||
|
|
||||||
|
self.memcached.set('first_valid_update_id', min_update)
|
||||||
|
|
||||||
|
def get_pid_update_time(self, pid):
|
||||||
|
return self.memcached.get('pid_update_time:%s' % pid) or 0
|
||||||
|
|
||||||
|
def set_pid_update_time(self, pid, time):
|
||||||
|
self.memcached.set('pid_update_time:%s' % pid, time)
|
||||||
|
|
||||||
|
def get_repo_update_time(self):
|
||||||
|
return self.memcached.get('repo_update_time') or 0
|
||||||
|
|
||||||
|
def set_repo_update_time(self, time):
|
||||||
|
self.memcached.set('repo_update_time', time)
|
||||||
|
|
||||||
|
def _get_update_count(self):
|
||||||
|
return self.memcached.get('update:count') or 0
|
||||||
|
|
||||||
|
def _set_pids(self, pid):
|
||||||
|
pids = self.memcached.get('pids') or set()
|
||||||
|
if pid in pids:
|
||||||
|
return
|
||||||
|
pids.add(pid)
|
||||||
|
self.memcached.set('pids', pids)
|
||||||
|
|
||||||
|
def _get_record_name(self, record_id):
|
||||||
|
return 'record:%s' % record_id
|
||||||
|
|
||||||
|
def _get_record_count(self):
|
||||||
|
return self.memcached.get('record:count') or 0
|
||||||
|
|
||||||
|
def _set_record_count(self, count):
|
||||||
|
self.memcached.set('record:count', count)
|
||||||
|
|
||||||
|
def _get_all_records(self):
|
||||||
|
count = self.memcached.get('record:count') or 0
|
||||||
|
for i in range(0, count):
|
||||||
|
yield self.memcached.get('record:%s' % i)
|
||||||
|
|
||||||
|
def _commit_update(self, record_id):
|
||||||
|
count = self._get_update_count()
|
||||||
|
self.memcached.set('update:%s' % count, record_id)
|
||||||
|
self.memcached.set('update:count', count + 1)
|
||||||
|
|
||||||
|
def _build_index(self):
|
||||||
|
self.commit_id_index = {}
|
||||||
|
for record in self._get_all_records():
|
||||||
|
self.commit_id_index[record['commit_id']] = record['record_id']
|
||||||
|
|
||||||
|
|
||||||
|
class RuntimeStorageFactory(object):
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_storage(uri):
|
||||||
|
LOG.debug('Runtime storage is requested for uri %s' % uri)
|
||||||
|
match = re.search(r'^memcached:\/\/', uri)
|
||||||
|
if match:
|
||||||
|
return MemcachedStorage(uri)
|
58
stackalytics/processor/user_utils.py
Normal file
58
stackalytics/processor/user_utils.py
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
# Copyright (c) 2013 Mirantis Inc.
|
||||||
|
#
|
||||||
|
# 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.
|
||||||
|
|
||||||
|
import datetime
|
||||||
|
import time
|
||||||
|
|
||||||
|
|
||||||
|
def normalize_user(user):
|
||||||
|
user['emails'] = [email.lower() for email in user['emails']]
|
||||||
|
user['launchpad_id'] = user['launchpad_id'].lower()
|
||||||
|
|
||||||
|
for c in user['companies']:
|
||||||
|
end_date_numeric = 0
|
||||||
|
if c['end_date']:
|
||||||
|
end_date_numeric = date_to_timestamp(c['end_date'])
|
||||||
|
c['end_date'] = end_date_numeric
|
||||||
|
|
||||||
|
# sort companies by end_date
|
||||||
|
def end_date_comparator(x, y):
|
||||||
|
if x["end_date"] == 0:
|
||||||
|
return 1
|
||||||
|
elif y["end_date"] == 0:
|
||||||
|
return -1
|
||||||
|
else:
|
||||||
|
return cmp(x["end_date"], y["end_date"])
|
||||||
|
|
||||||
|
user['companies'].sort(cmp=end_date_comparator)
|
||||||
|
return user
|
||||||
|
|
||||||
|
|
||||||
|
def date_to_timestamp(d):
|
||||||
|
if d == 'now':
|
||||||
|
return int(time.time())
|
||||||
|
return int(time.mktime(
|
||||||
|
datetime.datetime.strptime(d, '%Y-%b-%d').timetuple()))
|
||||||
|
|
||||||
|
|
||||||
|
def timestamp_to_week(timestamp):
|
||||||
|
# Jan 4th 1970 is the first Sunday in the Epoch
|
||||||
|
return (timestamp - 3 * 24 * 3600) // (7 * 24 * 3600)
|
||||||
|
|
||||||
|
|
||||||
|
def week_to_date(week):
|
||||||
|
timestamp = week * 7 * 24 * 3600 + 3 * 24 * 3600
|
||||||
|
return (datetime.datetime.fromtimestamp(timestamp).
|
||||||
|
strftime('%Y-%m-%d %H:%M:%S'))
|
175
stackalytics/processor/vcs.py
Normal file
175
stackalytics/processor/vcs.py
Normal file
@ -0,0 +1,175 @@
|
|||||||
|
# Copyright (c) 2013 Mirantis Inc.
|
||||||
|
#
|
||||||
|
# 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.
|
||||||
|
import logging
|
||||||
|
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
|
||||||
|
from oslo.config import cfg
|
||||||
|
import sh
|
||||||
|
|
||||||
|
|
||||||
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class Vcs(object):
|
||||||
|
def __init__(self, repo):
|
||||||
|
self.repo = repo
|
||||||
|
|
||||||
|
def fetch(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def log(self, branch, head_commit_id):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def get_head_commit_id(self, branch):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
GIT_LOG_PARAMS = [
|
||||||
|
('commit_id', '%H'),
|
||||||
|
('date', '%at'),
|
||||||
|
('author', '%an'),
|
||||||
|
('author_email', '%ae'),
|
||||||
|
('author_email', '%ae'),
|
||||||
|
('subject', '%s'),
|
||||||
|
('message', '%b'),
|
||||||
|
]
|
||||||
|
GIT_LOG_FORMAT = ''.join([(r[0] + ':' + r[1] + '%n')
|
||||||
|
for r in GIT_LOG_PARAMS]) + 'diff_stat:'
|
||||||
|
DIFF_STAT_PATTERN = ('[^\d]+(\d+)\s+[^\s]*\s+changed'
|
||||||
|
'(,\s+(\d+)\s+([^\d\s]*)\s+(\d+)?)?')
|
||||||
|
GIT_LOG_PATTERN = re.compile(''.join([(r[0] + ':(.*?)\n')
|
||||||
|
for r in GIT_LOG_PARAMS]) +
|
||||||
|
'diff_stat:' + DIFF_STAT_PATTERN,
|
||||||
|
re.DOTALL)
|
||||||
|
|
||||||
|
MESSAGE_PATTERNS = {
|
||||||
|
'bug_id': re.compile('bug\s+#?([\d]{5,7})', re.IGNORECASE),
|
||||||
|
'blueprint_id': re.compile('blueprint\s+([\w-]{6,})', re.IGNORECASE),
|
||||||
|
'change_id': re.compile('Change-Id: (I[0-9a-f]{40})', re.IGNORECASE),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class Git(Vcs):
|
||||||
|
|
||||||
|
def __init__(self, repo):
|
||||||
|
super(Git, self).__init__(repo)
|
||||||
|
uri = self.repo['uri']
|
||||||
|
match = re.search(r'([^\/]+)\.git$', uri)
|
||||||
|
if match:
|
||||||
|
self.module = match.group(1)
|
||||||
|
else:
|
||||||
|
raise Exception('Unexpected uri %s for git' % uri)
|
||||||
|
self.release_index = {}
|
||||||
|
|
||||||
|
def _chdir(self):
|
||||||
|
folder = os.path.normpath(cfg.CONF.sources_root + '/' + self.module)
|
||||||
|
os.chdir(folder)
|
||||||
|
|
||||||
|
def fetch(self):
|
||||||
|
LOG.debug('Fetching repo uri %s' % self.repo['uri'])
|
||||||
|
|
||||||
|
folder = os.path.normpath(cfg.CONF.sources_root + '/' + self.module)
|
||||||
|
|
||||||
|
if not os.path.exists(folder):
|
||||||
|
os.chdir(cfg.CONF.sources_root)
|
||||||
|
sh.git('clone', '%s' % self.repo['uri'])
|
||||||
|
os.chdir(folder)
|
||||||
|
else:
|
||||||
|
self._chdir()
|
||||||
|
sh.git('pull', 'origin')
|
||||||
|
|
||||||
|
for release in self.repo['releases']:
|
||||||
|
release_name = release['release_name'].lower()
|
||||||
|
tag_range = release['tag_from'] + '..' + release['tag_to']
|
||||||
|
git_log_iterator = sh.git('log', '--pretty=%H', tag_range,
|
||||||
|
_tty_out=False)
|
||||||
|
for commit_id in git_log_iterator:
|
||||||
|
self.release_index[commit_id.strip()] = release_name
|
||||||
|
|
||||||
|
def log(self, branch, head_commit_id):
|
||||||
|
LOG.debug('Parsing git log for repo uri %s' % self.repo['uri'])
|
||||||
|
|
||||||
|
self._chdir()
|
||||||
|
sh.git('checkout', '%s' % branch)
|
||||||
|
commit_range = 'HEAD'
|
||||||
|
if head_commit_id:
|
||||||
|
commit_range = head_commit_id + '..HEAD'
|
||||||
|
output = sh.git('log', '--pretty=%s' % GIT_LOG_FORMAT, '--shortstat',
|
||||||
|
'-M', '--no-merges', commit_range, _tty_out=False)
|
||||||
|
|
||||||
|
for rec in re.finditer(GIT_LOG_PATTERN, str(output)):
|
||||||
|
i = 1
|
||||||
|
commit = {}
|
||||||
|
for param in GIT_LOG_PARAMS:
|
||||||
|
commit[param[0]] = unicode(rec.group(i), 'utf8')
|
||||||
|
i += 1
|
||||||
|
|
||||||
|
commit['files_changed'] = int(rec.group(i))
|
||||||
|
i += 1
|
||||||
|
lines_changed_group = rec.group(i)
|
||||||
|
i += 1
|
||||||
|
lines_changed = rec.group(i)
|
||||||
|
i += 1
|
||||||
|
deleted_or_inserted = rec.group(i)
|
||||||
|
i += 1
|
||||||
|
lines_deleted = rec.group(i)
|
||||||
|
i += 1
|
||||||
|
|
||||||
|
if lines_changed_group: # there inserted or deleted lines
|
||||||
|
if not lines_deleted:
|
||||||
|
if deleted_or_inserted[0] == 'd': # deleted
|
||||||
|
lines_deleted = lines_changed
|
||||||
|
lines_changed = 0
|
||||||
|
|
||||||
|
commit['lines_added'] = int(lines_changed or 0)
|
||||||
|
commit['lines_deleted'] = int(lines_deleted or 0)
|
||||||
|
|
||||||
|
for key in MESSAGE_PATTERNS:
|
||||||
|
match = re.search(MESSAGE_PATTERNS[key], commit['message'])
|
||||||
|
if match:
|
||||||
|
commit[key] = match.group(1)
|
||||||
|
else:
|
||||||
|
commit[key] = None
|
||||||
|
|
||||||
|
commit['date'] = int(commit['date'])
|
||||||
|
commit['module'] = self.module
|
||||||
|
commit['branches'] = set([branch])
|
||||||
|
if commit['commit_id'] in self.release_index:
|
||||||
|
commit['release'] = self.release_index[commit['commit_id']]
|
||||||
|
else:
|
||||||
|
commit['release'] = None
|
||||||
|
|
||||||
|
yield commit
|
||||||
|
|
||||||
|
def get_head_commit_id(self, branch):
|
||||||
|
LOG.debug('Get head commit for repo uri %s' % self.repo['uri'])
|
||||||
|
|
||||||
|
self._chdir()
|
||||||
|
sh.git('checkout', '%s' % branch)
|
||||||
|
return str(sh.git('rev-parse', 'HEAD')).strip()
|
||||||
|
|
||||||
|
|
||||||
|
class VcsFactory(object):
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_vcs(repo):
|
||||||
|
uri = repo['uri']
|
||||||
|
LOG.debug('Factory is asked for Vcs uri %s' % uri)
|
||||||
|
match = re.search(r'\.git$', uri)
|
||||||
|
if match:
|
||||||
|
return Git(repo)
|
||||||
|
#todo others vcs to be implemented
|
@ -7,7 +7,7 @@ hacking>=0.5.3,<0.6
|
|||||||
coverage
|
coverage
|
||||||
discover
|
discover
|
||||||
fixtures>=0.3.12
|
fixtures>=0.3.12
|
||||||
mox
|
mock
|
||||||
python-subunit
|
python-subunit
|
||||||
testrepository>=0.0.13
|
testrepository>=0.0.13
|
||||||
testtools>=0.9.22
|
testtools>=0.9.22
|
||||||
|
231
tests/unit/test_commit_processor.py
Normal file
231
tests/unit/test_commit_processor.py
Normal file
@ -0,0 +1,231 @@
|
|||||||
|
from launchpadlib import launchpad
|
||||||
|
import mock
|
||||||
|
from oslo.config import cfg
|
||||||
|
import testtools
|
||||||
|
|
||||||
|
from stackalytics.processor import commit_processor
|
||||||
|
from stackalytics.processor import persistent_storage
|
||||||
|
|
||||||
|
|
||||||
|
class TestCommitProcessor(testtools.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
super(TestCommitProcessor, self).setUp()
|
||||||
|
|
||||||
|
p_storage = mock.Mock(persistent_storage.PersistentStorage)
|
||||||
|
p_storage.get_companies = mock.Mock(return_value=[
|
||||||
|
{
|
||||||
|
'company_name': 'SuperCompany',
|
||||||
|
'domains': ['super.com', 'super.no']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"domains": ["nec.com", "nec.co.jp"],
|
||||||
|
"company_name": "NEC"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'company_name': '*independent',
|
||||||
|
'domains': ['']
|
||||||
|
},
|
||||||
|
])
|
||||||
|
self.user = {
|
||||||
|
'launchpad_id': 'john_doe', 'user_name': 'John Doe',
|
||||||
|
'emails': ['johndoe@gmail.com', 'jdoe@super.no'],
|
||||||
|
'companies': [
|
||||||
|
{'company_name': '*independent',
|
||||||
|
'end_date': 1234567890},
|
||||||
|
{'company_name': 'SuperCompany',
|
||||||
|
'end_date': 0},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
p_storage.get_users = mock.Mock(return_value=[
|
||||||
|
self.user,
|
||||||
|
])
|
||||||
|
|
||||||
|
self.persistent_storage = p_storage
|
||||||
|
self.commit_processor = commit_processor.CachedProcessor(p_storage)
|
||||||
|
self.launchpad_patch = mock.patch('launchpadlib.launchpad.Launchpad')
|
||||||
|
self.launchpad_patch.start()
|
||||||
|
cfg.CONF = mock.MagicMock()
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
super(TestCommitProcessor, self).tearDown()
|
||||||
|
self.launchpad_patch.stop()
|
||||||
|
|
||||||
|
def test_get_company_by_email_mapped(self):
|
||||||
|
email = 'jdoe@super.no'
|
||||||
|
res = self.commit_processor._get_company_by_email(email)
|
||||||
|
self.assertEquals('SuperCompany', res)
|
||||||
|
|
||||||
|
def test_get_company_by_email_with_long_suffix_mapped(self):
|
||||||
|
email = 'man@mxw.nes.nec.co.jp'
|
||||||
|
res = self.commit_processor._get_company_by_email(email)
|
||||||
|
self.assertEquals('NEC', res)
|
||||||
|
|
||||||
|
def test_get_company_by_email_with_long_suffix_mapped_2(self):
|
||||||
|
email = 'man@mxw.nes.nec.com'
|
||||||
|
res = self.commit_processor._get_company_by_email(email)
|
||||||
|
self.assertEquals('NEC', res)
|
||||||
|
|
||||||
|
def test_get_company_by_email_not_mapped(self):
|
||||||
|
email = 'foo@boo.com'
|
||||||
|
res = self.commit_processor._get_company_by_email(email)
|
||||||
|
self.assertEquals(None, res)
|
||||||
|
|
||||||
|
def test_update_commit_existing_user(self):
|
||||||
|
commit = {
|
||||||
|
'author_email': 'johndoe@gmail.com',
|
||||||
|
'date': 1999999999,
|
||||||
|
}
|
||||||
|
self.commit_processor._update_commit_with_user_data(commit)
|
||||||
|
|
||||||
|
self.assertEquals('SuperCompany', commit['company_name'])
|
||||||
|
self.assertEquals('john_doe', commit['launchpad_id'])
|
||||||
|
|
||||||
|
def test_update_commit_existing_user_old_job(self):
|
||||||
|
commit = {
|
||||||
|
'author_email': 'johndoe@gmail.com',
|
||||||
|
'date': 1000000000,
|
||||||
|
}
|
||||||
|
self.commit_processor._update_commit_with_user_data(commit)
|
||||||
|
|
||||||
|
self.assertEquals('*independent', commit['company_name'])
|
||||||
|
self.assertEquals('john_doe', commit['launchpad_id'])
|
||||||
|
|
||||||
|
def test_update_commit_existing_user_new_email_known_company(self):
|
||||||
|
"""
|
||||||
|
User is known to LP, his email is new to us, and maps to other company
|
||||||
|
Should return other company instead of those mentioned in user db
|
||||||
|
"""
|
||||||
|
email = 'johndoe@nec.co.jp'
|
||||||
|
commit = {
|
||||||
|
'author_email': email,
|
||||||
|
'date': 1999999999,
|
||||||
|
}
|
||||||
|
lp_mock = mock.MagicMock()
|
||||||
|
launchpad.Launchpad.login_anonymously = mock.Mock(return_value=lp_mock)
|
||||||
|
lp_profile = mock.Mock()
|
||||||
|
lp_profile.name = 'john_doe'
|
||||||
|
lp_mock.people.getByEmail = mock.Mock(return_value=lp_profile)
|
||||||
|
user = self.user.copy()
|
||||||
|
# tell storage to return existing user
|
||||||
|
self.persistent_storage.get_users.return_value = [user]
|
||||||
|
|
||||||
|
self.commit_processor._update_commit_with_user_data(commit)
|
||||||
|
|
||||||
|
self.persistent_storage.update_user.assert_called_once_with(user)
|
||||||
|
lp_mock.people.getByEmail.assert_called_once_with(email=email)
|
||||||
|
self.assertIn(email, user['emails'])
|
||||||
|
self.assertEquals('NEC', commit['company_name'])
|
||||||
|
self.assertEquals('john_doe', commit['launchpad_id'])
|
||||||
|
|
||||||
|
def test_update_commit_existing_user_new_email_unknown_company(self):
|
||||||
|
"""
|
||||||
|
User is known to LP, but his email is new to us. Should match
|
||||||
|
the user and return current company
|
||||||
|
"""
|
||||||
|
email = 'johndoe@yahoo.com'
|
||||||
|
commit = {
|
||||||
|
'author_email': email,
|
||||||
|
'date': 1999999999,
|
||||||
|
}
|
||||||
|
lp_mock = mock.MagicMock()
|
||||||
|
launchpad.Launchpad.login_anonymously = mock.Mock(return_value=lp_mock)
|
||||||
|
lp_profile = mock.Mock()
|
||||||
|
lp_profile.name = 'john_doe'
|
||||||
|
lp_mock.people.getByEmail = mock.Mock(return_value=lp_profile)
|
||||||
|
user = self.user.copy()
|
||||||
|
# tell storage to return existing user
|
||||||
|
self.persistent_storage.get_users.return_value = [user]
|
||||||
|
|
||||||
|
self.commit_processor._update_commit_with_user_data(commit)
|
||||||
|
|
||||||
|
self.persistent_storage.update_user.assert_called_once_with(user)
|
||||||
|
lp_mock.people.getByEmail.assert_called_once_with(email=email)
|
||||||
|
self.assertIn(email, user['emails'])
|
||||||
|
self.assertEquals('SuperCompany', commit['company_name'])
|
||||||
|
self.assertEquals('john_doe', commit['launchpad_id'])
|
||||||
|
|
||||||
|
def test_update_commit_new_user(self):
|
||||||
|
"""
|
||||||
|
User is known to LP, but new to us
|
||||||
|
Should add new user and set company depending on email
|
||||||
|
"""
|
||||||
|
email = 'smith@nec.com'
|
||||||
|
commit = {
|
||||||
|
'author_email': email,
|
||||||
|
'date': 1999999999,
|
||||||
|
}
|
||||||
|
lp_mock = mock.MagicMock()
|
||||||
|
launchpad.Launchpad.login_anonymously = mock.Mock(return_value=lp_mock)
|
||||||
|
lp_profile = mock.Mock()
|
||||||
|
lp_profile.name = 'smith'
|
||||||
|
lp_profile.display_name = 'Smith'
|
||||||
|
lp_mock.people.getByEmail = mock.Mock(return_value=lp_profile)
|
||||||
|
self.persistent_storage.get_users.return_value = []
|
||||||
|
|
||||||
|
self.commit_processor._update_commit_with_user_data(commit)
|
||||||
|
|
||||||
|
lp_mock.people.getByEmail.assert_called_once_with(email=email)
|
||||||
|
self.assertEquals('NEC', commit['company_name'])
|
||||||
|
self.assertEquals('smith', commit['launchpad_id'])
|
||||||
|
|
||||||
|
def test_update_commit_new_user_unknown_to_lb(self):
|
||||||
|
"""
|
||||||
|
User is new to us and not known to LP
|
||||||
|
Should set user name and empty LPid
|
||||||
|
"""
|
||||||
|
email = 'inkognito@avs.com'
|
||||||
|
commit = {
|
||||||
|
'author_email': email,
|
||||||
|
'date': 1999999999,
|
||||||
|
}
|
||||||
|
lp_mock = mock.MagicMock()
|
||||||
|
launchpad.Launchpad.login_anonymously = mock.Mock(return_value=lp_mock)
|
||||||
|
lp_mock.people.getByEmail = mock.Mock(return_value=None)
|
||||||
|
self.persistent_storage.get_users.return_value = []
|
||||||
|
|
||||||
|
self.commit_processor._update_commit_with_user_data(commit)
|
||||||
|
|
||||||
|
lp_mock.people.getByEmail.assert_called_once_with(email=email)
|
||||||
|
self.assertEquals('*independent', commit['company_name'])
|
||||||
|
self.assertEquals(None, commit['launchpad_id'])
|
||||||
|
|
||||||
|
def test_update_commit_new_user_lb_raises_error(self):
|
||||||
|
"""
|
||||||
|
LP raises error during getting user info
|
||||||
|
"""
|
||||||
|
email = 'smith@avs.com'
|
||||||
|
commit = {
|
||||||
|
'author_email': email,
|
||||||
|
'date': 1999999999,
|
||||||
|
}
|
||||||
|
lp_mock = mock.MagicMock()
|
||||||
|
launchpad.Launchpad.login_anonymously = mock.Mock(return_value=lp_mock)
|
||||||
|
lp_mock.people.getByEmail = mock.Mock(return_value=None,
|
||||||
|
side_effect=Exception)
|
||||||
|
self.persistent_storage.get_users.return_value = []
|
||||||
|
|
||||||
|
self.commit_processor._update_commit_with_user_data(commit)
|
||||||
|
|
||||||
|
lp_mock.people.getByEmail.assert_called_once_with(email=email)
|
||||||
|
self.assertEquals('*independent', commit['company_name'])
|
||||||
|
self.assertEquals(None, commit['launchpad_id'])
|
||||||
|
|
||||||
|
def test_update_commit_invalid_email(self):
|
||||||
|
"""
|
||||||
|
User's email is malformed
|
||||||
|
"""
|
||||||
|
email = 'error.root'
|
||||||
|
commit = {
|
||||||
|
'author_email': email,
|
||||||
|
'date': 1999999999,
|
||||||
|
}
|
||||||
|
lp_mock = mock.MagicMock()
|
||||||
|
launchpad.Launchpad.login_anonymously = mock.Mock(return_value=lp_mock)
|
||||||
|
lp_mock.people.getByEmail = mock.Mock(return_value=None)
|
||||||
|
self.persistent_storage.get_users.return_value = []
|
||||||
|
|
||||||
|
self.commit_processor._update_commit_with_user_data(commit)
|
||||||
|
|
||||||
|
self.assertEquals(0, lp_mock.people.getByEmail.called)
|
||||||
|
self.assertEquals('*independent', commit['company_name'])
|
||||||
|
self.assertEquals(None, commit['launchpad_id'])
|
@ -1,22 +0,0 @@
|
|||||||
import os
|
|
||||||
import tempfile
|
|
||||||
import unittest
|
|
||||||
|
|
||||||
from dashboard import dashboard
|
|
||||||
|
|
||||||
|
|
||||||
class DashboardTestCase(unittest.TestCase):
|
|
||||||
|
|
||||||
def setUp(self):
|
|
||||||
self.db_fd, dashboard.app.config['DATABASE'] = tempfile.mkstemp()
|
|
||||||
dashboard.app.config['TESTING'] = True
|
|
||||||
self.app = dashboard.app.test_client()
|
|
||||||
# dashboard.init_db()
|
|
||||||
|
|
||||||
def tearDown(self):
|
|
||||||
os.close(self.db_fd)
|
|
||||||
os.unlink(dashboard.app.config['DATABASE'])
|
|
||||||
|
|
||||||
def test_home_page(self):
|
|
||||||
rv = self.app.get('/')
|
|
||||||
assert rv.status_code == 200
|
|
110
tests/unit/test_vcs.py
Normal file
110
tests/unit/test_vcs.py
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
import mock
|
||||||
|
import os
|
||||||
|
import testtools
|
||||||
|
|
||||||
|
from oslo.config import cfg
|
||||||
|
|
||||||
|
from stackalytics.processor import vcs
|
||||||
|
|
||||||
|
|
||||||
|
class TestVcsProcessor(testtools.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
super(TestVcsProcessor, self).setUp()
|
||||||
|
|
||||||
|
self.repo = {
|
||||||
|
'uri': 'git://github.com/dummy.git',
|
||||||
|
'releases': []
|
||||||
|
}
|
||||||
|
self.git = vcs.Git(self.repo)
|
||||||
|
cfg.CONF.sources_root = ''
|
||||||
|
os.chdir = mock.Mock()
|
||||||
|
|
||||||
|
def test_git_log(self):
|
||||||
|
with mock.patch('sh.git') as git_mock:
|
||||||
|
git_mock.return_value = '''
|
||||||
|
commit_id:b5a416ac344160512f95751ae16e6612aefd4a57
|
||||||
|
date:1369119386
|
||||||
|
author:Akihiro MOTOKI
|
||||||
|
author_email:motoki@da.jp.nec.com
|
||||||
|
author_email:motoki@da.jp.nec.com
|
||||||
|
subject:Remove class-based import in the code repo
|
||||||
|
message:Fixes bug 1167901
|
||||||
|
|
||||||
|
This commit also removes backslashes for line break.
|
||||||
|
|
||||||
|
Change-Id: Id26fdfd2af4862652d7270aec132d40662efeb96
|
||||||
|
|
||||||
|
diff_stat:
|
||||||
|
|
||||||
|
21 files changed, 340 insertions(+), 408 deletions(-)
|
||||||
|
commit_id:5be031f81f76d68c6e4cbaad2247044aca179843
|
||||||
|
date:1370975889
|
||||||
|
author:Monty Taylor
|
||||||
|
author_email:mordred@inaugust.com
|
||||||
|
author_email:mordred@inaugust.com
|
||||||
|
subject:Remove explicit distribute depend.
|
||||||
|
message:Causes issues with the recent re-merge with setuptools. Advice from
|
||||||
|
upstream is to stop doing explicit depends.
|
||||||
|
|
||||||
|
Change-Id: I70638f239794e78ba049c60d2001190910a89c90
|
||||||
|
|
||||||
|
diff_stat:
|
||||||
|
|
||||||
|
1 file changed, 1 deletion(-)
|
||||||
|
commit_id:92811c76f3a8308b36f81e61451ec17d227b453b
|
||||||
|
date:1369831203
|
||||||
|
author:Mark McClain
|
||||||
|
author_email:mark.mcclain@dreamhost.com
|
||||||
|
author_email:mark.mcclain@dreamhost.com
|
||||||
|
subject:add readme for 2.2.2
|
||||||
|
message:Change-Id: Id32a4a72ec1d13992b306c4a38e73605758e26c7
|
||||||
|
|
||||||
|
diff_stat:
|
||||||
|
|
||||||
|
1 file changed, 8 insertions(+)
|
||||||
|
commit_id:92811c76f3a8308b36f81e61451ec17d227b453b
|
||||||
|
date:1369831203
|
||||||
|
author:John Doe
|
||||||
|
author_email:john.doe@dreamhost.com
|
||||||
|
author_email:john.doe@dreamhost.com
|
||||||
|
subject:add readme for 2.2.2
|
||||||
|
message:Change-Id: Id32a4a72ec1d13992b306c4a38e73605758e26c7
|
||||||
|
|
||||||
|
diff_stat:
|
||||||
|
|
||||||
|
0 files changed
|
||||||
|
commit_id:92811c76f3a8308b36f81e61451ec17d227b453b
|
||||||
|
date:1369831203
|
||||||
|
author:Doug Hoffner
|
||||||
|
author_email:mark.mcclain@dreamhost.com
|
||||||
|
author_email:mark.mcclain@dreamhost.com
|
||||||
|
subject:add readme for 2.2.2
|
||||||
|
message:Change-Id: Id32a4a72ec1d13992b306c4a38e73605758e26c7
|
||||||
|
|
||||||
|
diff_stat:
|
||||||
|
|
||||||
|
0 files changed, 0 insertions(+), 0 deletions(-)
|
||||||
|
'''
|
||||||
|
|
||||||
|
commits = list(self.git.log('dummy', 'dummy'))
|
||||||
|
self.assertEquals(5, len(commits))
|
||||||
|
|
||||||
|
self.assertEquals(21, commits[0]['files_changed'])
|
||||||
|
self.assertEquals(340, commits[0]['lines_added'])
|
||||||
|
self.assertEquals(408, commits[0]['lines_deleted'])
|
||||||
|
|
||||||
|
self.assertEquals(1, commits[1]['files_changed'])
|
||||||
|
self.assertEquals(0, commits[1]['lines_added'])
|
||||||
|
self.assertEquals(1, commits[1]['lines_deleted'])
|
||||||
|
|
||||||
|
self.assertEquals(1, commits[2]['files_changed'])
|
||||||
|
self.assertEquals(8, commits[2]['lines_added'])
|
||||||
|
self.assertEquals(0, commits[2]['lines_deleted'])
|
||||||
|
|
||||||
|
self.assertEquals(0, commits[3]['files_changed'])
|
||||||
|
self.assertEquals(0, commits[3]['lines_added'])
|
||||||
|
self.assertEquals(0, commits[3]['lines_deleted'])
|
||||||
|
|
||||||
|
self.assertEquals(0, commits[4]['files_changed'])
|
||||||
|
self.assertEquals(0, commits[4]['lines_added'])
|
||||||
|
self.assertEquals(0, commits[4]['lines_deleted'])
|
5
tox.ini
5
tox.ini
@ -1,5 +1,5 @@
|
|||||||
[tox]
|
[tox]
|
||||||
envlist = py27,pep8
|
envlist = py26,py27,pep8
|
||||||
|
|
||||||
[testenv]
|
[testenv]
|
||||||
setenv = VIRTUAL_ENV={envdir}
|
setenv = VIRTUAL_ENV={envdir}
|
||||||
@ -26,7 +26,8 @@ downloadcache = ~/cache/pip
|
|||||||
|
|
||||||
[flake8]
|
[flake8]
|
||||||
# E125 continuation line does not distinguish itself from next logical line
|
# E125 continuation line does not distinguish itself from next logical line
|
||||||
ignore = E125
|
# H404 multi line docstring should start with a summary
|
||||||
|
ignore = E125,H404
|
||||||
show-source = true
|
show-source = true
|
||||||
builtins = _
|
builtins = _
|
||||||
exclude=.venv,.git,.tox,dist,doc,*lib/python*,*egg,tools,pycvsanaly2
|
exclude=.venv,.git,.tox,dist,doc,*lib/python*,*egg,tools,pycvsanaly2
|
||||||
|
Loading…
x
Reference in New Issue
Block a user