/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-04-05 19:20:06 UTC
  • Revision ID: tim@ed.am-20140405192006-fv0toip4b8m5pc88
fixed bug preventing skipping of subdirectories in walker; also added
walker.File.__str__()

Show diffs side-by-side

added added

removed removed

 
1
# deployment.py
 
2
#
 
3
# Copyright (C) 2013 to 2014 Tim Marston <tim@edm.am>
 
4
#
 
5
# This file is part of stdhome (hereafter referred to as "this program").
 
6
# See http://ed.am/dev/stdhome for more information.
 
7
#
 
8
# This program is free software: you can redistribute it and/or modify
 
9
# it under the terms of the GNU General Public License as published by
 
10
# the Free Software Foundation, either version 3 of the License, or
 
11
# (at your option) any later version.
 
12
#
 
13
# This program is distributed in the hope that it will be useful,
 
14
# but WITHOUT ANY WARRANTY; without even the implied warranty of
 
15
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 
16
# GNU General Public License for more details.
 
17
#
 
18
# You should have received a copy of the GNU General Public License
 
19
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
20
 
 
21
 
 
22
import os, re, shutil, filecmp, json
 
23
import the, util
 
24
from walker.copy_in import CopyInWalker
 
25
from walker.conflict import ConflictWalker
 
26
from walker.copy_out import CopyOutWalker
 
27
 
 
28
 
 
29
class Deployment:
 
30
 
 
31
 
 
32
        def __init__( self ):
 
33
                if the.repo is None:
 
34
                        raise RuntimeError( 'logic error: Deployment initialised when '
 
35
                                                                'the.repo is unset' )
 
36
                self.load_deployment_state()
 
37
                self.conflicts_checked = False
 
38
 
 
39
 
 
40
        def load_deployment_state( self ):
 
41
                """Load any deployment state.  If one is found then a deployment will be
 
42
                considered to be ongoing.
 
43
                """
 
44
 
 
45
                # list of files that were copied-in (or at least given the opportunity
 
46
        # to be) and updated through the vcs update.  This means that, while
 
47
        # there may have been conflicts during the update (which the user will
 
48
        # have to deal with in the repo), any conflicts arising with these files
 
49
        # in the home directory are no longer important and can be ignored.  In
 
50
        # short, this is a list of files that can safely be deployed, regardless
 
51
        # of the state of the home directory.
 
52
                self.deploy_files = None
 
53
 
 
54
                # list of files that were affected by a recent vcs update (so only these
 
55
                # files need to be checked for deployment conflicts or copied-out)
 
56
                self.affected_files = None
 
57
 
 
58
                # the revno that the repo was as prior to a recent update
 
59
                self.initial_revno = None
 
60
 
 
61
                # do we have a repo?
 
62
                if not os.path.exists( the.repo.full_dir ): return
 
63
 
 
64
                # if no file list exists, we're done
 
65
                file = os.path.join( the.full_mddir, "deploy.%s" % the.repo.name )
 
66
                if not os.path.isfile( file ):
 
67
                        return
 
68
 
 
69
                # read the file list
 
70
                if the.verbose: print "deployment state found; loading"
 
71
                f = open( file, 'r' )
 
72
                state = json.loads( f.read() )
 
73
 
 
74
                # unpack deployment state
 
75
                self.deploy_files = state['deploy_files'];
 
76
                self.initial_revno = state['initial_revno'];
 
77
                self.affected_files = state['affected_files'];
 
78
 
 
79
 
 
80
        def save_deployment_state( self ):
 
81
                """Save the current deployment state (so there will be a deployment ongoing).
 
82
                """
 
83
 
 
84
                if the.verbose: print "saving deployment state"
 
85
 
 
86
                # create metadata directory, as necessary
 
87
                if not os.path.isdir( the.full_mddir ):
 
88
                        os.mkdir( the.full_mddir )
 
89
 
 
90
                # pack deployment state
 
91
                state = {
 
92
                        'deploy_files': self.deploy_files,
 
93
                        'initial_revno': self.initial_revno,
 
94
                        'affected_files': self.affected_files,
 
95
                }
 
96
 
 
97
                # create file
 
98
                file = os.path.join( the.full_mddir, "deploy.%s" % the.repo.name )
 
99
                f = open( file, 'w' )
 
100
                f.write( json.dumps( state ) );
 
101
 
 
102
 
 
103
        def remove_deployment_state( self ):
 
104
                """Remove the current deployment state (so no deployment will be ongoing).
 
105
                """
 
106
 
 
107
                # check it exists
 
108
                file = os.path.join( the.full_mddir, "deploy.%s" % the.repo.name )
 
109
                if( os.path.isfile( file ) ):
 
110
 
 
111
                        # delete it
 
112
                        if the.verbose: print "removing deployment state"
 
113
                        os.unlink( file )
 
114
 
 
115
 
 
116
        def is_ongoing( self ):
 
117
                """Is there a deployment currently ongoing?
 
118
                """
 
119
 
 
120
                return False if self.deploy_files is None else True
 
121
 
 
122
 
 
123
        def check_ongoing( self, ongoing = True ):
 
124
                """Check that a deployment either is or is not ongoing and raise an error if
 
125
                not.
 
126
                """
 
127
 
 
128
                if( ongoing ):
 
129
                        if self.deploy_files is None:
 
130
                                raise self.DeploymentOngoing( False )
 
131
                else:
 
132
                        if self.deploy_files is not None:
 
133
                                raise self.DeploymentOngoing( True )
 
134
 
 
135
 
 
136
        def get_initial_revno( self ):
 
137
                """Get the initial revision identifier from the deployment state.
 
138
                """
 
139
 
 
140
                return self.initial_revno
 
141
 
 
142
 
 
143
        def copy_in( self, initial_revno = None ):
 
144
                """Copy-in changes from the home directory to the repository.  When finished,
 
145
                the state of deployment is saved, meaning that a deployment is ongoing.
 
146
                """
 
147
 
 
148
                # check we don't already have a file list
 
149
                self.check_ongoing( False )
 
150
 
 
151
                # if the repo doesn't exist, we have an empty file list
 
152
                if not os.path.exists( the.repo.full_dir ):
 
153
                        self.deploy_files = list()
 
154
                else:
 
155
                        # copy in
 
156
                        if the.verbose: print "importing files..."
 
157
                        walker = CopyInWalker()
 
158
                        walker.walk()
 
159
                        if( walker.changed ):
 
160
                                raise self.CopyInConflicts( walker.changed )
 
161
                        self.deploy_files = walker.walk_list
 
162
 
 
163
                        # obtain initial revno
 
164
                        self.initial_revno = the.repo.vcs.get_revno()
 
165
 
 
166
                # save state
 
167
                self.save_deployment_state()
 
168
 
 
169
 
 
170
        def get_conflicts( self, affected_files = None ):
 
171
                """Check to see if there are any delpoyment conflicts.  If a list of affected
 
172
                files is supplied, then only those files are checked (and they are also
 
173
                saved with the deployment state).  Otherwise, all files in the
 
174
                repository are checked.
 
175
                """
 
176
 
 
177
                # check we have a file list
 
178
                self.check_ongoing( True )
 
179
 
 
180
                # set updated files
 
181
                if affected_files is not None:
 
182
                        self.affected_files = affected_files
 
183
                        self.save_deployment_state()
 
184
 
 
185
                # check for deployment conflicts
 
186
                walker = ConflictWalker( self.deploy_files, self.affected_files )
 
187
                walker.walk()
 
188
 
 
189
                self.conflicts_checked = True
 
190
                return walker.changed + walker.obstructed
 
191
 
 
192
 
 
193
        def copy_out( self ):
 
194
                """Copy-out changed files frmo the repository to the home directory.  If the
 
195
                deployment state incudes a list of affected files, then only those fiels
 
196
                are copied-out.
 
197
                """
 
198
 
 
199
                # check we have a file list
 
200
                self.check_ongoing( True )
 
201
 
 
202
                # check that deployment conflicts have been checked-for
 
203
                if not self.conflicts_checked:
 
204
                        raise RuntimeError('logic error: deployment conflicts unchecked' )
 
205
 
 
206
                # copy out
 
207
                if the.verbose: print "exporting files..."
 
208
                walker = CopyOutWalker( self.affected_files )
 
209
                walker.walk()
 
210
 
 
211
                # clear state
 
212
                self.remove_deployment_state()
 
213
 
 
214
 
 
215
        class DeploymentOngoing( the.program.FatalError ):
 
216
 
 
217
                def __init__( self, ongoing ):
 
218
                        if( ongoing ):
 
219
                                self.msg = "there is an ongoing deployment"
 
220
                        else:
 
221
                                self.msg = "there is no ongoing deployment"
 
222
 
 
223
 
 
224
        class CopyInConflicts( the.program.FatalError ):
 
225
 
 
226
                def __init__( self, conflicts ):
 
227
                        self.conflicts = conflicts