summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSam Thursfield <sam.thursfield@codethink.co.uk>2015-06-16 15:46:17 +0100
committerSam Thursfield <sam.thursfield@codethink.co.uk>2015-06-16 15:46:17 +0100
commitdcb42d0a201ef560538aa1ed2014d1b7c4eb6224 (patch)
tree1443517e863ddde3277f85ffdc8f68b40f3b80fd
parent9d91283fc684ea16358fd5f4bbc88ac4a2368630 (diff)
downloadmorph-cache-server-dcb42d0a201ef560538aa1ed2014d1b7c4eb6224.tar.gz
Split API out of the main morph-cache-server script
-rwxr-xr-xmorph-cache-server367
-rw-r--r--morphcacheserver/__init__.py3
-rw-r--r--morphcacheserver/api_1_0.py342
-rw-r--r--morphcacheserver/api_2_0.py81
4 files changed, 434 insertions, 359 deletions
diff --git a/morph-cache-server b/morph-cache-server
index 5fe6b0f..053d9e8 100755
--- a/morph-cache-server
+++ b/morph-cache-server
@@ -15,20 +15,17 @@
# with this program. If not, see <http://www.gnu.org/licenses/>.
-from bottle import Bottle, request, response, run, static_file, template
+from bottle import Bottle, run
import cliapp
from flup.server.fcgi import WSGIServer
-import base64
import hashlib
-import json
-import logging
import shutil
import os
import urllib
import urllib2
-import morphcacheserver.frontend
+import morphcacheserver
from morphcacheserver.artifact_database import ArtifactDatabase
from morphcacheserver.repocache import RepoCache
@@ -42,76 +39,6 @@ defaults = {
}
-def checksum(filename, hasher):
- with open(filename,'rb') as f:
- for chunk in iter(lambda: f.read(128 * hasher.block_size), b''):
- hasher.update(chunk)
- return hasher.digest()
-
-
-def _fetch_artifact(self, url, filename):
- '''Fetch a single artifact for /fetch.'''
- in_fh = None
- try:
- in_fh = urllib2.urlopen(url)
- with open(filename, "w") as localtmp:
- shutil.copyfileobj(in_fh, localtmp)
- in_fh.close()
- except Exception, e:
- if in_fh is not None:
- in_fh.close()
- raise
- else:
- if in_fh is not None:
- in_fh.close()
-
- # FIXME: we could hash the artifact while we download it, instead, for
- # a bit of a speed increase.
- hash_sha1 = checksum(filename, hashlib.sha1())
-
- return os.stat(filename), hash_sha1
-
-def _fetch_artifacts(self, db, server, cacheid, artifacts,
- builder_name=None, build_datetime=None):
- '''Implements the /fetch method, to pull artifacts into the cache.
-
- This allows any server to overwrite the files in the cache. Be aware!!
-
- '''
- ret = {}
- try:
- for artifact in artifacts:
- artifact_name = "%s.%s" % (cacheid, artifact)
-
- tmpname = os.path.join(self.settings['artifact-dir'],
- ".dl.%s" % artifact_name)
- url = "http://%s/1.0/artifacts?filename=%s" % (
- server, urllib.quote(artifact_name))
- stinfo, hash_sha1 = self._fetch_artifact(url, tmpname)
-
- db.record_build(artifact_name, builder_name, build_datetime,
- hash_sha1)
-
- ret[artifact_name] = {
- "size": stinfo.st_size,
- "used": stinfo.st_blocks * 512,
- }
- except Exception, e:
- for artifact in ret.iterkeys():
- os.unlink(os.path.join(self.settings['artifact-dir'],
- ".dl.%s" % artifact))
- raise
-
- for artifact in ret.iterkeys():
- tmpname = os.path.join(self.settings['artifact-dir'],
- ".dl.%s" % artifact)
- artifilename = os.path.join(self.settings['artifact-dir'],
- artifact)
- os.rename(tmpname, artifilename)
-
- return ret
-
-
class MorphCacheServer(cliapp.Application):
def add_settings(self):
@@ -147,284 +74,6 @@ class MorphCacheServer(cliapp.Application):
'runs a fcgi-server',
default=True)
- def api_1_0(self, repo_cache, db):
- '''The /1.0 set of HTTP methods.'''
- app = Bottle()
-
- def writable(prefix):
- """Selectively enable bottle prefixes.
-
- prefix -- The path prefix we are enabling
-
- If the runtime configuration setting --enable-writes is provided
- then we return the app.get() decorator for the given path prefix
- otherwise we return a lambda which passes the function through
- undecorated.
-
- This has the effect of being a runtime-enablable @app.get(...)
-
- """
- if self.settings['enable-writes']:
- return app.get(prefix)
- return lambda fn: fn
-
- @writable('/list')
- def list():
- response.set_header('Cache-Control', 'no-cache')
- results = {}
- files = {}
- results["files"] = files
- for artifactdir, __, filenames in \
- os.walk(self.settings['artifact-dir']):
- fsstinfo = os.statvfs(artifactdir)
- results["freespace"] = fsstinfo.f_bsize * fsstinfo.f_bavail
- for fname in filenames:
- if not fname.startswith(".dl."):
- try:
- stinfo = os.stat("%s/%s" % (artifactdir, fname))
- files[fname] = {
- "atime": stinfo.st_atime,
- "size": stinfo.st_size,
- "used": stinfo.st_blocks * 512,
- }
- except Exception, e:
- print(e)
- return results
-
- @writable('/fetch')
- def fetch():
- host = self._unescape_parameter(request.query.host)
- cacheid = self._unescape_parameter(request.query.cacheid)
- artifacts = self._unescape_parameter(request.query.artifacts)
-
- # Optional parameters added for bit-for-bit reproducibility
- # checking.
- builder_name = self._unescape_parameter(
- request.query.get('builder_name'))
- build_datetime = self._unescape_parameter(
- request.query.get('build_datetime'))
-
- try:
- response.set_header('Cache-Control', 'no-cache')
- artifacts = artifacts.split(",")
- return self._fetch_artifacts(db, host, cacheid, artifacts,
- builder_name=builder_name,
- build_datetime=build_datetime)
-
- except Exception, e:
- response.status = 500
- logging.debug('%s' % e)
-
- @writable('/delete')
- def delete():
- artifact = self._unescape_parameter(request.query.artifact)
- try:
- os.unlink('%s/%s' % (self.settings['artifact-dir'],
- artifact))
- return { "status": 0, "reason": "success" }
- except OSError, ose:
- return { "status": ose.errno, "reason": ose.strerror }
- except Exception, e:
- response.status = 500
- logging.debug('%s' % e)
-
- @app.get('/sha1s')
- def sha1():
- repo = self._unescape_parameter(request.query.repo)
- ref = self._unescape_parameter(request.query.ref)
- try:
- response.set_header('Cache-Control', 'no-cache')
- sha1, tree = repo_cache.resolve_ref(repo, ref)
- return {
- 'repo': '%s' % repo,
- 'ref': '%s' % ref,
- 'sha1': '%s' % sha1,
- 'tree': '%s' % tree
- }
- except Exception, e:
- response.status = 404
- logging.debug('%s' % e)
-
- @app.post('/sha1s')
- def sha1s():
- result = []
- for pair in request.json:
- repo = pair['repo']
- ref = pair['ref']
- try:
- sha1, tree = repo_cache.resolve_ref(repo, ref)
- result.append({
- 'repo': '%s' % repo,
- 'ref': '%s' % ref,
- 'sha1': '%s' % sha1,
- 'tree': '%s' % tree
- })
- except Exception, e:
- logging.debug('%s' % e)
- result.append({
- 'repo': '%s' % repo,
- 'ref': '%s' % ref,
- 'error': '%s' % e
- })
- response.set_header('Cache-Control', 'no-cache')
- response.set_header('Content-Type', 'application/json')
- return json.dumps(result)
-
- @app.get('/files')
- def file():
- repo = self._unescape_parameter(request.query.repo)
- ref = self._unescape_parameter(request.query.ref)
- filename = self._unescape_parameter(request.query.filename)
- try:
- content = repo_cache.cat_file(repo, ref, filename)
- response.set_header('Content-Type', 'application/octet-stream')
- return content
- except Exception, e:
- response.status = 404
- logging.debug('%s' % e)
-
- @app.post('/files')
- def files():
- result = []
- for pair in request.json:
- repo = pair['repo']
- ref = pair['ref']
- filename = pair['filename']
- try:
- content = repo_cache.cat_file(repo, ref, filename)
- result.append({
- 'repo': '%s' % repo,
- 'ref': '%s' % ref,
- 'filename': '%s' % filename,
- 'data': '%s' % base64.b64encode(content),
- })
- except Exception, e:
- logging.debug('%s' % e)
- result.append({
- 'repo': '%s' % repo,
- 'ref': '%s' % ref,
- 'filename': '%s' % filename,
- 'error': '%s' % e
- })
- response.set_header('Content-Type', 'application/json')
- return json.dumps(result)
-
- @app.get('/trees')
- def tree():
- repo = self._unescape_parameter(request.query.repo)
- ref = self._unescape_parameter(request.query.ref)
- path = self._unescape_parameter(request.query.path)
- try:
- tree = repo_cache.ls_tree(repo, ref, path)
- return {
- 'repo': '%s' % repo,
- 'ref': '%s' % ref,
- 'tree': tree,
- }
- except Exception, e:
- response.status = 404
- logging.debug('%s' % e)
-
- @app.get('/bundles')
- def bundle():
- repo = self._unescape_parameter(request.query.repo)
- filename = repo_cache.get_bundle_filename(repo)
- dirname = os.path.dirname(filename)
- basename = os.path.basename(filename)
- return static_file(basename, root=dirname, download=True)
-
- @app.get('/artifacts')
- def artifact():
- basename = self._unescape_parameter(request.query.filename)
- filename = os.path.join(self.settings['artifact-dir'], basename)
- if os.path.exists(filename):
- return static_file(basename,
- root=self.settings['artifact-dir'],
- download=True)
- else:
- response.status = 404
- logging.debug('artifact %s does not exist' % basename)
-
- @app.post('/artifacts')
- def post_artifacts():
- if request.content_type != 'application/json':
- logging.warning('Content-type is not json: '
- 'expecting a json post request')
-
- artifacts = json.load(request.body)
- results = {}
-
- logging.debug('Received a POST request for /artifacts')
-
- for artifact in artifacts:
- if artifact.startswith('/'):
- response.status = 500
- logging.error("%s: artifact name cannot start with a '/'"
- % artifact)
- return
-
- filename = os.path.join(self.settings['artifact-dir'],
- artifact)
- results[artifact] = os.path.exists(filename)
-
- if results[artifact]:
- logging.debug('%s is in the cache', artifact)
- else:
- logging.debug('%s is NOT in the cache', artifact)
-
- return results
-
- def api_2_0(self, db):
- '''The 2.0/ set of HTTP methods.'''
- app = Bottle()
-
- @app.put('/builds')
- def put_build():
- '''Record a build.
-
- Expected parameters:
-
- - cache_name: artifact cache key plus name
- - builder_name: URL identifying the build worker
- - build_datetime: time artifact build was started
- - hash_sha1: SHA1 of built artifact
- - source_repo: (optional): repo this is built from
- - source_ref: (optional): ref this is built from
-
- '''
- cache_name = self._unescape_parameter(request.query.cache_name)
- builder_name = self._unescape_parameter(request.query.builder_name)
- build_datetime = self._unescape_parameter(
- request.query.build_datetime)
- hash_sha1 = self._unescape_parameter(request.query.hash_sha1)
-
- source_repo = self._unescape_parameter(
- request.query.get('source_repo'))
- source_ref = self._unescape_parameter(
- request.query.get('source_ref'))
-
- db.record_build(cache_name, builder_name, build_datetime,
- hash_sha1, source_repo, source_ref)
-
- @app.get('/builds')
- def get_builds():
- '''Return info on all known builds.'''
- return {'builds': db.view_artifact_statistics()}
-
- # This may not be the right name for this method, as its name is
- # 'builds' but it takes the cache name of an artifact, not a build.
- @app.get('/builds/<cache_name>')
- def get_builds_for_artifact(cache_name):
- '''Return info on builds of a given artifact.'''
- results = sorted(db.iter_builds_for_artifact_file(cache_name))
-
- if len(results) == 0:
- response.status = 404
- else:
- return {cache_name: results}
-
- return app
-
def process_args(self, args):
repo_cache = RepoCache(self,
self.settings['repo-dir'],
@@ -433,9 +82,12 @@ class MorphCacheServer(cliapp.Application):
db = ArtifactDatabase(self.settings['database-file'])
- api_1_0 = self.api_1_0(repo_cache, db)
- api_2_0 = self.api_2_0(db)
- web_frontend = morphcacheserver.frontend.web_frontend(db)
+ api_1_0 = morphcacheserver.api_1_0(
+ repo_cache, db,
+ artifact_dir=self.settings['artifact-dir'],
+ enable_writes=self.settings['enable-writes'])
+ api_2_0 = morphcacheserver.api_2_0(db)
+ web_frontend = morphcacheserver.web_frontend(db)
root = Bottle()
root.merge(web_frontend)
@@ -468,9 +120,6 @@ class MorphCacheServer(cliapp.Application):
run(root, host='0.0.0.0', port=self.settings['port'],
reloader=True)
- def _unescape_parameter(self, param):
- return urllib.unquote(param)
-
if __name__ == '__main__':
MorphCacheServer().run()
diff --git a/morphcacheserver/__init__.py b/morphcacheserver/__init__.py
index c646c1a..36e7ff5 100644
--- a/morphcacheserver/__init__.py
+++ b/morphcacheserver/__init__.py
@@ -13,4 +13,7 @@
# with this program. If not, see <http://www.gnu.org/licenses/>.
+from api_1_0 import api_1_0
+from api_2_0 import api_2_0
+from frontend import web_frontend
import repocache
diff --git a/morphcacheserver/api_1_0.py b/morphcacheserver/api_1_0.py
new file mode 100644
index 0000000..0d3d1d8
--- /dev/null
+++ b/morphcacheserver/api_1_0.py
@@ -0,0 +1,342 @@
+# Copyright (C) 2013, 2014-2015 Codethink Limited
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; version 2 of the License.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program. If not, see <http://www.gnu.org/licenses/>.
+
+
+'''The /1.0 set of HTTP methods.
+
+This API is used by:
+
+ - Morph's 'remoterepocache' module and YBD's 'repos' module, for getting info
+ about a given commit in Git repo without having to clone the whole repo. The
+ default provider used is http://git.baserock.org:8080/
+
+ - Morph's 'remoteartifactcache' module, which downloads prebuilt artifacts
+ that have already been built by the 'Mason' autobuilders. Public provider is
+ http://cache.baserock.org:8080/ (although it's not the default).
+
+ - Morph's 'distbuild' module (which distributes builds among multiple workers
+ at the chunk level) for coordinating the results of the distributed build.
+
+It should not be changed without testing all of the above things.
+
+Note that the 'write' API is totally unsecured and allows full access to the
+cache. You should leave it disabled, or secure it somehow.
+
+'''
+
+from bottle import Bottle, request, response, static_file
+
+import base64
+import hashlib
+import json
+import logging
+import os
+import shutil
+import urllib
+import urllib2
+
+
+def unescape_parameter(param):
+ return urllib.unquote(param)
+
+
+def checksum(filename, hasher):
+ with open(filename, 'rb') as f:
+ for chunk in iter(lambda: f.read(128 * hasher.block_size), b''):
+ hasher.update(chunk)
+ return hasher.digest()
+
+
+def fetch_artifact(url, filename):
+ '''Fetch a single artifact for /fetch.'''
+ in_fh = None
+ try:
+ in_fh = urllib2.urlopen(url)
+ with open(filename, "w") as localtmp:
+ shutil.copyfileobj(in_fh, localtmp)
+ in_fh.close()
+ except Exception, e:
+ if in_fh is not None:
+ in_fh.close()
+ raise
+ else:
+ if in_fh is not None:
+ in_fh.close()
+
+ # FIXME: we could hash the artifact while we download it, instead, for
+ # a bit of a speed increase.
+ hash_sha1 = checksum(filename, hashlib.sha1())
+
+ return os.stat(filename), hash_sha1
+
+def fetch_artifacts(db, artifact_dir, server, cacheid, artifacts,
+ builder_name=None, build_datetime=None):
+ '''Implements the /fetch method, to pull artifacts into the cache.
+
+ This allows any server to overwrite the files in the cache. Be aware!!
+
+ '''
+ ret = {}
+ try:
+ for artifact in artifacts:
+ artifact_name = "%s.%s" % (cacheid, artifact)
+
+ tmpname = os.path.join(artifact_dir, ".dl.%s" % artifact_name)
+ url = "http://%s/1.0/artifacts?filename=%s" % (
+ server, urllib.quote(artifact_name))
+ stinfo, hash_sha1 = fetch_artifact(url, tmpname)
+
+ db.record_build(artifact_name, builder_name, build_datetime,
+ hash_sha1)
+
+ ret[artifact_name] = {
+ "size": stinfo.st_size,
+ "used": stinfo.st_blocks * 512,
+ }
+ except Exception, e:
+ for artifact in ret.iterkeys():
+ os.unlink(os.path.join(artifact_dir, ".dl.%s" % artifact))
+ raise
+
+ for artifact in ret.iterkeys():
+ tmpname = os.path.join(artifact_dir, ".dl.%s" % artifact)
+ artifilename = os.path.join(artifact_dir, artifact)
+ os.rename(tmpname, artifilename)
+
+ return ret
+
+
+def api_1_0(repo_cache, db, artifact_dir, enable_writes):
+ app = Bottle()
+
+ def writable(prefix):
+ """Selectively enable bottle prefixes.
+
+ prefix -- The path prefix we are enabling
+
+ If the runtime configuration setting --enable-writes is provided
+ then we return the app.get() decorator for the given path prefix
+ otherwise we return a lambda which passes the function through
+ undecorated.
+
+ This has the effect of being a runtime-enablable @app.get(...)
+
+ """
+ if enable_writes:
+ return app.get(prefix)
+ return lambda fn: fn
+
+ @writable('/list')
+ def list():
+ response.set_header('Cache-Control', 'no-cache')
+ results = {}
+ files = {}
+ results["files"] = files
+ for dirname, __, filenames in \
+ os.walk(artifact_dir):
+ fsstinfo = os.statvfs(dirname)
+ results["freespace"] = fsstinfo.f_bsize * fsstinfo.f_bavail
+ for fname in filenames:
+ if not fname.startswith(".dl."):
+ try:
+ stinfo = os.stat("%s/%s" % (dirname, fname))
+ files[fname] = {
+ "atime": stinfo.st_atime,
+ "size": stinfo.st_size,
+ "used": stinfo.st_blocks * 512,
+ }
+ except Exception, e:
+ print(e)
+ return results
+
+ @writable('/fetch')
+ def fetch():
+ host = unescape_parameter(request.query.host)
+ cacheid = unescape_parameter(request.query.cacheid)
+ artifacts = unescape_parameter(request.query.artifacts)
+
+ # Optional parameters added for bit-for-bit reproducibility
+ # checking.
+ builder_name = unescape_parameter(
+ request.query.get('builder_name'))
+ build_datetime = unescape_parameter(
+ request.query.get('build_datetime'))
+
+ try:
+ response.set_header('Cache-Control', 'no-cache')
+ artifacts = artifacts.split(",")
+ return fetch_artifacts(db, artifact_dir, host, cacheid, artifacts,
+ builder_name=builder_name,
+ build_datetime=build_datetime)
+
+ except Exception, e:
+ response.status = 500
+ logging.debug('%s' % e)
+
+ @writable('/delete')
+ def delete():
+ artifact = unescape_parameter(request.query.artifact)
+ try:
+ os.unlink('%s/%s' % (artifact_dir, artifact))
+ return { "status": 0, "reason": "success" }
+ except OSError, ose:
+ return { "status": ose.errno, "reason": ose.strerror }
+ except Exception, e:
+ response.status = 500
+ logging.debug('%s' % e)
+
+ @app.get('/sha1s')
+ def sha1():
+ repo = unescape_parameter(request.query.repo)
+ ref = unescape_parameter(request.query.ref)
+ try:
+ response.set_header('Cache-Control', 'no-cache')
+ sha1, tree = repo_cache.resolve_ref(repo, ref)
+ return {
+ 'repo': '%s' % repo,
+ 'ref': '%s' % ref,
+ 'sha1': '%s' % sha1,
+ 'tree': '%s' % tree
+ }
+ except Exception, e:
+ response.status = 404
+ logging.debug('%s' % e)
+
+ @app.post('/sha1s')
+ def sha1s():
+ result = []
+ for pair in request.json:
+ repo = pair['repo']
+ ref = pair['ref']
+ try:
+ sha1, tree = repo_cache.resolve_ref(repo, ref)
+ result.append({
+ 'repo': '%s' % repo,
+ 'ref': '%s' % ref,
+ 'sha1': '%s' % sha1,
+ 'tree': '%s' % tree
+ })
+ except Exception, e:
+ logging.debug('%s' % e)
+ result.append({
+ 'repo': '%s' % repo,
+ 'ref': '%s' % ref,
+ 'error': '%s' % e
+ })
+ response.set_header('Cache-Control', 'no-cache')
+ response.set_header('Content-Type', 'application/json')
+ return json.dumps(result)
+
+ @app.get('/files')
+ def file():
+ repo = unescape_parameter(request.query.repo)
+ ref = unescape_parameter(request.query.ref)
+ filename = unescape_parameter(request.query.filename)
+ try:
+ content = repo_cache.cat_file(repo, ref, filename)
+ response.set_header('Content-Type', 'application/octet-stream')
+ return content
+ except Exception, e:
+ response.status = 404
+ logging.debug('%s' % e)
+
+ @app.post('/files')
+ def files():
+ result = []
+ for pair in request.json:
+ repo = pair['repo']
+ ref = pair['ref']
+ filename = pair['filename']
+ try:
+ content = repo_cache.cat_file(repo, ref, filename)
+ result.append({
+ 'repo': '%s' % repo,
+ 'ref': '%s' % ref,
+ 'filename': '%s' % filename,
+ 'data': '%s' % base64.b64encode(content),
+ })
+ except Exception, e:
+ logging.debug('%s' % e)
+ result.append({
+ 'repo': '%s' % repo,
+ 'ref': '%s' % ref,
+ 'filename': '%s' % filename,
+ 'error': '%s' % e
+ })
+ response.set_header('Content-Type', 'application/json')
+ return json.dumps(result)
+
+ @app.get('/trees')
+ def tree():
+ repo = unescape_parameter(request.query.repo)
+ ref = unescape_parameter(request.query.ref)
+ path = unescape_parameter(request.query.path)
+ try:
+ tree = repo_cache.ls_tree(repo, ref, path)
+ return {
+ 'repo': '%s' % repo,
+ 'ref': '%s' % ref,
+ 'tree': tree,
+ }
+ except Exception, e:
+ response.status = 404
+ logging.debug('%s' % e)
+
+ @app.get('/bundles')
+ def bundle():
+ repo = unescape_parameter(request.query.repo)
+ filename = repo_cache.get_bundle_filename(repo)
+ dirname = os.path.dirname(filename)
+ basename = os.path.basename(filename)
+ return static_file(basename, root=dirname, download=True)
+
+ @app.get('/artifacts')
+ def artifact():
+ basename = unescape_parameter(request.query.filename)
+ filename = os.path.join(artifact_dir, basename)
+ if os.path.exists(filename):
+ return static_file(basename,
+ root=artifact_dir,
+ download=True)
+ else:
+ response.status = 404
+ logging.debug('artifact %s does not exist' % basename)
+
+ @app.post('/artifacts')
+ def post_artifacts():
+ if request.content_type != 'application/json':
+ logging.warning('Content-type is not json: '
+ 'expecting a json post request')
+
+ artifacts = json.load(request.body)
+ results = {}
+
+ logging.debug('Received a POST request for /artifacts')
+
+ for artifact in artifacts:
+ if artifact.startswith('/'):
+ response.status = 500
+ logging.error("%s: artifact name cannot start with a '/'"
+ % artifact)
+ return
+
+ filename = os.path.join(artifact_dir, artifact)
+ results[artifact] = os.path.exists(filename)
+
+ if results[artifact]:
+ logging.debug('%s is in the cache', artifact)
+ else:
+ logging.debug('%s is NOT in the cache', artifact)
+
+ return results
diff --git a/morphcacheserver/api_2_0.py b/morphcacheserver/api_2_0.py
new file mode 100644
index 0000000..94e9341
--- /dev/null
+++ b/morphcacheserver/api_2_0.py
@@ -0,0 +1,81 @@
+# Copyright (C) 2015 Codethink Limited
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; version 2 of the License.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program. If not, see <http://www.gnu.org/licenses/>.
+
+
+from bottle import Bottle, request, response
+
+
+'''The 2.0/ set of HTTP methods.
+
+This is still in development and you can pretty much do what you want with it
+right now. Work began on it by Sam Thursfield as part of
+<http://wiki.baserock.org/projects/deterministic-builds/>, to provide a way of
+doing ongoing measurement.
+
+'''
+
+
+def unescape_parameter(param):
+ return urllib.unquote(param)
+
+
+def api_2_0(db):
+ app = Bottle()
+
+ @app.put('/builds')
+ def put_build():
+ '''Record a build.
+
+ Expected parameters:
+
+ - cache_name: artifact cache key plus name
+ - builder_name: URL identifying the build worker
+ - build_datetime: time artifact build was started
+ - hash_sha1: SHA1 of built artifact
+ - source_repo: (optional): repo this is built from
+ - source_ref: (optional): ref this is built from
+
+ '''
+ cache_name = unescape_parameter(request.query.cache_name)
+ builder_name = unescape_parameter(request.query.builder_name)
+ build_datetime = unescape_parameter(
+ request.query.build_datetime)
+ hash_sha1 = unescape_parameter(request.query.hash_sha1)
+
+ source_repo = unescape_parameter(
+ request.query.get('source_repo'))
+ source_ref = unescape_parameter(
+ request.query.get('source_ref'))
+
+ db.record_build(cache_name, builder_name, build_datetime,
+ hash_sha1, source_repo, source_ref)
+
+ @app.get('/builds')
+ def get_builds():
+ '''Return info on all known builds.'''
+ return {'builds': db.view_artifact_statistics()}
+
+ # This may not be the right name for this method, as its name is
+ # 'builds' but it takes the cache name of an artifact, not a build.
+ @app.get('/builds/<cache_name>')
+ def get_builds_for_artifact(cache_name):
+ '''Return info on builds of a given artifact.'''
+ results = sorted(db.iter_builds_for_artifact_file(cache_name))
+
+ if len(results) == 0:
+ response.status = 404
+ else:
+ return {cache_name: results}
+
+ return app