teuthology 1.0.0__py3-none-any.whl → 1.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (172) hide show
  1. scripts/describe.py +1 -0
  2. scripts/dispatcher.py +62 -0
  3. scripts/exporter.py +18 -0
  4. scripts/lock.py +1 -1
  5. scripts/node_cleanup.py +58 -0
  6. scripts/openstack.py +9 -9
  7. scripts/results.py +12 -11
  8. scripts/run.py +4 -0
  9. scripts/schedule.py +4 -0
  10. scripts/suite.py +61 -16
  11. scripts/supervisor.py +44 -0
  12. scripts/update_inventory.py +10 -4
  13. scripts/wait.py +31 -0
  14. teuthology/__init__.py +24 -21
  15. teuthology/beanstalk.py +4 -3
  16. teuthology/config.py +17 -6
  17. teuthology/contextutil.py +18 -14
  18. teuthology/describe_tests.py +25 -18
  19. teuthology/dispatcher/__init__.py +365 -0
  20. teuthology/dispatcher/supervisor.py +374 -0
  21. teuthology/exceptions.py +54 -0
  22. teuthology/exporter.py +347 -0
  23. teuthology/kill.py +76 -75
  24. teuthology/lock/cli.py +16 -7
  25. teuthology/lock/ops.py +276 -70
  26. teuthology/lock/query.py +61 -44
  27. teuthology/ls.py +9 -18
  28. teuthology/misc.py +152 -137
  29. teuthology/nuke/__init__.py +12 -351
  30. teuthology/openstack/__init__.py +4 -3
  31. teuthology/openstack/openstack-centos-7.0-user-data.txt +1 -1
  32. teuthology/openstack/openstack-centos-7.1-user-data.txt +1 -1
  33. teuthology/openstack/openstack-centos-7.2-user-data.txt +1 -1
  34. teuthology/openstack/openstack-debian-8.0-user-data.txt +1 -1
  35. teuthology/openstack/openstack-opensuse-42.1-user-data.txt +1 -1
  36. teuthology/openstack/openstack-teuthology.cron +0 -1
  37. teuthology/orchestra/cluster.py +51 -9
  38. teuthology/orchestra/connection.py +23 -16
  39. teuthology/orchestra/console.py +111 -50
  40. teuthology/orchestra/daemon/cephadmunit.py +23 -5
  41. teuthology/orchestra/daemon/state.py +10 -3
  42. teuthology/orchestra/daemon/systemd.py +10 -8
  43. teuthology/orchestra/opsys.py +32 -11
  44. teuthology/orchestra/remote.py +369 -152
  45. teuthology/orchestra/run.py +21 -12
  46. teuthology/packaging.py +54 -15
  47. teuthology/provision/__init__.py +30 -10
  48. teuthology/provision/cloud/openstack.py +12 -6
  49. teuthology/provision/cloud/util.py +1 -2
  50. teuthology/provision/downburst.py +83 -29
  51. teuthology/provision/fog.py +68 -20
  52. teuthology/provision/openstack.py +5 -4
  53. teuthology/provision/pelagos.py +13 -5
  54. teuthology/repo_utils.py +91 -44
  55. teuthology/report.py +57 -35
  56. teuthology/results.py +5 -3
  57. teuthology/run.py +21 -15
  58. teuthology/run_tasks.py +114 -40
  59. teuthology/schedule.py +4 -3
  60. teuthology/scrape.py +28 -22
  61. teuthology/suite/__init__.py +75 -46
  62. teuthology/suite/build_matrix.py +34 -24
  63. teuthology/suite/fragment-merge.lua +105 -0
  64. teuthology/suite/matrix.py +31 -2
  65. teuthology/suite/merge.py +175 -0
  66. teuthology/suite/placeholder.py +8 -8
  67. teuthology/suite/run.py +204 -102
  68. teuthology/suite/util.py +67 -211
  69. teuthology/task/__init__.py +1 -1
  70. teuthology/task/ansible.py +101 -31
  71. teuthology/task/buildpackages.py +2 -2
  72. teuthology/task/ceph_ansible.py +13 -6
  73. teuthology/task/cephmetrics.py +2 -1
  74. teuthology/task/clock.py +33 -14
  75. teuthology/task/exec.py +18 -0
  76. teuthology/task/hadoop.py +2 -2
  77. teuthology/task/install/__init__.py +51 -22
  78. teuthology/task/install/bin/adjust-ulimits +16 -0
  79. teuthology/task/install/bin/daemon-helper +114 -0
  80. teuthology/task/install/bin/stdin-killer +263 -0
  81. teuthology/task/install/deb.py +24 -4
  82. teuthology/task/install/redhat.py +36 -32
  83. teuthology/task/install/rpm.py +41 -14
  84. teuthology/task/install/util.py +48 -22
  85. teuthology/task/internal/__init__.py +69 -11
  86. teuthology/task/internal/edit_sudoers.sh +10 -0
  87. teuthology/task/internal/lock_machines.py +3 -133
  88. teuthology/task/internal/redhat.py +48 -28
  89. teuthology/task/internal/syslog.py +31 -8
  90. teuthology/task/kernel.py +155 -147
  91. teuthology/task/lockfile.py +1 -1
  92. teuthology/task/mpi.py +10 -10
  93. teuthology/task/pcp.py +1 -1
  94. teuthology/task/selinux.py +17 -8
  95. teuthology/task/ssh_keys.py +6 -6
  96. teuthology/task/tests/__init__.py +137 -77
  97. teuthology/task/tests/test_fetch_coredumps.py +116 -0
  98. teuthology/task/tests/test_run.py +4 -4
  99. teuthology/timer.py +3 -3
  100. teuthology/util/loggerfile.py +19 -0
  101. teuthology/util/scanner.py +159 -0
  102. teuthology/util/sentry.py +52 -0
  103. teuthology/util/time.py +52 -0
  104. teuthology-1.2.0.data/scripts/adjust-ulimits +16 -0
  105. teuthology-1.2.0.data/scripts/daemon-helper +114 -0
  106. teuthology-1.2.0.data/scripts/stdin-killer +263 -0
  107. teuthology-1.2.0.dist-info/METADATA +89 -0
  108. teuthology-1.2.0.dist-info/RECORD +174 -0
  109. {teuthology-1.0.0.dist-info → teuthology-1.2.0.dist-info}/WHEEL +1 -1
  110. {teuthology-1.0.0.dist-info → teuthology-1.2.0.dist-info}/entry_points.txt +5 -2
  111. scripts/nuke.py +0 -45
  112. scripts/worker.py +0 -37
  113. teuthology/nuke/actions.py +0 -456
  114. teuthology/openstack/test/__init__.py +0 -0
  115. teuthology/openstack/test/openstack-integration.py +0 -286
  116. teuthology/openstack/test/test_config.py +0 -35
  117. teuthology/openstack/test/test_openstack.py +0 -1695
  118. teuthology/orchestra/test/__init__.py +0 -0
  119. teuthology/orchestra/test/integration/__init__.py +0 -0
  120. teuthology/orchestra/test/integration/test_integration.py +0 -94
  121. teuthology/orchestra/test/test_cluster.py +0 -240
  122. teuthology/orchestra/test/test_connection.py +0 -106
  123. teuthology/orchestra/test/test_console.py +0 -217
  124. teuthology/orchestra/test/test_opsys.py +0 -404
  125. teuthology/orchestra/test/test_remote.py +0 -185
  126. teuthology/orchestra/test/test_run.py +0 -286
  127. teuthology/orchestra/test/test_systemd.py +0 -54
  128. teuthology/orchestra/test/util.py +0 -12
  129. teuthology/sentry.py +0 -18
  130. teuthology/test/__init__.py +0 -0
  131. teuthology/test/fake_archive.py +0 -107
  132. teuthology/test/fake_fs.py +0 -92
  133. teuthology/test/integration/__init__.py +0 -0
  134. teuthology/test/integration/test_suite.py +0 -86
  135. teuthology/test/task/__init__.py +0 -205
  136. teuthology/test/task/test_ansible.py +0 -624
  137. teuthology/test/task/test_ceph_ansible.py +0 -176
  138. teuthology/test/task/test_console_log.py +0 -88
  139. teuthology/test/task/test_install.py +0 -337
  140. teuthology/test/task/test_internal.py +0 -57
  141. teuthology/test/task/test_kernel.py +0 -243
  142. teuthology/test/task/test_pcp.py +0 -379
  143. teuthology/test/task/test_selinux.py +0 -35
  144. teuthology/test/test_config.py +0 -189
  145. teuthology/test/test_contextutil.py +0 -68
  146. teuthology/test/test_describe_tests.py +0 -316
  147. teuthology/test/test_email_sleep_before_teardown.py +0 -81
  148. teuthology/test/test_exit.py +0 -97
  149. teuthology/test/test_get_distro.py +0 -47
  150. teuthology/test/test_get_distro_version.py +0 -47
  151. teuthology/test/test_get_multi_machine_types.py +0 -27
  152. teuthology/test/test_job_status.py +0 -60
  153. teuthology/test/test_ls.py +0 -48
  154. teuthology/test/test_misc.py +0 -368
  155. teuthology/test/test_nuke.py +0 -232
  156. teuthology/test/test_packaging.py +0 -763
  157. teuthology/test/test_parallel.py +0 -28
  158. teuthology/test/test_repo_utils.py +0 -204
  159. teuthology/test/test_report.py +0 -77
  160. teuthology/test/test_results.py +0 -155
  161. teuthology/test/test_run.py +0 -238
  162. teuthology/test/test_safepath.py +0 -55
  163. teuthology/test/test_schedule.py +0 -45
  164. teuthology/test/test_scrape.py +0 -167
  165. teuthology/test/test_timer.py +0 -80
  166. teuthology/test/test_vps_os_vers_parameter_checking.py +0 -84
  167. teuthology/test/test_worker.py +0 -303
  168. teuthology/worker.py +0 -339
  169. teuthology-1.0.0.dist-info/METADATA +0 -76
  170. teuthology-1.0.0.dist-info/RECORD +0 -210
  171. {teuthology-1.0.0.dist-info → teuthology-1.2.0.dist-info}/LICENSE +0 -0
  172. {teuthology-1.0.0.dist-info → teuthology-1.2.0.dist-info}/top_level.txt +0 -0
@@ -1,10 +1,10 @@
1
+ import datetime
1
2
  import json
2
3
  import logging
3
4
  import requests
4
5
  import socket
5
6
  import re
6
7
 
7
- from datetime import datetime
8
8
  from paramiko import SSHException
9
9
  from paramiko.ssh_exception import NoValidConnectionsError
10
10
 
@@ -13,6 +13,7 @@ import teuthology.orchestra
13
13
  from teuthology.config import config
14
14
  from teuthology.contextutil import safe_while
15
15
  from teuthology.exceptions import MaxWhileTries
16
+ from teuthology.orchestra.opsys import OS
16
17
  from teuthology import misc
17
18
 
18
19
  log = logging.getLogger(__name__)
@@ -29,7 +30,7 @@ def enabled(warn=False):
29
30
  params = ['endpoint', 'api_token', 'user_token', 'machine_types']
30
31
  unset = [param for param in params if not fog_conf.get(param)]
31
32
  if unset and warn:
32
- log.warn(
33
+ log.warning(
33
34
  "FOG disabled; set the following config options to enable: %s",
34
35
  ' '.join(unset),
35
36
  )
@@ -89,6 +90,7 @@ class FOG(object):
89
90
  raise
90
91
  self._wait_for_ready()
91
92
  self._fix_hostname()
93
+ self._verify_installed_os()
92
94
  self.log.info("Deploy complete!")
93
95
 
94
96
  def do_request(self, url_suffix, data=None, method='GET', verify=True):
@@ -147,18 +149,39 @@ class FOG(object):
147
149
  represents it
148
150
  :returns: A dict describing the image
149
151
  """
150
- name = '_'.join([
151
- self.remote.machine_type, self.os_type.lower(), self.os_version])
152
- resp = self.do_request(
153
- '/image',
154
- data=json.dumps(dict(name=name)),
155
- )
156
- obj = resp.json()
157
- if not obj['count']:
152
+ def do_get(name):
153
+ resp = self.do_request(
154
+ '/image',
155
+ data=json.dumps(dict(name=name)),
156
+ )
157
+ obj = resp.json()
158
+ if obj['count']:
159
+ return obj['images'][0]
160
+
161
+ os_type = self.os_type.lower()
162
+ os_version = self.os_version
163
+ name = f"{self.remote.machine_type}_{os_type}_{os_version}"
164
+ if image := do_get(name):
165
+ return image
166
+ elif os_type == 'centos' and not os_version.endswith('.stream'):
167
+ image = do_get(f"{name}.stream")
168
+ if image:
169
+ return image
170
+ else:
158
171
  raise RuntimeError(
159
- "Could not find an image for %s %s" %
160
- (self.os_type, self.os_version))
161
- return obj['images'][0]
172
+ "Fog has no %s image. Available %s images: %s" %
173
+ (name, self.remote.machine_type, self.suggest_image_names()))
174
+
175
+ def suggest_image_names(self):
176
+ """
177
+ Suggest available image names for this machine type.
178
+
179
+ :returns: A list of image names.
180
+ """
181
+ resp = self.do_request('/image/search/%s' % self.remote.machine_type)
182
+ obj = resp.json()
183
+ images = obj['images']
184
+ return [image['name'] for image in images]
162
185
 
163
186
  def set_image(self, host_id):
164
187
  """
@@ -167,6 +190,8 @@ class FOG(object):
167
190
  """
168
191
  image_data = self.get_image_data()
169
192
  image_id = int(image_data['id'])
193
+ image_name = image_data.get("name")
194
+ self.log.debug(f"Requesting image {image_name} (ID {image_id})")
170
195
  self.do_request(
171
196
  '/host/%s' % host_id,
172
197
  method='PUT',
@@ -202,8 +227,8 @@ class FOG(object):
202
227
  for task in host_tasks:
203
228
  timestamp = task['createdTime']
204
229
  time_delta = (
205
- datetime.utcnow() - datetime.strptime(
206
- timestamp, self.timestamp_format)
230
+ datetime.datetime.now(datetime.timezone.utc) - datetime.datetime.strptime(
231
+ timestamp, self.timestamp_format).replace(tzinfo=datetime.timezone.utc)
207
232
  ).total_seconds()
208
233
  # There should only be one deploy task matching our host. Just in
209
234
  # case there are multiple, select a very recent one.
@@ -240,13 +265,14 @@ class FOG(object):
240
265
  completed)
241
266
  """
242
267
  self.log.info("Waiting for deploy to finish")
243
- with safe_while(sleep=15, tries=60) as proceed:
268
+ with safe_while(sleep=15, tries=120, timeout=config.fog_reimage_timeout) as proceed:
244
269
  while proceed():
245
270
  if not self.deploy_task_active(task_id):
246
271
  break
247
272
 
248
273
  def cancel_deploy_task(self, task_id):
249
274
  """ Cancel an active deploy task """
275
+ self.log.debug(f"Canceling deploy task with ID {task_id}")
250
276
  resp = self.do_request(
251
277
  '/task/cancel',
252
278
  method='DELETE',
@@ -256,7 +282,7 @@ class FOG(object):
256
282
 
257
283
  def _wait_for_ready(self):
258
284
  """ Attempt to connect to the machine via SSH """
259
- with safe_while(sleep=6, tries=100) as proceed:
285
+ with safe_while(sleep=6, timeout=config.fog_wait_for_ssh_timeout) as proceed:
260
286
  while proceed():
261
287
  try:
262
288
  self.remote.connect()
@@ -267,12 +293,26 @@ class FOG(object):
267
293
  NoValidConnectionsError,
268
294
  MaxWhileTries,
269
295
  EOFError,
270
- ):
271
- pass
296
+ ) as e:
297
+ # log this, because otherwise lots of failures just
298
+ # keep retrying without any notification (like, say,
299
+ # a mismatched host key in ~/.ssh/known_hosts, or
300
+ # something)
301
+ self.log.warning(e)
272
302
  sentinel_file = config.fog.get('sentinel_file', None)
273
303
  if sentinel_file:
274
304
  cmd = "while [ ! -e '%s' ]; do sleep 5; done" % sentinel_file
275
- self.remote.run(args=cmd, timeout=600)
305
+ action = f"wait for sentinel on {self.shortname}"
306
+ with safe_while(action=action, timeout=1800, increment=3) as proceed:
307
+ while proceed():
308
+ try:
309
+ self.remote.run(args=cmd, timeout=600)
310
+ break
311
+ except (
312
+ ConnectionError,
313
+ EOFError,
314
+ ) as e:
315
+ log.error(f"{e} on {self.shortname}")
276
316
  self.log.info("Node is ready")
277
317
 
278
318
  def _fix_hostname(self):
@@ -307,6 +347,14 @@ class FOG(object):
307
347
  check_status=False,
308
348
  )
309
349
 
350
+ def _verify_installed_os(self):
351
+ wanted_os = OS(name=self.os_type, version=self.os_version)
352
+ if self.remote.os != wanted_os:
353
+ raise RuntimeError(
354
+ f"Expected {self.remote.shortname}'s OS to be {wanted_os} but "
355
+ f"found {self.remote.os}"
356
+ )
357
+
310
358
  def destroy(self):
311
359
  """A no-op; we just leave idle nodes as-is"""
312
360
  pass
@@ -27,7 +27,8 @@ class ProvisionOpenStack(OpenStack):
27
27
  """
28
28
  def __init__(self):
29
29
  super(ProvisionOpenStack, self).__init__()
30
- self.user_data = tempfile.mktemp()
30
+ fd, self.user_data = tempfile.mkstemp()
31
+ os.close(fd)
31
32
  log.debug("ProvisionOpenStack: " + str(config.openstack))
32
33
  self.basename = 'target'
33
34
  self.up_string = 'The system is finally up'
@@ -77,7 +78,7 @@ class ProvisionOpenStack(OpenStack):
77
78
  if 'No volume with a name or ID' not in e.output:
78
79
  raise e
79
80
  if volume_id:
80
- log.warn("Volume {} already exists with ID {}; using it"
81
+ log.warning("Volume {} already exists with ID {}; using it"
81
82
  .format(volume_name, volume_id))
82
83
  volume_id = self._openstack(
83
84
  "volume create %s" % config['openstack'].get('volume-create','')
@@ -106,7 +107,7 @@ class ProvisionOpenStack(OpenStack):
106
107
  log.debug("volume %s not in '%s' status yet"
107
108
  % (volume_id, status))
108
109
  except subprocess.CalledProcessError:
109
- log.warn("volume " + volume_id +
110
+ log.warning("volume " + volume_id +
110
111
  " not information available yet")
111
112
 
112
113
  def _attach_volume(self, volume_id, name):
@@ -151,7 +152,7 @@ class ProvisionOpenStack(OpenStack):
151
152
  """
152
153
  return the instance name suffixed with the IP address.
153
154
  """
154
- digits = map(int, re.findall('(\d+)\.(\d+)\.(\d+)\.(\d+)', ip)[0])
155
+ digits = map(int, re.findall(r'(\d+)\.(\d+)\.(\d+)\.(\d+)', ip)[0])
155
156
  return prefix + "%03d%03d%03d%03d" % tuple(digits)
156
157
 
157
158
  def create(self, num, os_type, os_version, arch, resources_hint):
@@ -26,7 +26,7 @@ def enabled(warn=False):
26
26
  params = ['endpoint', 'machine_types']
27
27
  unset = [_ for _ in params if not conf.get(_)]
28
28
  if unset and warn:
29
- log.warn(
29
+ log.warning(
30
30
  "Pelagos is disabled; set the following config options to enable: %s",
31
31
  ' '.join(unset),
32
32
  )
@@ -49,8 +49,8 @@ def get_types():
49
49
  return [_ for _ in types if _]
50
50
 
51
51
  def park_node(name):
52
- p = Pelagos(name, "maintenance_image")
53
- p.create()
52
+ p = Pelagos(name, "maintenance_image")
53
+ p.create(wait=False)
54
54
 
55
55
 
56
56
  class Pelagos(object):
@@ -71,18 +71,26 @@ class Pelagos(object):
71
71
  self.os_name = os_type
72
72
  self.log = log.getChild(self.name)
73
73
 
74
- def create(self):
74
+ def create(self, wait=True):
75
75
  """
76
76
  Initiate deployment via REST requests and wait until completion
77
+ :param wait: optional, by default is True, if set to False, function
78
+ doesn't wait for the end of node provisioning
79
+ :returns: http response code if operation is successful
80
+ :raises: :class:`Exception`: if node provision failure reported by
81
+ Pelagos or if timeout is reached
82
+ :raises: :class:`RuntimeError`: if pelagos is not configured
77
83
 
78
84
  """
79
85
  if not enabled():
80
86
  raise RuntimeError("Pelagos is not configured!")
81
87
  location = None
82
88
  try:
83
- params=dict(os=self.os_name, node=self.name)
89
+ params = dict(os=self.os_name, node=self.name)
84
90
  response = self.do_request('node/provision',
85
91
  data=params, method='POST')
92
+ if not wait:
93
+ return response
86
94
  location = response.headers.get('Location')
87
95
  self.log.debug("provision task: '%s'", location)
88
96
  # gracefully wait till provision task gets created on pelagos
teuthology/repo_utils.py CHANGED
@@ -1,3 +1,4 @@
1
+ import functools
1
2
  import logging
2
3
  import os
3
4
  import re
@@ -5,11 +6,13 @@ import shutil
5
6
  import subprocess
6
7
  import time
7
8
 
9
+ import teuthology.exporter as exporter
10
+
8
11
  from teuthology import misc
9
12
  from teuthology.util.flock import FileLock
10
13
  from teuthology.config import config
11
14
  from teuthology.contextutil import MaxWhileTries, safe_while
12
- from teuthology.exceptions import BootstrapError, BranchNotFoundError, GitError
15
+ from teuthology.exceptions import BootstrapError, BranchNotFoundError, CommitNotFoundError, GitError
13
16
 
14
17
  log = logging.getLogger(__name__)
15
18
 
@@ -50,10 +53,11 @@ def build_git_url(project, project_owner='ceph'):
50
53
  base = config.get_ceph_git_url()
51
54
  else:
52
55
  base = 'https://github.com/{project_owner}/{project}'
53
- url_templ = re.sub('\.git$', '', base)
56
+ url_templ = re.sub(r'\.git$', '', base)
54
57
  return url_templ.format(project_owner=project_owner, project=project)
55
58
 
56
59
 
60
+ @functools.lru_cache()
57
61
  def ls_remote(url, ref):
58
62
  """
59
63
  Return the current sha1 for a given repository and ref
@@ -70,33 +74,61 @@ def ls_remote(url, ref):
70
74
  return sha1
71
75
 
72
76
 
73
- def enforce_repo_state(repo_url, dest_path, branch, remove_on_error=True):
77
+ def current_branch(path: str) -> str:
78
+ """
79
+ Return the current branch for a given on-disk repository.
80
+
81
+ :returns: the current branch, or an empty string if none is found.
82
+ """
83
+ # git branch --show-current was added in 2.22.0, and we can't assume
84
+ # our version is new enough.
85
+ cmd = "git rev-parse --abbrev-ref HEAD"
86
+ result = subprocess.Popen(
87
+ cmd,
88
+ shell=True,
89
+ cwd=path,
90
+ stdout=subprocess.PIPE,
91
+ stderr=subprocess.DEVNULL,
92
+ ).communicate()[0].strip().decode()
93
+ if result == "HEAD":
94
+ return ""
95
+ return result
96
+
97
+
98
+ def enforce_repo_state(repo_url, dest_path, branch, commit=None, remove_on_error=True):
74
99
  """
75
100
  Use git to either clone or update a given repo, forcing it to switch to the
76
101
  specified branch.
77
102
 
78
- :param repo_url: The full URL to the repo (not including the branch)
79
- :param dest_path: The full path to the destination directory
80
- :param branch: The branch.
81
- :param remove: Whether or not to remove dest_dir when an error occurs
82
- :raises: BranchNotFoundError if the branch is not found;
83
- GitError for other errors
103
+ :param repo_url: The full URL to the repo (not including the branch)
104
+ :param dest_path: The full path to the destination directory
105
+ :param branch: The branch.
106
+ :param commit: The sha1 to checkout. Defaults to None, which uses HEAD of the branch.
107
+ :param remove_on_error: Whether or not to remove dest_dir when an error occurs
108
+ :raises: BranchNotFoundError if the branch is not found;
109
+ CommitNotFoundError if the commit is not found;
110
+ GitError for other errors
84
111
  """
85
112
  validate_branch(branch)
86
113
  sentinel = os.path.join(dest_path, '.fetched')
114
+ # sentinel to track whether the repo has checked out the intended
115
+ # version, in addition to being cloned
116
+ repo_reset = os.path.join(dest_path, '.fetched_and_reset')
87
117
  try:
88
118
  if not os.path.isdir(dest_path):
89
- clone_repo(repo_url, dest_path, branch)
90
- elif not is_fresh(sentinel):
119
+ clone_repo(repo_url, dest_path, branch, shallow=commit is None)
120
+ elif not commit and not is_fresh(sentinel):
91
121
  set_remote(dest_path, repo_url)
92
122
  fetch_branch(dest_path, branch)
93
123
  touch_file(sentinel)
94
- else:
95
- log.info("%s was just updated; assuming it is current", dest_path)
96
124
 
97
- reset_repo(repo_url, dest_path, branch)
125
+ if commit and os.path.exists(repo_reset):
126
+ return
127
+
128
+ reset_repo(repo_url, dest_path, branch, commit)
129
+ touch_file(repo_reset)
98
130
  # remove_pyc_files(dest_path)
99
- except BranchNotFoundError:
131
+ except (BranchNotFoundError, CommitNotFoundError):
100
132
  if remove_on_error:
101
133
  shutil.rmtree(dest_path, ignore_errors=True)
102
134
  raise
@@ -118,7 +150,7 @@ def clone_repo(repo_url, dest_path, branch, shallow=True):
118
150
  if branch.startswith('refs/'):
119
151
  clone_repo_ref(repo_url, dest_path, branch)
120
152
  return
121
- args = ['git', 'clone']
153
+ args = ['git', 'clone', '--single-branch']
122
154
  if shallow:
123
155
  args.extend(['--depth', '1'])
124
156
  args.extend(['--branch', branch, repo_url, dest_path])
@@ -133,8 +165,9 @@ def clone_repo(repo_url, dest_path, branch, shallow=True):
133
165
  result = proc.wait()
134
166
  # Newer git versions will bail if the branch is not found, but older ones
135
167
  # will not. Fortunately they both output similar text.
136
- if not_found_str in out:
168
+ if result != 0:
137
169
  log.error(out)
170
+ if not_found_str in out:
138
171
  if result == 0:
139
172
  # Old git left a repo with the wrong branch. Remove it.
140
173
  shutil.rmtree(dest_path, ignore_errors=True)
@@ -242,7 +275,7 @@ def fetch_branch(repo_path, branch, shallow=True):
242
275
  GitError for other errors
243
276
  """
244
277
  validate_branch(branch)
245
- log.info("Fetching %s from origin", branch)
278
+ log.info("Fetching %s from origin", repo_path.split("/")[-1])
246
279
  args = ['git', 'fetch']
247
280
  if shallow:
248
281
  args.extend(['--depth', '1'])
@@ -262,13 +295,15 @@ def fetch_branch(repo_path, branch, shallow=True):
262
295
  raise GitError("git fetch failed!")
263
296
 
264
297
 
265
- def reset_repo(repo_url, dest_path, branch):
298
+ def reset_repo(repo_url, dest_path, branch, commit=None):
266
299
  """
267
300
 
268
301
  :param repo_url: The full URL to the repo (not including the branch)
269
302
  :param dest_path: The full path to the destination directory
270
303
  :param branch: The branch.
304
+ :param commit: The sha1 to checkout. Defaults to None, which uses HEAD of the branch.
271
305
  :raises: BranchNotFoundError if the branch is not found;
306
+ CommitNotFoundError if the commit is not found;
272
307
  GitError for other errors
273
308
  """
274
309
  validate_branch(branch)
@@ -276,15 +311,18 @@ def reset_repo(repo_url, dest_path, branch):
276
311
  reset_branch = lsstrip(remote_ref_from_ref(branch), 'refs/remotes/')
277
312
  else:
278
313
  reset_branch = 'origin/%s' % branch
279
- log.info('Resetting repo at %s to branch %s', dest_path, reset_branch)
314
+ reset_ref = commit or reset_branch
315
+ log.debug('Resetting repo at %s to %s', dest_path, reset_ref)
280
316
  # This try/except block will notice if the requested branch doesn't
281
317
  # exist, whether it was cloned or fetched.
282
318
  try:
283
319
  subprocess.check_output(
284
- ('git', 'reset', '--hard', reset_branch),
320
+ ('git', 'reset', '--hard', reset_ref),
285
321
  cwd=dest_path,
286
322
  )
287
323
  except subprocess.CalledProcessError:
324
+ if commit:
325
+ raise CommitNotFoundError(commit, repo_url)
288
326
  raise BranchNotFoundError(branch, repo_url)
289
327
 
290
328
 
@@ -299,7 +337,7 @@ def validate_branch(branch):
299
337
  raise ValueError("Illegal branch name: '%s'" % branch)
300
338
 
301
339
 
302
- def fetch_repo(url, branch, bootstrap=None, lock=True):
340
+ def fetch_repo(url, branch, commit=None, bootstrap=None, lock=True):
303
341
  """
304
342
  Make sure we have a given project's repo checked out and up-to-date with
305
343
  the current branch requested
@@ -308,24 +346,33 @@ def fetch_repo(url, branch, bootstrap=None, lock=True):
308
346
  :param bootstrap: An optional callback function to execute. Gets passed a
309
347
  dest_dir argument: the path to the repo on-disk.
310
348
  :param branch: The branch we want
349
+ :param commit: The sha1 to checkout. Defaults to None, which uses HEAD of the branch.
311
350
  :returns: The destination path
312
351
  """
313
352
  src_base_path = config.src_base_path
314
353
  if not os.path.exists(src_base_path):
315
354
  os.mkdir(src_base_path)
316
- branch_dir = ref_to_dirname(branch)
317
- dirname = '%s_%s' % (url_to_dirname(url), branch_dir)
355
+ ref_dir = ref_to_dirname(commit or branch)
356
+ dirname = '%s_%s' % (url_to_dirname(url), ref_dir)
318
357
  dest_path = os.path.join(src_base_path, dirname)
319
358
  # only let one worker create/update the checkout at a time
320
359
  lock_path = dest_path.rstrip('/') + '.lock'
321
360
  with FileLock(lock_path, noop=not lock):
322
- with safe_while(sleep=10, tries=60) as proceed:
361
+ with safe_while(sleep=10, tries=6) as proceed:
323
362
  try:
324
363
  while proceed():
325
364
  try:
326
- enforce_repo_state(url, dest_path, branch)
365
+ enforce_repo_state(url, dest_path, branch, commit)
327
366
  if bootstrap:
367
+ sentinel = os.path.join(dest_path, '.bootstrapped')
368
+ if commit and os.path.exists(sentinel) or is_fresh(sentinel):
369
+ log.info(
370
+ "Skipping bootstrap as it was already done in the last %ss",
371
+ FRESHNESS_INTERVAL,
372
+ )
373
+ break
328
374
  bootstrap(dest_path)
375
+ touch_file(sentinel)
329
376
  break
330
377
  except GitError:
331
378
  log.exception("Git error encountered; retrying")
@@ -368,36 +415,32 @@ def url_to_dirname(url):
368
415
  return string
369
416
 
370
417
 
371
- def fetch_qa_suite(branch, lock=True):
418
+ def fetch_qa_suite(branch, commit=None, lock=True):
372
419
  """
373
420
  Make sure ceph-qa-suite is checked out.
374
421
 
375
422
  :param branch: The branch to fetch
423
+ :param commit: The sha1 to checkout. Defaults to None, which uses HEAD of the branch.
376
424
  :returns: The destination path
377
425
  """
378
426
  return fetch_repo(config.get_ceph_qa_suite_git_url(),
379
- branch, lock=lock)
427
+ branch, commit, lock=lock)
380
428
 
381
429
 
382
- def fetch_teuthology(branch, lock=True):
430
+ def fetch_teuthology(branch, commit=None, lock=True):
383
431
  """
384
432
  Make sure we have the correct teuthology branch checked out and up-to-date
385
433
 
386
434
  :param branch: The branch we want
435
+ :param commit: The sha1 to checkout. Defaults to None, which uses HEAD of the branch.
387
436
  :returns: The destination path
388
437
  """
389
438
  url = config.ceph_git_base_url + 'teuthology.git'
390
- return fetch_repo(url, branch, bootstrap_teuthology, lock)
439
+ return fetch_repo(url, branch, commit, bootstrap_teuthology, lock)
391
440
 
392
441
 
393
442
  def bootstrap_teuthology(dest_path):
394
- sentinel = os.path.join(dest_path, '.bootstrapped')
395
- if is_fresh(sentinel):
396
- log.info(
397
- "Skipping bootstrap as it was already done in the last %ss",
398
- FRESHNESS_INTERVAL,
399
- )
400
- return
443
+ with exporter.BootstrapTime().time():
401
444
  log.info("Bootstrapping %s", dest_path)
402
445
  # This magic makes the bootstrap script not attempt to clobber an
403
446
  # existing virtualenv. But the branch's bootstrap needs to actually
@@ -405,17 +448,21 @@ def bootstrap_teuthology(dest_path):
405
448
  env = os.environ.copy()
406
449
  env['NO_CLOBBER'] = '1'
407
450
  cmd = './bootstrap'
408
- boot_proc = subprocess.Popen(cmd, shell=True, cwd=dest_path, env=env,
409
- stdout=subprocess.PIPE,
410
- stderr=subprocess.STDOUT)
411
- out, err = boot_proc.communicate()
451
+ boot_proc = subprocess.Popen(
452
+ cmd, shell=True,
453
+ cwd=dest_path,
454
+ env=env,
455
+ stdout=subprocess.PIPE,
456
+ stderr=subprocess.STDOUT,
457
+ universal_newlines=True
458
+ )
459
+ out, _ = boot_proc.communicate()
412
460
  returncode = boot_proc.wait()
413
461
  log.info("Bootstrap exited with status %s", returncode)
414
462
  if returncode != 0:
415
- for line in out.split():
416
- log.warn(line.strip())
463
+ for line in out.split("\n"):
464
+ log.warning(line.strip())
417
465
  venv_path = os.path.join(dest_path, 'virtualenv')
418
466
  log.info("Removing %s", venv_path)
419
467
  shutil.rmtree(venv_path, ignore_errors=True)
420
468
  raise BootstrapError("Bootstrap failed!")
421
- touch_file(sentinel)