/scrapy/commands/deploy.py

https://github.com/Rykka/scrapy · Python · 216 lines · 208 code · 3 blank · 5 comment · 0 complexity · b3a75c548e15334784e6330add9facde MD5 · raw file

  1. import sys
  2. import os
  3. import glob
  4. import tempfile
  5. import shutil
  6. import time
  7. import urllib2
  8. import netrc
  9. import json
  10. from urlparse import urlparse, urljoin
  11. from subprocess import Popen, PIPE, check_call
  12. from w3lib.form import encode_multipart
  13. from scrapy.command import ScrapyCommand
  14. from scrapy.exceptions import UsageError
  15. from scrapy.utils.http import basic_auth_header
  16. from scrapy.utils.python import retry_on_eintr
  17. from scrapy.utils.conf import get_config, closest_scrapy_cfg
  18. _SETUP_PY_TEMPLATE = \
  19. """# Automatically created by: scrapy deploy
  20. from setuptools import setup, find_packages
  21. setup(
  22. name = 'project',
  23. version = '1.0',
  24. packages = find_packages(),
  25. entry_points = {'scrapy': ['settings = %(settings)s']},
  26. )
  27. """
  28. class Command(ScrapyCommand):
  29. requires_project = True
  30. def syntax(self):
  31. return "[options] [ [target] | -l | -L <target> ]"
  32. def short_desc(self):
  33. return "Deploy project in Scrapyd target"
  34. def long_desc(self):
  35. return "Deploy the current project into the given Scrapyd server " \
  36. "(known as target)"
  37. def add_options(self, parser):
  38. ScrapyCommand.add_options(self, parser)
  39. parser.add_option("-p", "--project",
  40. help="the project name in the target")
  41. parser.add_option("-v", "--version",
  42. help="the version to deploy. Defaults to current timestamp")
  43. parser.add_option("-l", "--list-targets", action="store_true", \
  44. help="list available targets")
  45. parser.add_option("-L", "--list-projects", metavar="TARGET", \
  46. help="list available projects on TARGET")
  47. parser.add_option("--egg", metavar="FILE",
  48. help="use the given egg, instead of building it")
  49. parser.add_option("--build-egg", metavar="FILE",
  50. help="only build the egg, don't deploy it")
  51. def run(self, args, opts):
  52. try:
  53. import setuptools
  54. except ImportError:
  55. raise UsageError("setuptools not installed")
  56. if opts.list_targets:
  57. for name, target in _get_targets().items():
  58. print "%-20s %s" % (name, target['url'])
  59. return
  60. if opts.list_projects:
  61. target = _get_target(opts.list_projects)
  62. req = urllib2.Request(_url(target, 'listprojects.json'))
  63. _add_auth_header(req, target)
  64. f = urllib2.urlopen(req)
  65. projects = json.loads(f.read())['projects']
  66. print os.linesep.join(projects)
  67. return
  68. tmpdir = None
  69. if opts.build_egg: # build egg only
  70. egg, tmpdir = _build_egg()
  71. _log("Writing egg to %s" % opts.build_egg)
  72. shutil.copyfile(egg, opts.build_egg)
  73. else: # buld egg and deploy
  74. target_name = _get_target_name(args)
  75. target = _get_target(target_name)
  76. project = _get_project(target, opts)
  77. version = _get_version(target, opts)
  78. if opts.egg:
  79. _log("Using egg: %s" % opts.egg)
  80. egg = opts.egg
  81. else:
  82. _log("Building egg of %s-%s" % (project, version))
  83. egg, tmpdir = _build_egg()
  84. _upload_egg(target, egg, project, version)
  85. if tmpdir:
  86. shutil.rmtree(tmpdir)
  87. def _log(message):
  88. sys.stderr.write(message + os.linesep)
  89. def _get_target_name(args):
  90. if len(args) > 1:
  91. raise UsageError("Too many arguments: %s" % ' '.join(args))
  92. elif args:
  93. return args[0]
  94. elif len(args) < 1:
  95. return 'default'
  96. def _get_project(target, opts):
  97. project = opts.project or target.get('project')
  98. if not project:
  99. raise UsageError("Missing project")
  100. return project
  101. def _get_option(section, option, default=None):
  102. cfg = get_config()
  103. return cfg.get(section, option) if cfg.has_option(section, option) \
  104. else default
  105. def _get_targets():
  106. cfg = get_config()
  107. baset = dict(cfg.items('deploy')) if cfg.has_section('deploy') else {}
  108. targets = {}
  109. if 'url' in baset:
  110. targets['default'] = baset
  111. for x in cfg.sections():
  112. if x.startswith('deploy:'):
  113. t = baset.copy()
  114. t.update(cfg.items(x))
  115. targets[x[7:]] = t
  116. return targets
  117. def _get_target(name):
  118. try:
  119. return _get_targets()[name]
  120. except KeyError:
  121. raise UsageError("Unknown target: %s" % name)
  122. def _url(target, action):
  123. return urljoin(target['url'], action)
  124. def _get_version(target, opts):
  125. version = opts.version or target.get('version')
  126. if version == 'HG':
  127. p = Popen(['hg', 'tip', '--template', '{rev}'], stdout=PIPE)
  128. return 'r%s' % p.communicate()[0]
  129. elif version == 'GIT':
  130. p = Popen(['git', 'describe', '--always'], stdout=PIPE)
  131. return '%s' % p.communicate()[0].strip('\n')
  132. elif version:
  133. return version
  134. else:
  135. return str(int(time.time()))
  136. def _upload_egg(target, eggpath, project, version):
  137. with open(eggpath, 'rb') as f:
  138. eggdata = f.read()
  139. data = {
  140. 'project': project,
  141. 'version': version,
  142. 'egg': ('project.egg', eggdata),
  143. }
  144. body, boundary = encode_multipart(data)
  145. url = _url(target, 'addversion.json')
  146. headers = {
  147. 'Content-Type': 'multipart/form-data; boundary=%s' % boundary,
  148. 'Content-Length': str(len(body)),
  149. }
  150. req = urllib2.Request(url, body, headers)
  151. _add_auth_header(req, target)
  152. _log("Deploying %s-%s to %s" % (project, version, url))
  153. _http_post(req)
  154. def _add_auth_header(request, target):
  155. if 'username' in target:
  156. u, p = target.get('username'), target.get('password', '')
  157. request.add_header('Authorization', basic_auth_header(u, p))
  158. else: # try netrc
  159. try:
  160. host = urlparse(target['url']).hostname
  161. a = netrc.netrc().authenticators(host)
  162. request.add_header('Authorization', basic_auth_header(a[0], a[2]))
  163. except (netrc.NetrcParseError, IOError, TypeError):
  164. pass
  165. def _http_post(request):
  166. try:
  167. f = urllib2.urlopen(request)
  168. _log("Server response (%s):" % f.code)
  169. print f.read()
  170. except urllib2.HTTPError, e:
  171. _log("Deploy failed (%s):" % e.code)
  172. print e.read()
  173. except urllib2.URLError, e:
  174. _log("Deploy failed: %s" % e)
  175. def _build_egg():
  176. closest = closest_scrapy_cfg()
  177. os.chdir(os.path.dirname(closest))
  178. if not os.path.exists('setup.py'):
  179. settings = get_config().get('settings', 'default')
  180. _create_default_setup_py(settings=settings)
  181. d = tempfile.mkdtemp()
  182. f = tempfile.TemporaryFile(dir=d)
  183. retry_on_eintr(check_call, [sys.executable, 'setup.py', 'clean', '-a', 'bdist_egg', '-d', d], stdout=f)
  184. egg = glob.glob(os.path.join(d, '*.egg'))[0]
  185. return egg, d
  186. def _create_default_setup_py(**kwargs):
  187. with open('setup.py', 'w') as f:
  188. f.write(_SETUP_PY_TEMPLATE % kwargs)