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.
- scripts/describe.py +1 -0
- scripts/dispatcher.py +62 -0
- scripts/exporter.py +18 -0
- scripts/lock.py +1 -1
- scripts/node_cleanup.py +58 -0
- scripts/openstack.py +9 -9
- scripts/results.py +12 -11
- scripts/run.py +4 -0
- scripts/schedule.py +4 -0
- scripts/suite.py +61 -16
- scripts/supervisor.py +44 -0
- scripts/update_inventory.py +10 -4
- scripts/wait.py +31 -0
- teuthology/__init__.py +24 -21
- teuthology/beanstalk.py +4 -3
- teuthology/config.py +17 -6
- teuthology/contextutil.py +18 -14
- teuthology/describe_tests.py +25 -18
- teuthology/dispatcher/__init__.py +365 -0
- teuthology/dispatcher/supervisor.py +374 -0
- teuthology/exceptions.py +54 -0
- teuthology/exporter.py +347 -0
- teuthology/kill.py +76 -75
- teuthology/lock/cli.py +16 -7
- teuthology/lock/ops.py +276 -70
- teuthology/lock/query.py +61 -44
- teuthology/ls.py +9 -18
- teuthology/misc.py +152 -137
- teuthology/nuke/__init__.py +12 -351
- teuthology/openstack/__init__.py +4 -3
- teuthology/openstack/openstack-centos-7.0-user-data.txt +1 -1
- teuthology/openstack/openstack-centos-7.1-user-data.txt +1 -1
- teuthology/openstack/openstack-centos-7.2-user-data.txt +1 -1
- teuthology/openstack/openstack-debian-8.0-user-data.txt +1 -1
- teuthology/openstack/openstack-opensuse-42.1-user-data.txt +1 -1
- teuthology/openstack/openstack-teuthology.cron +0 -1
- teuthology/orchestra/cluster.py +51 -9
- teuthology/orchestra/connection.py +23 -16
- teuthology/orchestra/console.py +111 -50
- teuthology/orchestra/daemon/cephadmunit.py +23 -5
- teuthology/orchestra/daemon/state.py +10 -3
- teuthology/orchestra/daemon/systemd.py +10 -8
- teuthology/orchestra/opsys.py +32 -11
- teuthology/orchestra/remote.py +369 -152
- teuthology/orchestra/run.py +21 -12
- teuthology/packaging.py +54 -15
- teuthology/provision/__init__.py +30 -10
- teuthology/provision/cloud/openstack.py +12 -6
- teuthology/provision/cloud/util.py +1 -2
- teuthology/provision/downburst.py +83 -29
- teuthology/provision/fog.py +68 -20
- teuthology/provision/openstack.py +5 -4
- teuthology/provision/pelagos.py +13 -5
- teuthology/repo_utils.py +91 -44
- teuthology/report.py +57 -35
- teuthology/results.py +5 -3
- teuthology/run.py +21 -15
- teuthology/run_tasks.py +114 -40
- teuthology/schedule.py +4 -3
- teuthology/scrape.py +28 -22
- teuthology/suite/__init__.py +75 -46
- teuthology/suite/build_matrix.py +34 -24
- teuthology/suite/fragment-merge.lua +105 -0
- teuthology/suite/matrix.py +31 -2
- teuthology/suite/merge.py +175 -0
- teuthology/suite/placeholder.py +8 -8
- teuthology/suite/run.py +204 -102
- teuthology/suite/util.py +67 -211
- teuthology/task/__init__.py +1 -1
- teuthology/task/ansible.py +101 -31
- teuthology/task/buildpackages.py +2 -2
- teuthology/task/ceph_ansible.py +13 -6
- teuthology/task/cephmetrics.py +2 -1
- teuthology/task/clock.py +33 -14
- teuthology/task/exec.py +18 -0
- teuthology/task/hadoop.py +2 -2
- teuthology/task/install/__init__.py +51 -22
- teuthology/task/install/bin/adjust-ulimits +16 -0
- teuthology/task/install/bin/daemon-helper +114 -0
- teuthology/task/install/bin/stdin-killer +263 -0
- teuthology/task/install/deb.py +24 -4
- teuthology/task/install/redhat.py +36 -32
- teuthology/task/install/rpm.py +41 -14
- teuthology/task/install/util.py +48 -22
- teuthology/task/internal/__init__.py +69 -11
- teuthology/task/internal/edit_sudoers.sh +10 -0
- teuthology/task/internal/lock_machines.py +3 -133
- teuthology/task/internal/redhat.py +48 -28
- teuthology/task/internal/syslog.py +31 -8
- teuthology/task/kernel.py +155 -147
- teuthology/task/lockfile.py +1 -1
- teuthology/task/mpi.py +10 -10
- teuthology/task/pcp.py +1 -1
- teuthology/task/selinux.py +17 -8
- teuthology/task/ssh_keys.py +6 -6
- teuthology/task/tests/__init__.py +137 -77
- teuthology/task/tests/test_fetch_coredumps.py +116 -0
- teuthology/task/tests/test_run.py +4 -4
- teuthology/timer.py +3 -3
- teuthology/util/loggerfile.py +19 -0
- teuthology/util/scanner.py +159 -0
- teuthology/util/sentry.py +52 -0
- teuthology/util/time.py +52 -0
- teuthology-1.2.0.data/scripts/adjust-ulimits +16 -0
- teuthology-1.2.0.data/scripts/daemon-helper +114 -0
- teuthology-1.2.0.data/scripts/stdin-killer +263 -0
- teuthology-1.2.0.dist-info/METADATA +89 -0
- teuthology-1.2.0.dist-info/RECORD +174 -0
- {teuthology-1.0.0.dist-info → teuthology-1.2.0.dist-info}/WHEEL +1 -1
- {teuthology-1.0.0.dist-info → teuthology-1.2.0.dist-info}/entry_points.txt +5 -2
- scripts/nuke.py +0 -45
- scripts/worker.py +0 -37
- teuthology/nuke/actions.py +0 -456
- teuthology/openstack/test/__init__.py +0 -0
- teuthology/openstack/test/openstack-integration.py +0 -286
- teuthology/openstack/test/test_config.py +0 -35
- teuthology/openstack/test/test_openstack.py +0 -1695
- teuthology/orchestra/test/__init__.py +0 -0
- teuthology/orchestra/test/integration/__init__.py +0 -0
- teuthology/orchestra/test/integration/test_integration.py +0 -94
- teuthology/orchestra/test/test_cluster.py +0 -240
- teuthology/orchestra/test/test_connection.py +0 -106
- teuthology/orchestra/test/test_console.py +0 -217
- teuthology/orchestra/test/test_opsys.py +0 -404
- teuthology/orchestra/test/test_remote.py +0 -185
- teuthology/orchestra/test/test_run.py +0 -286
- teuthology/orchestra/test/test_systemd.py +0 -54
- teuthology/orchestra/test/util.py +0 -12
- teuthology/sentry.py +0 -18
- teuthology/test/__init__.py +0 -0
- teuthology/test/fake_archive.py +0 -107
- teuthology/test/fake_fs.py +0 -92
- teuthology/test/integration/__init__.py +0 -0
- teuthology/test/integration/test_suite.py +0 -86
- teuthology/test/task/__init__.py +0 -205
- teuthology/test/task/test_ansible.py +0 -624
- teuthology/test/task/test_ceph_ansible.py +0 -176
- teuthology/test/task/test_console_log.py +0 -88
- teuthology/test/task/test_install.py +0 -337
- teuthology/test/task/test_internal.py +0 -57
- teuthology/test/task/test_kernel.py +0 -243
- teuthology/test/task/test_pcp.py +0 -379
- teuthology/test/task/test_selinux.py +0 -35
- teuthology/test/test_config.py +0 -189
- teuthology/test/test_contextutil.py +0 -68
- teuthology/test/test_describe_tests.py +0 -316
- teuthology/test/test_email_sleep_before_teardown.py +0 -81
- teuthology/test/test_exit.py +0 -97
- teuthology/test/test_get_distro.py +0 -47
- teuthology/test/test_get_distro_version.py +0 -47
- teuthology/test/test_get_multi_machine_types.py +0 -27
- teuthology/test/test_job_status.py +0 -60
- teuthology/test/test_ls.py +0 -48
- teuthology/test/test_misc.py +0 -368
- teuthology/test/test_nuke.py +0 -232
- teuthology/test/test_packaging.py +0 -763
- teuthology/test/test_parallel.py +0 -28
- teuthology/test/test_repo_utils.py +0 -204
- teuthology/test/test_report.py +0 -77
- teuthology/test/test_results.py +0 -155
- teuthology/test/test_run.py +0 -238
- teuthology/test/test_safepath.py +0 -55
- teuthology/test/test_schedule.py +0 -45
- teuthology/test/test_scrape.py +0 -167
- teuthology/test/test_timer.py +0 -80
- teuthology/test/test_vps_os_vers_parameter_checking.py +0 -84
- teuthology/test/test_worker.py +0 -303
- teuthology/worker.py +0 -339
- teuthology-1.0.0.dist-info/METADATA +0 -76
- teuthology-1.0.0.dist-info/RECORD +0 -210
- {teuthology-1.0.0.dist-info → teuthology-1.2.0.dist-info}/LICENSE +0 -0
- {teuthology-1.0.0.dist-info → teuthology-1.2.0.dist-info}/top_level.txt +0 -0
teuthology/provision/fog.py
CHANGED
@@ -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.
|
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
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
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
|
-
"
|
160
|
-
(self.
|
161
|
-
|
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.
|
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=
|
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,
|
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
|
-
|
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.
|
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.
|
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.
|
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.
|
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):
|
teuthology/provision/pelagos.py
CHANGED
@@ -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.
|
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
|
-
|
53
|
-
|
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
|
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:
|
79
|
-
:param dest_path:
|
80
|
-
:param branch:
|
81
|
-
:param
|
82
|
-
:
|
83
|
-
|
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
|
-
|
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
|
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",
|
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
|
-
|
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',
|
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
|
-
|
317
|
-
dirname = '%s_%s' % (url_to_dirname(url),
|
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=
|
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
|
-
|
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(
|
409
|
-
|
410
|
-
|
411
|
-
|
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.
|
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)
|