«««

11 października 2010

Budujemy plugin dla yum

Zaczęło się jak zwykle niewinnie. Dawno dawno temu, jeszcze przed wynalezieniem węgla zauważyłem nie ja jeden, że w /etc gromadzą się pliki z końcówkami .rpmnew & .rpmsave . Ot zwykłe kopie, ale bałaganią. Zacząłem więc co jakiś czas sprawdzać, czy pojawili się nowi przybysze, porównywać je za pomocą meld (przyjemniejszy "diff") i sprzątać śmieci.
Z wiekiem systemu narastała kupka z własnymi modyfikacjami w /etc, więc dodałem repo z hg. Pozostało to zautomatyzować...

yum-plugin-hg

Kierując się poradnikiem Writing Yum Plugins naskrobałem takie coś:
$ cat /etc/yum/pluginconf.d/hg.conf
[main]
enabled=1
# lista folderów pod kontrolą hg (domyślnie tylko /etc)
# path=/etc:/var/log

# opcjonalnie można podać alternatywną ścieżkę do: hg, meld
# hg=/usr/local/bin/hg
# trik wyłączy commit repo
# hg=/bin/echo
$ cat /usr/lib/yum-plugins/hg.py
#!/usr/bin/env python
#-*- coding:utf-8 -*-

import os,sys,filecmp
from subprocess import Popen, PIPE, check_call, CalledProcessError
from yum.plugins import TYPE_CORE, TYPE_INTERACTIVE
if os.environ.has_key('DISPLAY'):
 import pygtk
 import gtk

requires_api_version = '2.6'
plugin_type = (TYPE_CORE, TYPE_INTERACTIVE)

def err(comment):
 sys.stderr.write(comment)

def sh(cmd):
 """ http://docs.python.org/library/subprocess.html """
 p = Popen(cmd, shell=True, stdout=PIPE, stderr=PIPE)
 stdout, stderr = p.communicate()
 err(stderr)
 out(stdout)

def mercurial(path):
 # Test: czy jest repo, jak nie to robimy nowe
 try:
  check_call([hg,"st"],stdout=PIPE,stderr=PIPE)
 except CalledProcessError:
  out('creating a new hg repository in '+path+' directory')
  sh(hg+" init")
 out('hg addremove ...')
 sh(hg+" addremove")
 out('hg status ...')
 sh(hg+" st")
 out('hg commit ... it may take a while')
 sh(hg+" ci -m 'commit by yum plugin'")

 # access only for root
 os.chmod('.hg',700)
 os.chown('.hg',0,0)

def question(name):
 dialog = gtk.Dialog("Are you sure?", None, 0,
     (gtk.STOCK_NO, gtk.RESPONSE_NO,
     gtk.STOCK_YES, gtk.RESPONSE_YES))
 label = gtk.Label("\nDelete file "+name+"?\n")
 label.show()
 dialog.get_content_area().add(label)
 dialog.set_position(gtk.WIN_POS_CENTER_ALWAYS)
 response = dialog.run()
 dialog.destroy()
 while gtk.events_pending():
  gtk.main_iteration(False)
 if response == gtk.RESPONSE_YES:
  return True
 else:
  return False

def find_confs(conf='.rpmsave'):
 for root, dirs, files in os.walk("/etc"):
  for f in files:
   if f.endswith(conf):
    backup=root+'/'+f
    current=backup.split(conf)[0]
    merg_confs(current,backup)

def merg_confs(current,backup):
 if os.path.isfile(current) and os.path.isfile(backup):
  if filecmp.cmp(current,backup): # ~diff
   out("files: '"+current+"' & '"+backup+"' are identical")
   out("removing: '"+backup+"'")
   os.remove(backup)
  elif os.environ.has_key('DISPLAY'):
   out("comparing files: '"+current+"' & '"+backup+"' with meld")
   sh(meld+" "+current+" "+backup)
   if question(backup):
    out("removing: '"+backup+"'")
    os.remove(backup)
  else:
   err("no X ;( use 'diff' to compare files: '"
   +current+"' & '"+backup+"'")
 elif not os.path.exists(current):
  err("no such file or directory: '"+current+"'")
  out("renaming: '"+backup+"' --> '"+current+"'")
  os.rename(backup,current)

def main(path):
 if os.getuid() != 0:
  return False
 cwd=os.getcwd()
 for dir in path.split(':'):
  try:
   os.chdir(dir)
  except:
   show("no such directory: '"+dir+"'")
  else:
   out('hg: '+dir)
   if dir == '/etc':
    find_confs(conf='.rpmsave')
    find_confs(conf='.rpmnew')
   mercurial(dir)
  finally:
   os.chdir(cwd) # back to main directory

def close_hook(conduit):
 global out, meld, hg
 path = conduit.confString('main', 'path', default='/etc')
 meld = conduit.confString('main', 'meld', default='/usr/bin/meld')
 hg   = conduit.confString('main', 'hg'  , default='/usr/bin/hg')
 def out(comment):
  if comment:
   conduit.info(2,comment)
 main(path)

def simulation(): # close_hook(conduit)
 global out, meld, hg
 path='/etc:/var/log'
 meld='/usr/bin/meld'
 hg='/usr/bin/hg'
 def out(comment):
  sys.stdout.write(comment) # print if not Null
 main(path)

if __name__ == '__main__':
 simulation()
Warto dodać, że podczas testowania pluginu można uruchomić yum'a z opcją -d 10 co ozn. wypisywanie wszystkich komunikatów.

Do pobrania:

Prosto z borzole.repo, albo paczka rpm

Aktualizacja:

  • 2010.10.22-23:50
    Poprawka błędu, który uniemożliwiał zainicjowanie repo. Dorzuciłem też przykładowy plik /etc/.hgignore
  • 2010.10.21-23:57
    Zrobiłem refaktoryzację tej partyzantki i pozbyłem się większości sztucznych konstrukcji. Zarówno 'meld' jak i 'hg' pozostają wywołane przez moduł 'subprocess' pomimo, że można napisać to w czystym pythonie. Niestety z natywnym 'hg' nie mogłem sobie poradzić, a 'meld' wymagał innego kodu dla Fedora 13 i 14.

Brak komentarzy: