1: <?php
2:
3: namespace Himedia\Padocc\Task\Base;
4:
5: use GAubry\Shell\PathStatus;
6: use Himedia\Padocc\AttributeProperties;
7: use Himedia\Padocc\Task\Extended\SwitchSymlink;
8:
9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25: 26: 27: 28: 29: 30: 31: 32: 33: 34: 35: 36:
37: class Environment extends Target
38: {
39:
40: 41: 42: 43: 44: 45: 46:
47: private static $aSmartyRsyncExclude = array('smarty/templates_c', 'smarty/*/wrt*', 'smarty/**/wrt*');
48:
49: 50: 51: 52:
53: const SERVERS_CONCERNED_WITH_BASE_DIR = 'SERVERS_CONCERNED_WITH_BASE_DIR';
54:
55: 56: 57:
58: protected function init()
59: {
60: parent::init();
61:
62: $this->aAttrProperties = array_merge(
63: $this->aAttrProperties,
64: array(
65: 'name' => AttributeProperties::REQUIRED,
66: 'mailto' => AttributeProperties::EMAIL | AttributeProperties::MULTI_VALUED,
67: 'withsymlinks' => AttributeProperties::BOOLEAN,
68: 'basedir' => AttributeProperties::DIR | AttributeProperties::REQUIRED
69: )
70: );
71:
72:
73: $sBaseDir = (empty($this->aAttValues['basedir']) ? '[setUp() will failed]' : $this->aAttValues['basedir']);
74: $this->oProperties->setProperty('basedir', $sBaseDir);
75: $sWithSymlinks = (empty($this->aAttValues['withsymlinks']) ? 'false' : $this->aAttValues['withsymlinks']);
76: $this->oProperties->setProperty('with_symlinks', $sWithSymlinks);
77:
78: $this->addSwithSymlinkTask();
79: }
80:
81: 82: 83: 84:
85: public static function getTagName ()
86: {
87: return 'env';
88: }
89:
90: 91: 92: 93:
94: private function addSwithSymlinkTask ()
95: {
96: if (SwitchSymlink::getNbInstances() === 0
97: && $this->oProperties->getProperty('with_symlinks') === 'true'
98: ) {
99: $this->oNumbering->addCounterDivision();
100: $oLinkTask = SwitchSymlink::getNewInstance(
101: array(),
102: $this->oProject,
103: $this->oDIContainer
104: );
105: array_push($this->aTasks, $oLinkTask);
106: $this->oNumbering->removeCounterDivision();
107: }
108: }
109:
110: 111: 112: 113: 114: 115: 116: 117: 118: 119: 120:
121: public function check ()
122: {
123: parent::check();
124: if ($this->aAttValues['basedir'][0] !== '/') {
125: throw new \DomainException("Attribute 'basedir' must begin by a '/'!");
126: }
127:
128: $aMsg = array();
129: foreach ($this->aAttValues as $sAttribute => $sValue) {
130: if (! empty($sValue) && $sAttribute !== 'name') {
131: $aMsg[] = "Attribute: $sAttribute = '$sValue'";
132: }
133: }
134: if (count($aMsg) > 0) {
135: $this->getLogger()->info('+++' . implode("\n", $aMsg) . '---');
136: }
137: }
138:
139: 140: 141: 142:
143: private function analyzeRegisteredPaths ()
144: {
145: $aPathsToHandle = array();
146: $aPaths = array_keys(self::$aRegisteredPaths);
147:
148: $sBaseSymLink = $this->oProperties->getProperty('basedir');
149: foreach ($aPaths as $sPath) {
150: $aExpandedPaths = $this->expandPath($sPath);
151: foreach ($aExpandedPaths as $sExpandedPath) {
152: list($bIsRemote, $sServer, $sRealPath) = $this->oShell->isRemotePath($sExpandedPath);
153: if ($bIsRemote && strpos($sRealPath, $sBaseSymLink) !== false) {
154: $aPathsToHandle[$sServer][] = $sRealPath;
155: }
156: }
157: }
158:
159: $aServersWithSymlinks = array_keys($aPathsToHandle);
160: if (count($aServersWithSymlinks) > 0) {
161: sort($aServersWithSymlinks);
162: $sMsg = "Servers concerned with base directory (#"
163: . count($aServersWithSymlinks) . "): '" . implode("', '", $aServersWithSymlinks) . "'.";
164: } else {
165: $sMsg = 'No server concerned with base directory.';
166: }
167: $this->getLogger()->info($sMsg);
168: $this->oProperties->setProperty(self::SERVERS_CONCERNED_WITH_BASE_DIR, implode(' ', $aServersWithSymlinks));
169: }
170:
171: 172: 173:
174: private function makeTransitionToSymlinks ()
175: {
176: $this->getLogger()->info('If needed, make transition to symlinks:+++');
177: $sBaseSymLink = $this->oProperties->getProperty('basedir');
178: $aServers = $this->expandPath('${' . self::SERVERS_CONCERNED_WITH_BASE_DIR . '}');
179: $bTransitionMade = false;
180:
181: $aPathStatusResult = $this->oShell->getParallelSSHPathStatus($sBaseSymLink, $aServers);
182: foreach ($aServers as $sServer) {
183: $sExpandedPath = $sServer . ':' . $sBaseSymLink;
184: if ($aPathStatusResult[$sServer] === PathStatus::STATUS_DIR) {
185: $bTransitionMade = true;
186: $sDir = $sExpandedPath . '/';
187: $sOriginRelease = $sServer . ':' . $sBaseSymLink . $this->aConfig['symlink_releases_dir_suffix']
188: . '/' . $this->oProperties->getProperty('execution_id') . '_origin';
189: $this->getLogger()->info("Backup '$sDir' to '$sOriginRelease'.+++");
190: $this->oShell->sync($sDir, $sOriginRelease, array(), array(), self::$aSmartyRsyncExclude);
191: $this->oShell->remove($sExpandedPath);
192: $this->oShell->createLink($sExpandedPath, $sOriginRelease);
193: $this->getLogger()->info('---');
194: }
195: }
196: if (! $bTransitionMade) {
197: $this->getLogger()->info('No transition.');
198: }
199: $this->getLogger()->info('---');
200: }
201:
202: 203: 204:
205: private function makeTransitionFromSymlinks ()
206: {
207: $this->getLogger()->info('If needed, make transition from symlinks:+++');
208: $sBaseSymLink = $this->oProperties->getProperty('basedir');
209: $sPath = '${' . self::SERVERS_CONCERNED_WITH_BASE_DIR . '}:' . $sBaseSymLink;
210: $bTransitionMade = false;
211: foreach ($this->expandPath($sPath) as $sExpandedPath) {
212: if ($this->oShell->getPathStatus($sExpandedPath) === PathStatus::STATUS_SYMLINKED_DIR) {
213: $bTransitionMade = true;
214: list(, , $sRealPath) = $this->oShell->isRemotePath($sExpandedPath);
215: $sDir = $sExpandedPath . '/';
216: $sTmpDest = $sExpandedPath . '_tmp';
217: $sMsg = "Remove symlink on '$sExpandedPath' base directory"
218: . " and initialize it with last release's content.";
219: $this->getLogger()->info($sMsg);
220: $this->oShell->sync($sDir, $sTmpDest, array(), array(), self::$aSmartyRsyncExclude);
221: $this->oShell->remove($sExpandedPath);
222: $this->oShell->execSSH("mv %s '" . $sRealPath . "'", $sTmpDest);
223: }
224: }
225: if (! $bTransitionMade) {
226: $this->getLogger()->info('No transition.');
227: }
228: $this->getLogger()->info('---');
229: }
230:
231: 232: 233:
234: private function initNewRelease ()
235: {
236: $this->getLogger()->info('Initialize with content of previous release:+++');
237: $sBaseSymLink = $this->oProperties->getProperty('basedir');
238: $aServers = $this->expandPath('${' . self::SERVERS_CONCERNED_WITH_BASE_DIR . '}');
239: $sReleaseSymLink = $sBaseSymLink . $this->aConfig['symlink_releases_dir_suffix']
240: . '/' . $this->oProperties->getProperty('execution_id');
241: $aPathStatusResult = $this->oShell->getParallelSSHPathStatus($sBaseSymLink, $aServers);
242:
243:
244: $aServersToInit = array();
245: foreach ($aServers as $sServer) {
246: if ($aPathStatusResult[$sServer] == PathStatus::STATUS_SYMLINKED_DIR) {
247: $aServersToInit[] = $sServer;
248: } else {
249: $this->getLogger()->info("No previous release to initialize '$sServer:$sReleaseSymLink'.");
250: }
251: }
252:
253:
254: if (count($aServersToInit) > 0) {
255: $aResults = $this->oShell->sync(
256: "[]:$sBaseSymLink/",
257: '[]:' . $sReleaseSymLink,
258: $aServersToInit,
259: array(),
260: self::$aSmartyRsyncExclude
261: );
262: foreach ($aResults as $sResult) {
263: $this->getLogger()->info($sResult);
264: }
265: }
266:
267: $this->getLogger()->info('---');
268: }
269:
270: 271: 272: 273: 274: 275: 276: 277:
278: private function getAllReleases ($sExpandedPath, array $aServers)
279: {
280: $sPattern = '^[0-9]{14}_[0-9]{5}(_origin)?$';
281: $sCmd = "if [ -d %1\$s ] && ls -1 %1\$s | grep -qE '$sPattern'; "
282: . "then ls -1 %1\$s | grep -E '$sPattern'; fi";
283: $sSSHCmd = $this->oShell->buildSSHCmd($sCmd, '[]:' . $sExpandedPath);
284: $aParallelResult = $this->oShell->parallelize(
285: $aServers,
286: $sSSHCmd,
287: $this->aConfig['parallelization_max_nb_processes']
288: );
289:
290: $aAllReleases = array();
291: foreach ($aParallelResult as $aServerResult) {
292: $sServer = $aServerResult['value'];
293: $aReleases = explode("\n", trim($aServerResult['output']));
294: sort($aReleases);
295: $aAllReleases[$sServer] = array_reverse($aReleases);
296: }
297: return $aAllReleases;
298: }
299:
300: 301: 302:
303: private function removeOldestReleases ()
304: {
305: $this->getLogger()->info('Remove too old releases:+++');
306:
307: if ($this->oProperties->getProperty(self::SERVERS_CONCERNED_WITH_BASE_DIR) == '') {
308: $this->getLogger()->info('No release found.');
309: } else {
310:
311:
312: $sBaseSymLink = $this->oProperties->getProperty('basedir') . $this->aConfig['symlink_releases_dir_suffix'];
313: $aServers = $this->expandPath('${' . self::SERVERS_CONCERNED_WITH_BASE_DIR . '}');
314: $this->getLogger()->info('Check releases on each server.+++');
315: $aAllReleases = $this->getAllReleases($sBaseSymLink, $aServers);
316: $this->getLogger()->info('---');
317:
318:
319: $aAllReleasesToDelete = array();
320: foreach ($aAllReleases as $sServer => $aReleases) {
321: $iNbReleases = count($aReleases);
322: if ($iNbReleases === 0) {
323: $this->getLogger()->info("No release found on server '$sServer'.");
324: } else {
325: $bIsQuotaExceeded = ($iNbReleases > $this->aConfig['symlink_max_nb_releases']);
326: $sMsg = $iNbReleases . " release(s) found on server '$sServer': quota "
327: . ($bIsQuotaExceeded ? 'exceeded' : 'not exceeded')
328: . ' (' . $this->aConfig['symlink_max_nb_releases'] . ' backups max).';
329: $this->getLogger()->info($sMsg);
330:
331: if ($bIsQuotaExceeded) {
332: $aReleasesToDelete = array_slice($aReleases, $this->aConfig['symlink_max_nb_releases']);
333: foreach ($aReleasesToDelete as $sReleaseToDelete) {
334: $aAllReleasesToDelete[$sReleaseToDelete][] = $sServer;
335: }
336: }
337: }
338: }
339:
340:
341: foreach ($aAllReleasesToDelete as $sRelease => $aServers) {
342: if (! empty($sRelease)) {
343: $sMsg = "Remove release '$sRelease' on following server(s): " . implode(', ', $aServers) . '.';
344: $this->getLogger()->info($sMsg);
345: $sPath = "[]:$sBaseSymLink/$sRelease";
346: $sSSHCmd = $this->oShell->buildSSHCmd('rm -rf %s', $sPath);
347: $this->oShell->parallelize($aServers, $sSSHCmd, $this->aConfig['parallelization_max_nb_processes']);
348: }
349: }
350:
351: }
352: $this->getLogger()->info('---');
353: }
354:
355: 356: 357: 358: 359:
360: private function removeUnnecessaryTasksForRollback ()
361: {
362: if ($this->oProperties->getProperty('rollback_id') !== '') {
363: $this->getLogger()->info('Remove unnecessary tasks for rollback.');
364: $aKeptTasks = array();
365: foreach ($this->aTasks as $oTask) {
366: if (($oTask instanceof Property)
367: || ($oTask instanceof ExternalProperty)
368: || ($oTask instanceof SwitchSymlink)
369: ) {
370: $aKeptTasks[] = $oTask;
371: }
372: }
373: $this->aTasks = $aKeptTasks;
374: }
375: }
376:
377: 378: 379: 380: 381: 382:
383: protected function preExecute ()
384: {
385: parent::preExecute();
386: $this->getLogger()->info('+++');
387:
388:
389: $this->removeUnnecessaryTasksForRollback();
390:
391:
392:
393: $oTask = reset($this->aTasks);
394: while (($oTask instanceof Property) || ($oTask instanceof ExternalProperty)) {
395: $oTask->execute();
396: array_shift($this->aTasks);
397: $oTask = reset($this->aTasks);
398: }
399:
400:
401: $this->analyzeRegisteredPaths();
402: if ($this->oProperties->getProperty('with_symlinks') === 'true') {
403: $this->oProperties->setProperty('with_symlinks', 'false');
404: if ($this->oProperties->getProperty('rollback_id') === '') {
405: $this->makeTransitionToSymlinks();
406: $this->initNewRelease();
407: $this->removeOldestReleases();
408: }
409: $this->oProperties->setProperty('with_symlinks', 'true');
410: } else {
411: $this->makeTransitionFromSymlinks();
412: }
413: $this->getLogger()->info('---');
414: }
415: }
416: