Overview

Namespaces

  • GAubry
    • ErrorHandler
    • Helpers
    • Logger
    • Shell
  • Himedia
    • Padocc
      • DB
      • Minifier
      • Numbering
      • Properties
      • Task
        • Base
        • Extended
  • None
  • Psr
    • Log

Classes

  • Backup
  • Call
  • Composer
  • Copy
  • Environment
  • ExternalProperty
  • FillTemplate
  • HTTP
  • Link
  • MkDir
  • Project
  • Property
  • Rename
  • Sync
  • Target
  • Overview
  • Namespace
  • Class
  • Tree
  • Deprecated
  • Todo
  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:  * Sous-division d'une tâche projet, décrit ce qu'est un déploiement pour un environnement donné.
 11:  *
 12:  * Attributs :
 13:  * - 'name' : nom de l'environnement à préciser lors ddu déploiement
 14:  * - 'mailto' : liste d'adresses email séparées par une virgule à qui adresser le mail de fin de déploiement
 15:  *   en plus de l'instigateur
 16:  * - 'withsymlinks' : activer ou non la gestion de déploiement par lien symbolique (false par défaut)
 17:  * - 'basedir' : répertoire de base, accessible par la propriété ${BASEDIR}.
 18:  *   Tous les répertoires mentionnés dans le XML, quel que soit le serveur, et qui se trouvent à l'intérieur
 19:  *   dans celui-ci, seront automatiquement redirigés en cas de withsymlinks=“true”.
 20:  * - 'loadtwengaservers' : charger ou non les alias de serveurs définis dans master_synchro.cfg
 21:  *   afin qu'ils soient accessibles en tant que propriété. Par exemple si master_synchro.cfg contient
 22:  *   SERVER_QA_PHPWEB_DC_EU1="www17.eu1"
 23:  *   SERVER_QA_PHPWEB_DC_US1="www-07.us1"
 24:  *   SERVER_QA_PHPWEB_ALL="$SERVER_QA_PHPWEB_DC_EU1 $SERVER_QA_PHPWEB_DC_US1"
 25:  *   alors la propriété ${SERVER_QA_PHPWEB_ALL} permettra d'adresser ces 2 serveurs.
 26:  *
 27:  * Exemple :
 28:  * <env name="prod"
 29:  *   mailto="devaa@twenga.com, qateam@twenga.com, herve.gouchet@twenga.com, sysops@twenga.com"
 30:  *   withsymlinks="true"
 31:  *   loadtwengaservers="true"
 32:  *   basedir="/home/httpd/www.twenga"
 33:  * >...</env>
 34:  *
 35:  * @author Geoffroy AUBRY <gaubry@hi-media.com>
 36:  */
 37: class Environment extends Target
 38: {
 39: 
 40:     /**
 41:      * Liste d'exclusions Smarty pour les rsync réalisés lors de l'initialisation des déploiements.
 42:      * @var array
 43:      * @see makeTransitionToSymlinks()
 44:      * @see makeTransitionFromSymlinks()
 45:      * @see initNewRelease()
 46:      */
 47:     private static $aSmartyRsyncExclude = array('smarty/templates_c', 'smarty/*/wrt*', 'smarty/**/wrt*');
 48: 
 49:     /**
 50:      * Propriété (au sens PropertiesInterface) contenant la liste des serveurs concernés par le déploiement.
 51:      * @var string
 52:      */
 53:     const SERVERS_CONCERNED_WITH_BASE_DIR = 'SERVERS_CONCERNED_WITH_BASE_DIR';
 54: 
 55:     /**
 56:      * {@inheritdoc}
 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:         // Positionnement des 2 propriétés basedir et withsymlinks :
 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:      * {@inheritdoc}
 83:      * @codeCoverageIgnore
 84:      */
 85:     public static function getTagName ()
 86:     {
 87:         return 'env';
 88:     }
 89: 
 90:     /**
 91:      * Ajoute une tâche SwitchSymlink en toute dernière étape de déploiement
 92:      * si le XML du projet n'en a pas spécifié.
 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:      * Vérifie au moyen de tests basiques que la tâche peut être exécutée.
112:      * Lance une exception si tel n'est pas le cas.
113:      *
114:      * Comme toute les tâches sont vérifiées avant que la première ne soit exécutée,
115:      * doit permettre de remonter au plus tôt tout dysfonctionnement.
116:      * Appelé avant la méthode execute().
117:      *
118:      * @throws \UnexpectedValueException en cas d'attribut ou fichier manquant
119:      * @throws \DomainException en cas de valeur non permise
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:      * Extrait la liste des serveurs concernés par le déploiement à partir de self::$aRegisteredPaths
141:      * et l'enregistre dans la propriété self::SERVERS_CONCERNED_WITH_BASE_DIR.
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:      * Gère la transition d'un déploiement sans stratégie de liens symboliques vers cette stratégie.
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:      * Gère la transition d'un déploiement avec stratégie de liens symboliques vers une approche sans.
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:      * Initialise la nouvelle release avec le contenu de l'ancienne, dans le but d'accélerer le déploiement.
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:         // Recherche des serveurs que l'on peut initialiser :
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:         // Initialisation de ces serveurs :
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:      * Retourne la liste triée chronologiquement des différentes releases présentes à l'endroit spécifié.
272:      *
273:      * @param string $sExpandedPath chemin sans serveur
274:      * @param array $aServers liste de serveurs au format [user@]servername_or_ip
275:      * @return array tableau associatif "sServer" => aReleases,
276:      * où aReleases est la liste des releases du serveur associé, de la plus jeune à la plus vieille.
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:      * Supprime les vieilles releases surnuméraires sur chaque serveur concerné par le déploiement.
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:             // Check releases:
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:             // Identification des releases à supprimer :
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:             // Suppression des releases surnuméraires les plus vieilles :
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:      * Supprime les tâches qui ne sont plus nécessaires pour le rollback.
357:      *
358:      * @see $this->aTasks
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:      * Phase de pré-traitements de l'exécution de la tâche.
379:      * Elle devrait systématiquement commencer par "parent::preExecute();".
380:      * Appelé par execute().
381:      * @see execute()
382:      */
383:     protected function preExecute ()
384:     {
385:         parent::preExecute();
386:         $this->getLogger()->info('+++');
387: 
388:         // Supprime les tâches qui ne sont plus nécessaires pour le rollback :
389:         $this->removeUnnecessaryTasksForRollback();
390: 
391:         // Exécute tout de suite toutes les tâches Property ou ExternalProperty qui
392:         // suivent directement :
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:         // Déduit les serveurs concernés par ce déploiement et prépare le terrain :
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: 
Platform for Automatized Deployments with pOwerful Concise Configuration API documentation generated by ApiGen 2.8.0