1 # -*- test-case-name: buildbot.test.test_transfer -*-
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
12 class _FileWriter(pb
.Referenceable
):
14 Helper class that acts as a file-object with write access
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
):
24 self
.destfile
= destfile
25 self
.fp
= open(destfile
, "wb")
27 os
.chmod(destfile
, mode
)
28 self
.remaining
= maxsize
30 def remote_write(self
, data
):
32 Called from remote slave to write L{data} to L{fp} within boundaries
36 @param data: String of data to write
38 if self
.remaining
is not None:
39 if len(data
) > self
.remaining
:
40 data
= data
[:self
.remaining
]
42 self
.remaining
= self
.remaining
- len(data
)
46 def remote_close(self
):
48 Called by remote slave to state that no more data will be transfered
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)
59 os
.unlink(self
.destfile
)
62 class _DirectoryWriter(_FileWriter
):
64 A DirectoryWriter is implemented as a FileWriter, with an added post-processing
65 step to unpack the archive, once the transfer has completed.
68 def __init__(self
, destroot
, maxsize
, mode
):
69 self
.destroot
= destroot
71 self
.fd
, self
.tarname
= tempfile
.mkstemp()
73 _FileWriter
.__init
__(self
, self
.tarname
, maxsize
, mode
)
75 def remote_unpack(self
):
77 Called by remote slave to state that no more data will be transfered
82 fileobj
= os
.fdopen(self
.fd
, 'r')
83 archive
= tarfile
.open(name
=self
.tarname
, mode
="r|bz2", fileobj
=fileobj
)
84 archive
.extractall(path
=self
.destroot
)
85 os
.remove(self
.tarname
)
88 class StatusRemoteCommand(RemoteCommand
):
89 def __init__(self
, remote_command
, args
):
90 RemoteCommand
.__init
__(self
, remote_command
, args
)
95 def remoteUpdate(self
, update
):
96 #log.msg('StatusRemoteCommand: update=%r' % update)
98 self
.rc
= update
['rc']
99 if 'stderr' in update
:
100 self
.stderr
= self
.stderr
+ update
['stderr'] + '\n'
102 class _TransferBuildStep(BuildStep
):
104 Base class for FileUpload and FileDownload to factor out common
107 DEFAULT_WORKDIR
= "build" # is this redundant?
109 def setDefaultWorkdir(self
, workdir
):
110 if self
.workdir
is None:
111 self
.workdir
= workdir
113 def _getWorkdir(self
):
114 properties
= self
.build
.getProperties()
115 if self
.workdir
is None:
116 workdir
= self
.DEFAULT_WORKDIR
118 workdir
= self
.workdir
119 return properties
.render(workdir
)
121 def finished(self
, result
):
122 # Subclasses may choose to skip a transfer. In those cases, self.cmd
123 # will be None, and we should just let BuildStep.finished() handle
125 if result
== SKIPPED
:
126 return BuildStep
.finished(self
, SKIPPED
)
127 if self
.cmd
.stderr
!= '':
128 self
.addCompleteLog('stderr', self
.cmd
.stderr
)
130 if self
.cmd
.rc
is None or self
.cmd
.rc
== 0:
131 return BuildStep
.finished(self
, SUCCESS
)
132 return BuildStep
.finished(self
, FAILURE
)
135 class FileUpload(_TransferBuildStep
):
137 Build step to transfer a file from the slave to the master.
141 - ['slavesrc'] filename of source file at slave, relative to workdir
142 - ['masterdest'] filename of destination file at master
143 - ['workdir'] string with slave working directory relative to builder
144 base dir, default 'build'
145 - ['maxsize'] maximum size of the file, default None (=unlimited)
146 - ['blocksize'] maximum size of each block being transfered
147 - ['mode'] file access mode for the resulting master-side file.
148 The default (=None) is to leave it up to the umask of
149 the buildmaster process.
155 def __init__(self
, slavesrc
, masterdest
,
156 workdir
=None, maxsize
=None, blocksize
=16*1024, mode
=None,
158 BuildStep
.__init
__(self
, **buildstep_kwargs
)
159 self
.addFactoryArguments(slavesrc
=slavesrc
,
160 masterdest
=masterdest
,
167 self
.slavesrc
= slavesrc
168 self
.masterdest
= masterdest
169 self
.workdir
= workdir
170 self
.maxsize
= maxsize
171 self
.blocksize
= blocksize
172 assert isinstance(mode
, (int, type(None)))
176 version
= self
.slaveVersion("uploadFile")
177 properties
= self
.build
.getProperties()
180 m
= "slave is too old, does not know about uploadFile"
181 raise BuildSlaveTooOldError(m
)
183 source
= properties
.render(self
.slavesrc
)
184 masterdest
= properties
.render(self
.masterdest
)
185 # we rely upon the fact that the buildmaster runs chdir'ed into its
186 # basedir to make sure that relative paths in masterdest are expanded
187 # properly. TODO: maybe pass the master's basedir all the way down
188 # into the BuildStep so we can do this better.
189 masterdest
= os
.path
.expanduser(masterdest
)
190 log
.msg("FileUpload started, from slave %r to master %r"
191 % (source
, masterdest
))
193 self
.step_status
.setText(['uploading', os
.path
.basename(source
)])
195 # we use maxsize to limit the amount of data on both sides
196 fileWriter
= _FileWriter(masterdest
, self
.maxsize
, self
.mode
)
201 'workdir': self
._getWorkdir
(),
202 'writer': fileWriter
,
203 'maxsize': self
.maxsize
,
204 'blocksize': self
.blocksize
,
207 self
.cmd
= StatusRemoteCommand('uploadFile', args
)
208 d
= self
.runCommand(self
.cmd
)
209 d
.addCallback(self
.finished
).addErrback(self
.failed
)
212 class DirectoryUpload(BuildStep
):
214 Build step to transfer a directory from the slave to the master.
218 - ['slavesrc'] name of source directory at slave, relative to workdir
219 - ['masterdest'] name of destination directory at master
220 - ['workdir'] string with slave working directory relative to builder
221 base dir, default 'build'
222 - ['maxsize'] maximum size of each file, default None (=unlimited)
223 - ['blocksize'] maximum size of each block being transfered
224 - ['mode'] file access mode for the resulting master-side file.
225 The default (=None) is to leave it up to the umask of
226 the buildmaster process.
232 def __init__(self
, slavesrc
, masterdest
,
233 workdir
="build", maxsize
=None, blocksize
=16*1024, mode
=None,
235 BuildStep
.__init
__(self
, **buildstep_kwargs
)
236 self
.addFactoryArguments(slavesrc
=slavesrc
,
237 masterdest
=masterdest
,
244 self
.slavesrc
= slavesrc
245 self
.masterdest
= masterdest
246 self
.workdir
= workdir
247 self
.maxsize
= maxsize
248 self
.blocksize
= blocksize
249 assert isinstance(mode
, (int, type(None)))
253 version
= self
.slaveVersion("uploadDirectory")
254 properties
= self
.build
.getProperties()
257 m
= "slave is too old, does not know about uploadDirectory"
258 raise BuildSlaveTooOldError(m
)
260 source
= properties
.render(self
.slavesrc
)
261 masterdest
= properties
.render(self
.masterdest
)
262 # we rely upon the fact that the buildmaster runs chdir'ed into its
263 # basedir to make sure that relative paths in masterdest are expanded
264 # properly. TODO: maybe pass the master's basedir all the way down
265 # into the BuildStep so we can do this better.
266 masterdest
= os
.path
.expanduser(masterdest
)
267 log
.msg("DirectoryUpload started, from slave %r to master %r"
268 % (source
, masterdest
))
270 self
.step_status
.setText(['uploading', os
.path
.basename(source
)])
272 # we use maxsize to limit the amount of data on both sides
273 dirWriter
= _DirectoryWriter(masterdest
, self
.maxsize
, self
.mode
)
278 'workdir': self
.workdir
,
280 'maxsize': self
.maxsize
,
281 'blocksize': self
.blocksize
,
284 self
.cmd
= StatusRemoteCommand('uploadDirectory', args
)
285 d
= self
.runCommand(self
.cmd
)
286 d
.addCallback(self
.finished
).addErrback(self
.failed
)
288 def finished(self
, result
):
289 # Subclasses may choose to skip a transfer. In those cases, self.cmd
290 # will be None, and we should just let BuildStep.finished() handle
292 if result
== SKIPPED
:
293 return BuildStep
.finished(self
, SKIPPED
)
294 if self
.cmd
.stderr
!= '':
295 self
.addCompleteLog('stderr', self
.cmd
.stderr
)
297 if self
.cmd
.rc
is None or self
.cmd
.rc
== 0:
298 return BuildStep
.finished(self
, SUCCESS
)
299 return BuildStep
.finished(self
, FAILURE
)
304 class _FileReader(pb
.Referenceable
):
306 Helper class that acts as a file-object with read access
309 def __init__(self
, fp
):
312 def remote_read(self
, maxlength
):
314 Called from remote slave to read at most L{maxlength} bytes of data
316 @type maxlength: C{integer}
317 @param maxlength: Maximum number of data bytes that can be returned
319 @return: Data read from L{fp}
320 @rtype: C{string} of bytes read from file
325 data
= self
.fp
.read(maxlength
)
328 def remote_close(self
):
330 Called by remote slave to state that no more data will be transfered
332 if self
.fp
is not None:
337 class FileDownload(_TransferBuildStep
):
339 Download the first 'maxsize' bytes of a file, from the buildmaster to the
340 buildslave. Set the mode of the file
344 ['mastersrc'] filename of source file at master
345 ['slavedest'] filename of destination file at slave
346 ['workdir'] string with slave working directory relative to builder
347 base dir, default 'build'
348 ['maxsize'] maximum size of the file, default None (=unlimited)
349 ['blocksize'] maximum size of each block being transfered
350 ['mode'] use this to set the access permissions of the resulting
351 buildslave-side file. This is traditionally an octal
352 integer, like 0644 to be world-readable (but not
353 world-writable), or 0600 to only be readable by
354 the buildslave account, or 0755 to be world-executable.
355 The default (=None) is to leave it up to the umask of
356 the buildslave process.
361 def __init__(self
, mastersrc
, slavedest
,
362 workdir
=None, maxsize
=None, blocksize
=16*1024, mode
=None,
364 BuildStep
.__init
__(self
, **buildstep_kwargs
)
365 self
.addFactoryArguments(mastersrc
=mastersrc
,
373 self
.mastersrc
= mastersrc
374 self
.slavedest
= slavedest
375 self
.workdir
= workdir
376 self
.maxsize
= maxsize
377 self
.blocksize
= blocksize
378 assert isinstance(mode
, (int, type(None)))
382 properties
= self
.build
.getProperties()
384 version
= self
.slaveVersion("downloadFile")
386 m
= "slave is too old, does not know about downloadFile"
387 raise BuildSlaveTooOldError(m
)
389 # we are currently in the buildmaster's basedir, so any non-absolute
390 # paths will be interpreted relative to that
391 source
= os
.path
.expanduser(properties
.render(self
.mastersrc
))
392 slavedest
= properties
.render(self
.slavedest
)
393 log
.msg("FileDownload started, from master %r to slave %r" %
396 self
.step_status
.setText(['downloading', "to",
397 os
.path
.basename(slavedest
)])
399 # setup structures for reading the file
401 fp
= open(source
, 'rb')
403 # if file does not exist, bail out with an error
404 self
.addCompleteLog('stderr',
405 'File %r not available at master' % source
)
406 # TODO: once BuildStep.start() gets rewritten to use
407 # maybeDeferred, just re-raise the exception here.
408 reactor
.callLater(0, BuildStep
.finished
, self
, FAILURE
)
410 fileReader
= _FileReader(fp
)
414 'slavedest': slavedest
,
415 'maxsize': self
.maxsize
,
416 'reader': fileReader
,
417 'blocksize': self
.blocksize
,
418 'workdir': self
._getWorkdir
(),
422 self
.cmd
= StatusRemoteCommand('downloadFile', args
)
423 d
= self
.runCommand(self
.cmd
)
424 d
.addCallback(self
.finished
).addErrback(self
.failed
)