| OLD | NEW |
| (Empty) |
| 1 # -*- test-case-name: buildbot.test.test_transfer -*- | |
| 2 | |
| 3 import os.path, tarfile, tempfile | |
| 4 from twisted.internet import reactor | |
| 5 from twisted.spread import pb | |
| 6 from twisted.python import log | |
| 7 from buildbot.process.buildstep import RemoteCommand, BuildStep | |
| 8 from buildbot.process.buildstep import SUCCESS, FAILURE, SKIPPED | |
| 9 from buildbot.interfaces import BuildSlaveTooOldError | |
| 10 | |
| 11 | |
| 12 class _FileWriter(pb.Referenceable): | |
| 13 """ | |
| 14 Helper class that acts as a file-object with write access | |
| 15 """ | |
| 16 | |
| 17 def __init__(self, destfile, maxsize, mode): | |
| 18 # Create missing directories. | |
| 19 destfile = os.path.abspath(destfile) | |
| 20 dirname = os.path.dirname(destfile) | |
| 21 if not os.path.exists(dirname): | |
| 22 os.makedirs(dirname) | |
| 23 | |
| 24 self.destfile = destfile | |
| 25 self.fp = open(destfile, "wb") | |
| 26 if mode is not None: | |
| 27 os.chmod(destfile, mode) | |
| 28 self.remaining = maxsize | |
| 29 | |
| 30 def remote_write(self, data): | |
| 31 """ | |
| 32 Called from remote slave to write L{data} to L{fp} within boundaries | |
| 33 of L{maxsize} | |
| 34 | |
| 35 @type data: C{string} | |
| 36 @param data: String of data to write | |
| 37 """ | |
| 38 if self.remaining is not None: | |
| 39 if len(data) > self.remaining: | |
| 40 data = data[:self.remaining] | |
| 41 self.fp.write(data) | |
| 42 self.remaining = self.remaining - len(data) | |
| 43 else: | |
| 44 self.fp.write(data) | |
| 45 | |
| 46 def remote_close(self): | |
| 47 """ | |
| 48 Called by remote slave to state that no more data will be transfered | |
| 49 """ | |
| 50 self.fp.close() | |
| 51 self.fp = None | |
| 52 | |
| 53 def __del__(self): | |
| 54 # unclean shutdown, the file is probably truncated, so delete it | |
| 55 # altogether rather than deliver a corrupted file | |
| 56 fp = getattr(self, "fp", None) | |
| 57 if fp: | |
| 58 fp.close() | |
| 59 os.unlink(self.destfile) | |
| 60 | |
| 61 | |
| 62 def _extractall(self, path=".", members=None): | |
| 63 """Fallback extractall method for TarFile, in case it doesn't have its own."
"" | |
| 64 | |
| 65 import copy | |
| 66 import operator | |
| 67 | |
| 68 directories = [] | |
| 69 | |
| 70 if members is None: | |
| 71 members = self | |
| 72 | |
| 73 for tarinfo in members: | |
| 74 if tarinfo.isdir(): | |
| 75 # Extract directories with a safe mode. | |
| 76 directories.append(tarinfo) | |
| 77 tarinfo = copy.copy(tarinfo) | |
| 78 tarinfo.mode = 0700 | |
| 79 self.extract(tarinfo, path) | |
| 80 | |
| 81 # Reverse sort directories. | |
| 82 directories.sort(lambda a, b: cmp(a.name, b.name)) | |
| 83 directories.reverse() | |
| 84 | |
| 85 # Set correct owner, mtime and filemode on directories. | |
| 86 for tarinfo in directories: | |
| 87 dirpath = os.path.join(path, tarinfo.name) | |
| 88 try: | |
| 89 self.chown(tarinfo, dirpath) | |
| 90 self.utime(tarinfo, dirpath) | |
| 91 self.chmod(tarinfo, dirpath) | |
| 92 except tarfile.ExtractError, e: | |
| 93 if self.errorlevel > 1: | |
| 94 raise | |
| 95 else: | |
| 96 self._dbg(1, "tarfile: %s" % e) | |
| 97 | |
| 98 class _DirectoryWriter(_FileWriter): | |
| 99 """ | |
| 100 A DirectoryWriter is implemented as a FileWriter, with an added post-process
ing | |
| 101 step to unpack the archive, once the transfer has completed. | |
| 102 """ | |
| 103 | |
| 104 def __init__(self, destroot, maxsize, compress, mode): | |
| 105 self.destroot = destroot | |
| 106 | |
| 107 self.fd, self.tarname = tempfile.mkstemp() | |
| 108 self.compress = compress | |
| 109 _FileWriter.__init__(self, self.tarname, maxsize, mode) | |
| 110 | |
| 111 def remote_unpack(self): | |
| 112 """ | |
| 113 Called by remote slave to state that no more data will be transfered | |
| 114 """ | |
| 115 if self.fp: | |
| 116 self.fp.close() | |
| 117 self.fp = None | |
| 118 fileobj = os.fdopen(self.fd, 'r') | |
| 119 if self.compress == 'bz2': | |
| 120 mode='r|bz2' | |
| 121 elif self.compress == 'gz': | |
| 122 mode='r|gz' | |
| 123 else: | |
| 124 mode = 'r' | |
| 125 if not hasattr(tarfile.TarFile, 'extractall'): | |
| 126 tarfile.TarFile.extractall = _extractall | |
| 127 archive = tarfile.open(name=self.tarname, mode=mode, fileobj=fileobj) | |
| 128 archive.extractall(path=self.destroot) | |
| 129 os.remove(self.tarname) | |
| 130 | |
| 131 | |
| 132 class StatusRemoteCommand(RemoteCommand): | |
| 133 def __init__(self, remote_command, args): | |
| 134 RemoteCommand.__init__(self, remote_command, args) | |
| 135 | |
| 136 self.rc = None | |
| 137 self.stderr = '' | |
| 138 | |
| 139 def remoteUpdate(self, update): | |
| 140 #log.msg('StatusRemoteCommand: update=%r' % update) | |
| 141 if 'rc' in update: | |
| 142 self.rc = update['rc'] | |
| 143 if 'stderr' in update: | |
| 144 self.stderr = self.stderr + update['stderr'] + '\n' | |
| 145 | |
| 146 class _TransferBuildStep(BuildStep): | |
| 147 """ | |
| 148 Base class for FileUpload and FileDownload to factor out common | |
| 149 functionality. | |
| 150 """ | |
| 151 DEFAULT_WORKDIR = "build" # is this redundant? | |
| 152 | |
| 153 def setDefaultWorkdir(self, workdir): | |
| 154 if self.workdir is None: | |
| 155 self.workdir = workdir | |
| 156 | |
| 157 def _getWorkdir(self): | |
| 158 properties = self.build.getProperties() | |
| 159 if self.workdir is None: | |
| 160 workdir = self.DEFAULT_WORKDIR | |
| 161 else: | |
| 162 workdir = self.workdir | |
| 163 return properties.render(workdir) | |
| 164 | |
| 165 def finished(self, result): | |
| 166 # Subclasses may choose to skip a transfer. In those cases, self.cmd | |
| 167 # will be None, and we should just let BuildStep.finished() handle | |
| 168 # the rest | |
| 169 if result == SKIPPED: | |
| 170 return BuildStep.finished(self, SKIPPED) | |
| 171 if self.cmd.stderr != '': | |
| 172 self.addCompleteLog('stderr', self.cmd.stderr) | |
| 173 | |
| 174 if self.cmd.rc is None or self.cmd.rc == 0: | |
| 175 return BuildStep.finished(self, SUCCESS) | |
| 176 return BuildStep.finished(self, FAILURE) | |
| 177 | |
| 178 | |
| 179 class FileUpload(_TransferBuildStep): | |
| 180 """ | |
| 181 Build step to transfer a file from the slave to the master. | |
| 182 | |
| 183 arguments: | |
| 184 | |
| 185 - ['slavesrc'] filename of source file at slave, relative to workdir | |
| 186 - ['masterdest'] filename of destination file at master | |
| 187 - ['workdir'] string with slave working directory relative to builder | |
| 188 base dir, default 'build' | |
| 189 - ['maxsize'] maximum size of the file, default None (=unlimited) | |
| 190 - ['blocksize'] maximum size of each block being transfered | |
| 191 - ['mode'] file access mode for the resulting master-side file. | |
| 192 The default (=None) is to leave it up to the umask of | |
| 193 the buildmaster process. | |
| 194 | |
| 195 """ | |
| 196 | |
| 197 name = 'upload' | |
| 198 | |
| 199 def __init__(self, slavesrc, masterdest, | |
| 200 workdir=None, maxsize=None, blocksize=16*1024, mode=None, | |
| 201 **buildstep_kwargs): | |
| 202 BuildStep.__init__(self, **buildstep_kwargs) | |
| 203 self.addFactoryArguments(slavesrc=slavesrc, | |
| 204 masterdest=masterdest, | |
| 205 workdir=workdir, | |
| 206 maxsize=maxsize, | |
| 207 blocksize=blocksize, | |
| 208 mode=mode, | |
| 209 ) | |
| 210 | |
| 211 self.slavesrc = slavesrc | |
| 212 self.masterdest = masterdest | |
| 213 self.workdir = workdir | |
| 214 self.maxsize = maxsize | |
| 215 self.blocksize = blocksize | |
| 216 assert isinstance(mode, (int, type(None))) | |
| 217 self.mode = mode | |
| 218 | |
| 219 def start(self): | |
| 220 version = self.slaveVersion("uploadFile") | |
| 221 properties = self.build.getProperties() | |
| 222 | |
| 223 if not version: | |
| 224 m = "slave is too old, does not know about uploadFile" | |
| 225 raise BuildSlaveTooOldError(m) | |
| 226 | |
| 227 source = properties.render(self.slavesrc) | |
| 228 masterdest = properties.render(self.masterdest) | |
| 229 # we rely upon the fact that the buildmaster runs chdir'ed into its | |
| 230 # basedir to make sure that relative paths in masterdest are expanded | |
| 231 # properly. TODO: maybe pass the master's basedir all the way down | |
| 232 # into the BuildStep so we can do this better. | |
| 233 masterdest = os.path.expanduser(masterdest) | |
| 234 log.msg("FileUpload started, from slave %r to master %r" | |
| 235 % (source, masterdest)) | |
| 236 | |
| 237 self.step_status.setText(['uploading', os.path.basename(source)]) | |
| 238 | |
| 239 # we use maxsize to limit the amount of data on both sides | |
| 240 fileWriter = _FileWriter(masterdest, self.maxsize, self.mode) | |
| 241 | |
| 242 # default arguments | |
| 243 args = { | |
| 244 'slavesrc': source, | |
| 245 'workdir': self._getWorkdir(), | |
| 246 'writer': fileWriter, | |
| 247 'maxsize': self.maxsize, | |
| 248 'blocksize': self.blocksize, | |
| 249 } | |
| 250 | |
| 251 self.cmd = StatusRemoteCommand('uploadFile', args) | |
| 252 d = self.runCommand(self.cmd) | |
| 253 d.addCallback(self.finished).addErrback(self.failed) | |
| 254 | |
| 255 | |
| 256 class DirectoryUpload(BuildStep): | |
| 257 """ | |
| 258 Build step to transfer a directory from the slave to the master. | |
| 259 | |
| 260 arguments: | |
| 261 | |
| 262 - ['slavesrc'] name of source directory at slave, relative to workdir | |
| 263 - ['masterdest'] name of destination directory at master | |
| 264 - ['workdir'] string with slave working directory relative to builder | |
| 265 base dir, default 'build' | |
| 266 - ['maxsize'] maximum size of the compressed tarfile containing the | |
| 267 whole directory | |
| 268 - ['blocksize'] maximum size of each block being transfered | |
| 269 - ['compress'] compression type to use: one of [None, 'gz', 'bz2'] | |
| 270 | |
| 271 """ | |
| 272 | |
| 273 name = 'upload' | |
| 274 | |
| 275 def __init__(self, slavesrc, masterdest, | |
| 276 workdir="build", maxsize=None, blocksize=16*1024, | |
| 277 compress=None, **buildstep_kwargs): | |
| 278 BuildStep.__init__(self, **buildstep_kwargs) | |
| 279 self.addFactoryArguments(slavesrc=slavesrc, | |
| 280 masterdest=masterdest, | |
| 281 workdir=workdir, | |
| 282 maxsize=maxsize, | |
| 283 blocksize=blocksize, | |
| 284 compress=compress, | |
| 285 ) | |
| 286 | |
| 287 self.slavesrc = slavesrc | |
| 288 self.masterdest = masterdest | |
| 289 self.workdir = workdir | |
| 290 self.maxsize = maxsize | |
| 291 self.blocksize = blocksize | |
| 292 assert compress in (None, 'gz', 'bz2') | |
| 293 self.compress = compress | |
| 294 | |
| 295 def start(self): | |
| 296 version = self.slaveVersion("uploadDirectory") | |
| 297 properties = self.build.getProperties() | |
| 298 | |
| 299 if not version: | |
| 300 m = "slave is too old, does not know about uploadDirectory" | |
| 301 raise BuildSlaveTooOldError(m) | |
| 302 | |
| 303 source = properties.render(self.slavesrc) | |
| 304 masterdest = properties.render(self.masterdest) | |
| 305 # we rely upon the fact that the buildmaster runs chdir'ed into its | |
| 306 # basedir to make sure that relative paths in masterdest are expanded | |
| 307 # properly. TODO: maybe pass the master's basedir all the way down | |
| 308 # into the BuildStep so we can do this better. | |
| 309 masterdest = os.path.expanduser(masterdest) | |
| 310 log.msg("DirectoryUpload started, from slave %r to master %r" | |
| 311 % (source, masterdest)) | |
| 312 | |
| 313 self.step_status.setText(['uploading', os.path.basename(source)]) | |
| 314 | |
| 315 # we use maxsize to limit the amount of data on both sides | |
| 316 dirWriter = _DirectoryWriter(masterdest, self.maxsize, self.compress, 06
00) | |
| 317 | |
| 318 # default arguments | |
| 319 args = { | |
| 320 'slavesrc': source, | |
| 321 'workdir': self.workdir, | |
| 322 'writer': dirWriter, | |
| 323 'maxsize': self.maxsize, | |
| 324 'blocksize': self.blocksize, | |
| 325 'compress': self.compress | |
| 326 } | |
| 327 | |
| 328 self.cmd = StatusRemoteCommand('uploadDirectory', args) | |
| 329 d = self.runCommand(self.cmd) | |
| 330 d.addCallback(self.finished).addErrback(self.failed) | |
| 331 | |
| 332 def finished(self, result): | |
| 333 # Subclasses may choose to skip a transfer. In those cases, self.cmd | |
| 334 # will be None, and we should just let BuildStep.finished() handle | |
| 335 # the rest | |
| 336 if result == SKIPPED: | |
| 337 return BuildStep.finished(self, SKIPPED) | |
| 338 if self.cmd.stderr != '': | |
| 339 self.addCompleteLog('stderr', self.cmd.stderr) | |
| 340 | |
| 341 if self.cmd.rc is None or self.cmd.rc == 0: | |
| 342 return BuildStep.finished(self, SUCCESS) | |
| 343 return BuildStep.finished(self, FAILURE) | |
| 344 | |
| 345 | |
| 346 | |
| 347 | |
| 348 class _FileReader(pb.Referenceable): | |
| 349 """ | |
| 350 Helper class that acts as a file-object with read access | |
| 351 """ | |
| 352 | |
| 353 def __init__(self, fp): | |
| 354 self.fp = fp | |
| 355 | |
| 356 def remote_read(self, maxlength): | |
| 357 """ | |
| 358 Called from remote slave to read at most L{maxlength} bytes of data | |
| 359 | |
| 360 @type maxlength: C{integer} | |
| 361 @param maxlength: Maximum number of data bytes that can be returned | |
| 362 | |
| 363 @return: Data read from L{fp} | |
| 364 @rtype: C{string} of bytes read from file | |
| 365 """ | |
| 366 if self.fp is None: | |
| 367 return '' | |
| 368 | |
| 369 data = self.fp.read(maxlength) | |
| 370 return data | |
| 371 | |
| 372 def remote_close(self): | |
| 373 """ | |
| 374 Called by remote slave to state that no more data will be transfered | |
| 375 """ | |
| 376 if self.fp is not None: | |
| 377 self.fp.close() | |
| 378 self.fp = None | |
| 379 | |
| 380 | |
| 381 class FileDownload(_TransferBuildStep): | |
| 382 """ | |
| 383 Download the first 'maxsize' bytes of a file, from the buildmaster to the | |
| 384 buildslave. Set the mode of the file | |
| 385 | |
| 386 Arguments:: | |
| 387 | |
| 388 ['mastersrc'] filename of source file at master | |
| 389 ['slavedest'] filename of destination file at slave | |
| 390 ['workdir'] string with slave working directory relative to builder | |
| 391 base dir, default 'build' | |
| 392 ['maxsize'] maximum size of the file, default None (=unlimited) | |
| 393 ['blocksize'] maximum size of each block being transfered | |
| 394 ['mode'] use this to set the access permissions of the resulting | |
| 395 buildslave-side file. This is traditionally an octal | |
| 396 integer, like 0644 to be world-readable (but not | |
| 397 world-writable), or 0600 to only be readable by | |
| 398 the buildslave account, or 0755 to be world-executable. | |
| 399 The default (=None) is to leave it up to the umask of | |
| 400 the buildslave process. | |
| 401 | |
| 402 """ | |
| 403 name = 'download' | |
| 404 | |
| 405 def __init__(self, mastersrc, slavedest, | |
| 406 workdir=None, maxsize=None, blocksize=16*1024, mode=None, | |
| 407 **buildstep_kwargs): | |
| 408 BuildStep.__init__(self, **buildstep_kwargs) | |
| 409 self.addFactoryArguments(mastersrc=mastersrc, | |
| 410 slavedest=slavedest, | |
| 411 workdir=workdir, | |
| 412 maxsize=maxsize, | |
| 413 blocksize=blocksize, | |
| 414 mode=mode, | |
| 415 ) | |
| 416 | |
| 417 self.mastersrc = mastersrc | |
| 418 self.slavedest = slavedest | |
| 419 self.workdir = workdir | |
| 420 self.maxsize = maxsize | |
| 421 self.blocksize = blocksize | |
| 422 assert isinstance(mode, (int, type(None))) | |
| 423 self.mode = mode | |
| 424 | |
| 425 def start(self): | |
| 426 properties = self.build.getProperties() | |
| 427 | |
| 428 version = self.slaveVersion("downloadFile") | |
| 429 if not version: | |
| 430 m = "slave is too old, does not know about downloadFile" | |
| 431 raise BuildSlaveTooOldError(m) | |
| 432 | |
| 433 # we are currently in the buildmaster's basedir, so any non-absolute | |
| 434 # paths will be interpreted relative to that | |
| 435 source = os.path.expanduser(properties.render(self.mastersrc)) | |
| 436 slavedest = properties.render(self.slavedest) | |
| 437 log.msg("FileDownload started, from master %r to slave %r" % | |
| 438 (source, slavedest)) | |
| 439 | |
| 440 self.step_status.setText(['downloading', "to", | |
| 441 os.path.basename(slavedest)]) | |
| 442 | |
| 443 # setup structures for reading the file | |
| 444 try: | |
| 445 fp = open(source, 'rb') | |
| 446 except IOError: | |
| 447 # if file does not exist, bail out with an error | |
| 448 self.addCompleteLog('stderr', | |
| 449 'File %r not available at master' % source) | |
| 450 # TODO: once BuildStep.start() gets rewritten to use | |
| 451 # maybeDeferred, just re-raise the exception here. | |
| 452 reactor.callLater(0, BuildStep.finished, self, FAILURE) | |
| 453 return | |
| 454 fileReader = _FileReader(fp) | |
| 455 | |
| 456 # default arguments | |
| 457 args = { | |
| 458 'slavedest': slavedest, | |
| 459 'maxsize': self.maxsize, | |
| 460 'reader': fileReader, | |
| 461 'blocksize': self.blocksize, | |
| 462 'workdir': self._getWorkdir(), | |
| 463 'mode': self.mode, | |
| 464 } | |
| 465 | |
| 466 self.cmd = StatusRemoteCommand('downloadFile', args) | |
| 467 d = self.runCommand(self.cmd) | |
| 468 d.addCallback(self.finished).addErrback(self.failed) | |
| 469 | |
| OLD | NEW |