options.py 37 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023
  1. import ConfigParser
  2. import getopt
  3. import os
  4. import datatypes
  5. import logging
  6. import sys
  7. import tempfile
  8. import socket
  9. import errno
  10. import signal
  11. import re
  12. import xmlrpclib
  13. import httplib
  14. import urllib
  15. class FileHandler(logging.StreamHandler):
  16. """File handler which supports reopening of logs.
  17. Re-opening should be used instead of the 'rollover' feature of
  18. the FileHandler from the standard library's logging package.
  19. """
  20. def __init__(self, filename, mode="a"):
  21. logging.StreamHandler.__init__(self, open(filename, mode))
  22. self.baseFilename = filename
  23. self.mode = mode
  24. def close(self):
  25. try:
  26. self.stream.close()
  27. except IOError:
  28. pass
  29. def reopen(self):
  30. self.close()
  31. self.stream = open(self.baseFilename, self.mode)
  32. def remove(self):
  33. try:
  34. os.remove(self.baseFilename)
  35. except (IOError, OSError):
  36. pass
  37. class RawHandler:
  38. def emit(self, record):
  39. """
  40. Override the handler to not insert a linefeed during emit.
  41. """
  42. try:
  43. msg = self.format(record)
  44. try:
  45. self.stream.write(msg)
  46. except UnicodeError:
  47. self.stream.write(msg.encode("UTF-8"))
  48. except IOError, why:
  49. if why[0] == errno.EINTR:
  50. pass
  51. else:
  52. self.flush()
  53. except:
  54. self.handleError(record)
  55. class RawFileHandler(RawHandler, FileHandler):
  56. pass
  57. class RawStreamHandler(RawHandler, logging.StreamHandler):
  58. def remove(self):
  59. pass
  60. class RotatingRawFileHandler(RawFileHandler):
  61. def __init__(self, filename, mode='a', maxBytes=512*1024*1024,
  62. backupCount=10):
  63. """
  64. Open the specified file and use it as the stream for logging.
  65. By default, the file grows indefinitely. You can specify particular
  66. values of maxBytes and backupCount to allow the file to rollover at
  67. a predetermined size.
  68. Rollover occurs whenever the current log file is nearly maxBytes in
  69. length. If backupCount is >= 1, the system will successively create
  70. new files with the same pathname as the base file, but with extensions
  71. ".1", ".2" etc. appended to it. For example, with a backupCount of 5
  72. and a base file name of "app.log", you would get "app.log",
  73. "app.log.1", "app.log.2", ... through to "app.log.5". The file being
  74. written to is always "app.log" - when it gets filled up, it is closed
  75. and renamed to "app.log.1", and if files "app.log.1", "app.log.2" etc.
  76. exist, then they are renamed to "app.log.2", "app.log.3" etc.
  77. respectively.
  78. If maxBytes is zero, rollover never occurs.
  79. """
  80. if maxBytes > 0:
  81. mode = 'a' # doesn't make sense otherwise!
  82. RawFileHandler.__init__(self, filename, mode)
  83. self.maxBytes = maxBytes
  84. self.backupCount = backupCount
  85. def emit(self, record):
  86. """
  87. Emit a record.
  88. Output the record to the file, catering for rollover as described
  89. in doRollover().
  90. """
  91. try:
  92. if self.shouldRollover(record):
  93. self.doRollover()
  94. RawFileHandler.emit(self, record)
  95. except:
  96. self.handleError(record)
  97. def doRollover(self):
  98. """
  99. Do a rollover, as described in __init__().
  100. """
  101. self.stream.close()
  102. if self.backupCount > 0:
  103. for i in range(self.backupCount - 1, 0, -1):
  104. sfn = "%s.%d" % (self.baseFilename, i)
  105. dfn = "%s.%d" % (self.baseFilename, i + 1)
  106. if os.path.exists(sfn):
  107. if os.path.exists(dfn):
  108. os.remove(dfn)
  109. os.rename(sfn, dfn)
  110. dfn = self.baseFilename + ".1"
  111. if os.path.exists(dfn):
  112. os.remove(dfn)
  113. os.rename(self.baseFilename, dfn)
  114. self.stream = open(self.baseFilename, 'w')
  115. def shouldRollover(self, record):
  116. """
  117. Determine if rollover should occur.
  118. Basically, see if the supplied record would cause the file to exceed
  119. the size limit we have.
  120. """
  121. if self.maxBytes > 0: # are we rolling over?
  122. msg = "%s\n" % self.format(record)
  123. self.stream.seek(0, 2) #due to non-posix-compliant Windows feature
  124. if self.stream.tell() + len(msg) >= self.maxBytes:
  125. return 1
  126. return 0
  127. def getLogger(filename, level, fmt, rotating=False,
  128. maxbytes=0, backups=0):
  129. import logging
  130. logger = logging.getLogger(filename)
  131. if rotating is False:
  132. hdlr = RawFileHandler(filename)
  133. else:
  134. hdlr = RotatingRawFileHandler(filename, 'a', maxbytes, backups)
  135. formatter = logging.Formatter(fmt)
  136. hdlr.setFormatter(formatter)
  137. logger.handlers = []
  138. logger.addHandler(hdlr)
  139. logger.setLevel(level)
  140. return logger
  141. class Dummy:
  142. pass
  143. class Options:
  144. uid = gid = None
  145. progname = sys.argv[0]
  146. configfile = None
  147. schemadir = None
  148. configroot = None
  149. # Class variable deciding whether positional arguments are allowed.
  150. # If you want positional arguments, set this to 1 in your subclass.
  151. positional_args_allowed = 0
  152. def __init__(self):
  153. self.names_list = []
  154. self.short_options = []
  155. self.long_options = []
  156. self.options_map = {}
  157. self.default_map = {}
  158. self.required_map = {}
  159. self.environ_map = {}
  160. self.add(None, None, "h", "help", self.help)
  161. self.add("configfile", None, "c:", "configure=")
  162. def help(self, dummy):
  163. """Print a long help message to stdout and exit(0).
  164. Occurrences of "%s" in are replaced by self.progname.
  165. """
  166. help = self.doc
  167. if help.find("%s") > 0:
  168. help = help.replace("%s", self.progname)
  169. print help,
  170. sys.exit(0)
  171. def usage(self, msg):
  172. """Print a brief error message to stderr and exit(2)."""
  173. sys.stderr.write("Error: %s\n" % str(msg))
  174. sys.stderr.write("For help, use %s -h\n" % self.progname)
  175. sys.exit(2)
  176. def remove(self,
  177. name=None, # attribute name on self
  178. confname=None, # dotted config path name
  179. short=None, # short option name
  180. long=None, # long option name
  181. ):
  182. """Remove all traces of name, confname, short and/or long."""
  183. if name:
  184. for n, cn in self.names_list[:]:
  185. if n == name:
  186. self.names_list.remove((n, cn))
  187. if self.default_map.has_key(name):
  188. del self.default_map[name]
  189. if self.required_map.has_key(name):
  190. del self.required_map[name]
  191. if confname:
  192. for n, cn in self.names_list[:]:
  193. if cn == confname:
  194. self.names_list.remove((n, cn))
  195. if short:
  196. key = "-" + short[0]
  197. if self.options_map.has_key(key):
  198. del self.options_map[key]
  199. if long:
  200. key = "--" + long
  201. if key[-1] == "=":
  202. key = key[:-1]
  203. if self.options_map.has_key(key):
  204. del self.options_map[key]
  205. def add(self,
  206. name=None, # attribute name on self
  207. confname=None, # dotted config path name
  208. short=None, # short option name
  209. long=None, # long option name
  210. handler=None, # handler (defaults to string)
  211. default=None, # default value
  212. required=None, # message if not provided
  213. flag=None, # if not None, flag value
  214. env=None, # if not None, environment variable
  215. ):
  216. """Add information about a configuration option.
  217. This can take several forms:
  218. add(name, confname)
  219. Configuration option 'confname' maps to attribute 'name'
  220. add(name, None, short, long)
  221. Command line option '-short' or '--long' maps to 'name'
  222. add(None, None, short, long, handler)
  223. Command line option calls handler
  224. add(name, None, short, long, handler)
  225. Assign handler return value to attribute 'name'
  226. In addition, one of the following keyword arguments may be given:
  227. default=... -- if not None, the default value
  228. required=... -- if nonempty, an error message if no value provided
  229. flag=... -- if not None, flag value for command line option
  230. env=... -- if not None, name of environment variable that
  231. overrides the configuration file or default
  232. """
  233. if flag is not None:
  234. if handler is not None:
  235. raise ValueError, "use at most one of flag= and handler="
  236. if not long and not short:
  237. raise ValueError, "flag= requires a command line flag"
  238. if short and short.endswith(":"):
  239. raise ValueError, "flag= requires a command line flag"
  240. if long and long.endswith("="):
  241. raise ValueError, "flag= requires a command line flag"
  242. handler = lambda arg, flag=flag: flag
  243. if short and long:
  244. if short.endswith(":") != long.endswith("="):
  245. raise ValueError, "inconsistent short/long options: %r %r" % (
  246. short, long)
  247. if short:
  248. if short[0] == "-":
  249. raise ValueError, "short option should not start with '-'"
  250. key, rest = short[:1], short[1:]
  251. if rest not in ("", ":"):
  252. raise ValueError, "short option should be 'x' or 'x:'"
  253. key = "-" + key
  254. if self.options_map.has_key(key):
  255. raise ValueError, "duplicate short option key '%s'" % key
  256. self.options_map[key] = (name, handler)
  257. self.short_options.append(short)
  258. if long:
  259. if long[0] == "-":
  260. raise ValueError, "long option should not start with '-'"
  261. key = long
  262. if key[-1] == "=":
  263. key = key[:-1]
  264. key = "--" + key
  265. if self.options_map.has_key(key):
  266. raise ValueError, "duplicate long option key '%s'" % key
  267. self.options_map[key] = (name, handler)
  268. self.long_options.append(long)
  269. if env:
  270. self.environ_map[env] = (name, handler)
  271. if name:
  272. if not hasattr(self, name):
  273. setattr(self, name, None)
  274. self.names_list.append((name, confname))
  275. if default is not None:
  276. self.default_map[name] = default
  277. if required:
  278. self.required_map[name] = required
  279. def realize(self, args=None, doc=None,
  280. progname=None, raise_getopt_errs=True):
  281. """Realize a configuration.
  282. Optional arguments:
  283. args -- the command line arguments, less the program name
  284. (default is sys.argv[1:])
  285. doc -- usage message (default is __main__.__doc__)
  286. """
  287. # Provide dynamic default method arguments
  288. if args is None:
  289. args = sys.argv[1:]
  290. if progname is None:
  291. progname = sys.argv[0]
  292. if doc is None:
  293. import __main__
  294. doc = __main__.__doc__
  295. self.progname = progname
  296. self.doc = doc
  297. self.options = []
  298. self.args = []
  299. # Call getopt
  300. try:
  301. self.options, self.args = getopt.getopt(
  302. args, "".join(self.short_options), self.long_options)
  303. except getopt.error, msg:
  304. if raise_getopt_errs:
  305. self.usage(msg)
  306. # Check for positional args
  307. if self.args and not self.positional_args_allowed:
  308. self.usage("positional arguments are not supported")
  309. # Process options returned by getopt
  310. for opt, arg in self.options:
  311. name, handler = self.options_map[opt]
  312. if handler is not None:
  313. try:
  314. arg = handler(arg)
  315. except ValueError, msg:
  316. self.usage("invalid value for %s %r: %s" % (opt, arg, msg))
  317. if name and arg is not None:
  318. if getattr(self, name) is not None:
  319. self.usage("conflicting command line option %r" % opt)
  320. setattr(self, name, arg)
  321. # Process environment variables
  322. for envvar in self.environ_map.keys():
  323. name, handler = self.environ_map[envvar]
  324. if name and getattr(self, name, None) is not None:
  325. continue
  326. if os.environ.has_key(envvar):
  327. value = os.environ[envvar]
  328. if handler is not None:
  329. try:
  330. value = handler(value)
  331. except ValueError, msg:
  332. self.usage("invalid environment value for %s %r: %s"
  333. % (envvar, value, msg))
  334. if name and value is not None:
  335. setattr(self, name, value)
  336. if self.configfile is None:
  337. self.configfile = self.default_configfile()
  338. if self.configfile is not None:
  339. # Process config file
  340. try:
  341. self.read_config(self.configfile)
  342. except ValueError, msg:
  343. self.usage(str(msg))
  344. # Copy config options to attributes of self. This only fills
  345. # in options that aren't already set from the command line.
  346. for name, confname in self.names_list:
  347. if confname and getattr(self, name) is None:
  348. parts = confname.split(".")
  349. obj = self.configroot
  350. for part in parts:
  351. if obj is None:
  352. break
  353. # Here AttributeError is not a user error!
  354. obj = getattr(obj, part)
  355. setattr(self, name, obj)
  356. # Process defaults
  357. for name, value in self.default_map.items():
  358. if getattr(self, name) is None:
  359. setattr(self, name, value)
  360. # Process required options
  361. for name, message in self.required_map.items():
  362. if getattr(self, name) is None:
  363. self.usage(message)
  364. class ServerOptions(Options):
  365. user = None
  366. sockchown = None
  367. sockchmod = None
  368. logfile = None
  369. loglevel = None
  370. pidfile = None
  371. passwdfile = None
  372. nodaemon = None
  373. AUTOMATIC = []
  374. def __init__(self):
  375. Options.__init__(self)
  376. self.configroot = Dummy()
  377. self.configroot.supervisord = Dummy()
  378. self.add("backofflimit", "supervisord.backofflimit",
  379. "b:", "backofflimit=", int, default=3)
  380. self.add("nodaemon", "supervisord.nodaemon", "n", "nodaemon", flag=1,
  381. default=0)
  382. self.add("forever", "supervisord.forever", "f", "forever",
  383. flag=1, default=0)
  384. self.add("user", "supervisord.user", "u:", "user=")
  385. self.add("umask", "supervisord.umask", "m:", "umask=",
  386. datatypes.octal_type, default='022')
  387. self.add("directory", "supervisord.directory", "d:", "directory=",
  388. datatypes.existing_directory)
  389. self.add("logfile", "supervisord.logfile", "l:", "logfile=",
  390. datatypes.existing_dirpath, default="supervisord.log")
  391. self.add("logfile_maxbytes", "supervisord.logfile_maxbytes",
  392. "y:", "logfile_maxbytes=", datatypes.byte_size,
  393. default=50 * 1024 * 1024) # 50MB
  394. self.add("logfile_backups", "supervisord.logfile_backups",
  395. "z:", "logfile_backups=", datatypes.integer, default=10)
  396. self.add("loglevel", "supervisord.loglevel", "e:", "loglevel=",
  397. datatypes.logging_level, default="info")
  398. self.add("pidfile", "supervisord.pidfile", "j:", "pidfile=",
  399. datatypes.existing_dirpath, default="supervisord.pid")
  400. self.add("identifier", "supervisord.identifier", "i:", "identifier=",
  401. datatypes.existing_dirpath, default="supervisor")
  402. self.add("childlogdir", "supervisord.childlogdir", "q:", "childlogdir=",
  403. datatypes.existing_directory, default=tempfile.gettempdir())
  404. self.add("http_port", "supervisord.http_port", "w:", "http_port=",
  405. datatypes.SocketAddress, default=None)
  406. self.add("http_username", "supervisord.http_username", "g:",
  407. "http_username=", str, default=None)
  408. self.add("http_password", "supervisord.http_password", "r:",
  409. "http_password=", str, default=None)
  410. self.add("minfds", "supervisord.minfds",
  411. "a:", "minfds=", int, default=1024)
  412. self.add("minprocs", "supervisord.minprocs",
  413. "", "minprocs=", int, default=200)
  414. self.add("nocleanup", "supervisord.nocleanup",
  415. "k", "nocleanup", flag=1, default=0)
  416. self.add("sockchmod", "supervisord.sockchmod", "p:", "socket-mode=",
  417. datatypes.octal_type, default=0700)
  418. self.add("sockchown", "supervisord.sockchown", "o:", "socket-owner=",
  419. datatypes.dot_separated_user_group)
  420. def getLogger(self, filename, level, fmt, rotating=False,
  421. maxbytes=0, backups=0):
  422. return getLogger(filename, level, fmt, rotating, maxbytes,
  423. backups)
  424. def default_configfile(self):
  425. """Return the name of the default config file, or None."""
  426. # This allows a default configuration file to be used without
  427. # affecting the -c command line option; setting self.configfile
  428. # before calling realize() makes the -C option unusable since
  429. # then realize() thinks it has already seen the option. If no
  430. # -c is used, realize() will call this method to try to locate
  431. # a configuration file.
  432. config = '/etc/supervisord.conf'
  433. if not os.path.exists(config):
  434. self.usage('No config file found at default path "%s"; create '
  435. 'this file or use the -c option to specify a config '
  436. 'file at a different path' % config)
  437. return config
  438. def realize(self, *arg, **kw):
  439. Options.realize(self, *arg, **kw)
  440. import socket
  441. # Additional checking of user option; set uid and gid
  442. if self.user is not None:
  443. import pwd
  444. uid = datatypes.name_to_uid(self.user)
  445. if uid is None:
  446. self.usage("No such user %s" % self.user)
  447. self.uid = uid
  448. self.gid = datatypes.gid_for_uid(uid)
  449. if not self.logfile:
  450. logfile = os.path.abspath(self.configroot.supervisord.logfile)
  451. else:
  452. logfile = os.path.abspath(self.logfile)
  453. self.logfile = logfile
  454. if not self.loglevel:
  455. self.loglevel = self.configroot.supervisord.loglevel
  456. if not self.pidfile:
  457. self.pidfile = os.path.abspath(self.configroot.supervisord.pidfile)
  458. else:
  459. self.pidfile = os.path.abspath(self.pidfile)
  460. self.programs = self.configroot.supervisord.programs
  461. if not self.sockchown:
  462. self.sockchown = self.configroot.supervisord.sockchown
  463. self.identifier = self.configroot.supervisord.identifier
  464. if self.nodaemon:
  465. self.daemon = False
  466. def convert_sockchown(self, sockchown):
  467. # Convert chown stuff to uid/gid
  468. user = sockchown[0]
  469. group = sockchown[1]
  470. uid = datatypes.name_to_uid(user)
  471. if uid is None:
  472. self.usage("No such sockchown user %s" % user)
  473. if group is None:
  474. gid = datatypes.gid_for_uid(uid)
  475. else:
  476. gid = datatypes.name_to_gid(group)
  477. if gid is None:
  478. self.usage("No such sockchown group %s" % group)
  479. return uid, gid
  480. def read_config(self, fp):
  481. section = self.configroot.supervisord
  482. if not hasattr(fp, 'read'):
  483. try:
  484. fp = open(fp, 'r')
  485. except (IOError, OSError):
  486. raise ValueError("could not find config file %s" % fp)
  487. config = UnhosedConfigParser()
  488. config.readfp(fp)
  489. sections = config.sections()
  490. if not 'supervisord' in sections:
  491. raise ValueError, '.ini file does not include supervisord section'
  492. minfds = config.getdefault('minfds', 1024)
  493. section.minfds = datatypes.integer(minfds)
  494. minprocs = config.getdefault('minprocs', 200)
  495. section.minprocs = datatypes.integer(minprocs)
  496. directory = config.getdefault('directory', None)
  497. if directory is None:
  498. section.directory = None
  499. else:
  500. directory = datatypes.existing_directory(directory)
  501. section.directory = directory
  502. backofflimit = config.getdefault('backofflimit', 3)
  503. try:
  504. limit = datatypes.integer(backofflimit)
  505. except:
  506. raise ValueError("backofflimit is not an integer: %s"
  507. % backofflimit)
  508. section.backofflimit = limit
  509. forever = config.getdefault('forever', 'false')
  510. section.forever = datatypes.boolean(forever)
  511. user = config.getdefault('user', None)
  512. section.user = user
  513. umask = datatypes.octal_type(config.getdefault('umask', '022'))
  514. section.umask = umask
  515. logfile = config.getdefault('logfile', 'supervisord.log')
  516. logfile = datatypes.existing_dirpath(logfile)
  517. section.logfile = logfile
  518. logfile_maxbytes = config.getdefault('logfile_maxbytes', '50MB')
  519. logfile_maxbytes = datatypes.byte_size(logfile_maxbytes)
  520. section.logfile_maxbytes = logfile_maxbytes
  521. logfile_backups = config.getdefault('logfile_backups', 10)
  522. logfile_backups = datatypes.integer(logfile_backups)
  523. section.logfile_backups = logfile_backups
  524. loglevel = config.getdefault('loglevel', 'info')
  525. loglevel = datatypes.logging_level(loglevel)
  526. section.loglevel = loglevel
  527. pidfile = config.getdefault('pidfile', 'supervisord.pid')
  528. pidfile = datatypes.existing_dirpath(pidfile)
  529. section.pidfile = pidfile
  530. identifier = config.getdefault('identifier', 'supervisor')
  531. section.identifier = identifier
  532. nodaemon = config.getdefault('nodaemon', 'false')
  533. section.nodaemon = datatypes.boolean(nodaemon)
  534. childlogdir = config.getdefault('childlogdir', tempfile.gettempdir())
  535. childlogdir = datatypes.existing_directory(childlogdir)
  536. section.childlogdir = childlogdir
  537. http_port = config.getdefault('http_port', None)
  538. if http_port is None:
  539. section.http_port = None
  540. else:
  541. section.http_port = datatypes.SocketAddress(http_port)
  542. http_password = config.getdefault('http_password', None)
  543. http_username = config.getdefault('http_username', None)
  544. if http_password or http_username:
  545. if http_password is None:
  546. raise ValueError('Must specify http_password if '
  547. 'http_username is specified')
  548. if http_username is None:
  549. raise ValueError('Must specify http_username if '
  550. 'http_password is specified')
  551. section.http_password = http_password
  552. section.http_username = http_username
  553. nocleanup = config.getdefault('nocleanup', 'false')
  554. section.nocleanup = datatypes.boolean(nocleanup)
  555. sockchown = config.getdefault('sockchown', None)
  556. if sockchown is None:
  557. section.sockchown = (-1, -1)
  558. else:
  559. try:
  560. section.sockchown = datatypes.dot_separated_user_group(
  561. sockchown)
  562. except ValueError:
  563. raise ValueError('Invalid sockchown value %s' % sockchown)
  564. sockchmod = config.getdefault('sockchmod', None)
  565. if sockchmod is None:
  566. section.sockchmod = 0700
  567. else:
  568. try:
  569. section.sockchmod = datatypes.octal_type(sockchmod)
  570. except (TypeError, ValueError):
  571. raise ValueError('Invalid sockchmod value %s' % sockchmod)
  572. section.programs = self.programs_from_config(config)
  573. return section
  574. def programs_from_config(self, config):
  575. programs = []
  576. for section in config.sections():
  577. if not section.startswith('program:'):
  578. continue
  579. name = section.split(':', 1)[1]
  580. command = config.saneget(section, 'command', None)
  581. if command is None:
  582. raise ValueError, (
  583. 'program section %s does not specify a command' )
  584. priority = config.saneget(section, 'priority', 999)
  585. priority = datatypes.integer(priority)
  586. autostart = config.saneget(section, 'autostart', 'true')
  587. autostart = datatypes.boolean(autostart)
  588. autorestart = config.saneget(section, 'autorestart', 'true')
  589. autorestart = datatypes.boolean(autorestart)
  590. uid = config.saneget(section, 'user', None)
  591. if uid is not None:
  592. uid = datatypes.name_to_uid(uid)
  593. logfile = config.saneget(section, 'logfile', None)
  594. if logfile in ('NONE', 'OFF'):
  595. logfile = None
  596. elif logfile in (None, 'AUTO'):
  597. logfile = self.AUTOMATIC
  598. else:
  599. logfile = datatypes.existing_dirpath(logfile)
  600. logfile_backups = config.saneget(section, 'logfile_backups', 10)
  601. logfile_backups = datatypes.integer(logfile_backups)
  602. logfile_maxbytes = config.saneget(section, 'logfile_maxbytes',
  603. datatypes.byte_size('50MB'))
  604. logfile_maxbytes = datatypes.integer(logfile_maxbytes)
  605. stopsignal = config.saneget(section, 'stopsignal', signal.SIGTERM)
  606. stopsignal = datatypes.signal(stopsignal)
  607. exitcodes = config.saneget(section, 'exitcodes', '0,2')
  608. try:
  609. exitcodes = datatypes.list_of_ints(exitcodes)
  610. except:
  611. raise ValueError("exitcodes must be a list of ints e.g. 1,2")
  612. log_stderr = config.saneget(section, 'log_stderr', 'false')
  613. log_stderr = datatypes.boolean(log_stderr)
  614. pconfig = ProcessConfig(name=name, command=command,
  615. priority=priority, autostart=autostart,
  616. autorestart=autorestart, uid=uid,
  617. logfile=logfile,
  618. logfile_backups=logfile_backups,
  619. logfile_maxbytes=logfile_maxbytes,
  620. stopsignal=stopsignal,
  621. exitcodes=exitcodes,
  622. log_stderr=log_stderr)
  623. programs.append(pconfig)
  624. programs.sort() # asc by priority
  625. return programs
  626. def clear_childlogdir(self):
  627. # must be called after realize()
  628. childlogdir = self.childlogdir
  629. fnre = re.compile(r'.+?---%s-\S+\.log\.{0,1}\d{0,4}' % self.identifier)
  630. try:
  631. filenames = os.listdir(childlogdir)
  632. except (IOError, OSError):
  633. self.logger.info('Could not clear childlog dir')
  634. return
  635. for filename in filenames:
  636. if fnre.match(filename):
  637. pathname = os.path.join(childlogdir, filename)
  638. try:
  639. os.remove(pathname)
  640. except (os.error, IOError):
  641. self.logger.info('Failed to clean up %r' % pathname)
  642. def make_logger(self, critical_messages, info_messages):
  643. # must be called after realize() and after supervisor does setuid()
  644. format = '%(asctime)s %(levelname)s %(message)s\n'
  645. self.logger = self.getLogger(
  646. self.logfile,
  647. self.loglevel,
  648. format,
  649. rotating=True,
  650. maxbytes=self.logfile_maxbytes,
  651. backups=self.logfile_backups,
  652. )
  653. if self.nodaemon:
  654. stdout_handler = RawStreamHandler(sys.stdout)
  655. formatter = logging.Formatter(format)
  656. stdout_handler.setFormatter(formatter)
  657. self.logger.addHandler(stdout_handler)
  658. for msg in critical_messages:
  659. self.logger.critical(msg)
  660. for msg in info_messages:
  661. self.logger.info(msg)
  662. class ClientOptions(Options):
  663. positional_args_allowed = 1
  664. interactive = None
  665. prompt = None
  666. serverurl = None
  667. username = None
  668. password = None
  669. def __init__(self):
  670. Options.__init__(self)
  671. self.configroot = Dummy()
  672. self.configroot.supervisorctl = Dummy()
  673. self.configroot.supervisorctl.interactive = None
  674. self.configroot.supervisorctl.prompt = None
  675. self.configroot.supervisorctl.serverurl = None
  676. self.configroot.supervisorctl.username = None
  677. self.configroot.supervisorctl.password = None
  678. self.add("interactive", "supervisorctl.interactive", "i",
  679. "interactive", flag=1, default=0)
  680. self.add("prompt", "supervisorctl.prompt", default="supervisor")
  681. self.add("serverurl", "supervisorctl.serverurl", "s:", "serverurl=",
  682. datatypes.url,
  683. default="http://localhost:9001")
  684. self.add("username", "supervisorctl.username", "u:", "username=")
  685. self.add("password", "supervisorctl.password", "p:", "password=")
  686. def realize(self, *arg, **kw):
  687. Options.realize(self, *arg, **kw)
  688. if not self.args:
  689. self.interactive = 1
  690. def default_configfile(self):
  691. """Return the name of the default config file, or None."""
  692. config = '/etc/supervisord.conf'
  693. if not os.path.exists(config):
  694. self.usage('No config file found at default path "%s"; create '
  695. 'this file or use the -c option to specify a config '
  696. 'file at a different path' % config)
  697. return config
  698. def read_config(self, fp):
  699. section = self.configroot.supervisorctl
  700. if not hasattr(fp, 'read'):
  701. try:
  702. fp = open(fp, 'r')
  703. except (IOError, OSError):
  704. raise ValueError("could not find config file %s" % fp)
  705. config = UnhosedConfigParser()
  706. config.mysection = 'supervisorctl'
  707. config.readfp(fp)
  708. sections = config.sections()
  709. if not 'supervisorctl' in sections:
  710. raise ValueError,'.ini file does not include supervisorctl section'
  711. section.serverurl = config.getdefault('serverurl',
  712. 'http://localhost:9001')
  713. section.prompt = config.getdefault('prompt', 'supervisor')
  714. section.username = config.getdefault('username', None)
  715. section.password = config.getdefault('password', None)
  716. return section
  717. def getServerProxy(self):
  718. # mostly put here for unit testing
  719. return xmlrpclib.ServerProxy(
  720. # dumbass ServerProxy won't allow us to pass in a non-HTTP url,
  721. # so we fake the url we pass into it and always use the transport's
  722. # 'serverurl' to figure out what to attach to
  723. 'http://127.0.0.1',
  724. transport = BasicAuthTransport(self.username,
  725. self.password,
  726. self.serverurl)
  727. )
  728. _marker = []
  729. class UnhosedConfigParser(ConfigParser.RawConfigParser):
  730. mysection = 'supervisord'
  731. def getdefault(self, option, default=_marker):
  732. try:
  733. return self.get(self.mysection, option)
  734. except ConfigParser.NoOptionError:
  735. if default is _marker:
  736. raise
  737. else:
  738. return default
  739. def saneget(self, section, option, default=_marker):
  740. try:
  741. return self.get(section, option)
  742. except ConfigParser.NoOptionError:
  743. if default is _marker:
  744. raise
  745. else:
  746. return default
  747. class ProcessConfig:
  748. def __init__(self, name, command, priority, autostart, autorestart,
  749. uid, logfile, logfile_backups, logfile_maxbytes, stopsignal,
  750. exitcodes, log_stderr):
  751. self.name = name
  752. self.command = command
  753. self.priority = priority
  754. self.autostart = autostart
  755. self.autorestart = autorestart
  756. self.uid = uid
  757. self.logfile = logfile
  758. self.logfile_backups = logfile_backups
  759. self.logfile_maxbytes = logfile_maxbytes
  760. self.stopsignal = stopsignal
  761. self.exitcodes = exitcodes
  762. self.log_stderr = log_stderr
  763. def __cmp__(self, other):
  764. return cmp(self.priority, other.priority)
  765. class BasicAuthTransport(xmlrpclib.Transport):
  766. """ A transport that understands basic auth and UNIX domain socket
  767. URLs """
  768. def __init__(self, username=None, password=None, serverurl=None):
  769. self.username = username
  770. self.password = password
  771. self.verbose = False
  772. self.serverurl = serverurl
  773. def request(self, host, handler, request_body, verbose=False):
  774. # issue XML-RPC request
  775. h = self.make_connection(host)
  776. if verbose:
  777. h.set_debuglevel(1)
  778. h.putrequest("POST", handler)
  779. # required by HTTP/1.1
  780. h.putheader("Host", host)
  781. # required by XML-RPC
  782. h.putheader("User-Agent", self.user_agent)
  783. h.putheader("Content-Type", "text/xml")
  784. h.putheader("Content-Length", str(len(request_body)))
  785. # basic auth
  786. if self.username is not None and self.password is not None:
  787. unencoded = "%s:%s" % (self.username, self.password)
  788. encoded = unencoded.encode('base64')
  789. encoded = encoded.replace('\012', '')
  790. h.putheader("Authorization", "Basic %s" % encoded)
  791. h.endheaders()
  792. if request_body:
  793. h.send(request_body)
  794. errcode, errmsg, headers = h.getreply()
  795. if errcode != 200:
  796. raise xmlrpclib.ProtocolError(
  797. host + handler,
  798. errcode, errmsg,
  799. headers
  800. )
  801. return self.parse_response(h.getfile())
  802. def make_connection(self, host):
  803. serverurl = self.serverurl
  804. if not serverurl.startswith('http'):
  805. if serverurl.startswith('unix://'):
  806. serverurl = serverurl[7:]
  807. http = UnixStreamHTTP(serverurl)
  808. return http
  809. else:
  810. type, uri = urllib.splittype(serverurl)
  811. host, path = urllib.splithost(uri)
  812. hostpath = host+path
  813. return xmlrpclib.Transport.make_connection(self, hostpath)
  814. class UnixStreamHTTPConnection(httplib.HTTPConnection):
  815. def connect(self):
  816. self.sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
  817. # we abuse the host parameter as the socketname
  818. self.sock.connect(self.host)
  819. class UnixStreamHTTP(httplib.HTTP):
  820. _connection_class = UnixStreamHTTPConnection
  821. def readFile(filename, offset, length):
  822. """ Read length bytes from the file named by filename starting at
  823. offset """
  824. absoffset = abs(offset)
  825. abslength = abs(length)
  826. try:
  827. f = open(filename, 'rb')
  828. if absoffset != offset:
  829. # negative offset returns offset bytes from tail of the file
  830. if length:
  831. raise ValueError('BAD_ARGUMENTS')
  832. f.seek(0, 2)
  833. sz = f.tell()
  834. pos = int(sz - absoffset)
  835. if pos < 0:
  836. pos = 0
  837. f.seek(pos)
  838. data = f.read(absoffset)
  839. else:
  840. if abslength != length:
  841. raise ValueError('BAD_ARGUMENTS')
  842. if length == 0:
  843. f.seek(offset)
  844. data = f.read()
  845. else:
  846. sz = f.seek(offset)
  847. data = f.read(length)
  848. except (os.error, IOError):
  849. raise ValueError('FAILED')
  850. return data
  851. def gettags(comment):
  852. """ Parse documentation strings into JavaDoc-like tokens """
  853. tags = []
  854. tag = None
  855. datatype = None
  856. name = None
  857. tag_lineno = lineno = 0
  858. tag_text = []
  859. for line in comment.split('\n'):
  860. line = line.strip()
  861. if line.startswith("@"):
  862. tags.append((tag_lineno, tag, datatype, name, '\n'.join(tag_text)))
  863. parts = line.split(None, 3)
  864. if len(parts) == 1:
  865. datatype = ''
  866. name = ''
  867. tag_text = []
  868. elif len(parts) == 2:
  869. datatype = parts[1]
  870. name = ''
  871. tag_text = []
  872. elif len(parts) == 3:
  873. datatype = parts[1]
  874. name = parts[2]
  875. tag_text = []
  876. elif len(parts) == 4:
  877. datatype = parts[1]
  878. name = parts[2]
  879. tag_text = [parts[3].lstrip()]
  880. tag = parts[0][1:]
  881. tag_lineno = lineno
  882. else:
  883. if line:
  884. tag_text.append(line)
  885. lineno = lineno + 1
  886. tags.append((tag_lineno, tag, datatype, name, '\n'.join(tag_text)))
  887. return tags