joe di castrohttp://joedicastro.com2011-05-06T22:55:00+02:00Generar informes de cambios en paquetes instalados en Debian y Ubuntu.2011-05-06T22:55:00+02:00joe di castrohttp://joedicastro.com/generar-informes-de-cambios-en-paquetes-instalados-en-debian-y-ubuntu.html<p>Una de las políticas de seguridad que tengo con mis sistemas <strong>Linux</strong>, es
además de efectuar respaldos periódicos (diarios) del contenido del directorio
<code>/home</code> (en mi caso siempre es una partición o disco independiente), es siempre
guardar también una lista de los <abbr title="En sistemas *NIX se denomina así a los programas">paquetes</abbr> instalados en los mismos.
Prácticamente nunca hago una copia completa de la partición o disco de de
sistema, y aunque si hago una copia de los directorios más importantes, prefiero
tener siempre una relación actualizada de los <abbr title="En sistemas *NIX se denomina así a los programas">paquetes</abbr> instalados en cada
maquina.</p>
<p>En sistemas Linux <a href="http://debian.org">Debian</a> y otras distribuciones derivadas (<a href="http://ubuntu.com">Ubuntu</a>,
<a href="http://www.linuxmint.com/">Mint</a>, <a href="http://www.mepis.org/">Mepis</a>, <a href="http://www.knoppix.org/">Knoppix</a>, ...) obtener esta lista es realmente
sencillo, pues solo es necesario ejecutar este comando:</p>
<div class="codehilite"><pre><span class="go">dpkg -l > lista_paquetes.txt</span>
</pre></div>
<p>Y con eso, se generaría una lista de los <abbr title="En sistemas *NIX se denomina así a los programas">paquetes</abbr> instalados<sup id="fnref:nota"><a href="#fn:nota" rel="footnote">1</a></sup> en el
sistema en el fichero <em>lista_paquetes.txt</em>. Y mantener actualizado este fichero
es tan sencillo como programar (via <code>crontab</code> por ejemplo) la ejecución de este
comando con un sencillo <a href="http://es.wikipedia.org/wiki/">script</a> <a href="http://es.wikipedia.org/wiki/Shell_de_UNIX">shell</a>. Y así lo he realizado durante
años, hasta que quise tener aun más información.</p>
<p>Entonces me interesó conocer también los cambios que se producen en los
<abbr title="En sistemas *NIX se denomina así a los programas">paquetes</abbr>, es decir nuevas (des)instalaciones y actualizaciones. Es lógico pensar
que si uno mismo es el que las realiza, pues ya sabe esta información de primera
mano. Pero la memoria es frágil y no demasiado confiable, además ¿que ocurre
cuando en una misma maquina tienen permisos para administrar los <abbr title="En sistemas *NIX se denomina así a los programas">paquetes</abbr> más de
un usuario? </p>
<p>Se pueden conocer estos cambios de los <abbr title="En sistemas *NIX se denomina así a los programas">paquetes</abbr> en el tiempo de varios modos,
desde acudir a los logs de <strong>dpkg</strong> en <code>/var/log/dpkg.log</code> y examinarlos con
algún analizador de logs (o el más sencillo e inmediato comando <code>less</code>) hasta
consultarlos de una manera más sencilla y gráfica con el gestor de <abbr title="En sistemas *NIX se denomina así a los programas">paquetes</abbr>
<strong>Synaptic</strong> (en el menú <em>Archivo -> Histórico</em>). Pero me interesaba automatizar
esto, para conocer esos cambios poco después de que se produjeran y lo lógico
era emplear el mismo script shell que empleaba para generar la lista de <abbr title="En sistemas *NIX se denomina así a los programas">paquetes</abbr>
instalados. Así lo hice entonces, combinando los comandos <code>awk</code>, <code>grep</code>, <code>diff</code>
y <code>mail</code> con el comando del principio para obtener la lista, tenia los cambios
en mi buzón de correo al poco tiempo de producirse. Y ha funcionado
perfectamente hasta ahora.</p>
<p>Pero desde que me introduje en el lenguaje <strong>Python</strong>, he ido migrando poco a
poco los scripts <a href="http://es.wikipedia.org/wiki/Bash">bash</a> que tengo para reescribirlos en este lenguaje. Y
recientemente le ha tocado a este. Ha sido muy fácil y la verdad es que el
resultado aunque funcionalmente es el mismo, me ha permitido entregar unos
informes más elegantes y fáciles de interpretar de un golpe de vista. </p>
<p>Ahora, cuando se realiza algún cambio en los <abbr title="En sistemas *NIX se denomina así a los programas">paquetes</abbr> del sistema, de forma
automática, por mi o por otro usuario autorizado, yo recibo en mi correo un
informe similar a este:</p>
<div class="codehilite"><pre>SCRIPT =========================================================================
dpkg_diff (ver. 0.1)
http://joedicastro.com
Changes of packages installed on yourmachine
===============================================================================
START TIME =====================================================================
Thursday 05/05/11, 10:30:01
===============================================================================
INSTALLED PACKAGES LIST FILE ___________________________________________________
/your/path/to/package_list.txt
CHANGES DIFF ___________________________________________________________________
<span class="gd">--- previous Wed May 4 22:59:51 2011</span>
<span class="gi">+++ current Thu May 5 10:30:01 2011</span>
<span class="gu">@@ -34,1 +34,1 @@</span>
<span class="gd">-ii apt 0.7.25.3ubuntu9.3</span>
<span class="gi">+ii apt 0.7.25.3ubuntu9.4</span>
<span class="gu">@@ -36,2 +36,2 @@</span>
<span class="gd">-ii apt-transport-https 0.7.25.3ubuntu9.3</span>
<span class="gd">-ii apt-utils 0.7.25.3ubuntu9.3</span>
<span class="gi">+ii apt-transport-https 0.7.25.3ubuntu9.4</span>
<span class="gi">+ii apt-utils 0.7.25.3ubuntu9.4</span>
END TIME =======================================================================
Thursday 05/05/11, 10:30:01
===============================================================================
</pre></div>
<p>Donde se puede ver la información que nos indica el script que ha generado el
correo, la maquina en la que se han realizado los cambios, la localización del
fichero con la relación de todos los <abbr title="En sistemas *NIX se denomina así a los programas">paquetes</abbr> instalados en el sistema, la fecha
y hora de la ejecución del script y el <a href="http://es.wikipedia.org/wiki/Diff">diff</a> con la relación de cambios en
los <abbr title="En sistemas *NIX se denomina así a los programas">paquetes</abbr> en formato <a href="http://en.wikipedia.org/wiki/Diff#Unified_format">Unified format</a>. En este ejemplo podemos ver como se
han actualizado tres <abbr title="En sistemas *NIX se denomina así a los programas">paquetes</abbr> en esa maquina. No nos dice la hora en que se
efectuó la modificación (podemos verlo en el log de <code>dpkg</code>) pero si podemos
saber que se efectuó entre dos intervalos de ejecución del script, información
que será más que suficiente la mayoría de las veces. Para datos concretos, ir al
log y filtrarlo con <code>grep</code> y en segundos sabremos la respuesta. Si además como
es mi caso, ejecutamos el script cada 12 o 24 horas, pues será fácil saber
cuando se han realizado los cambios.</p>
<p>Un fragmento de código de la parte principal del script es el siguiente:</p>
<div class="codehilite"><pre><span class="k">def</span> <span class="nf">pretty_diff</span><span class="p">(</span><span class="n">diff</span><span class="p">):</span>
<span class="sd">"""Better format for package lines in diff."""</span>
<span class="n">pkg</span> <span class="o">=</span> <span class="p">{}</span> <span class="c"># diff's packages lines</span>
<span class="c"># Get columns info for diff package lines</span>
<span class="k">for</span> <span class="n">idx</span><span class="p">,</span> <span class="n">line</span> <span class="ow">in</span> <span class="nb">enumerate</span><span class="p">(</span><span class="n">diff</span><span class="p">):</span>
<span class="k">if</span> <span class="ow">not</span> <span class="n">findall</span><span class="p">(</span><span class="s">"^-{3}|^\+{3}|^@{2}"</span><span class="p">,</span> <span class="n">line</span><span class="p">):</span>
<span class="c"># split the line in columns and remove the description column</span>
<span class="n">cols</span> <span class="o">=</span> <span class="n">split</span><span class="p">(</span><span class="s">"\s{2,}"</span><span class="p">,</span> <span class="n">line</span><span class="p">,</span> <span class="mi">3</span><span class="p">)[:</span><span class="mi">3</span><span class="p">]</span>
<span class="c"># A nested dict, for each line index we have a dict that contains </span>
<span class="c"># the package line columns: 's' (status), 'n' (name) & 'v' (version)</span>
<span class="c"># and the width of the name column: w(width)</span>
<span class="n">pkg</span><span class="p">[</span><span class="n">idx</span><span class="p">]</span> <span class="o">=</span> <span class="p">{</span><span class="s">'s'</span><span class="p">:</span><span class="n">cols</span><span class="p">[</span><span class="mi">0</span><span class="p">],</span> <span class="s">'n'</span><span class="p">:</span><span class="n">cols</span><span class="p">[</span><span class="mi">1</span><span class="p">],</span> <span class="s">'v'</span><span class="p">:</span><span class="n">cols</span><span class="p">[</span><span class="mi">2</span><span class="p">],</span> <span class="s">'w'</span><span class="p">:</span><span class="nb">len</span><span class="p">(</span><span class="n">cols</span><span class="p">[</span><span class="mi">1</span><span class="p">])}</span>
<span class="c"># maximum width in packages' name column for all lines</span>
<span class="n">mxw</span> <span class="o">=</span> <span class="nb">max</span><span class="p">((</span><span class="n">pkg</span><span class="p">[</span><span class="n">index</span><span class="p">][</span><span class="s">'w'</span><span class="p">]</span> <span class="k">for</span> <span class="n">index</span> <span class="ow">in</span> <span class="n">pkg</span><span class="p">))</span>
<span class="c"># Replace each package line for a prettier one (more legible) </span>
<span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="nb">len</span><span class="p">(</span><span class="n">diff</span><span class="p">)):</span>
<span class="k">if</span> <span class="n">i</span> <span class="ow">in</span> <span class="n">pkg</span><span class="p">:</span>
<span class="n">diff</span><span class="p">[</span><span class="n">i</span><span class="p">]</span> <span class="o">=</span> <span class="p">(</span><span class="s">"{0} {1} {2}"</span><span class="o">.</span><span class="n">format</span><span class="p">(</span><span class="n">pkg</span><span class="p">[</span><span class="n">i</span><span class="p">][</span><span class="s">'s'</span><span class="p">],</span> <span class="n">pkg</span><span class="p">[</span><span class="n">i</span><span class="p">][</span><span class="s">'n'</span><span class="p">]</span> <span class="o">+</span> <span class="s">" "</span> <span class="o">*</span>
<span class="p">(</span><span class="n">mxw</span> <span class="o">-</span> <span class="n">pkg</span><span class="p">[</span><span class="n">i</span><span class="p">][</span><span class="s">'w'</span><span class="p">]),</span> <span class="n">pkg</span><span class="p">[</span><span class="n">i</span><span class="p">][</span><span class="s">'v'</span><span class="p">]))</span>
<span class="k">return</span> <span class="n">diff</span>
<span class="k">def</span> <span class="nf">main</span><span class="p">(</span><span class="n">old</span><span class="o">=</span><span class="s">""</span><span class="p">):</span>
<span class="sd">"""Main section"""</span>
<span class="c"># The path to store the debian packages list file</span>
<span class="n">pkg_lst_file</span> <span class="o">=</span> <span class="s">"./package_list.txt"</span>
<span class="c"># Start logging</span>
<span class="n">log</span> <span class="o">=</span> <span class="n">Logger</span><span class="p">()</span>
<span class="n">url</span> <span class="o">=</span> <span class="s">"http://joedicastro.com"</span>
<span class="n">head</span> <span class="o">=</span> <span class="s">"Changes of packages installed on {0}"</span><span class="o">.</span><span class="n">format</span><span class="p">(</span><span class="n">platform</span><span class="o">.</span><span class="n">node</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="n">url</span><span class="p">,</span> <span class="n">head</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">"Start time"</span><span class="p">)</span>
<span class="c"># Read the old file and clean the list</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">pkg_lst_file</span><span class="p">):</span>
<span class="n">old</span> <span class="o">=</span> <span class="nb">open</span><span class="p">(</span><span class="n">pkg_lst_file</span><span class="p">,</span> <span class="s">'r'</span><span class="p">)</span><span class="o">.</span><span class="n">readlines</span><span class="p">()</span>
<span class="n">old_date</span> <span class="o">=</span> <span class="n">time</span><span class="o">.</span><span class="n">ctime</span><span class="p">(</span><span class="n">os</span><span class="o">.</span><span class="n">stat</span><span class="p">(</span><span class="n">pkg_lst_file</span><span class="p">)</span><span class="o">.</span><span class="n">st_mtime</span><span class="p">)</span>
<span class="c"># Get the current list of debian packages installed on system </span>
<span class="n">current</span> <span class="o">=</span> <span class="n">Popen</span><span class="p">([</span><span class="s">"dpkg"</span><span class="p">,</span> <span class="s">"-l"</span><span class="p">],</span> <span class="n">stdout</span><span class="o">=</span><span class="n">PIPE</span><span class="p">)</span><span class="o">.</span><span class="n">stdout</span><span class="o">.</span><span class="n">readlines</span><span class="p">()</span>
<span class="c"># First, save the list file</span>
<span class="k">with</span> <span class="nb">open</span><span class="p">(</span><span class="n">pkg_lst_file</span><span class="p">,</span> <span class="s">'w'</span><span class="p">)</span> <span class="k">as</span> <span class="n">out</span><span class="p">:</span>
<span class="n">out</span><span class="o">.</span><span class="n">writelines</span><span class="p">(</span><span class="n">current</span><span class="p">)</span>
<span class="n">curr_date</span> <span class="o">=</span> <span class="n">time</span><span class="o">.</span><span class="n">ctime</span><span class="p">(</span><span class="n">os</span><span class="o">.</span><span class="n">stat</span><span class="p">(</span><span class="n">pkg_lst_file</span><span class="p">)</span><span class="o">.</span><span class="n">st_mtime</span><span class="p">)</span>
<span class="c"># Compare both lists</span>
<span class="k">if</span> <span class="n">old</span><span class="p">:</span>
<span class="n">file_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">realpath</span><span class="p">(</span><span class="n">pkg_lst_file</span><span class="p">)</span>
<span class="n">diff</span> <span class="o">=</span> <span class="p">[</span><span class="n">ln</span> <span class="k">for</span> <span class="n">ln</span> <span class="ow">in</span> <span class="n">unified_diff</span><span class="p">(</span><span class="n">old</span><span class="p">,</span> <span class="n">current</span><span class="p">,</span> <span class="n">fromfile</span><span class="o">=</span><span class="s">"previous"</span><span class="p">,</span>
<span class="n">tofile</span><span class="o">=</span><span class="s">"current "</span><span class="p">,</span>
<span class="n">fromfiledate</span><span class="o">=</span><span class="n">old_date</span><span class="p">,</span>
<span class="n">tofiledate</span><span class="o">=</span><span class="n">curr_date</span><span class="p">,</span> <span class="n">n</span><span class="o">=</span><span class="mi">0</span><span class="p">,</span>
<span class="n">lineterm</span><span class="o">=</span><span class="s">""</span><span class="p">)]</span>
<span class="c"># If there are differences write the log to disk and send mail</span>
<span class="k">if</span> <span class="n">diff</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">"Installed packages list file"</span><span class="p">,</span> <span class="n">file_path</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">"Changes diff"</span><span class="p">,</span> <span class="n">pretty_diff</span><span class="p">(</span><span class="n">diff</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">"End time"</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="c"># Send mail to current system user. For other options, see logger </span>
<span class="c"># module info</span>
<span class="n">log</span><span class="o">.</span><span class="n">send</span><span class="p">(</span><span class="s">"Debian packages changes"</span><span class="p">)</span>
</pre></div>
<p>Este script hace uso del modulo logger, que comento en este <a href="http://joedicastro.com/logger_informes_legibles_para_tus_scripts_python">artículo</a>. </p>
<p>Para obtener la versión más reciente del script y del modulo logger, consultar
mi repositorio <em>Python Recipes</em> en <a href="http://github.com/joedicastro/python-recipes">github</a>.</p>
<div class="footnote">
<hr />
<ol>
<li id="fn:nota">
<p>Es una relación de los programas instalados empleando el sistema de
paqueteria de Debian, los programas instalados manualmente vía compilación
u otros medios no aparecerán en ella.  <a href="#fnref:nota" rev="footnote" title="Jump back to footnote 1 in the text">↩</a></p>
</li>
</ol>
</div>