pdns_redis.py 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666
  1. #!/usr/bin/python
  2. #
  3. # pdns-redis.py, Copyright 2011, Bjarni R. Einarsson <http://bre.klaki.net/>
  4. # and The Beanstalks Project ehf.
  5. #
  6. # This program is free software: you can redistribute it and/or modify
  7. # it under the terms of the GNU Lesser General Public License as published
  8. # by the Free Software Foundation, either version 3 of the License, or (at
  9. # your option) any later version.
  10. #
  11. # This program is distributed in the hope that it will be useful,
  12. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  14. # GNU Lesser General Public License for more details.
  15. #
  16. # You should have received a copy of the GNU Lesser General Public License
  17. # along with this program. If not, see <http://www.gnu.org/licenses/>.
  18. #
  19. BANNER = "pdns-redis.py, by Bjarni R. Einarsson"
  20. DOC = """\
  21. pdns-redis.py is Copyright 2012, Bjarni R. Einarsson, http://bre.klaki.net/
  22. and The Beanstalks Project ehf.
  23. This program implements a PowerDNS pipe-backend for looking up domain info in a
  24. Redis database. It also includes basic CLI functionality for querying, setting
  25. and deleting DNS records in Redis.
  26. Usage: pdns-redis.py [-R <host:port>] [-A <password-file>] [-P]
  27. pdns-redis.py [-R <host:port>] [-A <password-file>]
  28. [-D <domain>] [-r <type>] [-d <data>] [-k] [-q] [-a <ttl>]
  29. Flags:
  30. -R <host:port> Set the Redis back-end.
  31. -W <host:port> Set the Redis back-end for writes.
  32. -A <password-file> Read a Redis password from the named file.
  33. -P Run as a PowerDNS pipe-backend.
  34. -w Enable wild-card lookups in PowerDNS pipe-backend.
  35. -D <domain> Select a domain for -q or -a.
  36. -r <record-type> Choose which record to modify/query/delete.
  37. -d <data> Data we are looking for or adding.
  38. -z Reset record and data.
  39. -q Query.
  40. -k Kill (delete).
  41. -a <ttl> Add using a given TTL (requires -r and -d). The TTL
  42. is in seconds, but may use a suffix of M, H, D or W
  43. for minutes, hours, days or weeks respectively.
  44. WARNING: This program does NOTHING to ensure the records you create are valid
  45. according to the DNS spec. Use at your own risk!
  46. Queries and kills (deletions) are filtered by -r and -d, if present. If
  47. neither is specified, the entire domain is processed.
  48. Note that arguments are processed in order so multiple adds and deletes can
  49. be done at once, just by repeating the -D, -r, -d, -k and -a arguments, varying
  50. the data as you go along.
  51. Domain entries starting with a '*', for example *.foo.com, will be treated as
  52. wild-card entries by the PowerDNS pipe-backend, if the -w flag precedes -P.
  53. Examples:
  54. # Configure an A and two MX records for domain.com.
  55. pdns-redis.py -R localhost:9076 -D domain.com \\
  56. -r A -d 1.2.3.4 -a 5M \\
  57. -r MX -d '10 mx1.domain.com.' -a 1D \\
  58. -d '20 mx2.domain.com.' -a 1D
  59. # Delete all CNAME records for foo.domain.com
  60. pdns-redis.py -R localhost:9076 -D foo.domain.com -r CNAME -k
  61. # Delete the 2nd MX from domain.com
  62. pdns-redis.py -R localhost:9076 -D domain.com -d '20 mx2.domain.com.' -k
  63. # Make self.domain.com return the IP of the DNS server
  64. pdns-redis.py -R localhost:9076 -D self.domain.com -r A -d self -a 5M
  65. # Delete domain.com completely
  66. pdns-redis.py -R localhost:9076 -D bar.domain.com -k
  67. # Chat with pdns-redis.py using the PowerDNS protocol
  68. pdns-redis.py -R localhost:9076 -P
  69. pdns-redis.py -R localhost:9076 -w -P # Now with wildcard domains!
  70. """
  71. import getopt
  72. import hashlib
  73. import random
  74. import re
  75. import redis
  76. import socket
  77. import sys
  78. import syslog
  79. import time
  80. import urllib
  81. DEBUG = False
  82. OPT_COMMON_FLAGS = 'A:R:W:z'
  83. OPT_COMMON_ARGS = ['auth=', 'redis=', 'redis_write=', 'reset']
  84. OPT_FLAGS = 'PwD:r:d:kqa:'
  85. OPT_ARGS = ['pdnsbe', 'domain', 'record', 'data', 'kill', 'delete', 'query',
  86. 'add']
  87. VALID_RECORDS = ['A', 'AAAA', 'NS', 'MX', 'CNAME', 'SOA', 'TXT']
  88. TTL_SUFFIXES = {
  89. 'M': 60,
  90. 'H': 60*60,
  91. 'D': 60*60*24,
  92. 'W': 60*60*24*7,
  93. }
  94. MAGIC_SELF_IP = 'self'
  95. MAGIC_TEST_VALIDITY = 60 # seconds
  96. REDIS_PREFIX = 'pdns.'
  97. class MockRedis(object):
  98. """A mock-redis object for quick offline tests."""
  99. def __init__(self, host=None, port=None, password=None):
  100. self.data = {}
  101. def ping(self): return True
  102. def get(self, key):
  103. if key in self.data: return self.data[key]
  104. return None
  105. def encode(self, val):
  106. if isinstance(val, str):
  107. return val
  108. if isinstance(val, unicode):
  109. return val.encode('utf-8')
  110. return str(val)
  111. def set(self, key, val):
  112. self.data[key] = self.encode(val)
  113. return True
  114. def setnx(self, key, val):
  115. if key in self.data: return None
  116. self.data[key] = self.encode(val)
  117. return val
  118. def incr(self, key):
  119. if key not in self.data: self.data[key] = 0
  120. self.data[key] = self.encode(int(self.data[key])+1)
  121. return int(self.data[key])
  122. def incrby(self, key, val):
  123. if key not in self.data: self.data[key] = 0
  124. self.data[key] = self.encode(int(self.data[key])+int(val))
  125. return int(self.data[key])
  126. def delete(self, key):
  127. if key in self.data:
  128. del(self.data[key])
  129. return True
  130. else:
  131. return False
  132. def hget(self, key, hkey):
  133. if key in self.data and hkey in self.data[key]: return self.data[key][hkey]
  134. return None
  135. def hincrby(self, key, hkey, val):
  136. if key not in self.data: self.data[key] = {}
  137. if hkey not in self.data[key]: self.data[key][hkey] = 0
  138. self.data[key][hkey] = self.encode(int(self.data[key][hkey])+int(val))
  139. return int(self.data[key][hkey])
  140. def hgetall(self, key):
  141. if key in self.data: return self.data[key]
  142. return {}
  143. def hdel(self, key, hkey):
  144. if key in self.data and hkey in self.data[key]: del(self.data[key][hkey])
  145. return True
  146. def hset(self, key, hkey, val):
  147. if key not in self.data: self.data[key] = {}
  148. self.data[key][hkey] = self.encode(val)
  149. return True
  150. def sadd(self, key, member):
  151. if key not in self.data: self.data[key] = {}
  152. self.data[key][member] = 1
  153. return True
  154. def srem(self, key, member):
  155. if key in self.data and member in self.data[key]:
  156. del self.data[key][member]
  157. return True
  158. return False
  159. def lpush(self, key, value):
  160. if key not in self.data:
  161. self.data[key] = []
  162. self.data[key].append(value)
  163. return True
  164. def llen(self, key):
  165. if key not in self.data: return 0
  166. return len(self.data[key])
  167. def lpop(self, key):
  168. return self.data[key].pop(0)
  169. class Error(Exception):
  170. pass
  171. class ArgumentError(Exception):
  172. pass
  173. class Task(object):
  174. """Tasks are all runnable."""
  175. def Run(self):
  176. return "Run not implemented! Woah!"
  177. class QueryOp(Task):
  178. """This object will query Redis for a given record."""
  179. def __init__(self, redis_pdns, domain, record=None, data=None):
  180. if not redis_pdns:
  181. raise ArgumentError('Redis master object required!')
  182. if not domain:
  183. raise ArgumentError('Domain is a required parameter.')
  184. self.redis_pdns = redis_pdns
  185. # FIXME: What about i18n domains? Does this make any sense?
  186. self.domain = domain and domain.lower() or None
  187. self.record = record and record.upper() or None
  188. self.data = data
  189. def BE(self):
  190. return self.redis_pdns.BE()
  191. def DSplit(self, domain, count=1024):
  192. return domain.split('.', count)
  193. def WildQuery(self, domain):
  194. try:
  195. sub, dom = self.DSplit(domain, 1)
  196. if sub == '*':
  197. sub, dom = self.DSplit(dom, 1)
  198. return self.Query(domain='*.%s' % dom)
  199. except ValueError:
  200. return []
  201. def _Query(self, domain=None, wildcards=False):
  202. pdns_be = self.BE()
  203. pdns_key = REDIS_PREFIX+(domain or self.domain)
  204. if self.record and self.data:
  205. key = "\t".join([self.record, self.data])
  206. ttl = pdns_be.hget(pdns_key, key)
  207. if ttl is not None:
  208. pdns_be.hincrby(pdns_key, 'TXT\tQC', 1)
  209. return [(self.domain, self.record, ttl, self.data)]
  210. elif wildcards:
  211. return self.WildQuery(domain or self.domain)
  212. else:
  213. return []
  214. rv = []
  215. ddata = pdns_be.hgetall(pdns_key)
  216. if self.record:
  217. for entry in ddata:
  218. record, data = entry.split("\t", 1)
  219. if record == self.record:
  220. rv.append((self.domain, record, ddata[entry], data))
  221. elif self.data:
  222. for entry in ddata:
  223. record, data = entry.split("\t", 1)
  224. if data == self.data:
  225. rv.append((self.domain, record, ddata[entry], data))
  226. else:
  227. for entry in ddata:
  228. record, data = entry.split("\t", 1)
  229. rv.append((self.domain, record, ddata[entry], data))
  230. if rv:
  231. pdns_be.hincrby(pdns_key, 'TXT\tQC', 1)
  232. return rv
  233. elif wildcards:
  234. return self.WildQuery(domain or self.domain)
  235. else:
  236. return []
  237. def Query(self, *args, **kwargs):
  238. try:
  239. return self._Query(*args, **kwargs)
  240. except redis.RedisError:
  241. self.redis_pdns.Disconnect()
  242. raise
  243. def Run(self):
  244. return '%s' % (self.Query(), )
  245. class WriteOp(QueryOp):
  246. def BE(self):
  247. return self.redis_pdns.WBE()
  248. class DeleteOp(WriteOp):
  249. """This object will delete records from Redis."""
  250. def Run(self):
  251. if not self.record and not self.data:
  252. self.BE().delete(REDIS_PREFIX+self.domain)
  253. return 'Deleted all records for %s.' % self.domain
  254. deleted = 0
  255. if self.record and self.data:
  256. deleted += self.BE().hdel(REDIS_PREFIX+self.domain,
  257. "\t".join([self.record, self.data]))
  258. else:
  259. for record in self.Query():
  260. deleted += self.BE().hdel(REDIS_PREFIX+self.domain,
  261. "\t".join([record[1], record[3]]))
  262. return 'Deleted %d records from %s.' % (deleted, self.domain)
  263. class AddOp(WriteOp):
  264. """This object will add a record to Redis."""
  265. def __init__(self, redis_pdns, domain, record, data, ttl):
  266. QueryOp.__init__(self, redis_pdns, domain, record, data)
  267. if self.record not in VALID_RECORDS:
  268. raise ArgumentError('Invalid record type: %s' % self.record)
  269. if not self.data:
  270. raise ArgumentError('Cannot add empty records.')
  271. if ttl and ttl[-1].upper() in TTL_SUFFIXES:
  272. self.ttl = str(int(ttl[:-1]) * TTL_SUFFIXES[ttl[-1].upper()])
  273. else:
  274. self.ttl = str(int(ttl))
  275. def Run(self):
  276. self.BE().hset(REDIS_PREFIX+self.domain,
  277. "\t".join([self.record, self.data]), self.ttl)
  278. return 'Added %s record to %s.' % (self.record, self.domain)
  279. class PdnsChatter(Task):
  280. """This object will chat with the pDNS server."""
  281. def __init__(self, infile, outfile, redis_pdns,
  282. query_op=None, wildcards=False):
  283. self.infile = infile
  284. self.outfile = outfile
  285. self.query_count = 0
  286. self.redis_pdns = redis_pdns
  287. self.local_ip = None
  288. self.magic_tests = {}
  289. self.qop = query_op or QueryOp
  290. self.wildcards = wildcards
  291. self.log_buffer = []
  292. syslog.openlog((sys.argv[0] or 'pdns_redis.py').split('/')[-1],
  293. syslog.LOG_PID, syslog.LOG_DAEMON)
  294. def reply(self, text):
  295. self.outfile.write(text)
  296. self.outfile.write("\n")
  297. self.outfile.flush()
  298. def readline(self):
  299. line = self.infile.readline()
  300. if len(line) == 0: raise IOError('EOF')
  301. return line.strip()
  302. def SendMxOrSrv(self, d1, d2, d3, d4):
  303. self.reply('DATA\t%s\tIN\t%s\t%s\t-1\t%s' % (d1, d2, d3, d4))
  304. def MagicTest(self, want, url, now=None):
  305. now = now or time.time()
  306. result = self.magic_tests.get(url, {})
  307. if result.get('time', 0) < (now - MAGIC_TEST_VALIDITY):
  308. result['time'] = now
  309. try:
  310. tdata = ''.join(urllib.urlopen(url).readlines())
  311. result['ok'] = tdata.startswith(want)
  312. except:
  313. result['ok'] = False
  314. self.magic_tests[url] = result
  315. if not result.get('ok', False):
  316. raise ValueError('Failed self-test %s != %s' % (want, url))
  317. def ResponseFilter(self, name, rt, ttl, val):
  318. return name, rt, ttl, val
  319. def SendRecord(self, record, remote_ip=None):
  320. if not remote_ip:
  321. remote_ip = '127.%d.%d.1' % (random.randint(1, 255),
  322. random.randint(1, 255))
  323. if record[3].startswith(MAGIC_SELF_IP):
  324. if ':' in record[3]:
  325. magic, test_want, test_url = record[3].split(':', 2)
  326. self.MagicTest(want, test_url)
  327. if not self.local_ip:
  328. raise ValueError("Local IP address is unknown")
  329. self.reply('DATA\t%s\tIN\t%s\t%s\t-1\t%s' % self.ResponseFilter(
  330. record[0], record[1], record[2], self.local_ip))
  331. elif record[1] not in ('A', 'AAAA'):
  332. self.reply('DATA\t%s\tIN\t%s\t%s\t-1\t%s' % self.ResponseFilter(
  333. record[0], record[1], record[2], record[3]))
  334. elif remote_ip and '/' in record[3]:
  335. # The goal of this, is to hash different clients to different IPs,
  336. # and rotate through the available set at 1 rotation-per-day, but
  337. # avoid rotating all the clients at the same time.
  338. values = record[3].split('/')
  339. o = int((time.time() +
  340. # Hash the first 3 octets of the IP to 5 hex digits; this
  341. # gives us 0-12 days of offset to shift the time by.
  342. int(hashlib.md5(remote_ip.rsplit('.', 1)[0]).hexdigest()[:5], 16)
  343. ) // (24 * 3600)
  344. ) % len(values)
  345. self.reply('DATA\t%s\tIN\t%s\t%s\t-1\t%s' % self.ResponseFilter(
  346. record[0], record[1], record[2], values[o]))
  347. else:
  348. value = random.choice(record[3].split('|'))
  349. self.reply('DATA\t%s\tIN\t%s\t%s\t-1\t%s' % self.ResponseFilter(
  350. record[0], record[1], record[2], value))
  351. def FlushLogBuffer(self):
  352. lb, self.log_buffer = self.log_buffer, []
  353. for message in lb:
  354. self.reply('LOG\t%s' % message)
  355. def SendLog(self, message):
  356. self.log_buffer.append(message)
  357. def EndReply(self):
  358. self.FlushLogBuffer()
  359. self.reply('END')
  360. def SetLocalIp(self, value):
  361. if not (value == '0.0.0.0' or
  362. value.startswith('127.') or
  363. value.startswith('192.168.') or
  364. value.startswith('10.')):
  365. self.local_ip = value
  366. def SlowGetOwnIp(self, target=('google.com', 80)):
  367. try:
  368. s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
  369. s.connect(target)
  370. self.SetLocalIp(s.getsockname()[0])
  371. s.close()
  372. except:
  373. pass
  374. SRV_SPLIT = re.compile('[\\s,]+')
  375. def Lookup(self, query):
  376. (pdns_qtype, domain, qclass, rtype, _id, remote_ip, local_ip) = query
  377. if not self.local_ip:
  378. self.SetLocalIp(local_ip)
  379. if not self.local_ip:
  380. try:
  381. self.SetLocalIp(socket.getaddrinfo(socket.gethostname(), None)[0][4][0])
  382. except:
  383. pass
  384. if pdns_qtype == 'Q':
  385. if not domain:
  386. records = []
  387. elif rtype == 'ANY':
  388. records = self.qop(self.redis_pdns, domain
  389. ).Query(wildcards=self.wildcards)
  390. else:
  391. records = self.qop(self.redis_pdns, domain, rtype
  392. ).Query(wildcards=self.wildcards)
  393. for record in records:
  394. if record[1] in ('MX', 'SRV'):
  395. data = '\t'.join(self.SRV_SPLIT.split(record[3], 1))
  396. self.SendMxOrSrv(record[0], record[1], record[2], data)
  397. elif record[1] != 'TXT' or record[3] != 'QC':
  398. self.SendRecord(record, remote_ip)
  399. self.EndReply()
  400. else:
  401. self.SendLog("PowerDNS requested %s, we only do Q." % pdns_qtype)
  402. self.FlushLogBuffer()
  403. self.reply('FAIL')
  404. def KeepRunning(self):
  405. return True
  406. def Run(self):
  407. line1 = self.readline()
  408. if not line1 == "HELO\t2":
  409. self.reply('FAIL')
  410. self.readline()
  411. sys.exit(1)
  412. else:
  413. self.reply('OK\t%s' % BANNER)
  414. if not self.local_ip:
  415. self.SlowGetOwnIp()
  416. while self.KeepRunning():
  417. line = self.readline()
  418. if line == '':
  419. break
  420. try:
  421. query = line.split("\t")
  422. if DEBUG: syslog.syslog(syslog.LOG_DEBUG, 'Q: %s' % query)
  423. if len(query) == 7:
  424. self.Lookup(query)
  425. self.query_count += 1
  426. elif len(query) == 2 and query[0] == 'AXFR':
  427. # Just fail silently on this one
  428. self.reply("FAIL")
  429. else:
  430. self.FlushLogBuffer()
  431. self.reply("LOG\tPowerDNS sent bad request: %s" % query)
  432. self.reply("FAIL")
  433. except Exception, err:
  434. self.redis_pdns.Disconnect()
  435. self.FlushLogBuffer()
  436. self.reply("LOG\tInternal Error: %s" % err)
  437. self.reply("FAIL")
  438. class PdnsRedis(object):
  439. """Main loop..."""
  440. def __init__(self):
  441. self.redis_host = None
  442. self.redis_port = None
  443. self.redis_pass = None
  444. self.redis_write_host = None
  445. self.redis_write_port = None
  446. self.be = None
  447. self.wbe = None
  448. self.chat_wildcards = False
  449. self.q_domain = None
  450. self.q_record = None
  451. self.q_data = None
  452. self.tasks = []
  453. def GetPass(self, filename):
  454. f = open(filename)
  455. for line in f.readlines():
  456. if line.startswith('requirepass') or line.startswith('pass'):
  457. rp, password = line.strip().split(' ', 1)
  458. return password
  459. return None
  460. def ParseWithCommonArgs(self, argv, flaglist, arglist):
  461. al = arglist[:]
  462. al.extend(OPT_COMMON_ARGS)
  463. opts, args = getopt.getopt(argv, ''.join([OPT_COMMON_FLAGS, flaglist]), al)
  464. for opt, arg in opts:
  465. if opt in ('-R', '--redis'):
  466. self.redis_host, self.redis_port = arg.split(':')
  467. if opt in ('-W', '--redis_write'):
  468. self.redis_write_host, self.redis_write_port = arg.split(':')
  469. if opt in ('-A', '--auth'):
  470. self.redis_pass = self.GetPass(arg)
  471. return opts, args
  472. def ParseArgs(self, argv):
  473. opts, args = self.ParseWithCommonArgs(argv, OPT_FLAGS, OPT_ARGS)
  474. for opt, arg in opts:
  475. if opt in ('-D', '--domain'): self.q_domain = arg
  476. if opt in ('-r', '--record'): self.q_record = arg
  477. if opt in ('-d', '--data'): self.q_data = arg
  478. if opt in ('-z', '--reset'):
  479. self.q_record, self.q_data = None, None
  480. if opt in ('-q', '--query'):
  481. self.tasks.append(QueryOp(self,
  482. self.q_domain, self.q_record, self.q_data))
  483. if opt in ('-k', '--delete', '--kill'):
  484. self.tasks.append(DeleteOp(self,
  485. self.q_domain, self.q_record, self.q_data))
  486. if opt in ('-a', '--add'):
  487. self.tasks.append(AddOp(self,
  488. self.q_domain, self.q_record, self.q_data, arg))
  489. if opt in ('-w', ):
  490. self.chat_wildcards = True
  491. if opt in ('-P', '--pdnsbe'):
  492. self.tasks.append(PdnsChatter(sys.stdin, sys.stdout, self,
  493. wildcards=self.chat_wildcards))
  494. return self
  495. def Disconnect(self):
  496. self.be = self.wbe = None
  497. def BE(self):
  498. errors = 0
  499. while not self.be:
  500. try:
  501. if self.redis_host == 'mock':
  502. self.be = MockRedis()
  503. else:
  504. self.be = redis.Redis(host=self.redis_host,
  505. port=int(self.redis_port),
  506. password=self.redis_pass)
  507. self.be.ping()
  508. except redis.RedisError:
  509. self.be = None
  510. errors += 1
  511. if errors > 24:
  512. raise
  513. else:
  514. time.sleep(5)
  515. return self.be
  516. def WBE(self):
  517. if not self.redis_write_host: return self.BE()
  518. errors = 0
  519. while not self.wbe:
  520. try:
  521. if self.redis_write_host == 'mock':
  522. self.wbe = MockRedis()
  523. else:
  524. self.wbe = redis.Redis(host=self.redis_write_host,
  525. port=int(self.redis_write_port),
  526. password=self.redis_pass)
  527. self.wbe.ping()
  528. except redis.RedisError:
  529. self.wbe = None
  530. errors += 1
  531. if errors > 24:
  532. raise
  533. else:
  534. time.sleep(5)
  535. return self.wbe
  536. def RunTasks(self):
  537. if not self.tasks:
  538. raise ArgumentError('Nothing to do!')
  539. else:
  540. self.BE()
  541. for task in self.tasks:
  542. sys.stdout.write(task.Run().encode('utf-8')+'\n')
  543. if __name__ == '__main__':
  544. try:
  545. pr = PdnsRedis().ParseArgs(sys.argv[1:]).RunTasks()
  546. except ArgumentError, e:
  547. print DOC
  548. print 'Error: %s' % e
  549. sys.exit(1)