1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
/** |
4
|
|
|
* Peforms rollback of a pipeline to a previous status. |
5
|
|
|
* |
6
|
|
|
* Note that this step would usually only be used in the special conditional rollback situation configured |
7
|
|
|
* on the Pipeline itself - see the Pipeline documentation for details. |
8
|
|
|
* |
9
|
|
|
* <code> |
10
|
|
|
* RollbackStep1: |
11
|
|
|
* Class: RollbackStep |
12
|
|
|
* RestoreDB: true |
13
|
|
|
* MaxDuration: 3600 |
14
|
|
|
* </code> |
15
|
|
|
* |
16
|
|
|
* @property string $Doing |
17
|
|
|
* |
18
|
|
|
* @property string $Title |
19
|
|
|
* |
20
|
|
|
* @method DNDeployment RollbackDeployment() The current rollback deployment |
21
|
|
|
* @property int $RollbackDeploymentID |
22
|
|
|
* @method DNDataTransfer RollbackDatabase() |
23
|
|
|
* @property int RollbackDatabaseID |
24
|
|
|
*/ |
25
|
|
|
class RollbackStep extends LongRunningPipelineStep { |
26
|
|
|
|
27
|
|
|
/** |
28
|
|
|
* @var array |
29
|
|
|
*/ |
30
|
|
|
private static $db = array( |
31
|
|
|
'Doing' => "Enum('Deployment,Snapshot,Queued', 'Queued')" |
32
|
|
|
); |
33
|
|
|
|
34
|
|
|
/** |
35
|
|
|
* @var array |
36
|
|
|
*/ |
37
|
|
|
private static $has_one = array( |
38
|
|
|
'RollbackDeployment' => 'DNDeployment', |
39
|
|
|
'RollbackDatabase' => 'DNDataTransfer' |
40
|
|
|
); |
41
|
|
|
|
42
|
|
|
/** |
43
|
|
|
* @return string |
44
|
|
|
*/ |
45
|
|
|
public function getTitle() { |
46
|
|
|
// Make sure the title includes the subtask |
47
|
|
|
return parent::getTitle() . ":{$this->Doing}"; |
48
|
|
|
} |
49
|
|
|
|
50
|
|
|
/** |
51
|
|
|
* @return bool|null |
52
|
|
|
*/ |
53
|
|
View Code Duplication |
public function start() { |
|
|
|
|
54
|
|
|
parent::start(); |
55
|
|
|
|
56
|
|
|
switch($this->Status) { |
57
|
|
|
case 'Started': |
|
|
|
|
58
|
|
|
// If we are doing a subtask, check which one to continue |
59
|
|
|
switch($this->Doing) { |
60
|
|
|
case 'Deployment': |
61
|
|
|
return $this->continueRevertDeploy(); |
62
|
|
|
case 'Snapshot': |
63
|
|
|
return $this->continueRevertDatabase(); |
64
|
|
|
default: |
65
|
|
|
$this->log("Unable to process {$this->Title} with subtask of {$this->Doing}"); |
66
|
|
|
$this->markFailed(); |
67
|
|
|
return false; |
68
|
|
|
} |
69
|
|
|
case 'Queued': |
70
|
|
|
// Begin rollback by initiating deployment |
71
|
|
|
return $this->startRevertDeploy(); |
72
|
|
|
default: |
73
|
|
|
$this->log("Unable to process {$this->Title} with status of {$this->Status}"); |
74
|
|
|
$this->markFailed(); |
75
|
|
|
return false; |
76
|
|
|
} |
77
|
|
|
} |
78
|
|
|
|
79
|
|
|
/** |
80
|
|
|
* Begin a new deployment |
81
|
|
|
* |
82
|
|
|
* @return boolean |
83
|
|
|
*/ |
84
|
|
|
protected function startRevertDeploy() { |
85
|
|
|
$this->Status = 'Started'; |
86
|
|
|
$this->Doing = 'Deployment'; |
87
|
|
|
$this->log("{$this->Title} starting revert deployment"); |
88
|
|
|
|
89
|
|
|
// Skip deployment for dry run |
90
|
|
|
if($this->Pipeline()->DryRun) { |
91
|
|
|
$this->log("[Skipped] Create DNDeployment"); |
92
|
|
|
$this->write(); |
93
|
|
|
return true; |
94
|
|
|
} |
95
|
|
|
|
96
|
|
|
// Get old deployment from pipeline |
97
|
|
|
$pipeline = $this->Pipeline(); |
98
|
|
|
$previous = $pipeline->PreviousDeployment(); |
99
|
|
|
if(empty($previous) || empty($previous->SHA)) { |
100
|
|
|
$this->log("No available SHA for {$this->Title}"); |
101
|
|
|
$this->markFailed(); |
102
|
|
|
return false; |
103
|
|
|
} |
104
|
|
|
|
105
|
|
|
// Initialise deployment |
106
|
|
|
$strategy = new DeploymentStrategy($pipeline->Environment(), array( |
107
|
|
|
'sha'=>$pipeline->SHA, |
108
|
|
|
// Leave the maintenance page up if we are restoring the DB |
109
|
|
|
'leaveMaintenancePage' => $this->doRestoreDB() |
110
|
|
|
)); |
111
|
|
|
$deployment = $strategy->createDeployment(); |
112
|
|
|
|
113
|
|
|
$deployment->DeployerID = $pipeline->AuthorID; |
114
|
|
|
$deployment->write(); |
115
|
|
|
$deployment->start(); |
116
|
|
|
$this->RollbackDeploymentID = $deployment->ID; |
117
|
|
|
$this->write(); |
118
|
|
|
|
119
|
|
|
return true; |
120
|
|
|
} |
121
|
|
|
|
122
|
|
|
/** |
123
|
|
|
* Create a snapshot of the db and store the ID on the Pipline |
124
|
|
|
* |
125
|
|
|
* @return bool True if success |
126
|
|
|
*/ |
127
|
|
|
protected function startRevertDatabase() { |
128
|
|
|
// Mark self as creating a snapshot |
129
|
|
|
$this->Status = 'Started'; |
130
|
|
|
$this->Doing = 'Snapshot'; |
131
|
|
|
$this->log("{$this->Title} reverting database from snapshot"); |
132
|
|
|
|
133
|
|
|
// Skip deployment for dry run |
134
|
|
|
if($this->Pipeline()->DryRun) { |
135
|
|
|
$this->write(); |
136
|
|
|
$this->log("[Skipped] Create DNDataTransfer restore"); |
137
|
|
|
return true; |
138
|
|
|
} |
139
|
|
|
|
140
|
|
|
// Get snapshot |
141
|
|
|
$pipeline = $this->Pipeline(); |
142
|
|
|
$backup = $pipeline->PreviousSnapshot(); |
143
|
|
|
if(empty($backup) || !$backup->exists()) { |
144
|
|
|
$this->log("No database to revert for {$this->Title}"); |
145
|
|
|
$this->markFailed(); |
146
|
|
|
return false; |
147
|
|
|
} |
148
|
|
|
|
149
|
|
|
// Create restore job |
150
|
|
|
$job = DNDataTransfer::create(); |
151
|
|
|
$job->EnvironmentID = $pipeline->EnvironmentID; |
152
|
|
|
$job->Direction = 'push'; |
153
|
|
|
$job->Mode = 'db'; |
154
|
|
|
$job->DataArchiveID = $backup->DataArchiveID; |
155
|
|
|
$job->AuthorID = $pipeline->AuthorID; |
156
|
|
|
$job->EnvironmentID = $pipeline->EnvironmentID; |
157
|
|
|
$job->write(); |
158
|
|
|
$job->start(); |
159
|
|
|
|
160
|
|
|
// Save rollback |
161
|
|
|
$this->RollbackDatabaseID = $job->ID; |
162
|
|
|
$this->write(); |
163
|
|
|
return true; |
164
|
|
|
} |
165
|
|
|
|
166
|
|
|
/** |
167
|
|
|
* Check status of current snapshot |
168
|
|
|
*/ |
169
|
|
View Code Duplication |
protected function continueRevertDatabase() { |
|
|
|
|
170
|
|
|
$this->log("Checking status of {$this->Title}..."); |
171
|
|
|
|
172
|
|
|
// Skip snapshot for dry run |
173
|
|
|
if($this->Pipeline()->DryRun) { |
174
|
|
|
$this->log("[Skipped] Checking progress of snapshot restore"); |
175
|
|
|
return $this->finish(); |
176
|
|
|
} |
177
|
|
|
|
178
|
|
|
// Get related snapshot |
179
|
|
|
$transfer = $this->RollbackDatabase(); |
180
|
|
|
if(empty($transfer) || !$transfer->exists()) { |
181
|
|
|
$this->log("Missing database transfer for in-progress {$this->Title}"); |
182
|
|
|
$this->markFailed(); |
183
|
|
|
return false; |
184
|
|
|
} |
185
|
|
|
|
186
|
|
|
// Check finished state |
187
|
|
|
$status = $transfer->ResqueStatus(); |
188
|
|
|
if($this->checkResqueStatus($status)) { |
189
|
|
|
// Re-enable the site by disabling the maintenance, since the DB restored successfully |
190
|
|
|
$this->Pipeline()->Environment()->disableMaintenance($this->Pipeline()->getLogger()); |
191
|
|
|
|
192
|
|
|
// After revert is complete we are done |
193
|
|
|
return $this->finish(); |
194
|
|
|
} |
195
|
|
|
} |
196
|
|
|
|
197
|
|
|
/** |
198
|
|
|
* Check status of deployment and finish task if complete, or fail if timedout |
199
|
|
|
* |
200
|
|
|
* @return boolean |
201
|
|
|
*/ |
202
|
|
|
protected function continueRevertDeploy() { |
203
|
|
|
$this->log("Checking status of {$this->Title}..."); |
204
|
|
|
|
205
|
|
|
// Skip deployment for dry run |
206
|
|
|
if($this->Pipeline()->DryRun) { |
207
|
|
|
$this->log("[Skipped] Checking progress of deployment"); |
208
|
|
|
if($this->getConfigSetting('RestoreDB')) { |
209
|
|
|
return $this->startRevertDatabase(); |
210
|
|
|
} else { |
211
|
|
|
$this->finish(); |
212
|
|
|
return true; |
213
|
|
|
} |
214
|
|
|
} |
215
|
|
|
|
216
|
|
|
// Get related deployment |
217
|
|
|
$deployment = $this->RollbackDeployment(); |
218
|
|
|
if(empty($deployment) || !$deployment->exists()) { |
219
|
|
|
$this->log("Missing deployment for in-progress {$this->Title}"); |
220
|
|
|
$this->markFailed(); |
221
|
|
|
return false; |
222
|
|
|
} |
223
|
|
|
|
224
|
|
|
// Check finished state |
225
|
|
|
$status = $deployment->ResqueStatus(); |
226
|
|
|
if($this->checkResqueStatus($status)) { |
227
|
|
|
// Since deployment is finished, check if we should also do a db restoration |
228
|
|
|
if($this->doRestoreDB()) { |
229
|
|
|
return $this->startRevertDatabase(); |
230
|
|
|
} else { |
231
|
|
|
$this->finish(); |
232
|
|
|
} |
233
|
|
|
} |
234
|
|
|
return !$this->isFailed(); |
235
|
|
|
} |
236
|
|
|
|
237
|
|
|
/** |
238
|
|
|
* Check if we are intending to restore the DB after this deployment |
239
|
|
|
* |
240
|
|
|
* @return boolean |
241
|
|
|
*/ |
242
|
|
|
protected function doRestoreDB() { |
243
|
|
|
return $this->getConfigSetting('RestoreDB') && $this->Pipeline()->PreviousSnapshot(); |
244
|
|
|
} |
245
|
|
|
|
246
|
|
|
/** |
247
|
|
|
* Check the status of a resque sub-task |
248
|
|
|
* |
249
|
|
|
* @param string $status Resque task status |
250
|
|
|
* @return boolean True if the task is finished successfully |
251
|
|
|
*/ |
252
|
|
View Code Duplication |
protected function checkResqueStatus($status) { |
|
|
|
|
253
|
|
|
switch($status) { |
254
|
|
|
case "Complete": |
255
|
|
|
return true; |
256
|
|
|
case "Failed": |
257
|
|
|
case "Invalid": |
258
|
|
|
$this->log("{$this->Title} failed with task status $status"); |
259
|
|
|
$this->markFailed(); |
260
|
|
|
return false; |
261
|
|
|
case "Queued": |
262
|
|
|
case "Running": |
263
|
|
|
default: |
264
|
|
|
// For running or queued tasks ensure that we have not exceeded |
265
|
|
|
// a reasonable time-elapsed to consider this job inactive |
266
|
|
|
if($this->isTimedOut()) { |
267
|
|
|
$this->log("{$this->Title} took longer than {$this->MaxDuration} seconds to run and has timed out"); |
268
|
|
|
$this->markFailed(); |
269
|
|
|
return false; |
270
|
|
|
} else { |
271
|
|
|
// While still running report no error, waiting for resque job to eventually finish |
272
|
|
|
// some time in the future |
273
|
|
|
$this->log("{$this->Title} is still in progress"); |
274
|
|
|
return false; |
275
|
|
|
} |
276
|
|
|
} |
277
|
|
|
} |
278
|
|
|
} |
279
|
|
|
|
Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.
You can also find more detailed suggestions in the “Code” section of your repository.