3 A backend for PowerDNS to direct RADOS Gateway bucket traffic to the correct regions.
5 For example, two regions exist, US and EU.
10 A global domain o.myobjects.com exists.
12 Bucket 'foo' exists in the region EU and 'bar' in US.
14 foo.o.myobjects.com will return a CNAME to foo.o.myobjects.eu
15 bar.o.myobjects.com will return a CNAME to foo.o.myobjects.us
17 The HTTP Remote Backend from PowerDNS is used in this case: http://doc.powerdns.com/html/remotebackend.html
19 PowerDNS must be compiled with Remote HTTP backend support enabled, this is not default.
21 Configuration for PowerDNS:
24 remote-connection-string=http:url=http://localhost:6780/dns
26 Usage for this backend is showed by invoking with --help. See rgw-pdns.conf.in for a configuration example
28 The ACCESS and SECRET key pair requires the caps "metadata=read"
32 $ curl -X GET http://localhost:6780/dns/lookup/foo.o.myobjects.com/ANY
34 Should return something like:
39 "content": "foo.o.myobjects.eu",
41 "qname": "foo.o.myobjects.com",
49 # Copyright: Wido den Hollander <wido@42on.com> 2014
52 from ConfigParser import SafeConfigParser, NoSectionError
53 from flask import abort, Flask, request, Response
54 from hashlib import sha1 as sha
55 from time import gmtime, strftime
56 from urlparse import urlparse
67 config_locations = ['rgw-pdns.conf', '~/rgw-pdns.conf', '/etc/ceph/rgw-pdns.conf']
69 # PowerDNS expects a 200 what ever happends and always wants
70 # 'result' to 'true' if the query fails
72 return json.dumps({'result': 'true'}) + "\n"
74 # Generate the Signature string for S3 Authorization with the RGW Admin API
75 def generate_signature(method, date, uri, headers=None):
76 sign = "%s\n\n" % method
78 if 'Content-Type' in headers:
79 sign += "%s\n" % headers['Content-Type']
83 sign += "%s\n/%s/%s" % (date, config['rgw']['admin_entry'], uri)
84 h = hmac.new(config['rgw']['secret_key'].encode('utf-8'), sign.encode('utf-8'), digestmod=sha)
85 return base64.encodestring(h.digest()).strip()
87 def generate_auth_header(signature):
88 return str("AWS %s:%s" % (config['rgw']['access_key'], signature.decode('utf-8')))
90 # Do a HTTP request to the RGW Admin API
91 def do_rgw_request(uri, params=None, data=None, headers=None):
95 headers['Date'] = strftime("%a, %d %b %Y %H:%M:%S +0000", gmtime())
96 signature = generate_signature("GET", headers['Date'], uri, headers)
97 headers['Authorization'] = generate_auth_header(signature)
101 query = '&'.join("%s=%s" % (key,val) for (key,val) in params.iteritems())
104 b = StringIO.StringIO()
105 url = "http://" + config['rgw']['endpoint'] + "/" + config['rgw']['admin_entry'] + "/" + uri + "?format=json"
107 url += "&" + urllib.quote_plus(query)
110 for header in headers.keys():
111 http_headers.append(header + ": " + headers[header])
113 c.setopt(pycurl.URL, str(url))
114 c.setopt(pycurl.HTTPHEADER, http_headers)
115 c.setopt(pycurl.WRITEFUNCTION, b.write)
116 c.setopt(pycurl.FOLLOWLOCATION, 0)
117 c.setopt(pycurl.CONNECTTIMEOUT, 5)
120 response = b.getvalue()
121 if len(response) > 0:
122 return json.loads(response)
126 def get_radosgw_metadata(key):
127 return do_rgw_request('metadata', {'key': key})
129 # Returns a string of the region where the bucket is in
130 def get_bucket_region(bucket):
131 meta = get_radosgw_metadata("bucket:%s" % bucket)
132 bucket_id = meta['data']['bucket']['bucket_id']
133 meta_instance = get_radosgw_metadata("bucket.instance:%s:%s" % (bucket, bucket_id))
134 region = meta_instance['data']['bucket_info']['region']
137 # Returns the correct host for the bucket based on the regionmap
138 def get_bucket_host(bucket, region_map):
139 region = get_bucket_region(bucket)
140 return bucket + "." + region_map[region]
142 # This should support multiple endpoints per region!
143 def parse_region_map(map):
145 for region in map['regions']:
146 url = urlparse(region['val']['endpoints'][0])
147 regions.update({region['key']: url.netloc})
152 return s.lower() in ("yes", "true", "1")
155 parser = argparse.ArgumentParser()
156 parser.add_argument("--config", help="The configuration file to use.", action="store")
158 args = parser.parse_args()
161 'listen_addr': '127.0.0.1',
162 'listen_port': '6780',
163 'dns_zone': 'rgw.local.lan',
164 'dns_soa_record': 'dns1.icann.org. hostmaster.icann.org. 2012080849 7200 3600 1209600 3600',
165 'dns_soa_ttl': '3600',
166 'dns_default_ttl': '60',
167 'rgw_endpoint': 'localhost:8080',
168 'rgw_admin_entry': 'admin',
169 'rgw_access_key': 'access',
170 'rgw_secret_key': 'secret',
174 cfg = SafeConfigParser(defaults)
175 if args.config == None:
176 cfg.read(config_locations)
178 if not os.path.isfile(args.config):
179 print "Could not open configuration file %s" % args.config
182 cfg.read(args.config)
184 config_section = 'powerdns'
189 'port': cfg.getint(config_section, 'listen_port'),
190 'addr': cfg.get(config_section, 'listen_addr')
193 'zone': cfg.get(config_section, 'dns_zone'),
194 'soa_record': cfg.get(config_section, 'dns_soa_record'),
195 'soa_ttl': cfg.get(config_section, 'dns_soa_ttl'),
196 'default_ttl': cfg.get(config_section, 'dns_default_ttl')
199 'endpoint': cfg.get(config_section, 'rgw_endpoint'),
200 'admin_entry': cfg.get(config_section, 'rgw_admin_entry'),
201 'access_key': cfg.get(config_section, 'rgw_access_key'),
202 'secret_key': cfg.get(config_section, 'rgw_secret_key')
204 'debug': str2bool(cfg.get(config_section, 'debug'))
207 except NoSectionError:
210 def generate_app(config):
212 app = Flask(__name__)
214 # Get the RGW Region Map
215 region_map = parse_region_map(do_rgw_request('config'))
221 @app.route("/dns/lookup/<qname>/<qtype>")
222 def bucket_location(qname, qtype):
226 split = qname.split(".", 1)
233 # If the received qname doesn't match our zone we abort
234 if zone != config['dns']['zone']:
237 # We do not serve MX records
241 # The basic result we always return, this is what PowerDNS expects.
242 response = {'result': 'true'}
245 # A hardcoded SOA response (FIXME!)
247 result.update({'qtype': qtype})
248 result.update({'qname': qname})
249 result.update({'content': config['dns']['soa_record']})
250 result.update({'ttl': config['dns']['soa_ttl']})
252 region_hostname = get_bucket_host(bucket, region_map)
253 result.update({'qtype': 'CNAME'})
254 result.update({'qname': qname})
255 result.update({'content': region_hostname})
256 result.update({'ttl': config['dns']['default_ttl']})
261 response['result'] = res
263 return json.dumps(response, indent=1) + "\n"
268 # Initialize the configuration and generate the Application
269 config = init_config()
271 print "Could not parse configuration file. Tried to parse %s" % config_locations
274 app = generate_app(config)
275 app.debug = config['debug']
277 # Only run the App if this script is invoked from a Shell
278 if __name__ == '__main__':
279 app.run(host=config['listen']['addr'], port=config['listen']['port'])
281 # Otherwise provide a variable called 'application' for mod_wsgi