/stdhome

To get this branch, use:
bzr branch http://bzr.ed.am/stdhome

« back to all changes in this revision

Viewing changes to lib/stdhome/deployment.py

  • Committer: Tim Marston
  • Date: 2014-01-05 11:51:35 UTC
  • Revision ID: tim@ed.am-20140105115135-6ses87ggwyjrxzfj
added global objects (the.repo, the.program), deployment object and implemented
init command

Show diffs side-by-side

added added

removed removed

1
1
# deployment.py
2
2
#
3
 
# Copyright (C) 2013 to 2014 Tim Marston <tim@edm.am>
 
3
# Copyright (C) 2013 Tim Marston <tim@edm.am>
4
4
#
5
5
# This file is part of stdhome (hereafter referred to as "this program").
6
6
# See http://ed.am/dev/stdhome for more information.
19
19
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
20
20
 
21
21
 
22
 
import os, re, shutil, filecmp
23
 
import the, util
 
22
import os
 
23
import the
24
24
 
25
25
 
26
26
class Deployment:
27
27
 
28
28
 
29
29
        def __init__( self ):
30
 
                self.load_deployment_state()
31
 
                self.conflicts_checked = False
32
 
 
33
 
 
34
 
        def load_deployment_state( self ):
35
 
 
36
 
                # list of files that were copied-in (or at least given the opportunity
37
 
        # to be) and updated through the vcs update.  This means that, while
38
 
        # there may have been conflicts during the update (which will be dealt
39
 
        # with in the repo), any arising conflicts with the filesystem are now
40
 
        # assumed to be because of the vcs update and can be ignored (that is to
41
 
        # say, we no longer care about the file in the home directory).  In
42
 
        # short, this is a list of files that it is safe to deploy, regardless
43
 
        # of the state of the filesystem.
44
 
                self.deploy_files = None
45
 
 
46
 
                # do we have a repo?
47
 
                if not os.path.exists( the.repo.full_dir ): return
48
 
 
49
 
                # if no file list exists, we're done
50
 
                file = os.path.join( the.full_mddir, "deploy.%s" % the.repo.name )
51
 
                if not os.path.isfile( file ):
52
 
                        if the.verbose: print "no deployment state found"
53
 
                        return
54
 
 
55
 
                # read the file list
56
 
                if the.verbose: print "loading deployment state"
57
 
                f = open( file, 'r' )
58
 
                self.deploy_files = f.read().splitlines()
59
 
 
60
 
 
61
 
        def save_deployment_state( self ):
62
 
                if the.verbose: print "saving deployment state"
63
 
 
64
 
                # create metadata directory, as necessary
65
 
                if not os.path.isdir( the.full_mddir ):
66
 
                        os.mkdir( the.full_mddir )
67
 
 
68
 
                # create file
69
 
                file = os.path.join( the.full_mddir, "deploy.%s" % the.repo.name )
70
 
                f = open( file, 'w' )
71
 
                f.write( '\n'.join( self.deploy_files ) + '\n' )
72
 
 
73
 
 
74
 
        def remove_deployment_state( self ):
75
 
 
76
 
                # delete it, if it exists
77
 
                file = os.path.join( the.full_mddir, "deploy.%s" % the.repo.name )
78
 
                if( os.path.isfile( file ) ):
79
 
                        os.unlink( file )
80
 
 
81
 
 
82
 
        def is_ongoing( self ):
83
 
                return False if self.deploy_files is None else True
84
 
 
85
 
 
86
 
        def check_ongoing( self, ongoing = True ):
87
 
                if( ongoing ):
88
 
                        if self.deploy_files is None:
89
 
                                raise self.DeploymentOngoing( False )
 
30
 
 
31
                # initialise the file list
 
32
                self.file_list = None
 
33
                if not os.path.exists( the.repo.expanded_dir ):
 
34
                        self.file_list = []
90
35
                else:
91
 
                        if self.deploy_files is not None:
92
 
                                raise self.DeploymentOngoing( True )
93
 
 
94
 
 
95
 
        def confirm( self, message ):
96
 
                # TODO: write interactive confirmation routine
97
 
                return False
98
 
 
99
 
 
100
 
        def walk_repo( self, dir_func = None, file_func = None, link_func = None,
101
 
                                   relative_dir = '', ignore_errors = False ):
102
 
                """Walk through all files and directories in the repo, passing the relative path
103
 
                to each to the provided functions.
104
 
                """
105
 
                full_dir = os.path.join( the.repo.full_dir, relative_dir )
106
 
                for file in os.listdir( full_dir ):
107
 
                        relative_file = os.path.join( relative_dir, file )
108
 
 
109
 
                        # skip some stuff
110
 
                        if relative_file == '.bzr': continue
111
 
 
112
 
                        repo_file = os.path.join( the.repo.full_dir, relative_file )
113
 
                        fs_file = os.path.join( the.full_fsdir, relative_file )
114
 
                        if os.path.islink( repo_file ):
115
 
                                if link_func is not None:
116
 
                                        link_func( relative_file, repo_file, fs_file )
117
 
                        elif os.path.isfile( repo_file ):
118
 
                                if file_func is not None:
119
 
                                        file_func( relative_file, repo_file, fs_file )
120
 
                        elif os.path.isdir( repo_file ):
121
 
                                if dir_func is not None:
122
 
                                        dir_func( relative_path, repo_file, fs_file )
123
 
                                # recurse in directories
124
 
                                self.walk_repo( dir_func, file_func, link_func, relative_dir,
125
 
                                                                ignore_errors )
126
 
                        elif not ignore_errors:
127
 
                                raise RuntimeError(
128
 
                                        'repo contains unknown/missing entity' )
 
36
                        self.load_file_list()
 
37
 
 
38
                assert False # not implemented
 
39
 
 
40
 
 
41
        def load_file_list( self ):
 
42
 
 
43
                # if no file list, don't load one
 
44
                if .....
 
45
                        return
 
46
 
 
47
                # load it
129
48
 
130
49
 
131
50
        def copy_in( self ):
132
51
 
133
 
                def copy_in_dir( relative_file, dst, src ):
134
 
                        self.deploy_files.append( relative_file )
135
 
 
136
 
                        # if src doesn't exist, delete it from the repo
137
 
                        if not os.path.lexists( src ):
138
 
                                if the.verbose: print " _<d " + relative_file
139
 
                                shutil.rmtree( dst )
140
 
 
141
 
                        # if src is a directory, copy permissions, as necessary
142
 
                        elif os.path.isdir( src ):
143
 
                                # TODO: should check permissions and only do as necessary
144
 
                                if the.verbose: print " d<d " + relative_file
145
 
                                shutil.copystat( src, dst )
146
 
 
147
 
                        # TODO: serious differences in between ~/ and repo (e.g., files in
148
 
                        # one that are directories in the other) should be ignored (e.g.,
149
 
                        # not copied-in).  And the stuff that is ignored during copy-in
150
 
                        # should also be ignored during copy-out and must not be added to
151
 
                        # the deploy_files list.  Since these ignored files/directories are
152
 
                        # transparent to the user, they should have to explicitly permit
153
 
                        # them via an ignore file (e.g., ~/.stdhome/.ignore, akin to bzr's
154
 
                        # .bzrignore file).  If these serious differences are not matched by
155
 
                        # the ignore file, an error should show (which will requie a
156
 
                        # separate "check" walk of the repo, as is done in copy_out).
157
 
                        else:
158
 
                                raise self.Conflict( "%s differs too severely from the repo" %
159
 
                                                                         os.path.join( the.fsdir, relative_file ) )
160
 
 
161
 
                def copy_in_file( relative_file, dst, src ):
162
 
                        self.deploy_files.append( relative_file )
163
 
 
164
 
                        # if src doesn't exist, delete it in the repo
165
 
                        if not os.path.lexists( src ):
166
 
                                if the.verbose: print " ?<f " + relative_file
167
 
                                os.unlink( dst )
168
 
 
169
 
                        # if src is a symlink, replace it in repo
170
 
                        elif os.path.islink( src ):
171
 
                                if the.verbose: print " l<f " + relative_file
172
 
                                os.unlink( dst )
173
 
                                os.symlink( os.readlink( src ), dst )
174
 
 
175
 
                        # if src is a file, replace it if different
176
 
                        elif os.path.isfile( src ):
177
 
                                if not filecmp.cmp( src, dst ):
178
 
                                        if the.verbose: print " f<f " + relative_file
179
 
                                        os.unlink( dst )
180
 
                                        shutil.copy( src, dst )
181
 
                                        shutil.copystat( src, dst )
182
 
                                else:
183
 
                                        if the.verbose: print " f=f " + relative_file
184
 
 
185
 
                        # TODO: serious differences in between ~/ and repo (e.g., files in
186
 
                        # one that are directories in the other) should be ignored (e.g.,
187
 
                        # not copied-in).  And the stuff that is ignored during copy-in
188
 
                        # should also be ignored during copy-out and must not be added to
189
 
                        # the deploy_files list.  Since these ignored files/directories are
190
 
                        # transparent to the user, they should have to explicitly permit
191
 
                        # them via an ignore file (e.g., ~/.stdhome/.ignore, akin to bzr's
192
 
                        # .bzrignore file).  If these serious differences are not matched by
193
 
                        # the ignore file, an error should show (which will requie a
194
 
                        # separate "check" walk of the repo, as is done in copy_out).
195
 
                        else:
196
 
                                raise self.Conflict( "%s differs too severely from the repo" %
197
 
                                                                         os.path.join( the.fsdir, relative_file ) )
198
 
 
199
 
                def copy_in_link( relative_file, dst, src ):
200
 
                        self.deploy_files.append( relative_file )
201
 
 
202
 
                        # if src doesn't exist, delete it in the repo
203
 
                        if not os.path.lexists( src ):
204
 
                                if the.verbose: print " _<l " + relative_file
205
 
                                os.unlink( dst )
206
 
 
207
 
                        # if src is a symlink, replace it if different in repo
208
 
                        elif os.path.islink( src ):
209
 
                                if os.readlink( src ) != os.readlink( dst ):
210
 
                                        if the.verbose: print " l<l " + relative_file
211
 
                                        os.unlink( dst )
212
 
                                        os.symlink( os.readlink( src ), dst )
213
 
                                else:
214
 
                                        if the.verbose: print " l=l " + relative_file
215
 
 
216
 
                        # if src is a file, replace it in repo
217
 
                        elif os.path.isfile( src ):
218
 
                                if the.verbose: print " f<l " + relative_file
219
 
                                os.unlink( dst )
220
 
                                shutil.copy( src, dst )
221
 
                                shutil.copystat( src, dst )
222
 
 
223
 
                        # TODO: serious differences in between ~/ and repo (e.g., files in
224
 
                        # one that are directories in the other) should be ignored (e.g.,
225
 
                        # not copied-in).  And the stuff that is ignored during copy-in
226
 
                        # should also be ignored during copy-out and must not be added to
227
 
                        # the deploy_files list.  Since these ignored files/directories are
228
 
                        # transparent to the user, they should have to explicitly permit
229
 
                        # them via an ignore file (e.g., ~/.stdhome/.ignore, akin to bzr's
230
 
                        # .bzrignore file).  If these serious differences are not matched by
231
 
                        # the ignore file, an error should show (which will requie a
232
 
                        # separate "check" walk of the repo, as is done in copy_out).
233
 
                        else:
234
 
                                raise self.Conflict( "%s differs too severely from the repo" %
235
 
                                                                         os.path.join( the.fsdir, relative_file ) )
236
 
 
237
52
                # check we don't already have a file list
238
 
                self.check_ongoing( False )
239
 
 
240
 
                # new file list
241
 
                self.deploy_files = list()
242
 
 
243
 
                # if the repo doesn't exist, we're done
244
 
                if not os.path.exists( the.repo.full_dir ): return
245
 
 
246
 
                # copy in
247
 
                if the.verbose: print "importing files"
248
 
                self.walk_repo( dir_func = copy_in_dir, file_func = copy_in_file,
249
 
                                           link_func = copy_in_link, ignore_errors = True )
250
 
 
251
 
                # save state
252
 
                self.save_deployment_state()
 
53
                if self.file_list is not None:
 
54
                        raise the.FatalError( 'deployment in progress; ' + \
 
55
                                'see "%s resolve --help" for information' % the.program.name )
 
56
                        
 
57
                assert False # not implemented
253
58
 
254
59
 
255
60
        def copy_out( self ):
256
 
 
257
 
                def copy_out_dir( relative_file, src, dst ):
258
 
 
259
 
                        # if dst doesn't exist, create it
260
 
                        if not os.path.lexists( dst ):
261
 
                                if the.verbose: print " d>_ " + relative_file
262
 
                                os.mkdir( dst )
263
 
                                shutil.copystat( src, dst )
264
 
 
265
 
                        # if dst is a file/symlink, replace it
266
 
                        elif os.path.isfile( dst ):
267
 
                                if the.verbose: print " d>f " + relative_file
268
 
                                os.unlink( dst )
269
 
                                os.mkdir( dst )
270
 
                                shutil.copystat( src, dst )
271
 
 
272
 
                        # if dst is a directory, copy permissions as required
273
 
                        elif os.path.isdir( dst ):
274
 
                                # TODO: should check permission and only do as necessary
275
 
                                if the.verbose: print " d>d " + relative_file
276
 
                                shutil.copystat( src, dst )
277
 
 
278
 
                        else:
279
 
                                raise NotImplementedError()
280
 
 
281
 
                def copy_out_file( relative_file, src, dst ):
282
 
 
283
 
                        # if dst doesn't exist, copy
284
 
                        if not os.path.lexists( dst ):
285
 
                                if the.verbose: print " f>_ " + relative_file
286
 
 
287
 
                                shutil.copy( src, dst )
288
 
                                shutil.copystat( src, dst )
289
 
 
290
 
                        # if dst is a symlink, replace it
291
 
                        elif os.path.islink( dst ):
292
 
                                if the.verbose: print " f>l " + relative_file
293
 
                                os.unlink( dst )
294
 
                                shutil.copy( src, dst )
295
 
                                shutil.copystat( src, dst )
296
 
 
297
 
                        # if dst is a file, replace it if different
298
 
                        elif os.path.isfile( dst ):
299
 
                                if not filecmp.cmp( src, dst ):
300
 
                                        if the.verbose: print " f>f " + relative_file
301
 
                                        os.unlink( dst )
302
 
                                        shutil.copy( src, dst )
303
 
                                        shutil.copystat( src, dst )
304
 
                                else:
305
 
                                        if the.verbose: print " f=f " + relative_file
306
 
 
307
 
                        # if dst is a directory, replace it
308
 
                        elif os.path.isdir( dst ):
309
 
                                if the.verbose: print " f>d " + relative_file
310
 
                                shutil.rmtree( dst )
311
 
                                shutil.copy( src, dst )
312
 
                                shutil.copystat( src, dst )
313
 
 
314
 
                        else:
315
 
                                raise NotImplementedError()
316
 
 
317
 
                def copy_out_link( relative_file, src, dst ):
318
 
 
319
 
                        # if dst doesn't exist, copy
320
 
                        if not os.path.lexists( dst ):
321
 
                                if the.verbose: print " l>_ " + relative_file
322
 
                                os.symlink( os.readlink( src ), dst )
323
 
 
324
 
                        # if dst is a symlink, replace it if different
325
 
                        elif os.path.islink( dst ):
326
 
                                if os.readlink( src ) != os.readlink( dst ):
327
 
                                        if the.verbose: print " l>l " + relative_file
328
 
                                        os.unlink( dst )
329
 
                                        os.symlink( os.readlink( src ), dst )
330
 
                                else:
331
 
                                        if the.verbose: print " l=l " + relative_file
332
 
 
333
 
                        # if dst is a file, replace it
334
 
                        elif os.path.isfile( dst ):
335
 
                                if the.verbose: print " l>f " + relative_file
336
 
                                os.unlink( dst )
337
 
                                os.symlink( os.readlink( src ), dst )
338
 
 
339
 
                        # if dst is a directory, replace it
340
 
                        elif os.path.isdir( dst ):
341
 
                                if the.verbose: print " l>d " + relative_file
342
 
                                shutil.rmtree( dst )
343
 
                                os.symlink( os.readlink( src ), dst )
344
 
 
345
 
                        else:
346
 
                                raise NotImplementedError()
347
 
 
348
 
                # check we have a file list
349
 
                self.check_ongoing( True )
350
 
 
351
 
                # we should already have handled conflicts
352
 
                if not self.conflicts_checked:
353
 
                        raise RuntimeError(
354
 
                                'logic error: conflicts should have been checked!' )
355
 
 
356
 
                # copy out
357
 
                if the.verbose: print "exporting files"
358
 
                self.walk_repo( dir_func = copy_out_dir, file_func = copy_out_file,
359
 
                                           link_func = copy_out_link, ignore_errors = True )
360
 
 
361
 
                # clear state
362
 
                self.remove_deployment_state()
363
 
 
364
 
 
365
 
        def get_conflicts( self ):
366
 
 
367
 
                def check_dir( relative_file, src, dst ):
368
 
 
369
 
                        # files that existed at copy-in can be copied-out and overwritten
370
 
                        if self.deploy_files is not None and \
371
 
                           relative_file in self.deploy_files: return
372
 
 
373
 
                        # files that don't exist in the filesystem can be copied-out
374
 
                        if not os.path.lexists( dst ): return
375
 
 
376
 
                        # accept/reject existing files
377
 
                        if os.path.islink( dst ):
378
 
                                self.files.append( "%s already exists (as a symlink)" %
379
 
                                                                   os.path.join( the.fsdir, relative_file ) )
380
 
                        elif os.path.isfile( dst ):
381
 
                                self.files.append( "%s already exists (as a file)" %
382
 
                                                                   os.path.join( the.fsdir, relative_file ) )
383
 
                        elif os.path.isdir( dst ):
384
 
                                return
385
 
                        else:
386
 
                                self.files.append( "%s already exists" %
387
 
                                                                   os.path.join( the.fsdir, relative_file ) )
388
 
 
389
 
                def check_file( relative_file, src, dst ):
390
 
 
391
 
                        # files that existed at copy-in can be copied-out and overwritten
392
 
                        if self.deploy_files is not None and \
393
 
                           relative_file in self.deploy_files: return
394
 
 
395
 
                        # files that don't exist in the filesystem can be copied-out
396
 
                        if not os.path.lexists( dst ): return
397
 
 
398
 
                        # accept/reject existing files
399
 
                        if os.path.isfile( dst ):
400
 
                                self.files.append( "%s already exists" %
401
 
                                                                   os.path.join( the.fsdir, relative_file ) )
402
 
                        elif os.path.isdir( dst ):
403
 
                                self.files.append( "%s already exists (as a directory)" %
404
 
                                                                   os.path.join( the.fsdir, relative_file ) )
405
 
                        else:
406
 
                                self.files.append( "%s already exists" %
407
 
                                                                   os.path.join( the.fsdir, relative_file ) )
408
 
 
409
 
                self.conflicts_checked = True
410
 
 
411
 
                # check for conflicts
412
 
                self.files = list()
413
 
                self.walk_repo( dir_func = check_dir, file_func = check_file,
414
 
                                           link_func = check_file )
415
 
                return self.files
416
 
 
417
 
 
418
 
        class DeploymentOngoing( the.program.FatalError ):
419
 
 
420
 
                def __init__( self, ongoing ):
421
 
                        if( ongoing ):
422
 
                                self.msg = "there is an ongoing deployment"
423
 
                        else:
424
 
                                self.msg = "there is no ongoing deployment"
425
 
 
426
 
 
427
 
        class Conflict( Exception ):
428
 
 
429
 
                def __init__( self, message ):
430
 
                        self.msg = message
 
61
                if self.pre_copy_out_list is None:
 
62
                        return
 
63
 
 
64
                print "deploying files"
 
65
 
 
66
                # walk the repo
 
67
                for ( repodir, directories, files ) in os.walk( the.repo.expanded_dir ):
 
68
                        assert path[ : len( the.repo.expanded_dir ) ] == \
 
69
                                the.repo.expanded_dir
 
70
                        relative_path = path[ len( the.repo.expanded_dir ) : ]
 
71
                        fsdir = the.expanded_fsdir + relative_path
 
72
 
 
73
                        print relative_path
 
74
 
 
75
                        # check directories
 
76
                        for dir in directories:
 
77
                                if os.path.exists( fsdir + dir ) and \
 
78
                                   not os.path.isdir( fsdir + dir ):
 
79
 
 
80
                                        #
 
81
 
 
82
                        # if exists in repo as directory and also in fs as non-directory
 
83
                        if os.path.isdir( the.repo.expnaded_dir + relative_path +