joe di castrohttp://joedicastro.com2011-07-13T20:21:00+02:00Fabric & Rsync para realizar Backups2011-07-13T20:21:00+02:00joe di castrohttp://joedicastro.com/fabric-rsync-para-realizar-backups.html<p>En el <a href="http://joedicastro.com/sincronizar-dos-directorios-con-fabric-y-rsync.html">anterior articulo</a> empleaba <a href="http://fabfile.org/">fabric</a> y <a href="http://es.wikipedia.org/wiki/Rsync">rsync</a> para sincronizar un directorio local y uno remoto en ambas direcciones. Además le añadía las funcionalidades de <a href="http://joedicastro.com/logger-informes-legibles-para-tus-scripts-python.html">logger</a> y <a href="http://joedicastro.com/notificaciones-de-escritorio-en-ubuntu-desde-python.html">notify</a> para proporcionar información sobre el proceso durante y después de su ejecución. Y comenzaba el articulo recordando a <a href="http://joedicastro.com/sincronizar-una-carpeta-local-y-una-remota-a-traves-de-ftp-lftp-mirror.html">lftp-mirror</a>, el script que había creado para realizar la sincronización a través de FTP. Pero <strong>lftp-mirror</strong> realiza algo más que la sincronización, pues también permite realizar el archivado del directorio local en ficheros comprimidos y lanzar varias tareas en una sola ejecución.</p> <p>Ahora he añadido esta funcionalidad al fichero <strong>fabric</strong> creado anteriormente. Así empleando este fichero podemos realizar el Backup periódico de varios servidores en una sola operación y de forma completamente automática (basta con programar su ejecución). Se sincronizan los dos directorios y se crea un archivo comprimido del directorio local por cada día de la semana. De este modo siempre tenemos una copia del estado del directorio remoto de los últimos siete días. Y al final del proceso en nuestro correo, un email con el informe del resultado por cada una de las tareas ejecutadas.</p> <p>En este fichero, <strong>rsync_fabric.py</strong>, disponemos de tres posibles tareas:</p> <div class="codehilite"><pre><span class="gp">$</span> fab -l <span class="go"> A Fabric file for sync two directories (remote ⇄ local) with rsync.</span> <span class="go">Available commands:</span> <span class="go"> backup Sync from remote to local &amp; archive the local directory.</span> <span class="go"> down Sync from remote to local.</span> <span class="go"> up Sync from local to remote.</span> </pre></div> <p>Con la primera realizamos el backup (sincronización + archivado) y con las siguientes solo la sincronización desde o hacia el servidor. Una de las ventajas de fabric es que nos permite concatenar tareas fácilmente desde la línea de comandos, así podemos lanzar varias sincronizaciones de forma simultanea. Para poder realizar esto, creo una configuración de sincronización por defecto y después creo una función para cada una las tareas adicionales que simplemente redefinen los valores de estas variables globales. Por ejemplo:</p> <div class="codehilite"><pre><span class="c"># Variables globales de sincronización predefenidas</span> <span class="n">env</span><span class="o">.</span><span class="n">host_string</span> <span class="o">=</span> <span class="s">&quot;username@example.com&quot;</span> <span class="n">env</span><span class="o">.</span><span class="n">remote</span> <span class="o">=</span> <span class="s">&quot;/my_directory&quot;</span> <span class="n">env</span><span class="o">.</span><span class="n">local</span> <span class="o">=</span> <span class="s">&quot;/home/my_user/backups/my_directory&quot;</span> <span class="c"># Redefinimos estas variables para otra configuración de sincronización. Por </span> <span class="c"># supuesto, pueden tratarse de servidores distintos.</span> <span class="k">def</span> <span class="nf">_databases</span><span class="p">():</span> <span class="k">global</span> <span class="n">env</span> <span class="n">env</span><span class="o">.</span><span class="n">host_string</span> <span class="o">=</span> <span class="s">&quot;username@example.com&quot;</span> <span class="n">env</span><span class="o">.</span><span class="n">remote</span> <span class="o">=</span> <span class="s">&quot;/databases&quot;</span> <span class="n">env</span><span class="o">.</span><span class="n">local</span> <span class="o">=</span> <span class="s">&quot;/home/my_user/backups/databases&quot;</span> </pre></div> <p>Veamos ejemplos de como podemos utilizar estas tareas:</p> <div class="codehilite"><pre><span class="gp">#</span> <span class="s2">&quot;Si queremos sincronizar el contenido local hacia el remoto, por ejemplo </span> <span class="gp">#</span><span class="s2"> para subir los ficheros al servidor por primera vez. Empleando los valores </span> <span class="gp">#</span><span class="s2"> por defecto. El modificador -w lo empleo para que no se detenga en los </span> <span class="gp">#</span><span class="s2"> errores, que de ocurrir, los veremos luego en el informe final.&quot;</span> <span class="gp">$</span> fab -w up <span class="go">[localhost] local: rsync -pthrvz --delete /home/my_user/backups/my_directory/ </span> <span class="go"> username@example.com:my_directory</span> <span class="go">Done.</span> <span class="gp">#</span> <span class="s2">&quot;Pero también podemos especificar una tarea distinta a la por defecto de </span> <span class="gp">#</span><span class="s2"> este modo. Sincronizando desde el servidor a nuestro directorio local las </span> <span class="gp">#</span><span class="s2"> bases de datos.&quot;</span> <span class="gp">$</span> fab -w down:databases <span class="go">[localhost] local: rsync -pthrvz --delete username@example.com:databases/ </span> <span class="go">/home/my_user/backups/databases</span> <span class="go">Done.</span> <span class="gp">#</span> <span class="s2">&quot;Y por supuesto, podemos realizar varias tareas a la vez.&quot;</span> <span class="gp">$</span> fab -w down backup:databases <span class="go">[localhost] local: rsync -pthrvz --delete username@example.com:my_directory/ </span> <span class="go">/home/my_user/backups/my_directory</span> <span class="go">[localhost] local: rsync -pthrvz --delete username@example.com:databases/ </span> <span class="go">/home/my_user/backups/databases</span> <span class="go">Done.</span> </pre></div> <p>No empleo contraseña alguna, ni en el fichero ni en la línea de comandos, podría hacerse perfectamente, pero prefiero emplear una clave <a href="http://es.wikipedia.org/wiki/RSA">RSA</a> <a href="http://es.wikipedia.org/wiki/Criptograf%C3%ADa_asim%C3%A9trica">pública</a> autorizada para las sesiones SSH en el servidor. Es bastante más seguro y cómodo. En los ejemplos no se ve la salida de <em>rsync</em>, pues es capturada (así como los erores) para ser mostrada a posteriori en los informes. </p> <p>Un ejemplo de informe sería el siguiente:</p> <div class="codehilite"><pre>START TIME ===================================================================== miércoles 13/07/11, 19:48:55 ================================================================================ SCRIPT ========================================================================= fab (ver. Unknown) Fabric Rsync http://joedicastro.com Syncing username@example.com:databases to /home/my_user/backups/databases ================================================================================ RSYNC OUTPUT ___________________________________________________________________ receiving file list ... done sent 20 bytes received 825 bytes 153.64 bytes/sec total size is 827.76M speedup is 979595.42 ROTATE COMPRESSED COPIES _______________________________________________________ Created file: /home/my_user/backups/databases_13jul2011_19:49_mié.tar.gz Deleted old file: databases_13jul2011_19:37_mié.tar.gz DISK SPACE USED ================================================================ 1.60 GiB ================================================================================ END TIME ======================================================================= miércoles 13/07/11, 19:50:02 ================================================================================ </pre></div> <p>Que como podemos ver, ha tardado poco más de un minuto en sincronizar 827.56 Megabytes y el total de espacio ocupado por el directorio y los siete archivos comprimidos es de 1.60 Gibibytes (1,72 Gigabytes). </p> <h2 id="ventajas">Ventajas</h2> <p>Las ventajas de sincronizarlo con <strong>rsync + ssh</strong> vs <strong>ftp</strong>, como ya comenté en el anterior articulo son enormes. Se ahorra muchísimo tiempo y ancho de banda, lo que ayuda a no saturar la red y no tener que planificar con tanto cuidado las ventanas de backup. Por ejemplo he realizado unas pruebas y para las mismas condiciones: <strong>mismo servidor, mismo directorio, mismo horario y condiciones de red; la sincronización remoto → local a través de FTP emplea entre 35 y 45 minutos y cuando lo hacemos a través de rsync emplea entre 2 y 4 minutos</strong>. Ahí es nada, estamos hablando de un proceso ~13 veces más rápido. </p> <h2 id="c+digo">Código</h2> <p>El código del fichero fabric es el siguiente:</p> <div class="codehilite"><pre><span class="c">#!/usr/bin/env python</span> <span class="c"># -*- coding: utf8 -*-</span> <span class="kn">import</span> <span class="nn">os</span> <span class="kn">import</span> <span class="nn">glob</span> <span class="kn">import</span> <span class="nn">tarfile</span> <span class="kn">import</span> <span class="nn">time</span> <span class="kn">from</span> <span class="nn">get_size</span> <span class="kn">import</span> <span class="n">get_size</span> <span class="k">as</span> <span class="n">_get_size</span> <span class="kn">from</span> <span class="nn">get_size</span> <span class="kn">import</span> <span class="n">best_unit_size</span> <span class="k">as</span> <span class="n">_best_unit_size</span> <span class="kn">from</span> <span class="nn">logger</span> <span class="kn">import</span> <span class="n">Logger</span> <span class="k">as</span> <span class="n">_logger</span> <span class="kn">from</span> <span class="nn">notify</span> <span class="kn">import</span> <span class="n">notify</span> <span class="k">as</span> <span class="n">_notify</span> <span class="kn">from</span> <span class="nn">fabric.api</span> <span class="kn">import</span> <span class="n">env</span><span class="p">,</span> <span class="n">local</span> <span class="n">LOG</span> <span class="o">=</span> <span class="n">_logger</span><span class="p">()</span> <span class="c">#===============================================================================</span> <span class="c"># RSYNC HOSTS</span> <span class="c">#===============================================================================</span> <span class="c"># Your default host. No need any more if only wants a host.</span> <span class="n">env</span><span class="o">.</span><span class="n">host_string</span> <span class="o">=</span> <span class="s">&quot;username@host&quot;</span> <span class="n">env</span><span class="o">.</span><span class="n">remote</span> <span class="o">=</span> <span class="s">&quot;/your/remote/path&quot;</span> <span class="n">env</span><span class="o">.</span><span class="n">local</span> <span class="o">=</span> <span class="s">&quot;/your/local/path&quot;</span> <span class="c"># If wants to use various hosts, then define the previous variables like this, </span> <span class="c"># one function per host. </span> <span class="k">def</span> <span class="nf">_host_1</span><span class="p">():</span> <span class="sd">&quot;&quot;&quot;Host variables for host_1.&quot;&quot;&quot;</span> <span class="k">global</span> <span class="n">env</span> <span class="n">env</span><span class="o">.</span><span class="n">host_string</span> <span class="o">=</span> <span class="s">&quot;username@host_1&quot;</span> <span class="n">env</span><span class="o">.</span><span class="n">remote</span> <span class="o">=</span> <span class="s">&quot;/your/remote/path/in/host_1&quot;</span> <span class="n">env</span><span class="o">.</span><span class="n">local</span> <span class="o">=</span> <span class="s">&quot;/your/local/path/for/host_1&quot;</span> <span class="k">def</span> <span class="nf">_host_2</span><span class="p">():</span> <span class="sd">&quot;&quot;&quot;Host variables for host_2.&quot;&quot;&quot;</span> <span class="k">global</span> <span class="n">env</span> <span class="n">env</span><span class="o">.</span><span class="n">host_string</span> <span class="o">=</span> <span class="s">&quot;username@host_2&quot;</span> <span class="n">env</span><span class="o">.</span><span class="n">remote</span> <span class="o">=</span> <span class="s">&quot;/your/remote/path/in/host_2&quot;</span> <span class="n">env</span><span class="o">.</span><span class="n">local</span> <span class="o">=</span> <span class="s">&quot;/your/local/path/for/host_2&quot;</span> <span class="c"># ...</span> <span class="c">#</span> <span class="c"># def _host_n():</span> <span class="c"># &quot;&quot;&quot;Host variables for host_n.&quot;&quot;&quot;</span> <span class="c"># global env</span> <span class="c"># env.host_string = &quot;username@host_n&quot;</span> <span class="c"># env.remote = &quot;/your/remote/path/in/host_n&quot;</span> <span class="c"># env.local = &quot;/your/local/path/for/host_n&quot;</span> <span class="c">#===============================================================================</span> <span class="c"># END RSYNC HOSTS</span> <span class="c">#===============================================================================</span> <span class="k">def</span> <span class="nf">_log_start</span><span class="p">():</span> <span class="sd">&quot;&quot;&quot;Create the Start time info block for the log.&quot;&quot;&quot;</span> <span class="c"># Init the log for multiple hosts. Do not repeat the previous logs.</span> <span class="k">if</span> <span class="n">LOG</span><span class="o">.</span><span class="n">get</span><span class="p">():</span> <span class="n">LOG</span><span class="o">.</span><span class="n">__init__</span><span class="p">()</span> <span class="n">LOG</span><span class="o">.</span><span class="n">time</span><span class="p">(</span><span class="s">&quot;Start time&quot;</span><span class="p">)</span> <span class="k">def</span> <span class="nf">_log_end</span><span class="p">(</span><span class="n">task</span><span class="p">):</span> <span class="sd">&quot;&quot;&quot;Create the End time info block and send &amp; write the log.&quot;&quot;&quot;</span> <span class="n">_notify</span><span class="p">(</span><span class="s">&quot;Rsync&quot;</span><span class="p">,</span> <span class="s">&quot;Ended&quot;</span> <span class="p">,</span> <span class="s">&quot;ok&quot;</span><span class="p">)</span> <span class="n">LOG</span><span class="o">.</span><span class="n">time</span><span class="p">(</span><span class="s">&quot;End time&quot;</span><span class="p">)</span> <span class="n">LOG</span><span class="o">.</span><span class="n">free</span><span class="p">(</span><span class="n">os</span><span class="o">.</span><span class="n">linesep</span> <span class="o">*</span> <span class="mi">2</span><span class="p">)</span> <span class="n">LOG</span><span class="o">.</span><span class="n">write</span><span class="p">(</span><span class="bp">True</span><span class="p">)</span> <span class="n">LOG</span><span class="o">.</span><span class="n">send</span><span class="p">(</span><span class="s">&quot;Fabric Rsync ({0})&quot;</span><span class="o">.</span><span class="n">format</span><span class="p">(</span><span class="n">task</span><span class="p">))</span> <span class="k">def</span> <span class="nf">_check_local</span><span class="p">():</span> <span class="sd">&quot;&quot;&quot;Create local directory if no exists.&quot;&quot;&quot;</span> <span class="k">if</span> <span class="ow">not</span> <span class="n">os</span><span class="o">.</span><span class="n">path</span><span class="o">.</span><span class="n">exists</span><span class="p">(</span><span class="n">env</span><span class="o">.</span><span class="n">local</span><span class="p">):</span> <span class="n">os</span><span class="o">.</span><span class="n">mkdir</span><span class="p">(</span><span class="n">env</span><span class="o">.</span><span class="n">local</span><span class="p">)</span> <span class="k">def</span> <span class="nf">_rsync</span><span class="p">(</span><span class="n">source</span><span class="p">,</span> <span class="n">target</span><span class="p">,</span> <span class="n">delete</span><span class="p">):</span> <span class="sd">&quot;&quot;&quot;Process the _rsync command.&quot;&quot;&quot;</span> <span class="n">_log_start</span><span class="p">()</span> <span class="n">LOG</span><span class="o">.</span><span class="n">header</span><span class="p">(</span><span class="s">&quot;Fabric Rsync</span><span class="se">\n</span><span class="s">http://joedicastro.com&quot;</span><span class="p">,</span> <span class="s">&quot;Syncing {0} to {1}&quot;</span><span class="o">.</span><span class="n">format</span><span class="p">(</span><span class="n">source</span><span class="p">,</span> <span class="n">target</span><span class="p">))</span> <span class="n">_notify</span><span class="p">(</span><span class="s">&quot;Rsync&quot;</span><span class="p">,</span> <span class="s">&quot;Start syncing {0} to {1}&quot;</span><span class="o">.</span><span class="n">format</span><span class="p">(</span><span class="n">source</span><span class="p">,</span> <span class="n">target</span><span class="p">),</span> <span class="s">&quot;info&quot;</span><span class="p">)</span> <span class="n">out</span> <span class="o">=</span> <span class="n">local</span><span class="p">(</span><span class="s">&quot;rsync -pthrvz {2} {0}/ {1}&quot;</span><span class="o">.</span> <span class="n">format</span><span class="p">(</span><span class="n">source</span><span class="p">,</span> <span class="n">target</span><span class="p">,</span> <span class="s">&quot;--delete&quot;</span> <span class="k">if</span> <span class="n">delete</span> <span class="o">==</span> <span class="s">&quot;yes&quot;</span> <span class="k">else</span> <span class="s">&quot;&quot;</span><span class="p">),</span> <span class="n">capture</span><span class="o">=</span><span class="bp">True</span><span class="p">)</span> <span class="n">_notify</span><span class="p">(</span><span class="s">&quot;Rsync&quot;</span><span class="p">,</span> <span class="s">&quot;Finished synchronization&quot;</span><span class="p">,</span> <span class="s">&quot;ok&quot;</span><span class="p">)</span> <span class="n">LOG</span><span class="o">.</span><span class="n">list</span><span class="p">(</span><span class="s">&quot;Rsync Output&quot;</span><span class="p">,</span> <span class="n">out</span><span class="p">)</span> <span class="k">if</span> <span class="n">out</span><span class="o">.</span><span class="n">failed</span><span class="p">:</span> <span class="n">LOG</span><span class="o">.</span><span class="n">list</span><span class="p">(</span><span class="s">&quot;Rsync Errors&quot;</span><span class="p">,</span> <span class="n">out</span><span class="o">.</span><span class="n">stderr</span><span class="p">)</span> <span class="k">def</span> <span class="nf">_compress</span><span class="p">(</span><span class="n">path</span><span class="p">):</span> <span class="sd">&quot;&quot;&quot;Compress a local directory into a gz file.</span> <span class="sd"> Creates a file for each weekday, an removes the old files if exists&quot;&quot;&quot;</span> <span class="n">os</span><span class="o">.</span><span class="n">chdir</span><span class="p">(</span><span class="n">os</span><span class="o">.</span><span class="n">path</span><span class="o">.</span><span class="n">join</span><span class="p">(</span><span class="n">path</span><span class="p">,</span> <span class="n">os</span><span class="o">.</span><span class="n">pardir</span><span class="p">))</span> <span class="n">dir2gz</span> <span class="o">=</span> <span class="n">os</span><span class="o">.</span><span class="n">path</span><span class="o">.</span><span class="n">basename</span><span class="p">(</span><span class="n">path</span><span class="p">)</span> <span class="n">old_gzs</span> <span class="o">=</span> <span class="n">glob</span><span class="o">.</span><span class="n">glob</span><span class="p">(</span><span class="s">&#39;{0}*{1}.tar.gz&#39;</span><span class="o">.</span><span class="n">format</span><span class="p">(</span><span class="n">dir2gz</span><span class="p">,</span> <span class="n">time</span><span class="o">.</span><span class="n">strftime</span><span class="p">(</span><span class="s">&#39;%a&#39;</span><span class="p">)))</span> <span class="n">gz_name</span> <span class="o">=</span> <span class="s">&quot;{0}_{1}.tar.gz&quot;</span><span class="o">.</span><span class="n">format</span><span class="p">(</span><span class="n">dir2gz</span><span class="p">,</span> <span class="n">time</span><span class="o">.</span><span class="n">strftime</span><span class="p">(</span><span class="s">&#39;</span><span class="si">%d</span><span class="s">%b%Y_%H:%M_%a&#39;</span><span class="p">))</span> <span class="n">gz_file</span> <span class="o">=</span> <span class="n">tarfile</span><span class="o">.</span><span class="n">open</span><span class="p">(</span><span class="n">gz_name</span><span class="p">,</span> <span class="s">&quot;w:gz&quot;</span><span class="p">)</span> <span class="n">gz_file</span><span class="o">.</span><span class="n">add</span><span class="p">(</span><span class="n">path</span><span class="p">,</span> <span class="n">arcname</span><span class="o">=</span><span class="n">dir2gz</span><span class="p">)</span> <span class="n">gz_file</span><span class="o">.</span><span class="n">close</span><span class="p">()</span> <span class="n">output</span> <span class="o">=</span> <span class="n">os</span><span class="o">.</span><span class="n">linesep</span><span class="o">.</span><span class="n">join</span><span class="p">([</span><span class="s">&#39;Created file:&#39;</span><span class="p">,</span> <span class="s">&#39;&#39;</span><span class="p">,</span> <span class="n">os</span><span class="o">.</span><span class="n">path</span><span class="o">.</span><span class="n">join</span><span class="p">(</span><span class="n">os</span><span class="o">.</span><span class="n">getcwd</span><span class="p">(),</span> <span class="n">gz_name</span><span class="p">)])</span> <span class="k">for</span> <span class="n">old_gz</span> <span class="ow">in</span> <span class="n">old_gzs</span><span class="p">:</span> <span class="n">os</span><span class="o">.</span><span class="n">remove</span><span class="p">(</span><span class="n">old_gz</span><span class="p">)</span> <span class="n">output</span> <span class="o">+=</span> <span class="n">os</span><span class="o">.</span><span class="n">linesep</span><span class="o">.</span><span class="n">join</span><span class="p">([</span><span class="n">os</span><span class="o">.</span><span class="n">linesep</span><span class="p">,</span> <span class="s">&#39;Deleted old file:&#39;</span><span class="p">,</span> <span class="s">&#39;&#39;</span><span class="p">,</span> <span class="n">old_gz</span><span class="p">])</span> <span class="k">return</span> <span class="n">output</span> <span class="k">def</span> <span class="nf">_archive</span><span class="p">():</span> <span class="sd">&quot;&quot;&quot;Archive the local directory in a gz file for each weekday.&quot;&quot;&quot;</span> <span class="n">_notify</span><span class="p">(</span><span class="s">&#39;Rsync&#39;</span><span class="p">,</span> <span class="s">&#39;Compressing folder...&#39;</span><span class="p">,</span> <span class="s">&#39;info&#39;</span><span class="p">)</span> <span class="n">LOG</span><span class="o">.</span><span class="n">list</span><span class="p">(</span><span class="s">&#39;Rotate compressed copies&#39;</span><span class="p">,</span> <span class="n">_compress</span><span class="p">(</span><span class="n">env</span><span class="o">.</span><span class="n">local</span><span class="p">))</span> <span class="n">_notify</span><span class="p">(</span><span class="s">&quot;Rsync&quot;</span><span class="p">,</span> <span class="s">&quot;Finished compression&quot;</span><span class="p">,</span> <span class="s">&quot;ok&quot;</span><span class="p">)</span> <span class="k">def</span> <span class="nf">_get_diskspace</span><span class="p">():</span> <span class="sd">&quot;&quot;&quot;Get the disk space used by the local directory and archives.&quot;&quot;&quot;</span> <span class="n">gz_size</span> <span class="o">=</span> <span class="nb">sum</span><span class="p">([</span><span class="n">_get_size</span><span class="p">(</span><span class="n">gz</span><span class="p">)</span> <span class="k">for</span> <span class="n">gz</span> <span class="ow">in</span> <span class="n">glob</span><span class="o">.</span><span class="n">glob</span><span class="p">(</span><span class="s">&#39;{0}*.gz&#39;</span><span class="o">.</span> <span class="n">format</span><span class="p">(</span><span class="n">env</span><span class="o">.</span><span class="n">local</span><span class="p">))])</span> <span class="n">log_size</span> <span class="o">=</span> <span class="n">_get_size</span><span class="p">(</span><span class="n">LOG</span><span class="o">.</span><span class="n">filename</span><span class="p">)</span> <span class="k">if</span> <span class="n">os</span><span class="o">.</span><span class="n">path</span><span class="o">.</span><span class="n">exists</span><span class="p">(</span><span class="n">LOG</span><span class="o">.</span><span class="n">filename</span><span class="p">)</span> <span class="k">else</span> <span class="mi">0</span> <span class="n">local_size</span> <span class="o">=</span> <span class="n">_get_size</span><span class="p">(</span><span class="n">env</span><span class="o">.</span><span class="n">local</span><span class="p">)</span> <span class="n">size</span> <span class="o">=</span> <span class="n">_best_unit_size</span><span class="p">(</span><span class="n">local_size</span> <span class="o">+</span> <span class="n">gz_size</span> <span class="o">+</span> <span class="n">log_size</span><span class="p">)</span> <span class="n">LOG</span><span class="o">.</span><span class="n">block</span><span class="p">(</span><span class="s">&#39;Disk space used&#39;</span><span class="p">,</span> <span class="s">&#39;{0:&gt;76.2f} {1}&#39;</span><span class="o">.</span><span class="n">format</span><span class="p">(</span><span class="n">size</span><span class="p">[</span><span class="s">&#39;s&#39;</span><span class="p">],</span> <span class="n">size</span><span class="p">[</span><span class="s">&#39;u&#39;</span><span class="p">]))</span> <span class="k">def</span> <span class="nf">up</span><span class="p">(</span><span class="n">server</span><span class="o">=</span><span class="bp">None</span><span class="p">,</span> <span class="n">dlt</span><span class="o">=</span><span class="s">&#39;yes&#39;</span><span class="p">):</span> <span class="sd">&quot;&quot;&quot;Sync from local to remote.&quot;&quot;&quot;</span> <span class="nb">globals</span><span class="p">()[</span><span class="s">&quot;_&quot;</span> <span class="o">+</span> <span class="n">server</span><span class="p">]()</span> <span class="k">if</span> <span class="n">server</span> <span class="k">else</span> <span class="bp">None</span> <span class="n">_rsync</span><span class="p">(</span><span class="n">env</span><span class="o">.</span><span class="n">local</span><span class="p">,</span> <span class="s">&quot;:&quot;</span><span class="o">.</span><span class="n">join</span><span class="p">([</span><span class="n">env</span><span class="o">.</span><span class="n">host_string</span><span class="p">,</span> <span class="n">env</span><span class="o">.</span><span class="n">remote</span><span class="p">]),</span> <span class="n">dlt</span><span class="p">)</span> <span class="n">_log_end</span><span class="p">(</span><span class="n">server</span><span class="p">)</span> <span class="k">def</span> <span class="nf">down</span><span class="p">(</span><span class="n">server</span><span class="o">=</span><span class="bp">None</span><span class="p">,</span> <span class="n">dlt</span><span class="o">=</span><span class="s">&#39;yes&#39;</span><span class="p">,</span> <span class="n">archive</span><span class="o">=</span><span class="bp">False</span><span class="p">):</span> <span class="sd">&quot;&quot;&quot;Sync from remote to local.&quot;&quot;&quot;</span> <span class="nb">globals</span><span class="p">()[</span><span class="s">&quot;_&quot;</span> <span class="o">+</span> <span class="n">server</span><span class="p">]()</span> <span class="k">if</span> <span class="n">server</span> <span class="k">else</span> <span class="bp">None</span> <span class="n">_check_local</span><span class="p">()</span> <span class="n">_rsync</span><span class="p">(</span><span class="s">&quot;:&quot;</span><span class="o">.</span><span class="n">join</span><span class="p">([</span><span class="n">env</span><span class="o">.</span><span class="n">host_string</span><span class="p">,</span> <span class="n">env</span><span class="o">.</span><span class="n">remote</span><span class="p">]),</span> <span class="n">env</span><span class="o">.</span><span class="n">local</span><span class="p">,</span> <span class="n">dlt</span><span class="p">)</span> <span class="k">if</span> <span class="ow">not</span> <span class="n">archive</span><span class="p">:</span> <span class="n">_log_end</span><span class="p">(</span><span class="n">server</span><span class="p">)</span> <span class="k">def</span> <span class="nf">backup</span><span class="p">(</span><span class="n">server</span><span class="o">=</span><span class="bp">None</span><span class="p">):</span> <span class="sd">&quot;&quot;&quot;Sync from remote to local &amp; archive the local directory.&quot;&quot;&quot;</span> <span class="n">down</span><span class="p">(</span><span class="n">server</span><span class="p">,</span> <span class="n">archive</span><span class="o">=</span><span class="bp">True</span><span class="p">)</span> <span class="n">_archive</span><span class="p">()</span> <span class="n">_get_diskspace</span><span class="p">()</span> <span class="n">_log_end</span><span class="p">(</span><span class="n">server</span><span class="p">)</span> </pre></div> <p>El fichero siempre actualizado puede ser encontrado en el repositorio <em>Python Recipes</em> que está alojado en <a href="http://github.com/joedicastro/python-recipes">github</a> con el nombre <code>rsync_fabfile.py</code> </p>Sincronizar dos directorios con Fabric y Rsync2011-07-06T22:02:00+02:00joe di castrohttp://joedicastro.com/sincronizar-dos-directorios-con-fabric-y-rsync.html<p>Anteriormente habíamos visto como <a href="http://joedicastro.com/sincronizar-una-carpeta-local-y-una-remota-a-traves-de-ftp-lftp-mirror.html">sincronizar un directorio remoto y uno local empleando solamente FTP</a>. Ahora vamos a ver la forma de hacerlo empleando <a href="http://es.wikipedia.org/wiki/Ssh">ssh</a> y <a href="http://es.wikipedia.org/wiki/Rsync">rsync</a>. Para ello vamos a utilizar otra vez <strong>Python</strong> y una herramienta muy valiosa para cualquier <a href="http://es.wikipedia.org/wiki/Administrador_de_sistemas">sysadmin</a> que se precie como es <a href="http://fabfile.org/">fabric</a> (que descubrí gracias a Manuel Viera en <a href="http://python.majibu.org/preguntas/11/libreria-para-emplear-con-ssh">esta pregunta en majibu</a>). Evidentemente realizar la sincronización con rsync esta a años luz de hacerlo con FTP, la velocidad de sincronización, el tiempo empleado y la cantidad de datos a mover son mucho menores. FTP es algo que debería utilizarse únicamente cuando no disponemos de acceso via SSH.</p> <p>La gran ventaja de <strong>fabric</strong> es que nos permite ahorrarnos el tener que implementar el acceso SSH con <a href="http://www.lag.net/paramiko/">paramiko</a> y la entrada de opciones y argumentos con <em>argparse</em>. Gracias a esto los scripts necesarios son mucho más cortos y limpios y su utilización es bastante más sencilla. Fabric ya incorpora una función para emplear rsync, <code>rsync_project</code>, dentro de su modulo de proyectos contribuidos <code>fabric.contrib.project</code></p> <p>Una forma de implementar esta sincronización en ambas direcciones empleando esta función predefinida sería esta:</p> <div class="codehilite"><pre><span class="kn">from</span> <span class="nn">fabric.api</span> <span class="kn">import</span> <span class="n">env</span><span class="p">,</span> <span class="n">hosts</span><span class="p">,</span> <span class="n">local</span> <span class="kn">from</span> <span class="nn">fabric.contrib.project</span> <span class="kn">import</span> <span class="n">rsync_project</span> <span class="n">env</span><span class="o">.</span><span class="n">host_string</span> <span class="o">=</span> <span class="s">&quot;username@host&quot;</span> <span class="n">REMOTE_PATH</span> <span class="o">=</span> <span class="s">&quot;/your/remote/path&quot;</span> <span class="n">LOCAL_PATH</span> <span class="o">=</span> <span class="s">&quot;/your/local/path&quot;</span> <span class="nd">@hosts</span><span class="p">(</span><span class="n">env</span><span class="o">.</span><span class="n">host_string</span><span class="p">)</span> <span class="k">def</span> <span class="nf">rsync_up</span><span class="p">(</span><span class="n">dlt</span><span class="o">=</span><span class="s">&quot;yes&quot;</span><span class="p">):</span> <span class="n">rsync_project</span><span class="p">(</span><span class="n">REMOTE_PATH</span><span class="p">,</span> <span class="n">LOCAL_PATH</span> <span class="o">+</span> <span class="s">&quot;/&quot;</span><span class="p">,</span> <span class="n">delete</span><span class="o">=</span> <span class="bp">True</span> <span class="k">if</span> <span class="n">dlt</span> <span class="o">==</span> <span class="s">&quot;yes&quot;</span> <span class="k">else</span> <span class="bp">False</span><span class="p">)</span> <span class="k">def</span> <span class="nf">rsync_down</span><span class="p">(</span><span class="n">dlt</span><span class="o">=</span><span class="s">&quot;yes&quot;</span><span class="p">):</span> <span class="n">local</span><span class="p">(</span><span class="s">&quot;rsync -pthrvz {0}:{1}/ {2} {3}&quot;</span><span class="o">.</span> <span class="n">format</span><span class="p">(</span><span class="n">env</span><span class="o">.</span><span class="n">host_string</span><span class="p">,</span> <span class="n">REMOTE_PATH</span><span class="p">,</span> <span class="n">LOCAL_PATH</span><span class="p">,</span> <span class="s">&quot;--delete&quot;</span> <span class="k">if</span> <span class="n">dlt</span> <span class="o">==</span> <span class="s">&quot;yes&quot;</span> <span class="k">else</span> <span class="s">&quot;&quot;</span><span class="p">))</span> </pre></div> <p>Y luego solo tendríamos que llamar a la función deseada:</p> <div class="codehilite"><pre><span class="gp">#</span> <span class="s2">&quot;Para sincronizar de remoto a local&quot;</span> <span class="gp">$</span> fab rsync_down </pre></div> <blockquote> <p>Hay que tener en cuenta un detalle con fabric. Cuando se le pasa un parámetro, este es siempre convertido a una cadena. Luego al pasarle <code>True</code> o <code>False</code> no se convierte en un valor booleano, sino una cadena <code>"True"</code>o <code>"False"</code>. De ahí que compruebe si el parámetro coincide con <code>"yes"</code> en vez de un valor booleano.</p> </blockquote> <p>El problema con la función rsync predefinida de fabric es que esta pensada únicamente para subir archivos a un servidor remoto, es decir, es una sincronización en una sola dirección, por eso implemento la sincronización en sentido contrario sin emplearla y empleando <code>local</code>. La autentificación de la sesión SSH puede realizarse especificando la contraseña dentro del propio fichero, pero va en contra del sentido común emplear un método tan inseguro como este. Lo lógico es emplear autorizaciones de sesiones SSH sin contraseña por medio de una <a href="http://es.wikipedia.org/wiki/Criptograf%C3%ADa_asim%C3%A9trica">clave pública</a>.</p> <p>Podríamos prescindir de la librería incorporada dentro de fabric y tendríamos algo como esto:</p> <div class="codehilite"><pre><span class="kn">from</span> <span class="nn">fabric.api</span> <span class="kn">import</span> <span class="n">env</span><span class="p">,</span> <span class="n">local</span> <span class="n">env</span><span class="o">.</span><span class="n">host_string</span> <span class="o">=</span> <span class="s">&quot;username@host&quot;</span> <span class="n">REMOTE_PATH</span> <span class="o">=</span> <span class="s">&quot;/your/remote/path&quot;</span> <span class="n">LOCAL_PATH</span> <span class="o">=</span> <span class="s">&quot;/your/local/path&quot;</span> <span class="k">def</span> <span class="nf">_rsync</span><span class="p">(</span><span class="n">source</span><span class="p">,</span> <span class="n">target</span><span class="p">,</span> <span class="n">delete</span><span class="p">):</span> <span class="sd">&quot;&quot;&quot;Process the _rsync command.&quot;&quot;&quot;</span> <span class="n">output</span> <span class="o">=</span> <span class="n">local</span><span class="p">(</span><span class="s">&quot;rsync -pthrvz {0}/ {1} {2}&quot;</span><span class="o">.</span> <span class="n">format</span><span class="p">(</span><span class="n">source</span><span class="p">,</span> <span class="n">target</span><span class="p">,</span> <span class="s">&quot;--delete&quot;</span> <span class="k">if</span> <span class="n">delete</span> <span class="o">==</span> <span class="s">&quot;yes&quot;</span> <span class="k">else</span> <span class="s">&quot;&quot;</span><span class="p">))</span> <span class="k">def</span> <span class="nf">up</span><span class="p">(</span><span class="n">dlt</span><span class="o">=</span><span class="s">&#39;yes&#39;</span><span class="p">):</span> <span class="sd">&quot;&quot;&quot;Sync from local to remote.&quot;&quot;&quot;</span> <span class="n">_rsync</span><span class="p">(</span><span class="n">LOCAL_PATH</span><span class="p">,</span> <span class="s">&quot;:&quot;</span><span class="o">.</span><span class="n">join</span><span class="p">([</span><span class="n">env</span><span class="o">.</span><span class="n">host_string</span><span class="p">,</span> <span class="n">REMOTE_PATH</span><span class="p">]),</span> <span class="n">dlt</span><span class="p">)</span> <span class="k">def</span> <span class="nf">down</span><span class="p">(</span><span class="n">dlt</span><span class="o">=</span><span class="s">&#39;yes&#39;</span><span class="p">):</span> <span class="sd">&quot;&quot;&quot;Sync from remote to local.&quot;&quot;&quot;</span> <span class="n">_rsync</span><span class="p">(</span><span class="s">&quot;:&quot;</span><span class="o">.</span><span class="n">join</span><span class="p">([</span><span class="n">env</span><span class="o">.</span><span class="n">host_string</span><span class="p">,</span> <span class="n">REMOTE_PATH</span><span class="p">]),</span> <span class="n">LOCAL_PATH</span><span class="p">,</span> <span class="n">dlt</span><span class="p">)</span> </pre></div> <p>Pero... un momento, si estamos empleado un comando local, no empleamos <code>rsync_project</code> y empleamos una clave pública para el acceso SSH, entonces no estamos empleando <strong>paramiko</strong>, ¿de que nos sirve emplear fabric?. Bueno, en realidad <code>rsync_project</code> también emplea <code>local</code>, por lo que no emplea paramiko. Pero las ventajas vienen de que, por ejemplo, este mismo script se podría modificar fácilmente para ejecutar rsync en el servidor en vez de en nuestra maquina local, empleando <code>run</code> en vez de <code>local</code>. Además podemos emplear el mismo fichero para añadir varias tareas más a realizar en el servidor, aparte de la sincronización. Podríamos prescindir de fabric y hacer esto mismo con un script con un número similar de líneas, pero esto nos permite centralizar todas las tareas más comunes sobre ese servidor en un único fichero. Por ejemplo podríamos añadir una tarea para hacer un respaldo previo de una base de datos en el servidor, empleando un comando remoto en el servidor, luego hacer la sincronización separada de la BDD y el resto de ficheros y finalmente eliminar ese respaldo. Puede haber cientos de razones para preferir emplear fabric antes de un script independiente para la sincronización.</p> <h2 id="ejecuci+n_desatendida_de_la_sincronizaci+n">Ejecución desatendida de la sincronización</h2> <p>Si queremos programar esta tarea, no sería mala idea que nos avisara de cuando comienza a ejecutarse y del resultado de la misma. Para ello puedo emplear <a href="http://joedicastro.com/logger-informes-legibles-para-tus-scripts-python.html">Logger</a> y <a href="http://joedicastro.com/notificaciones-de-escritorio-en-ubuntu-desde-python.html">notify</a>, para implementar esta funcionalidad.</p> <div class="codehilite"><pre><span class="kn">from</span> <span class="nn">logger</span> <span class="kn">import</span> <span class="n">Logger</span> <span class="k">as</span> <span class="n">_logger</span> <span class="kn">from</span> <span class="nn">notify</span> <span class="kn">import</span> <span class="n">notify</span> <span class="k">as</span> <span class="n">_notify</span> <span class="kn">from</span> <span class="nn">fabric.api</span> <span class="kn">import</span> <span class="n">env</span><span class="p">,</span> <span class="n">local</span> <span class="n">env</span><span class="o">.</span><span class="n">host_string</span> <span class="o">=</span> <span class="s">&quot;username@host&quot;</span> <span class="n">REMOTE_PATH</span> <span class="o">=</span> <span class="s">&quot;/your/remote/path&quot;</span> <span class="n">LOCAL_PATH</span> <span class="o">=</span> <span class="s">&quot;/your/local/path&quot;</span> <span class="k">def</span> <span class="nf">_rsync</span><span class="p">(</span><span class="n">source</span><span class="p">,</span> <span class="n">target</span><span class="p">,</span> <span class="n">delete</span><span class="p">):</span> <span class="sd">&quot;&quot;&quot;Process the _rsync command.&quot;&quot;&quot;</span> <span class="n">log</span> <span class="o">=</span> <span class="n">_logger</span><span class="p">()</span> <span class="n">log</span><span class="o">.</span><span class="n">header</span><span class="p">(</span><span class="s">&quot;Fabric Rsync</span><span class="se">\n</span><span class="s">http://joedicastro.com&quot;</span><span class="p">,</span> <span class="s">&quot;Syncing {0} to {1}&quot;</span><span class="o">.</span><span class="n">format</span><span class="p">(</span><span class="n">source</span><span class="p">,</span> <span class="n">target</span><span class="p">))</span> <span class="n">log</span><span class="o">.</span><span class="n">time</span><span class="p">(</span><span class="s">&quot;Start time&quot;</span><span class="p">)</span> <span class="n">_notify</span><span class="p">(</span><span class="s">&quot;Rsync&quot;</span><span class="p">,</span> <span class="s">&quot;Start syncing {0} to {1}&quot;</span><span class="o">.</span><span class="n">format</span><span class="p">(</span><span class="n">source</span><span class="p">,</span> <span class="n">target</span><span class="p">),</span> <span class="s">&quot;info&quot;</span><span class="p">)</span> <span class="n">output</span> <span class="o">=</span> <span class="n">local</span><span class="p">(</span><span class="s">&quot;rsync -pthrvz {0}/ {1} {2}&quot;</span><span class="o">.</span> <span class="n">format</span><span class="p">(</span><span class="n">source</span><span class="p">,</span> <span class="n">target</span><span class="p">,</span> <span class="s">&quot;--delete&quot;</span> <span class="k">if</span> <span class="n">delete</span> <span class="o">==</span> <span class="s">&quot;yes&quot;</span> <span class="k">else</span> <span class="s">&quot;&quot;</span><span class="p">),</span> <span class="n">capture</span><span class="o">=</span><span class="bp">True</span><span class="p">)</span> <span class="n">_notify</span><span class="p">(</span><span class="s">&quot;Rsync&quot;</span><span class="p">,</span> <span class="s">&quot;Finished&quot;</span><span class="p">,</span> <span class="s">&quot;ok&quot;</span><span class="p">)</span> <span class="n">log</span><span class="o">.</span><span class="n">list</span><span class="p">(</span><span class="s">&quot;Output&quot;</span><span class="p">,</span> <span class="n">output</span><span class="p">)</span> <span class="k">if</span> <span class="n">output</span><span class="o">.</span><span class="n">failed</span><span class="p">:</span> <span class="n">log</span><span class="o">.</span><span class="n">list</span><span class="p">(</span><span class="s">&quot;Error&quot;</span><span class="p">,</span> <span class="n">output</span><span class="o">.</span><span class="n">stderr</span><span class="p">)</span> <span class="n">log</span><span class="o">.</span><span class="n">time</span><span class="p">(</span><span class="s">&quot;End time&quot;</span><span class="p">)</span> <span class="n">log</span><span class="o">.</span><span class="n">send</span><span class="p">(</span><span class="s">&quot;Fabric Rsync&quot;</span><span class="p">)</span> <span class="k">def</span> <span class="nf">up</span><span class="p">(</span><span class="n">dlt</span><span class="o">=</span><span class="s">&#39;yes&#39;</span><span class="p">):</span> <span class="sd">&quot;&quot;&quot;Sync from local to remote.&quot;&quot;&quot;</span> <span class="n">_rsync</span><span class="p">(</span><span class="n">LOCAL_PATH</span><span class="p">,</span> <span class="s">&quot;:&quot;</span><span class="o">.</span><span class="n">join</span><span class="p">([</span><span class="n">env</span><span class="o">.</span><span class="n">host_string</span><span class="p">,</span> <span class="n">REMOTE_PATH</span><span class="p">]),</span> <span class="n">dlt</span><span class="p">)</span> <span class="k">def</span> <span class="nf">down</span><span class="p">(</span><span class="n">dlt</span><span class="o">=</span><span class="s">&#39;yes&#39;</span><span class="p">):</span> <span class="sd">&quot;&quot;&quot;Sync from remote to local.&quot;&quot;&quot;</span> <span class="n">_rsync</span><span class="p">(</span><span class="s">&quot;:&quot;</span><span class="o">.</span><span class="n">join</span><span class="p">([</span><span class="n">env</span><span class="o">.</span><span class="n">host_string</span><span class="p">,</span> <span class="n">REMOTE_PATH</span><span class="p">]),</span> <span class="n">LOCAL_PATH</span><span class="p">,</span> <span class="n">dlt</span><span class="p">)</span> </pre></div> <p>De esta forma, nos avisaría con una notificación en el escritorio de su inicio y fin, y al acabarse la sincronización, tendríamos un informe en nuestro correo parecido a este:</p> <div class="codehilite"><pre>SCRIPT ========================================================================= fab (ver. Unknown) Fabric Rsync Syncing username@host:/your/remote/path to /your/local/path ================================================================================ START TIME ===================================================================== miércoles 06/07/11, 21:50:48 ================================================================================ OUTPUT _________________________________________________________________________ receiving file list ... done ./ index.php sent 48 bytes received 200 bytes 45.09 bytes/sec total size is 99 speedup is 0.40 END TIME ======================================================================= miércoles 06/07/11, 21:50:54 ================================================================================ </pre></div> <p>Este fichero está disponible en el repositorio <em>Python Recipes</em> alojado en <a href="http://github.com/joedicastro/python-recipes">github</a>.</p>Pelican - Publicación y automatización2011-06-28T23:54:00+02:00joe di castrohttp://joedicastro.com/pelican-publicacion-y-automatizacion.html<p>Una vez que sabemos como <a href="http://joedicastro.com/pelican-introduccion-e-instalacion.html">instalar Pelican</a> y <a href="http://joedicastro.com/pelican-creacion-de-contenido.html">crear contenido</a> con él, es hora de saber como convertir ese contenido en un blog real disponible en internet. Es decir, saber como publicar ese contenido. Como hemos visto hasta ahora, al constar básicamente de simples ficheros HTML, un servidor de archivos es más que suficiente para servir el blog. Esto nos abre un gran abanico de posibilidades, desde emplear un potente (y barato) servidor de ficheros en la <em>nube</em> como <strong>Amazon S3</strong>, pasando por las páginas web estáticas que nos permiten repositorios como <strong>Bitbucket</strong> o <strong>GitHub</strong>, por los tradiciones hostings compartidos (e.g. este blog), hasta un servidor casero sencillo montado sobre un <a href="http://es.wikipedia.org/wiki/Network-attached_storage">NAS</a>. </p> <p>Aún cuando es posible emplear un simple servidor de archivos para alojar el blog, siempre es mejor contar con un servidor web detrás (Apache, nginx, lighttpd, ...) que nos permita hacer redirecciones para nuestras antiguas páginas si ya disponíamos de un blog anterior o manejar los errores HTTP <a href="http://es.wikipedia.org/wiki/Error_404">404</a> o <a href="http://es.wikipedia.org/wiki/Anexo:C%C3%B3digos_de_estado_HTTP">403</a> de forma personalizada. </p> <h2 id="publicar_el_contenido">Publicar el contenido</h2> <p>Publicar el contenido de una web es tan sencillo como volcar el contenido del directorio que nos genera Pelican (en nuestro ejemplo sería <em>myblog.com/site/output/*</em>) en el directorio destino de nuestro servidor web. Dependiendo del método que hayamos elegido para alojar nuestro blog, puede ser tan sencillo como una copia de archivos o emplear FTP (SFTP) ó <a href="http://es.wikipedia.org/wiki/SSH">SSH</a> (<a href="http://es.wikipedia.org/wiki/SCP">SCP</a>, <a href="http://en.wikipedia.org/wiki/Unison_%28file_synchronizer%29">Unison</a> ó <a href="http://es.wikipedia.org/wiki/Rsync">rsync</a>). Aquí el tema radica no en la primera vez que vayamos a subir el contenido al servidor, si no en las sucesivas, a medida que vayamos creando contenido nuevo. No tendría ningún sentido volver a subir todo el contenido cada vez, si no solamente el nuevo o el que haya cambiado. Para eso necesitamos sincronizar los dos directorios. </p> <p>Si solamente disponemos de acceso FTP (o SFTP) a nuestro servidor, entonces tendremos que emplear una herramienta que nos permita la sincronización sobre FTP, como puede ser <strong>lftp</strong>. Y el proceso se puede automatizar con un script como el que describo en <a href="http://joedicastro.com/sincronizar-una-carpeta-local-y-una-remota-a-traves-de-ftp-lftp-mirror.html">Sincronizar una carpeta local y una remota a través de FTP: lftp-mirror</a>. Si disponemos de acceso a través de SSH, entonces la elección es clarisima, <strong>rsync</strong>. Más adelante explico una manera de emplearlo de forma automática.</p> <p>Cualquiera de ambas soluciones nos permite subir el contenido en apenas segundos, (sobre todo en el caso de rsync) cuando se trata de añadir un articulo nuevo, por ejemplo. Y lo mismo a la hora de hacer una rectificación, es tan inmediato como lo pueden ser plataformas como Wordpress, Drupal y similares. Además, con las potentes herramientas que existen para hacer cambios múltiples en varios ficheros de texto a la vez, se pueden realizar tareas casi imposibles con una plataforma de blogs tradicional sin recurrir a consultas SQL en la BDD o a plugins externos. Una de estas herramientas, sin recurrir a <code>find</code>, <code>grep</code>, <code>awk</code> y <code>sed</code>, puede ser <a href="http://regexxer.sourceforge.net/">regexxer</a>.</p> <h2 id="generar_el_contenido_en_el_propio_servidor">Generar el contenido en el propio servidor</h2> <p>Si el alojamiento que hemos escogido nos permite instalar programas python, entonces tenemos la posibilidad de instalar Pelican en el servidor remoto. De esta formar podríamos subir únicamente los archivos markdown o reStructuredText al servidor y generar allí mismo el contenido web. De este modo la cantidad de datos a subir sería ridícula y un simple comando FTP nos serviría. Luego bien podríamos lanzar Pelican a través de una consola SSH o bien dependiendo del servidor, tener un <a href="http://es.wikipedia.org/wiki/Demonio_%28inform%C3%A1tica%29">demonio</a> corriendo que cuando detectara un cambio en el sistema de ficheros lanzará un script que generara el contenido con Pelican. </p> <p>Otra posibilidad que me convence más, es de la poder instalar un repositorio en el servidor con un software de control de versiones, como Git o Mercurial. La idea sería tener un repositorio local, y al hacer un push hacia el repositorio remoto, a través de un <em>hook</em> activar la generación de la página con Pelican. Esto nos permitiría además poder tener varias copias del repositorio (por ejemplo en GitHub o Bitbucket) y por lo tanto de la web, haciendo "innecesarias" las copias de seguridad. </p> <h2 id="automatizar_todos_los_procesos">Automatizar todos los procesos</h2> <p>Pero lo ideal es poder automatizar todas las tareas que hemos visto hasta ahora, empleando unos pocos comandos para realizarlas sin esfuerzo alguno (bueno, a menos que tengas tú <a href="http://es.wikipedia.org/wiki/Negro_%28escritor%29">ghostwriter</a> particular, me temo que los artículos los seguirás teniendo que escribir tú). Para poder realizar esto disponemos de la fantástica y potente herramienta <a href="http://fabfile.org">Fabric</a> (el <a href="http://en.wikipedia.org/wiki/Capistrano">Capistrano</a> para Python) que nos permite ejecutar comandos locales o remotos en múltiples servidores. Esto nos permite hacer despliegues de software sin apenas esfuerzo en distintas máquinas, copiar ficheros o ejecutar tareas repetitivas empleado una corta serie de comandos. Una grandísima herramienta para administradores de sistema y desarrolladores.</p> <p>Lo único que necesitamos es instalar <strong>fabric</strong> y crear un fichero llamado <code>fabfile.py</code> donde especificaremos las tareas que queremos programar. Para instalar la última versión estable de fabric, lo mejor es emplear <code>easy_install</code> o <code>pip</code></p> <div class="codehilite"><pre><span class="gp">$</span> pip install fabric </pre></div> <p>Una vez creado el fichero <code>fabfile.py</code>, lo único que tendremos que hacer para ejecutar una tarea del mismo, sería escribir el comando <code>fab</code> seguido del nombre que le hayamos dado a la tarea (este sería el funcionamiento básico). Y la tarea se ejecutaría inmediatamente. </p> <p>Para comprender mejor como funciona Fabric, muestro aquí el contenido actual de mi fichero <code>fabfile.py</code><br /> </p> <div class="codehilite"><pre><span class="c">#!/usr/bin/env python</span> <span class="c"># -*- coding: utf8 -*-</span> <span class="sd">&quot;&quot;&quot;</span> <span class="sd"> fabfile.py: A fabric script for generate my personal blog</span> <span class="sd">&quot;&quot;&quot;</span> <span class="n">__author__</span> <span class="o">=</span> <span class="s">&quot;joe di castro &lt;joe@joedicastro.com&gt;&quot;</span> <span class="n">__license__</span> <span class="o">=</span> <span class="s">&quot;GNU General Public License version 3&quot;</span> <span class="n">__date__</span> <span class="o">=</span> <span class="s">&quot;28/06/2011&quot;</span> <span class="n">__version__</span> <span class="o">=</span> <span class="s">&quot;0.2&quot;</span> <span class="kn">import</span> <span class="nn">os</span> <span class="kn">from</span> <span class="nn">fabric.api</span> <span class="kn">import</span> <span class="o">*</span> <span class="kn">from</span> <span class="nn">fabric.contrib.project</span> <span class="kn">import</span> <span class="n">rsync_project</span> <span class="kn">from</span> <span class="nn">fabric.contrib.console</span> <span class="kn">import</span> <span class="n">confirm</span> <span class="n">PELICAN_REPOSITORY</span> <span class="o">=</span> <span class="s">&quot;git://github.com/ametaireau/pelican.git&quot;</span> <span class="n">PROD</span> <span class="o">=</span> <span class="s">&quot;joedicastro.com&quot;</span> <span class="n">PROD_PATH</span> <span class="o">=</span> <span class="s">&quot;/home/joedicastro/webapps/joedicastro&quot;</span> <span class="n">LOCAL_WEB</span> <span class="o">=</span> <span class="n">os</span><span class="o">.</span><span class="n">path</span><span class="o">.</span><span class="n">join</span><span class="p">(</span><span class="s">&quot;~/www&quot;</span><span class="p">,</span> <span class="n">PROD</span><span class="p">)</span> <span class="n">ROOT_PATH</span> <span class="o">=</span> <span class="n">os</span><span class="o">.</span><span class="n">path</span><span class="o">.</span><span class="n">abspath</span><span class="p">(</span><span class="n">os</span><span class="o">.</span><span class="n">path</span><span class="o">.</span><span class="n">dirname</span><span class="p">(</span><span class="n">__file__</span><span class="p">))</span> <span class="n">ENV_PATH</span> <span class="o">=</span> <span class="n">os</span><span class="o">.</span><span class="n">path</span><span class="o">.</span><span class="n">join</span><span class="p">(</span><span class="n">ROOT_PATH</span><span class="p">,</span> <span class="s">&quot;env&quot;</span><span class="p">)</span> <span class="n">PELICAN</span> <span class="o">=</span> <span class="n">os</span><span class="o">.</span><span class="n">path</span><span class="o">.</span><span class="n">join</span><span class="p">(</span><span class="n">ROOT_PATH</span><span class="p">,</span> <span class="s">&quot;pelican&quot;</span><span class="p">)</span> <span class="n">CONFIG_FILE</span> <span class="o">=</span> <span class="n">os</span><span class="o">.</span><span class="n">path</span><span class="o">.</span><span class="n">join</span><span class="p">(</span><span class="n">ROOT_PATH</span><span class="p">,</span> <span class="s">&quot;site/pelican.conf.py&quot;</span><span class="p">)</span> <span class="n">OUTPUT</span> <span class="o">=</span> <span class="n">os</span><span class="o">.</span><span class="n">path</span><span class="o">.</span><span class="n">join</span><span class="p">(</span><span class="n">ROOT_PATH</span><span class="p">,</span> <span class="s">&quot;site/output&quot;</span><span class="p">)</span> <span class="k">def</span> <span class="nf">_valid_HTML</span><span class="p">():</span> <span class="sd">&quot;&quot;&quot;Remove the obsolete rel=&quot;&quot; and rev=&quot;&quot; links in footnotes.&quot;&quot;&quot;</span> <span class="k">for</span> <span class="n">path</span><span class="p">,</span> <span class="n">dirs</span><span class="p">,</span> <span class="n">files</span> <span class="ow">in</span> <span class="n">os</span><span class="o">.</span><span class="n">walk</span><span class="p">(</span><span class="n">OUTPUT</span><span class="p">):</span> <span class="k">for</span> <span class="n">fil</span> <span class="ow">in</span> <span class="n">files</span><span class="p">:</span> <span class="k">if</span> <span class="n">fil</span><span class="p">[</span><span class="o">-</span><span class="mi">5</span><span class="p">:]</span> <span class="o">==</span> <span class="s">&quot;.html&quot;</span><span class="p">:</span> <span class="n">local</span><span class="p">(</span><span class="s">&quot;sed -i {0} -r -e &#39;s/re[l|v]=</span><span class="se">\&quot;</span><span class="s">footnote</span><span class="se">\&quot;</span><span class="s">//g&#39; {0}&quot;</span><span class="o">.</span> <span class="n">format</span><span class="p">(</span><span class="n">os</span><span class="o">.</span><span class="n">path</span><span class="o">.</span><span class="n">join</span><span class="p">(</span><span class="n">path</span><span class="p">,</span> <span class="n">fil</span><span class="o">.</span><span class="n">replace</span><span class="p">(</span><span class="s">&quot; &quot;</span><span class="p">,</span> <span class="s">r&quot;\ &quot;</span><span class="p">))))</span> <span class="k">def</span> <span class="nf">_make_env</span><span class="p">():</span> <span class="sd">&quot;&quot;&quot;Make a virtual enviroment&quot;&quot;&quot;</span> <span class="n">local</span><span class="p">(</span><span class="s">&quot;virtualenv {0}&quot;</span><span class="o">.</span><span class="n">format</span><span class="p">(</span><span class="n">ENV_PATH</span><span class="p">))</span> <span class="k">def</span> <span class="nf">_del_env</span><span class="p">():</span> <span class="sd">&quot;&quot;&quot;Delete a virtual enviroment.&quot;&quot;&quot;</span> <span class="n">local</span><span class="p">(</span><span class="s">&quot;rm -rf {0}&quot;</span><span class="o">.</span><span class="n">format</span><span class="p">(</span><span class="n">ENV_PATH</span><span class="p">))</span> <span class="k">def</span> <span class="nf">_clone_pelican</span><span class="p">():</span> <span class="sd">&quot;&quot;&quot;Clone Pelican from repository.&quot;&quot;&quot;</span> <span class="n">local</span><span class="p">(</span><span class="s">&quot;git clone {0}&quot;</span><span class="o">.</span><span class="n">format</span><span class="p">(</span><span class="n">PELICAN_REPOSITORY</span><span class="p">))</span> <span class="k">def</span> <span class="nf">_install</span><span class="p">():</span> <span class="sd">&quot;&quot;&quot;Install Pelican in the virtual enviroment.&quot;&quot;&quot;</span> <span class="k">with</span> <span class="n">lcd</span><span class="p">(</span><span class="n">PELICAN</span><span class="p">):</span> <span class="n">local</span><span class="p">(</span><span class="s">&quot;{0}/bin/python setup.py install&quot;</span><span class="o">.</span><span class="n">format</span><span class="p">(</span><span class="n">ENV_PATH</span><span class="p">))</span> <span class="k">def</span> <span class="nf">_browse</span><span class="p">():</span> <span class="sd">&quot;&quot;&quot;Browse the local Apache site.&quot;&quot;&quot;</span> <span class="n">local</span><span class="p">(</span><span class="s">&quot;firefox -new-window http://localhost/joedicastro.com 2&gt;/dev/null &amp;&quot;</span><span class="p">)</span> <span class="k">def</span> <span class="nf">_gen</span><span class="p">(</span><span class="n">autoreload</span><span class="o">=</span><span class="bp">False</span><span class="p">):</span> <span class="sd">&quot;&quot;&quot;Generate the site from source.&quot;&quot;&quot;</span> <span class="n">local</span><span class="p">(</span><span class="s">&quot;{0}/bin/pelican {2} -s {1}&quot;</span><span class="o">.</span><span class="n">format</span><span class="p">(</span><span class="n">ENV_PATH</span><span class="p">,</span> <span class="n">CONFIG_FILE</span><span class="p">,</span> <span class="s">&quot;-r&quot;</span> <span class="k">if</span> <span class="n">autoreload</span> <span class="k">else</span> <span class="s">&quot;&quot;</span><span class="p">))</span> <span class="k">def</span> <span class="nf">_clean</span><span class="p">():</span> <span class="s">&quot;Remove the output folder.&quot;</span> <span class="n">local</span><span class="p">(</span><span class="s">&quot;rm -rf {0}&quot;</span><span class="o">.</span><span class="n">format</span><span class="p">(</span><span class="n">OUTPUT</span><span class="p">))</span> <span class="k">def</span> <span class="nf">_local_deploy</span><span class="p">():</span> <span class="sd">&quot;&quot;&quot;Deploy to the local apache web server.&quot;&quot;&quot;</span> <span class="n">local</span><span class="p">(</span><span class="s">&quot;rm -rf {0}&quot;</span><span class="o">.</span><span class="n">format</span><span class="p">(</span><span class="n">LOCAL_WEB</span><span class="p">))</span> <span class="n">local</span><span class="p">(</span><span class="s">&quot;cp -r {0} {1}&quot;</span><span class="o">.</span><span class="n">format</span><span class="p">(</span><span class="n">OUTPUT</span><span class="p">,</span> <span class="n">LOCAL_WEB</span><span class="p">))</span> <span class="k">def</span> <span class="nf">pull_pelican</span><span class="p">():</span> <span class="sd">&quot;&quot;&quot;Update Pelican to last revision from repository.&quot;&quot;&quot;</span> <span class="k">with</span> <span class="n">lcd</span><span class="p">(</span><span class="n">PELICAN</span><span class="p">):</span> <span class="n">local</span><span class="p">(</span><span class="s">&quot;git pull&quot;</span><span class="p">)</span> <span class="k">def</span> <span class="nf">bootstrap</span><span class="p">():</span> <span class="sd">&quot;&quot;&quot;Get Pelican and install it in a virtual enviroment.&quot;&quot;&quot;</span> <span class="k">with</span> <span class="n">settings</span><span class="p">(</span><span class="n">warn_only</span><span class="o">=</span><span class="bp">True</span><span class="p">):</span> <span class="n">_del_env</span><span class="p">()</span> <span class="n">_make_env</span><span class="p">()</span> <span class="k">with</span> <span class="n">settings</span><span class="p">(</span><span class="n">warn_only</span><span class="o">=</span><span class="bp">True</span><span class="p">):</span> <span class="n">_clone_pelican</span><span class="p">()</span> <span class="n">_install</span><span class="p">()</span> <span class="k">def</span> <span class="nf">regen</span><span class="p">():</span> <span class="sd">&quot;&quot;&quot;Regenerate the site from source.&quot;&quot;&quot;</span> <span class="n">_clean</span><span class="p">()</span> <span class="n">_gen</span><span class="p">()</span> <span class="n">_valid_HTML</span><span class="p">()</span> <span class="n">_local_deploy</span><span class="p">()</span> <span class="nd">@hosts</span><span class="p">(</span><span class="s">&quot;my_user@&quot;</span> <span class="o">+</span> <span class="n">PROD</span><span class="p">)</span> <span class="k">def</span> <span class="nf">publish</span><span class="p">():</span> <span class="sd">&quot;&quot;&quot;Publish into remote web server with rsync.&quot;&quot;&quot;</span> <span class="n">regen</span><span class="p">()</span> <span class="n">_browse</span><span class="p">()</span> <span class="k">if</span> <span class="n">confirm</span><span class="p">(</span><span class="s">&quot;¿Estas seguro de querer publicarlo?&quot;</span><span class="p">):</span> <span class="n">rsync_project</span><span class="p">(</span><span class="n">PROD_PATH</span><span class="p">,</span> <span class="n">OUTPUT</span> <span class="o">+</span> <span class="s">&quot;/&quot;</span><span class="p">,</span> <span class="n">delete</span><span class="o">=</span><span class="bp">True</span><span class="p">)</span> <span class="k">def</span> <span class="nf">new</span><span class="p">(</span><span class="n">title</span><span class="p">):</span> <span class="sd">&quot;&quot;&quot;Create a new blog article.&quot;&quot;&quot;</span> <span class="n">local</span><span class="p">(</span><span class="s">&quot;gedit --new-window {0}/site/source/blog/{1}.md 2&gt;/dev/null &amp;&quot;</span><span class="o">.</span> <span class="n">format</span><span class="p">(</span><span class="n">ROOT_PATH</span><span class="p">,</span> <span class="n">title</span><span class="o">.</span><span class="n">replace</span><span class="p">(</span><span class="s">&quot; &quot;</span><span class="p">,</span> <span class="s">&quot;\ &quot;</span><span class="p">)))</span> <span class="n">local</span><span class="p">(</span><span class="s">&quot;firefox --new-window {0}/index.html &amp;&quot;</span><span class="o">.</span><span class="n">format</span><span class="p">(</span><span class="n">OUTPUT</span><span class="p">))</span> <span class="n">_gen</span><span class="p">(</span><span class="bp">True</span><span class="p">)</span> <span class="k">def</span> <span class="nf">img4web</span><span class="p">(</span><span class="n">delete</span><span class="o">=</span><span class="bp">True</span><span class="p">,</span> <span class="n">source</span><span class="o">=</span><span class="s">&quot;&quot;</span><span class="p">):</span> <span class="sd">&quot;&quot;&quot;Optimize .jpg &amp; .png images and copy them into source pictures dir.&quot;&quot;&quot;</span> <span class="n">local</span><span class="p">(</span><span class="s">&quot;./img4web.py -d {0} {1} {2}&quot;</span><span class="o">.</span> <span class="n">format</span><span class="p">(</span><span class="n">os</span><span class="o">.</span><span class="n">path</span><span class="o">.</span><span class="n">join</span><span class="p">(</span><span class="n">ROOT_PATH</span><span class="p">,</span> <span class="s">&quot;site/source/pictures&quot;</span><span class="p">),</span> <span class="s">&quot;--delete&quot;</span> <span class="k">if</span> <span class="n">delete</span> <span class="k">else</span> <span class="s">&quot;&quot;</span><span class="p">,</span> <span class="s">&quot;-s {0}&quot;</span><span class="o">.</span><span class="n">format</span><span class="p">(</span><span class="n">source</span><span class="p">)</span> <span class="k">if</span> <span class="n">source</span> <span class="k">else</span> <span class="s">&quot;&quot;</span><span class="p">))</span> </pre></div> <p>Ahora, veremos el funcionamiento básico que nos permite este script. Primero vemos las tareas que tenemos disponibles:</p> <div class="codehilite"><pre><span class="gp">$</span> fab -l <span class="go"> fabfile.py: A fabric script for generate my personal blog</span> <span class="go">Available commands:</span> <span class="go"> bootstrap Get Pelican and install it in a virtual enviroment.</span> <span class="go"> img4web Optimize .jpg &amp; .png images and copy them into source pict...</span> <span class="go"> new Create a new blog article.</span> <span class="go"> publish Publish into remote server with rsync.</span> <span class="go"> pull_pelican Update Pelican to last revision from repository.</span> <span class="go"> regen Regenerate the site from source.</span> </pre></div> <p>Veamos que hacen cada una de ellas:</p> <ul> <li> <p><strong><em>bootstrap</em></strong> Si observamos el código, veremos que lo hace es, en este orden: eliminar cualquier entorno virtual previo, crear un entorno virtual nuevo, descargar Pelican desde el repositorio (si no lo hemos hecho anteriormente) e instalar Pelican dentro de este entorno virtual. Y todo esto en un solo paso, casi todos los comandos que explicaba en <a href="http://joedicastro.com/pelican-introduccion-e-instalacion.html">Pelican - Introducción e Instalación</a> con solo teclear <code>fab bootstrap</code>. Así de fácil. Con este comando podemos tanto crear una instalación de Pelican desde cero, como actualizar la instalación de Pelican después de actualizar este a la última versión con <strong><em>pull-pelican</em></strong>. Siguiendo con nuestro ejemplo, lo que haría este comando es crear los directorios <em>env</em> y <em>pelican</em> dentro de <em>myblog.com</em> con el entorno virtual creado y pelican instalado.</p> <div class="codehilite"><pre><span class="gp">$</span> fab bootstrap </pre></div> </li> <li> <p><strong><em>img4web</em></strong> Este comando hace uso del script que describía en <a href="http://joedicastro.com/optimizar-imagenes-para-la-web.html">Optimizar imágenes para la web</a> para hacer precisamente eso, reducir el peso de las imágenes que empleo en los artículos. Lo que hago es a medida que voy escribiendo el articulo es ir guardando las imágenes en el directorio raíz (en nuestro ejemplo, <em>myblog.com/</em>) y cuando lo termino, simplemente ejecuto el comando <code>fab img4web</code> y este me optimiza las imágenes, me guarda las optimizadas en el directorio de imágenes del contenido (<em>myblog.com/site/source/pictures/</em>) y me elimina las imágenes originales del directorio raíz. Cuando termina me muestra un resumen con la cantidad de imágenes procesadas y el ahorro en espacio conseguido. Espacio en disco ahorrado que se resume en menos ancho de banda consumido y en páginas web que se cargan más rápido.</p> <div class="codehilite"><pre><span class="gp">$</span> fab img4web </pre></div> </li> <li> <p><strong><em>new</em></strong> Con este creo o edito los artículos del blog. Realiza tres funciones: me abre una venta de Gedit con el articulo que le indico con la extensión <code>.md</code>, me abre una ventana de Firefox que me muestra el fichero <em>index.html</em> del directorio del sitio generado por Pelican (<em>myblog.com/site/output/index.html</em>) y finalmente me activa Pelican con la opción <code>autoreload</code>. Luego empleando el plugin <strong>Grid</strong> de Compiz, divido la pantalla en dos mitades y coloco a la izquierda Gedit y a la derecha Firefox. Esto me permite, como explicaba en <a href="http://joedicastro.com/de-drupal-a-pelican.html">De Drupal a Pelican</a> editar el contenido y previsualizar el resultado casi en tiempo real, disponiendo al mismo tiempo de un buen corrector ortográfico. </p> <div class="codehilite"><pre><span class="gp">$</span> fab new:<span class="s2">&quot;Articulo de prueba&quot;</span> </pre></div> </li> <li> <p><strong><em>publish</em></strong> El más importante, el que sube los artículos al servidor web. Publicar el contenido de la web es tan sencillo como ejecutar este comando. Lo que hace es regenerar el contenido (por si hubiera algún cambio sin guardar) y luego mostrarme el resultado en firefox. Pero el resultado que me muestra no es el del directorio de salida de Pelican, si no de una copia que tengo en un servidor Apache local. Esto me permite ver los cambios de manera más fiel a la versión web, puesto que hace uso del fichero .htaccess y de las reglas que tengo establecidas en él. Finalmente me pregunta si realmente deseo publicar el contenido, por si se me hubiera escapado algo. Si le digo que no, aborta la publicación, pero si le digo que si, me sincroniza el contenido de la carpeta local con la remota empleando <strong>rsync</strong>. De esta manera solo se transmiten los ficheros nuevos, se borran los que se hayan eliminado en local y <strong>solo transmite la parte que haya cambiado de los archivos modificados</strong>. Gracias a esto, modificar o añadir contenido es cuestión de segundos. Y sencillisimo.</p> <div class="codehilite"><pre><span class="gp">$</span> fab publish </pre></div> </li> <li> <p><strong><em>pull_pelican</em></strong> Nos sirve para actualizar Pelican a la última revisión disponible en el repositorio oficial. Si después de ejecutarlo, queremos instalar la nueva versión en nuestro entorno virtual para poder emplearla, simplemente tenemos que volver a ejecutar <code>fab bootstrap</code> y todo se realizará de forma automática.</p> <div class="codehilite"><pre><span class="gp">$</span> fab pull_pelican </pre></div> </li> <li> <p><strong><em>regen</em></strong> El proceso principal, es el que le pide a Pelican que genere el sitio web a partir de nuestro directorio de origen. También realiza varios procesos: primero eliminar el directorio de salida actual (para tener una copia fresca), genera el nuevo contenido, luego procesa los archivos para que validen en HTML5 y finalmente hace una copia del directorio de salida a mi servidor local Apache. El procesar los archivos para validar en HTML5 se debe a que markdown crea unos enlaces <code>rel = "footnote"</code> y <code>rev = "footnote"</code> en las notas al pie que se han quedado obsoletos y no son necesarios. De momento es un post-procesado, pero puede que finalmente modifique Pelican para que se haga en tiempo de generación del sitio. Aunque creo que el rendimiento de esta manera sería menor que emplear el comando <code>sed</code> que ejecuta este proceso, será cuestión de probarlo. También se podría modificar markdown.</p> </li> </ul> <p>Con solo <strong>6</strong> comandos tengo automatizadas todas las tareas básicas para administrar y crear contenido en mi blog. Ni siquiera el potente y buenísimo <a href="http://drupal.org/project/drush">drush</a> de Drupal me permitía este nivel de automatización (aunque se acercaba bastante). De esta manera solo me tengo que preocupar de crear artículos y de las posibles personalizaciones que le quiera realizar al tema del sitio. Me olvido de todo lo demás, de todo lo que conlleva un CMS. Solo hay una manera de bloguear más cómoda para los que a estos les parezca algo complejo, servicios como Tumblr. Aunque si quieres tener el control sobre tu sitio, no conozco manera más cómoda y con más ventajas que esta. </p>