Fix some bugs when testing opensds ansible
[stor4nfv.git] / src / ceph / src / powerdns / pdns-backend-rgw.py
1 #!/usr/bin/python
2 '''
3 A backend for PowerDNS to direct RADOS Gateway bucket traffic to the correct regions.
4
5 For example, two regions exist, US and EU.
6
7 EU: o.myobjects.eu
8 US: o.myobjects.us
9
10 A global domain o.myobjects.com exists.
11
12 Bucket 'foo' exists in the region EU and 'bar' in US.
13
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
16
17 The HTTP Remote Backend from PowerDNS is used in this case: http://doc.powerdns.com/html/remotebackend.html
18
19 PowerDNS must be compiled with Remote HTTP backend support enabled, this is not default.
20
21 Configuration for PowerDNS:
22
23 launch=remote
24 remote-connection-string=http:url=http://localhost:6780/dns
25
26 Usage for this backend is showed by invoking with --help. See rgw-pdns.conf.in for a configuration example
27
28 The ACCESS and SECRET key pair requires the caps "metadata=read"
29
30 To test:
31
32 $ curl -X GET http://localhost:6780/dns/lookup/foo.o.myobjects.com/ANY
33
34 Should 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
50 # License:   LGPL2.1
51
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
57 import argparse
58 import base64
59 import hmac
60 import json
61 import pycurl
62 import StringIO
63 import urllib
64 import os
65 import sys
66
67 config_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
71 def abort_early():
72     return json.dumps({'result': 'true'}) + "\n"
73
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
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
87 def 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
91 def 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
126 def 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
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']
135     return region
136
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]
141
142 # This should support multiple endpoints per region!
143 def 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
151 def str2bool(s):
152     return s.lower() in ("yes", "true", "1")
153
154 def 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):
179             print "Could not open configuration file %s" % args.config
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
210 def 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
269 config = init_config()
270 if config == None:
271     print "Could not parse configuration file. Tried to parse %s" % config_locations
272     sys.exit(1)
273
274 app = generate_app(config)
275 app.debug = config['debug']
276
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'])
280
281 # Otherwise provide a variable called 'application' for mod_wsgi
282 else:
283     application = app