Wer als Drupal-Entwickler seinen Workflow automatisieren will, kommt an Drush nicht vorbei. Das Consolen-Tool ist das Schweizer Messer für den etwas erfahrereren Drupal-Entwickler. Anfänger versuchen es gerne ohne und das ist eine Zeitlang auch ganz in Ordnung so. Vor allem die Mausschubser-Fraktion - zu denen die Entwickler unter Windows oft gehören - vermeidet den Einsatz dieses etwas sperrigen Tools, solange es geht.
Aber spätestens, wenn der Workflow automatisiert werden muss, lernt man Drush zu schätzen. Es erledigt komplexe Aufgaben für die Wartung und Instandhaltung, wie z.B. die Erstellung von kompletten Backups inklusive Datenbank genauso, wie kleinere Konfigurationsaufgaben, z.B. das Aktivieren oder das Deinstallieren von Modulen.
Da ein paar Eigenwilligkeiten bei der Installation sowohl lokal als auch remote zu berücksichtigen sind, werden wir diese ausgehend von Windows als lokales System darstellen.
Hier wird jetzt gezeigt, wie Drush mit Grunt zusammenarbeitet. Dies hat auch den Vorteil, dass man mit den Grundbefehlen von Drush auskommt. Einige Features von Drush setzen auf eigene SSH-Mechanismen bzw. Fremd-Tools, die wir in unserem Tool-Stack gar nicht verwenden. Wer Drush zusammen mit Grunt einsetzt, kommt mit stabilen älteren Versionen von Drush gut aus (z.B. Version 5.9).
Einen guten Überblick über die Möglichkeiten von Drush bekommt man, wenn man die Erläuterungen der Kommandos auf dieser Seite einmal kurz überfliegt. Ein paar Kommandos, für die ich mir für den täglichen Gebrauch einen Spickzettel gemacht habe, sind im Folgenden aufgelistet (Dies ist mein Spickzettel :-) ). Alles andere muss ich ausführlicher nachschlagen:
- drush version,
- drush status,
- drush archive-dump @sites --destination=C:\work\last_local_drupal.tar.gz,
- drush up drupal (aktualisiert auf letzte Drupal-Version, sehr nützlich, weil der sites-Ordner davon unberücksichtigt bleibt. Achtung: .htaccess und robots.txt aber vorher retten),
- drush -v sql-dump --result-file=C:\work\drupal_sql_dump.sql,
- drush @sites updatedb (entspricht update.php),
- drush cc all (Clear Cache),
- drush @sites en views, drush @sites dis service (Enablen, Disablen von Modulen),
- drush @sites pm-uninstall service (Deinstallieren von Modulen).
Die Drush-Kommandos sollen nach Aussage der Entwickler am besten in der Git-Shell funktionieren. Git haben wir ja schon installiert, weil es für die Installation von Grunt bei unserer Vorgehensweise vorausgesetzt wurde. Ich benutzt meistens die normale Windows-Console.
Installation von Drush lokal
Für Windows bietet sich für unsere Zwecke zum aktuellen Zeitpunkt die Installation von Version 6 mit dem Windows-Installer an, die hier völlig ausreicht. Bei der Installation sind ein paar Eigenwilligkeiten zu beachten.
Man sollte nicht die Standard-Installationseinstellungen nehmen, sondern zusätzlich die Option: Register Environmental Variables aktivieren während des Installationsdialogs (vgl. Abb. 1).
Abb.1: Auswahl von Register Environmental Variables während der Installation von Drush
Interessant ist übrigens, dass wir cwRsync und das Windows Remote Management nicht auswählen. Dies sind genau die Komponenten von Drush, die wir aufgrund der Zusammenarbeit mit Grunt in der hier vorgestellten Vorgehensweise nicht benötigen. Allerdings ist für unsere Vorgehensweise eine zusätzliche Installation von Drush auf dem Remote-Server erforderlich, was weiter unten beschrieben wird.
Wir können jetzt in einer Console testen, ob unser Drush funktioniert. Wir sollten eine vernünftige Ausgabe bekommen, wenn wir drush --version eingeben:
....>drush --version Drush Version : 6.0
Wenn nicht, dann müssen wir unsere Umgebungsvariable PATH noch anpassen. Wenn wir z.B. mit XAMPP arbeiten, bei dem ja keine Daten in der Windows-Registierung gespeichert werden, kennt die Drush-Installation vielleicht nicht den Pfad zu Mysql. Deshalb ergänzen wir PATH, indem wir folgendes anfügen:
- ;C:\\xampp\mysql\bin
Den ultimativen Test hat Drush aber damit noch nicht bestanden. Eine der besten Kommandos von Drush ist die Erstellung eines komprimierten Backups einer kompletten Webseite inklusive der Datenbank. Dazu öffnet man im Root-Verzeichnis einer lokalen Drupal-Installation eine Console und gibt folgendes ein:
drush archive-dump @sites -v
Das @sites kann man weglassen, wenn man keine Multi-Site-Installation besitzt. Unter Windows wird dann das Archiv mit Core, installierten Modulen, Themes und kompletter Datenbank gespeichert in zum Beispiel:
- C:\Users\<user>\drush-backups\archive-dump\20141122155551\multiple_sites.20141122_155637.tar.gz
Leider bricht der Dump aber zunächst mit folgender Meldung innerhalb seiner umfangreichen Ausgaben ab mit:
..... drush database dump failed ! ....
Der Datenbank-Dump funktioniert mit Drush unter Windows nur, wenn man noch folgendes beachtet:
In allen Datenbank-Deklarationen in den settings.php's der Drupal-Site, z.B. in sites/default/settings.php, sollte man für host nicht localhost setzen, sondern:
... 'host' => '127.0.0.1', ...
Drush kommuniziert mit der Datenbank über eine Socket-Verbindung. Der vorhandene Treiber akzeptiert offensichtlich nur 127.0.0.1 und nicht localhost. Wer das Problem näher untersuchen will, dem sei diese Diskussion empfohlen.
Nochmal zusammengefasst:
- Windows-Installer für Drush verwenden mit zusätzlicher Optionsauswahl: Register Environmental Variables (s. Abb. 1),
- In der Windows-Umgebungsvariable PATH Folgendes anfügen: ;C:\\xampp\mysql\bin,
- In allen settings.php des lokalen Drupal-Portals allen host's, denen localhost zugeordnet wurde, 127.0.0.1 zuweisen,
- Apache- und Mysql-Server müssen gestartet sein (vergisst man leicht bei XAMPP).
Wenn einem jetzt die fehlerfreie Erstellung eines Portal-Archivs gelingt mit
- drush ard -v,
dann hat man erfolgreich sein Drush unter Windows installiert.
Beispiel für Grunt & Drush lokal
Wir wollen hier eine naheliegende Aufgabe mit Grunt automatisieren, nämlich die Erstellung eines gepackten Backups unserer lokalen Drupal-Seite mit Hilfe des Drush-Kommandos drush archive-dump.
Wir benötigen nur das grunt-shell-Plugin von Grunt. Das Grundgerüst für Automatisierungsaufgaben kennen wir aus vorhergehenden Blogbeiträgen und beschränken uns im Folgenden nur auf die notwendigsten Snippets:
... grunt.initConfig({ pkg : grunt.file.readJSON('package.json'), ... util: require('./grunt-helpers.js')(grunt), ... shell : { ..., "localBackup" : { command : [ 'echo localBackup started at: <%= util.timestamp() %>', 'drush archive-dump @sites --destination=E:\\Backups_Grunt\\local\\drupal_dump_<%= util.timestamp() %>.tar.gz', 'echo localBackup stopped at: <%= util.timestamp() %>', ].join('&&'), options: { stderr: false, execOptions: { cwd: '<%= pkg.projectPath %>\\..' } } }, ... } } grunt.loadNpmTasks('grunt-shell'); ... grunt.registerTask('localBackup', ['shell:localBackup']); ...
Wir verwenden das Drush-Kommando als Shell-Kommando von grunt-shell. Die angegebenen Pfade werden im Beispiel spezifiziert entweder direkt wie hier z.B. der Pfad zum Backup-Archiv oder z.B. in package.json wie hier der Pfad für das current working directory (cwd). Dies muss jeder an seine eigenen Bedürfnisse anpassen.
Hier wird das Skript nicht im Root-Verzeichnis der Drupal-Installation aufgerufen, sondern im übergeordneten Verzeichnis. Der Grund ist der, dass bei einer Drupal-Installation im Rootverzeichnis möglichst keine zusätzlichen Dateien vorhanden sein sollten, da hierdurch das Upgraden schwieriger wird. Ich halte es jedenfalls so, dass ich bei einer Drupal-Installation das Rootverzeichnis clean halte und eigene Sourcen nur unterhalb des sites-Ordners hinzufüge. Folgerichtig ist im Beispiel mein Projektpfad auch dieser sites-Ordner und nicht das Rootverzeichnis meiner Drupal-Installation.
Wir benötigen für die Speicherung des Archivs mit einem Zeitstempel eine Funktion timestamp(), die wir in die Hilfsdatei grunt-helpers.js ausgelagern zusammen mit wenigen weiteren nützlichen Funktionen, die wir aber in diesem Beispiel nicht benötigen.
// File: grunt-helpers.js module.exports = function (grunt) { currentTask = grunt.task.current; // needed for '_replaceAll' function escapeRegExp(string) { return string.replace(/([.*+?^=!:${}()|\[\]\/\\])/g, "\\$1"); } function _replaceAll(string, find, replace) { return string.replace(new RegExp(escapeRegExp(find), 'g'), replace); } // e.g.: var S = new Object(); // S.val='Berlin-Wedding'; // P=shift_word('-', S); // => P='Berlin', S.val='Wedding' function _shiftWord(Separator, S){ var Q = ''; var P = S.val.indexOf(Separator, S.val); var L = Separator.length; if (P==-1) { Q = S.val; S.val = ''; return Q; } if (P==0) { S.val = S.val.substr(L); return ''; } Q = S.val.substr(0,P); S.val = S.val.substr(P+L); return Q; } function _timestamp() { var now = new Date(); var date = [ now.getFullYear(), now.getMonth() + 1, now.getDate()]; var time = [ now.getHours(), now.getMinutes(), now.getSeconds() ]; for ( var i = 1; i < 3; i++ ) { if ( time[i] < 10 ) { time[i] = "0" + time[i]; } } for ( var i = 1; i < 3; i++ ) { if ( date[i] < 10 ) { date[i] = "0" + date[i]; } } return date.join("") + "-" + time.join(""); } return { replaceAll: function(aPath, find, replace) { return _replaceAll(aPath, find, replace); }, shiftWord: function(Separator, S) { return _shiftWord(Separator, S); }, timestamp: function(){ return _timestamp(); } } }
Man beachte die Art und Weise, wie das Helper-Skript in das Gruntscript eingebunden wird und wie die Funktion timestamp() für die Benennung des Archivs mit Datum im Namen eingesetzt wird.
Hier ein paar weitere Ideen für kleine Automatisierungsaufgaben mit Drush und Grunt für den lokalen Workflow:
- Upgraden auf die letzte Drupal-Version (drush up drupal , drush updatedb),
- Installieren einer frischen (latest stable) Drupal-Version (drush dl drupal),
- Automatisches Installieren der eigenen Lieblingsmodule.
Installation von Drush remote
Die Installation von Drush auf dem Remote-Server ist natürlich immer etwas abhängig vom Hoster. Ich habe meine Drupal-Seiten bei domain factory gehostet und zahle für einen ManagedHosting Pro-Tarif mit mehr Speicherplatz als der reine ManagedHosting Tarif weniger als 15 € im Monat. Für das Geld geht sehr viel, aber nicht alles, z.B. kann ich noch nicht die neuen Möglichkeiten nutzen, Installationen mit Hilfe von Composer vorzunehmen, wie das mit Drupal 8 wahrscheinlich zum Standard werden wird.
Auch die aktuellste Drush-Version setzt auf Composer, aber die brauchen wir nicht, sondern wir laden uns Drush 7.x-5.9 als tar.gz in unser oberstes Kundenverzeichnis herunter und installieren Drush, indem wir es in einen Ordner drush im Root unseres Kunden-Verzeichnisses entpacken, z.B. aus dem Root heraus mit
Das Archiv enthält selbst einen Ordner drush. Unser Drush befindet sich bei df demnach in folgendem Ordner: /kunden/7...._4..../drush.
Im Ordner drush legen wir nun eine neue .bash_profile - Datei an:
export DRUSH_PHP=/usr/local/bin/php5-53STABLE-CLI alias drush='/kunden/7...._4..../drush/drush'
Diese Datei exportiert für Drush den Datei-Pfad der relevanten PHP-Version in die globale Umgebungsvariable DRUSH_PHP und legt außerdem einen Alias für drush an, so dass wir das Kommando drush aus jedem Verzeichnis heraus benutzen können. Dazu muss aber mindestens vor Ausführung des Drush-Befehls .bash_profile in unserer Konsole gestartet worden sein.
Dies können wir erreichen mit folgendem Aufruf:
- source / kunden/7...._4..../drush/.bash_profile
Komfortabler ist es, im Kunden-Root /kunden/7...._4..../ eine Datei .bashrc anzulegen und damit die Datei drush/.bash_profile zu starten.
export PATH="$PATH:/kunden/70451_40723/drush" source /kunden/70451_40723/drush/.bash_profile
.bashrc wird jedesmal, wenn wir eine interaktive Konsole starten, aufgerufen. Wenn wir also mit unserer SSH-Console oder mit Putty eine Konsole auf dem Remote-Server öffnen, haben wir Drush immer sofort zur Verfügung. Wir probieren das aus, indem wir auf unserem Webspace mit cd/webseiten/... in einen untegeordneten Ordner gehen und dort eingeben:
- drush --version
Wir bekommen jetzt wieder eine Versions-Information, genauso, wie wir oben den Erfolg unserer lokalen Installation getestet haben.
Allerdings funktioniert das mit dem automatischen Starten von .bash_profile leider nicht mit unserem SSH, wenn wir es in Grunt verwenden. Hier müssen wir vor jedem Drush-Kommando vorher entweder .bash_profile oder .bashrc starten, wenn wir ein Drush-Kommando mit unserem grunt-shell-Modul ausführen wollen (s. nächstes Kapitel).
Beispiel für Grunt & Drush remote
Wir möchten jetzt einen Drupal-Archiv-Dump unserer Multi-Site Drupal-Installation bei unserem Hoster erzeugen und in ein lokales Backup-Verzeichnis transferieren. Wir erweitern deshalb unser Gruntfile.js oben um folgende Snippets:
... grunt.initConfig({ pkg : grunt.file.readJSON('package.json'), ssh : 'ssh.exe -i <%= pkg.remote.keyPath %> <%= pkg.remote.login %> -v', scp : 'scp -i <%= pkg.remote.keyPath %> -vr', bashrc: 'source <%= pkg.remote.home %>/.bashrc;', drush: '<%= pkg.remote.home %>/drush/drush', util: require('./grunt-helpers.js')(grunt), ... shell : { ..., "remoteBackup" : { command : [ '<%= ssh %> <%= bashrc %> cd <%= pkg.remote.home %>/<%= pkg.remote.drupalHome %>; <%= drush %> archive-dump @sites --overwrite --destination=<%= pkg.remote.home %>/<%= pkg.remote.targetPath %>/last_drupal_archive.tar.gz;', '<%= scp %> <%= pkg.remote.login %>:<%= pkg.remote.targetPath %>/last_drupal_archive.tar.gz <%= pkg.localArchivePath %>', 'ren <%= util.replaceAll(pkg.localArchivePath, "/", "\\\\") %>\\last_drupal_archive.tar.gz drupal_archive-<%= util.timestamp() %>.tar.gz', ].join('&&') }, ... } } grunt.loadNpmTasks('grunt-shell'); ... grunt.registerTask('remoteBackup', ['shell:remoteBackup']); ...
Unsere package.json-Konfiguration sieht dabei folgendermaßen aus:
{ "name": "Drupal_Service", "version": "1.0.0", "devDependencies": { "grunt": "^0.4.5", "grunt-contrib-jshint": "^0.10.0", "grunt-shell": "^1.1.1" }, "localArchivePath": "../../../Work/BackupsFromDrupalRemote", "remote": { "drupalHome": "webseiten/staging.test.com/projects/home", "drupalHomeName":"home", "login": "ssh-7....-xy@hauertmann.com", "home": "/kunden/7...._4....", "keyPath": "C:\\Users\\admin\\SSH_private_key\\id_rsa", "targetPath": "ssh-archive" } }
Folgende Ordner sollten zusätzlich manuell angelegt werden, damit das Skript funktioniert:
- lokal: C:\work\BackupsFromDrupalRemote - die Anzahl der Slash's im Skript sind abhängig vom Ordner, aus dem das Skript lokal aufgerufen wird. Leider funktionieren hier aufgrund der Beschränkungen von scp unter Windows nur relative Pfade. Wer also das Archiv endgültig in einem lokalen Backup-Ordner wie oben das lokale speichern möchte (z.B.: in E:\\Backups_Grunt\\local\\drupal_dump_<%= util.timestamp() %>.tar.gz), der muss das Skript noch um einen lokalen Copy-Befehl erweitern (z.B. xcopy verwenden).
- remote: /kunden/7...._4..../ssh-archive
Zusammenfassung
Wir haben jetzt die lokale und die Installation remote des Consolen-Tools Drush für Drupal kennengelernt und für beide Fälle ein recht nützliches Beispiel für die Automatisierung mit Grunt skizziert: Die Erstellung eines kompletten Backups und zwar sowohl einer lokalen als auch einer Drupal-Installation beim Hoster unseres Vertrauens.
Anzumerken ist bei der Kombination von Drush und Grunt, dass die Möglichkeiten von Drush selbst für die Kontrolle einer Remote-Installation nicht mehr benötigt werden, dafür aber eine Drush-Installation auch auf dem Remote-Server erforderlich ist.
Für die Automatisierung des Staging & Deploment - Workflows von Drupal-Portalen sowie deren Wartung (Maintenance) ist Grunt ein ausgezeichnetes Hilfsmittel, welches die Produktivität des Entwicklers erheblich steigern kann.