]> git.proxmox.com Git - ceph.git/blame - ceph/src/powerdns/pdns-backend-rgw.py
update source to Ceph Pacific 16.2.2
[ceph.git] / ceph / src / powerdns / pdns-backend-rgw.py
CommitLineData
7c673cae
FG
1#!/usr/bin/python
2'''
3A backend for PowerDNS to direct RADOS Gateway bucket traffic to the correct regions.
4
5For example, two regions exist, US and EU.
6
7EU: o.myobjects.eu
8US: o.myobjects.us
9
10A global domain o.myobjects.com exists.
11
12Bucket 'foo' exists in the region EU and 'bar' in US.
13
14foo.o.myobjects.com will return a CNAME to foo.o.myobjects.eu
15bar.o.myobjects.com will return a CNAME to foo.o.myobjects.us
16
17The HTTP Remote Backend from PowerDNS is used in this case: http://doc.powerdns.com/html/remotebackend.html
18
19PowerDNS must be compiled with Remote HTTP backend support enabled, this is not default.
20
21Configuration for PowerDNS:
22
23launch=remote
24remote-connection-string=http:url=http://localhost:6780/dns
25
26Usage for this backend is showed by invoking with --help. See rgw-pdns.conf.in for a configuration example
27
28The ACCESS and SECRET key pair requires the caps "metadata=read"
29
30To test:
31
32$ curl -X GET http://localhost:6780/dns/lookup/foo.o.myobjects.com/ANY
33
34Should return something like:
35
36{
37 "result": [
38 {
39 "content": "foo.o.myobjects.eu",
40 "qtype": "CNAME",
41 "qname": "foo.o.myobjects.com",
42 "ttl": 60
43 }
44 ]
45}
46
47'''
48
49# Copyright: Wido den Hollander <wido@42on.com> 2014
9f95a23c 50# License: LGPL-2.1 or LGPL-3.0
7c673cae
FG
51
52from ConfigParser import SafeConfigParser, NoSectionError
53from flask import abort, Flask, request, Response
54from hashlib import sha1 as sha
55from time import gmtime, strftime
f67539c2 56from urllib.parse import urlparse
7c673cae
FG
57import argparse
58import base64
59import hmac
60import json
61import pycurl
62import StringIO
63import urllib
64import os
65import sys
66
67config_locations = ['rgw-pdns.conf', '~/rgw-pdns.conf', '/etc/ceph/rgw-pdns.conf']
68
69# PowerDNS expects a 200 what ever happends and always wants
70# 'result' to 'true' if the query fails
71def abort_early():
72 return json.dumps({'result': 'true'}) + "\n"
73
74# Generate the Signature string for S3 Authorization with the RGW Admin API
75def generate_signature(method, date, uri, headers=None):
76 sign = "%s\n\n" % method
77
78 if 'Content-Type' in headers:
79 sign += "%s\n" % headers['Content-Type']
80 else:
81 sign += "\n"
82
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()
86
87def generate_auth_header(signature):
88 return str("AWS %s:%s" % (config['rgw']['access_key'], signature.decode('utf-8')))
89
90# Do a HTTP request to the RGW Admin API
91def do_rgw_request(uri, params=None, data=None, headers=None):
92 if headers == None:
93 headers = {}
94
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)
98
99 query = None
100 if params != None:
101 query = '&'.join("%s=%s" % (key,val) for (key,val) in params.iteritems())
102
103 c = pycurl.Curl()
104 b = StringIO.StringIO()
105 url = "http://" + config['rgw']['endpoint'] + "/" + config['rgw']['admin_entry'] + "/" + uri + "?format=json"
106 if query != None:
107 url += "&" + urllib.quote_plus(query)
108
109 http_headers = []
110 for header in headers.keys():
111 http_headers.append(header + ": " + headers[header])
112
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)
118 c.perform()
119
120 response = b.getvalue()
121 if len(response) > 0:
122 return json.loads(response)
123
124 return None
125
126def get_radosgw_metadata(key):
127 return do_rgw_request('metadata', {'key': key})
128
129# Returns a string of the region where the bucket is in
130def 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']
135 return region
136
137# Returns the correct host for the bucket based on the regionmap
138def get_bucket_host(bucket, region_map):
139 region = get_bucket_region(bucket)
140 return bucket + "." + region_map[region]
141
142# This should support multiple endpoints per region!
143def parse_region_map(map):
144 regions = {}
145 for region in map['regions']:
146 url = urlparse(region['val']['endpoints'][0])
147 regions.update({region['key']: url.netloc})
148
149 return regions
150
151def str2bool(s):
152 return s.lower() in ("yes", "true", "1")
153
154def init_config():
155 parser = argparse.ArgumentParser()
156 parser.add_argument("--config", help="The configuration file to use.", action="store")
157
158 args = parser.parse_args()
159
160 defaults = {
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',
171 'debug': False
172 }
173
174 cfg = SafeConfigParser(defaults)
175 if args.config == None:
176 cfg.read(config_locations)
177 else:
178 if not os.path.isfile(args.config):
9f95a23c 179 print("Could not open configuration file %s" % args.config)
7c673cae
FG
180 sys.exit(1)
181
182 cfg.read(args.config)
183
184 config_section = 'powerdns'
185
186 try:
187 return {
188 'listen': {
189 'port': cfg.getint(config_section, 'listen_port'),
190 'addr': cfg.get(config_section, 'listen_addr')
191 },
192 'dns': {
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')
197 },
198 'rgw': {
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')
203 },
204 'debug': str2bool(cfg.get(config_section, 'debug'))
205 }
206
207 except NoSectionError:
208 return None
209
210def generate_app(config):
211 # The Flask App
212 app = Flask(__name__)
213
214 # Get the RGW Region Map
215 region_map = parse_region_map(do_rgw_request('config'))
216
217 @app.route('/')
218 def index():
219 abort(404)
220
221 @app.route("/dns/lookup/<qname>/<qtype>")
222 def bucket_location(qname, qtype):
223 if len(qname) == 0:
224 return abort_early()
225
226 split = qname.split(".", 1)
227 if len(split) != 2:
228 return abort_early()
229
230 bucket = split[0]
231 zone = split[1]
232
233 # If the received qname doesn't match our zone we abort
234 if zone != config['dns']['zone']:
235 return abort_early()
236
237 # We do not serve MX records
238 if qtype == "MX":
239 return abort_early()
240
241 # The basic result we always return, this is what PowerDNS expects.
242 response = {'result': 'true'}
243 result = {}
244
245 # A hardcoded SOA response (FIXME!)
246 if qtype == "SOA":
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']})
251 else:
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']})
257
258 if len(result) > 0:
259 res = []
260 res.append(result)
261 response['result'] = res
262
263 return json.dumps(response, indent=1) + "\n"
264
265 return app
266
267
268# Initialize the configuration and generate the Application
269config = init_config()
270if config == None:
9f95a23c
TL
271 print("Could not parse configuration file. "
272 "Tried to parse %s" % config_locations)
7c673cae
FG
273 sys.exit(1)
274
275app = generate_app(config)
276app.debug = config['debug']
277
278# Only run the App if this script is invoked from a Shell
279if __name__ == '__main__':
280 app.run(host=config['listen']['addr'], port=config['listen']['port'])
281
282# Otherwise provide a variable called 'application' for mod_wsgi
283else:
284 application = app