PageRenderTime 59ms CodeModel.GetById 32ms RepoModel.GetById 0ms app.codeStats 0ms

/Server/bkr/server/util.py

https://github.com/beaker-project/beaker
Python | 237 lines | 163 code | 8 blank | 66 comment | 14 complexity | fa7fee42c08efe6c21668451cb6cfe33 MD5 | raw file
Possible License(s): GPL-2.0, CC-BY-SA-3.0
  1. # This program is free software; you can redistribute it and/or modify
  2. # it under the terms of the GNU General Public License as published by
  3. # the Free Software Foundation; either version 2 of the License, or
  4. # (at your option) any later version.
  5. """
  6. Random functions that don't fit elsewhere
  7. """
  8. import contextlib
  9. import logging
  10. import os
  11. import re
  12. import socket
  13. import subprocess
  14. import sys
  15. from collections import namedtuple
  16. import lxml.etree
  17. import turbogears
  18. from sqlalchemy.orm.exc import NoResultFound
  19. from turbogears import config, url
  20. from bkr.server.app import app
  21. from bkr.server.bexceptions import DatabaseLookupError
  22. log = logging.getLogger(__name__)
  23. _config_loaded = None
  24. def load_config_or_exit(configfile=None):
  25. try:
  26. load_config(configfile=configfile)
  27. except Exception as e:
  28. sys.stderr.write('Failed to read server configuration. %s.\n'
  29. 'Hint: run this command as root\n' % e)
  30. sys.exit(1)
  31. def load_config(configfile=None):
  32. """
  33. Loads Beaker's configuration and configures logging.
  34. """
  35. setupdir = os.path.dirname(os.path.dirname(os.path.dirname(__file__)))
  36. curdir = os.getcwd()
  37. if configfile and os.path.exists(configfile):
  38. pass
  39. elif 'BEAKER_CONFIG_FILE' in os.environ:
  40. configfile = os.environ['BEAKER_CONFIG_FILE']
  41. elif os.path.exists(os.path.join(setupdir, 'setup.py')) \
  42. and os.path.exists(os.path.join(setupdir, 'dev.cfg')):
  43. configfile = os.path.join(setupdir, 'dev.cfg')
  44. elif os.path.exists(os.path.join(curdir, 'beaker.cfg')):
  45. configfile = os.path.join(curdir, 'beaker.cfg')
  46. elif os.path.exists('/etc/beaker.cfg'):
  47. configfile = '/etc/beaker.cfg'
  48. elif os.path.exists('/etc/beaker/server.cfg'):
  49. configfile = '/etc/beaker/server.cfg'
  50. else:
  51. raise RuntimeError("Unable to find configuration to load!")
  52. # We only allow the config to be loaded once, update_config()
  53. # doesn't seem to update the config when called more than once
  54. # anyway
  55. configfile = os.path.realpath(configfile)
  56. global _config_loaded
  57. if _config_loaded is not None and configfile == _config_loaded:
  58. return
  59. elif _config_loaded is not None and configfile != _config_loaded:
  60. raise RuntimeError('Config has already been loaded from %s' % \
  61. _config_loaded)
  62. # In general, we want all messages from application code, but no debugging
  63. # messages from the libraries we are using.
  64. logging.getLogger().setLevel(logging.INFO)
  65. logging.getLogger('bkr').setLevel(logging.DEBUG)
  66. # We don't need access logs from TurboGears, we have the Apache logs.
  67. logging.getLogger('turbogears.access').setLevel(logging.WARN)
  68. # Note that the actual level of log output is controlled by the handlers,
  69. # not the loggers (for example command line tools will typically log to
  70. # stderr at WARNING level). The main entry point for the program should
  71. # call bkr.log.log_to_{syslog,stream} to set up a handler.
  72. # We do not want TurboGears to touch the logging config, so let's
  73. # double-check the user hasn't left an old [logging] section in their
  74. # config file.
  75. from configobj import ConfigObj
  76. configdata = ConfigObj(configfile, unrepr=True)
  77. if 'logging' in configdata:
  78. raise RuntimeError('TurboGears logging configuration is not supported, '
  79. 'remove [logging] section from config file %s' % configfile)
  80. if not 'global' in configdata:
  81. raise RuntimeError('Config file is missing section [global]')
  82. # Read our beaker config and store it to Flask config
  83. app.config.update(configdata['global'])
  84. # Keep this until we completely remove TurboGears
  85. turbogears.update_config(configfile=configfile, modulename="bkr.server.config")
  86. _config_loaded = configfile
  87. def to_unicode(obj, encoding='utf-8'):
  88. # TODO: Not needed for Python 3
  89. if isinstance(obj, basestring):
  90. if not isinstance(obj, unicode):
  91. obj = unicode(obj, encoding, 'replace')
  92. return obj
  93. def strip_webpath(url):
  94. webpath = (config.get('server.webpath') or '').rstrip('/')
  95. if webpath and url.startswith(webpath):
  96. return url[len(webpath):]
  97. return url
  98. # TG1.1 has this: http://docs.turbogears.org/1.1/URLs#turbogears-absolute-url
  99. def absolute_url(tgpath, tgparams=None, scheme=None,
  100. labdomain=False, webpath=True, **kw):
  101. """
  102. Like turbogears.url, but makes the URL absolute (with scheme, hostname,
  103. and port from the tg.url_scheme and tg.url_domain configuration
  104. directives).
  105. If labdomain is True we serve an alternate tg.proxy_domain if defined
  106. in server.cfg. This is to support multi-home systems which have
  107. different external vs internal names.
  108. """
  109. if labdomain and app.config.get('tg.lab_domain'):
  110. host_port = app.config.get('tg.lab_domain')
  111. elif app.config.get('tg.url_domain'):
  112. host_port = app.config.get('tg.url_domain')
  113. elif app.config.get('servername'): # deprecated
  114. host_port = app.config.get('servername')
  115. else:
  116. # System hostname is cheap to look up (no DNS calls) but there is no
  117. # requirement that it be fully qualified.
  118. kernel_hostname = socket.gethostname()
  119. if '.' in kernel_hostname:
  120. host_port = kernel_hostname
  121. else:
  122. # Last resort, let glibc do a DNS lookup through search domains etc.
  123. host_port = socket.getfqdn()
  124. # TODO support relative paths
  125. theurl = url(tgpath, tgparams, **kw)
  126. if not webpath:
  127. theurl = strip_webpath(theurl)
  128. assert theurl.startswith('/')
  129. scheme = scheme or app.config.get('tg.url_scheme', 'http')
  130. return '%s://%s%s' % (scheme, host_port, theurl)
  131. _reports_engine = None
  132. # Based on a similar decorator from kobo.decorators
  133. def log_traceback(logger):
  134. """
  135. A decorator which will log uncaught exceptions to the given logger, before
  136. re-raising them.
  137. """
  138. def decorator(func):
  139. def decorated(*args, **kwargs):
  140. try:
  141. return func(*args, **kwargs)
  142. except:
  143. logger.exception('Uncaught exception in %s', func.__name__)
  144. raise
  145. decorated.__name__ = func.__name__
  146. decorated.__doc__ = func.__doc__
  147. decorated.__dict__.update(func.__dict__)
  148. return decorated
  149. return decorator
  150. def run_createrepo(cwd=None, update=False):
  151. createrepo_command = config.get('beaker.createrepo_command', 'createrepo_c')
  152. args = [createrepo_command, '-q', '--no-database', '--checksum', 'sha']
  153. if update:
  154. args.append('--update')
  155. args.append('.')
  156. log.debug('Running createrepo as %r in %s', args, cwd)
  157. p = subprocess.Popen(args, cwd=cwd, stderr=subprocess.PIPE,
  158. stdout=subprocess.PIPE)
  159. out, err = p.communicate()
  160. # Perhaps a bit fragile, but maybe better than checking version?
  161. if p.returncode != 0 and 'no such option: --no-database' in err:
  162. args.remove('--no-database')
  163. log.debug('Re-trying createrepo as %r in %s', args, cwd)
  164. p = subprocess.Popen(args, cwd=cwd, stderr=subprocess.PIPE,
  165. stdout=subprocess.PIPE)
  166. out, err = p.communicate()
  167. RepoCreate = namedtuple("RepoCreate", "command returncode out err")
  168. return RepoCreate(createrepo_command, p.returncode, out, err)
  169. # Validate FQDN for a system
  170. # http://stackoverflow.com/questions/1418423/_/1420225#1420225
  171. VALID_FQDN_REGEX = (r"^(?=.{1,255}$)[0-9A-Za-z]"
  172. r"(?:(?:[0-9A-Za-z]|\b-){0,61}[0-9A-Za-z])?(?:\.[0-9A-Za-z]"
  173. r"(?:(?:[0-9A-Za-z]|\b-){0,61}[0-9A-Za-z])?)*\.?$")
  174. # do this at the global scope to avoid compiling it on every call
  175. regex_compiled = re.compile(VALID_FQDN_REGEX)
  176. def is_valid_fqdn(fqdn):
  177. return regex_compiled.search(fqdn)
  178. @contextlib.contextmanager
  179. def convert_db_lookup_error(msg):
  180. """
  181. Context manager to handle SQLA's NoResultFound and report
  182. a custom error message
  183. """
  184. try:
  185. yield
  186. except NoResultFound:
  187. raise DatabaseLookupError(msg)
  188. def parse_untrusted_xml(s):
  189. """
  190. Parses untrusted XML as a string and raises a ValueError if system entities
  191. are found.
  192. See: http://lxml.de/FAQ.html#how-do-i-use-lxml-safely-as-a-web-service-endpoint
  193. """
  194. parser = lxml.etree.XMLParser(resolve_entities=False, strip_cdata=False)
  195. root = lxml.etree.fromstring(s, parser)
  196. for ent in root.iter(lxml.etree.Entity):
  197. # fail once we find any system entity which is not supported
  198. raise ValueError('XML entity with name %s not permitted' % ent)
  199. return root