initial code repo
[stor4nfv.git] / src / ceph / src / powerdns / pdns-backend-rgw.py
diff --git a/src/ceph/src/powerdns/pdns-backend-rgw.py b/src/ceph/src/powerdns/pdns-backend-rgw.py
new file mode 100755 (executable)
index 0000000..6cb42d8
--- /dev/null
@@ -0,0 +1,283 @@
+#!/usr/bin/python
+'''
+A backend for PowerDNS to direct RADOS Gateway bucket traffic to the correct regions.
+
+For example, two regions exist, US and EU.
+
+EU: o.myobjects.eu
+US: o.myobjects.us
+
+A global domain o.myobjects.com exists.
+
+Bucket 'foo' exists in the region EU and 'bar' in US.
+
+foo.o.myobjects.com will return a CNAME to foo.o.myobjects.eu
+bar.o.myobjects.com will return a CNAME to foo.o.myobjects.us
+
+The HTTP Remote Backend from PowerDNS is used in this case: http://doc.powerdns.com/html/remotebackend.html
+
+PowerDNS must be compiled with Remote HTTP backend support enabled, this is not default.
+
+Configuration for PowerDNS:
+
+launch=remote
+remote-connection-string=http:url=http://localhost:6780/dns
+
+Usage for this backend is showed by invoking with --help. See rgw-pdns.conf.in for a configuration example
+
+The ACCESS and SECRET key pair requires the caps "metadata=read"
+
+To test:
+
+$ curl -X GET http://localhost:6780/dns/lookup/foo.o.myobjects.com/ANY
+
+Should return something like:
+
+{
+ "result": [
+  {
+   "content": "foo.o.myobjects.eu",
+   "qtype": "CNAME",
+   "qname": "foo.o.myobjects.com",
+   "ttl": 60
+  }
+ ]
+}
+
+'''
+
+# Copyright: Wido den Hollander <wido@42on.com> 2014
+# License:   LGPL2.1
+
+from ConfigParser import SafeConfigParser, NoSectionError
+from flask import abort, Flask, request, Response
+from hashlib import sha1 as sha
+from time import gmtime, strftime
+from urlparse import urlparse
+import argparse
+import base64
+import hmac
+import json
+import pycurl
+import StringIO
+import urllib
+import os
+import sys
+
+config_locations = ['rgw-pdns.conf', '~/rgw-pdns.conf', '/etc/ceph/rgw-pdns.conf']
+
+# PowerDNS expects a 200 what ever happends and always wants
+# 'result' to 'true' if the query fails
+def abort_early():
+    return json.dumps({'result': 'true'}) + "\n"
+
+# Generate the Signature string for S3 Authorization with the RGW Admin API
+def generate_signature(method, date, uri, headers=None):
+    sign = "%s\n\n" % method
+
+    if 'Content-Type' in headers:
+        sign += "%s\n" % headers['Content-Type']
+    else:
+        sign += "\n"
+
+    sign += "%s\n/%s/%s" % (date, config['rgw']['admin_entry'], uri)
+    h = hmac.new(config['rgw']['secret_key'].encode('utf-8'), sign.encode('utf-8'), digestmod=sha)
+    return base64.encodestring(h.digest()).strip()
+
+def generate_auth_header(signature):
+    return str("AWS %s:%s" % (config['rgw']['access_key'], signature.decode('utf-8')))
+
+# Do a HTTP request to the RGW Admin API
+def do_rgw_request(uri, params=None, data=None, headers=None):
+    if headers == None:
+        headers = {}
+
+    headers['Date'] = strftime("%a, %d %b %Y %H:%M:%S +0000", gmtime())
+    signature = generate_signature("GET", headers['Date'], uri, headers)
+    headers['Authorization'] = generate_auth_header(signature)
+
+    query = None
+    if params != None:
+        query = '&'.join("%s=%s" % (key,val) for (key,val) in params.iteritems())
+
+    c = pycurl.Curl()
+    b = StringIO.StringIO()
+    url = "http://" + config['rgw']['endpoint'] + "/" + config['rgw']['admin_entry'] + "/" + uri + "?format=json"
+    if query != None:
+        url += "&" + urllib.quote_plus(query)
+
+    http_headers = []
+    for header in headers.keys():
+        http_headers.append(header + ": " + headers[header])
+
+    c.setopt(pycurl.URL, str(url))
+    c.setopt(pycurl.HTTPHEADER, http_headers)
+    c.setopt(pycurl.WRITEFUNCTION, b.write)
+    c.setopt(pycurl.FOLLOWLOCATION, 0)
+    c.setopt(pycurl.CONNECTTIMEOUT, 5)
+    c.perform()
+
+    response = b.getvalue()
+    if len(response) > 0:
+        return json.loads(response)
+
+    return None
+
+def get_radosgw_metadata(key):
+    return do_rgw_request('metadata', {'key': key})
+
+# Returns a string of the region where the bucket is in
+def get_bucket_region(bucket):
+    meta = get_radosgw_metadata("bucket:%s" % bucket)
+    bucket_id = meta['data']['bucket']['bucket_id']
+    meta_instance = get_radosgw_metadata("bucket.instance:%s:%s" % (bucket, bucket_id))
+    region = meta_instance['data']['bucket_info']['region']
+    return region
+
+# Returns the correct host for the bucket based on the regionmap
+def get_bucket_host(bucket, region_map):
+    region = get_bucket_region(bucket)
+    return bucket + "." + region_map[region]
+
+# This should support multiple endpoints per region!
+def parse_region_map(map):
+    regions = {}
+    for region in map['regions']:
+        url = urlparse(region['val']['endpoints'][0])
+        regions.update({region['key']: url.netloc})
+
+    return regions
+
+def str2bool(s):
+    return s.lower() in ("yes", "true", "1")
+
+def init_config():
+    parser = argparse.ArgumentParser()
+    parser.add_argument("--config", help="The configuration file to use.", action="store")
+
+    args = parser.parse_args()
+
+    defaults = {
+                   'listen_addr': '127.0.0.1',
+                   'listen_port': '6780',
+                   'dns_zone': 'rgw.local.lan',
+                   'dns_soa_record': 'dns1.icann.org. hostmaster.icann.org. 2012080849 7200 3600 1209600 3600',
+                   'dns_soa_ttl': '3600',
+                   'dns_default_ttl': '60',
+                   'rgw_endpoint': 'localhost:8080',
+                   'rgw_admin_entry': 'admin',
+                   'rgw_access_key': 'access',
+                   'rgw_secret_key': 'secret',
+                   'debug': False
+               }
+
+    cfg = SafeConfigParser(defaults)
+    if args.config == None:
+        cfg.read(config_locations)
+    else:
+        if not os.path.isfile(args.config):
+            print "Could not open configuration file %s" % args.config
+            sys.exit(1)
+
+        cfg.read(args.config)
+
+    config_section = 'powerdns'
+
+    try:
+        return {
+            'listen': {
+                'port': cfg.getint(config_section, 'listen_port'),
+                'addr': cfg.get(config_section, 'listen_addr')
+                },
+            'dns': {
+                'zone': cfg.get(config_section, 'dns_zone'),
+                'soa_record': cfg.get(config_section, 'dns_soa_record'),
+                'soa_ttl': cfg.get(config_section, 'dns_soa_ttl'),
+                'default_ttl': cfg.get(config_section, 'dns_default_ttl')
+            },
+            'rgw': {
+                'endpoint': cfg.get(config_section, 'rgw_endpoint'),
+                'admin_entry': cfg.get(config_section, 'rgw_admin_entry'),
+                'access_key': cfg.get(config_section, 'rgw_access_key'),
+                'secret_key': cfg.get(config_section, 'rgw_secret_key')
+            },
+            'debug': str2bool(cfg.get(config_section, 'debug'))
+        }
+
+    except NoSectionError:
+         return None
+
+def generate_app(config):
+    # The Flask App
+    app = Flask(__name__)
+
+    # Get the RGW Region Map
+    region_map = parse_region_map(do_rgw_request('config'))
+
+    @app.route('/')
+    def index():
+        abort(404)
+
+    @app.route("/dns/lookup/<qname>/<qtype>")
+    def bucket_location(qname, qtype):
+        if len(qname) == 0:
+            return abort_early()
+
+        split = qname.split(".", 1)
+        if len(split) != 2:
+            return abort_early()
+
+        bucket = split[0]
+        zone = split[1]
+
+        # If the received qname doesn't match our zone we abort
+        if zone != config['dns']['zone']:
+            return abort_early()
+
+        # We do not serve MX records
+        if qtype == "MX":
+            return abort_early()
+
+        # The basic result we always return, this is what PowerDNS expects.
+        response = {'result': 'true'}
+        result = {}
+
+        # A hardcoded SOA response (FIXME!)
+        if qtype == "SOA":
+            result.update({'qtype': qtype})
+            result.update({'qname': qname})
+            result.update({'content': config['dns']['soa_record']})
+            result.update({'ttl': config['dns']['soa_ttl']})
+        else:
+            region_hostname = get_bucket_host(bucket, region_map)
+            result.update({'qtype': 'CNAME'})
+            result.update({'qname': qname})
+            result.update({'content': region_hostname})
+            result.update({'ttl': config['dns']['default_ttl']})
+
+        if len(result) > 0:
+            res = []
+            res.append(result)
+            response['result'] = res
+
+        return json.dumps(response, indent=1) + "\n"
+
+    return app
+
+
+# Initialize the configuration and generate the Application
+config = init_config()
+if config == None:
+    print "Could not parse configuration file. Tried to parse %s" % config_locations
+    sys.exit(1)
+
+app = generate_app(config)
+app.debug = config['debug']
+
+# Only run the App if this script is invoked from a Shell
+if __name__ == '__main__':
+    app.run(host=config['listen']['addr'], port=config['listen']['port'])
+
+# Otherwise provide a variable called 'application' for mod_wsgi
+else:
+    application = app