Table des matières

Update de geonetwork

samedi 27 mai 2017, 09:08:59 (UTC+0200) cmartin

Contient un script à lancer pour faire les mises à jour de geonetwork. Tout ce qu'il faut c'est :

  1. connaître le dossier où le war courant est déployé
  2. connaître le chemin du war actuel
  3. avoir un nouveau war sous la main
Valable pour les installations de geonetwork faites en déployant un WAR, pas en utilisant le JAR d'installation


Testé pour passage de geonetwork 3.0.4 à 3.2.1

SANS GARANTIE

Précautions

AVANT de lancer le script :

En cas d'échec ça permettra de remettre les choses comme il faut.

Avant de lancer le script

Arreter tomcat avant de lancer le script !!

fonctionnement du script

DOIT ÊTRE LANCÉ SOUS LE COMPTE ROOT

  1. inspection du war actuel
  2. détection des fichiers ajoutés
  3. détection des fichiers modifiés (comme connexion à une BDD)
  4. inspection du nouveau WAR
  5. mise en place du nouveau WAR dans le dossier
  6. déploiement du nouveau WAR
  7. suppression des fichiers obsoletes
  8. propose de remettre les fichiers modifiés (mais il faut vérifier manuellement que les modifs sont toujours valables, sinon, patatra)
  9. suppression du cache

Après utilisation du script

Redémarrage de tomcat à votre charge.

Un mot sur le script

C'était un machin en bash, fait par copier/coller de l'historique de mon shell lors des sessions de mise à jour. Évidemment, un tel machin est très instable et ne demande qu'à se casser la figure à la première occasion. Ce qui n'a pas manqué de se produire, lorsque que la locale était différente sur la machine de test par rapport à la machine de prod…

Tout réécrit en python. Le script n'est pas plus clair, AMHA, mais plus robuste certainement. Réécrit dans la nuit d'un vendredi à un samedi, d'où le copyright à Schplurtz.

NB pour moi même :
Le script à utiliser est update-geonetwork. Il est généré par le Makefile qui inclut le .mo en français. Le script a éditer est enpython

Le script

note perso :
Ce script est dans le git IUEM des sysadmins.

Cliquez sur le titre pour télécharger.

update-geonetwork
#! /usr/bin/env python
# encoding: utf-8
# vim: se ts=2 sw=2 et ai:
license='''
 
 
         +-----------------------------------------------------------+
         |                                                           |
         |  Attention, ce logiciel est potentiellement dangereux !   |
         |       Warting ! This software may be dangerous            |
         |                                                           |
         | Veuillez prendre connaissance de la licence d'utilisation |
         |           Please read the following license text          |
         |                                                           |
         +-----------------------------------------------------------+
 
 
Software author : Christophe Martin, Schplurtz@laposte•net
 
                          Copyright (C) Schplurtz le Déboulonné 2017
 
               FRENCH                 |             ENGLISH
 -------------------------------------+--------------------------------------
 Ce logiciel est un programme         |  This software is a computer program
 informatique servant à mettre à      |  whose purpose is to update
 jour geonetwork.                     |  geonetwork.
                                      |
 Ce logiciel est régi par la licence  |  This software is governed by the
 CeCILL soumise au droit français et  |  CeCILL license under French law and
 respectant les principes de          |  abiding by the rules of
 diffusion des logiciels libres.      |  distribution of free software.  You
 Vous pouvez utiliser, modifier       |  can  use, modify and/ or
 et/ou redistribuer ce programme      |  redistribute the software under the
 sous les conditions de la licence    |  terms of the CeCILL license as
 CeCILL telle que diffusée par le     |  circulated by CEA, CNRS and INRIA
 CEA, le CNRS et l'INRIA sur le site  |  at the following URL
 "http://www.cecill.info/".           |  "http://www.cecill.info/".
                                      |
 En contrepartie de l'accessibilité   |  As a counterpart to the access to
 au code source et des droits de      |  the source code and  rights to
 copie, de modification et de         |  copy, modify and redistribute
 redistribution accordés par cette    |  granted by the license, users are
 licence, il n'est offert aux         |  provided only with a limited
 utilisateurs qu'une garantie         |  warranty  and the software's
 limitée.  Pour les mêmes raisons,    |  author,  the holder of the economic
 seule une responsabilité restreinte  |  rights,  and the successive
 pèse sur l'auteur du programme,  le  |  licensors  have only  limited
 titulaire des droits patrimoniaux    |  liability.
 et les concédants successifs.        |
                                      |
 A cet égard  l'attention de          |  In this respect, the user's
 l'utilisateur est attirée sur les    |  attention is drawn to the risks
 risques associés au chargement,  à   |  associated with loading,  using,
 l'utilisation,  à la modification    |  modifying and/or developing or
 et/ou au développement et à la       |  reproducing the software by the
 reproduction du logiciel par         |  user in light of its specific
 l'utilisateur étant donné sa         |  status of free software, that may
 spécificité de logiciel libre, qui   |  mean  that it is complicated to
 peut le rendre complexe à manipuler  |  manipulate,  and  that  also
 et qui le réserve donc à des         |  therefore means  that it is
 développeurs et des professionnels   |  reserved for developers  and
 avertis possédant  des               |  experienced professionals having
 connaissances  informatiques         |  in-depth computer knowledge. Users
 approfondies.  Les utilisateurs      |  are therefore encouraged to load
 sont donc invités à charger  et      |  and test the software's suitability
 tester  l'adéquation  du logiciel à  |  as regards their requirements in
 leurs besoins dans des conditions    |  conditions enabling the security of
 permettant d'assurer la sécurité de  |  their systems and/or data to be
 leurs systèmes et ou de leurs        |  ensured and,  more generally, to
 données et, plus généralement, à     |  use and operate it in the same
 l'utiliser et l'exploiter dans les   |  conditions as regards security.
 mêmes conditions de sécurité.        |
                                      |
 Le fait que vous puissiez accéder à  |   The fact that you are presently
 cet en-tête signifie que vous avez   |   reading this means that you have
 pris connaissance de la licence      |   had knowledge of the CeCILL license
 CeCILL, et que vous en avez accepté  |   and that you accept its terms.
 les termes.                          |
 
'''
 
import glob
import zipfile
import filecmp
import tempfile
import atexit
import os
import sys
import subprocess
import shutil
import re
import gettext
import base64
 
 
def error( *txt ):
  sys.stderr.write( "\n".join(txt) + "\n")
 
# thanks to stackoverflow
def which(program):
  import os
  def is_exe(fpath):
    return os.path.isfile(fpath) and os.access(fpath, os.X_OK)
 
  fpath, fname = os.path.split(program)
  if fpath:
    if is_exe(program):
      return program
  else:
    for path in os.environ['PATH'].split(os.pathsep):
      path = path.strip('"')
      exe_file = os.path.join(path, program)
      if is_exe(exe_file):
        return exe_file
 
  return None
 
def cleanup():
  os.chdir('/')
  # rmr(tmpdir) # Surtout pas ! Il peut y rester des fichiers importants
  rmr(os.path.join(tmpdir,'locale')) # C'est le moins qu'on puisse faire.
 
def question( var, txt ):
  rep=raw_input( '{0} ({1}) '.format(txt, var) ).strip()
  if rep == 'exit':
    exit(0)
  if rep == '':
    return var
  return rep
 
def yn(txt):
  while True:
    rep=''
    rep=question( rep, txt ).strip().lower()
    if re.match( '\A(o|oui|y|yes)\Z', rep ):
      return True
    if re.match( '\A(n|non|no)\Z', rep ):
      return False
 
def iswar( fn ):
  '''
  a good war file is an existing zip file that
  contains at least META-INF/MANIFEST.MF .
  '''
  ret=True
  try:
    zip=zipfile.ZipFile( fn )
    zip.getinfo('META-INF/MANIFEST.MF')
  except:
    ret=False
  return ret
 
def rmr( *fname ):
  for filename in fname:
    if os.path.islink(filename) or os.path.isfile(filename):
      os.remove(filename)
      # print 'rm    {0}'.format(filename)
    else:
      for root, dirs, files in os.walk(filename, False):
        for fn in files:
          os.remove(os.path.join(root,fn))
          # print 'rm    {0}'.format(os.path.join(root,fn))
        for dn in dirs:
          os.rmdir(os.path.join(root,dn))
          # print 'rmdir {0}'.format(os.path.join(root,dn))
      os.rmdir(filename)
      # print 'rmdir {0}'.format(filename)
 
def bindata(fn):
  lines=''
  with open(fn) as data:
    ok=False
    for line in data:
      if not ok and line == '# __DATA__\n':
        ok=True
        continue
      if ok:
        lines += line[1:]
  return base64.b64decode(lines)
 
 
#
#  Main main Main
#
 
me=os.path.abspath(__file__.rstrip('co'))
tmpdir=tempfile.mkdtemp()
os.makedirs(os.path.join(tmpdir,'locale/fr_FR/LC_MESSAGES'))
with open(os.path.join(tmpdir,'locale/fr_FR/LC_MESSAGES','cat.mo'),'w',0644) as cat:
  cat.write(bindata(me))
#gettext.install('enpython', os.path.join(os.path.dirname(__file__), 'locale'))
gettext.install('cat', os.path.join(os.path.join(tmpdir,'locale')))
 
atexit.register( cleanup )
missing=False
for cmd in ['less']:
  if not which( cmd ):
    error( _('missing ') + cmd )
    missing=True
 
if( missing ):
  error( '',
         _("There are missing commands"),
         _("   bye bye")
       )
  exit( 1 )
 
#
# Accept the license. Or not...
#
fnlic=os.path.join(tmpdir,'license')
with open(fnlic,'w',0644) as f:
  f.write(license)
subprocess.call(['less',
                 '-E', '-X', '-K',
                 _('-PSpace for next page'),
                 fnlic
                ]
               )
os.remove(fnlic)
if not yn( _("Do you accept the risks and the license ?") ):
  print _('Ok. exiting')
  exit(0)
 
 
 
#
# Now the real job
#
geonetdir='/var/lib/tomcat8/webapps/geonetwork'
while True:
  geonetdir = question( geonetdir,
                _('Directory containing current geonetwork'))
  if os.path.isdir( geonetdir ):
    webinf=os.path.join( geonetdir, 'WEB-INF' )
    if os.path.isdir( webinf ):
      break
  error( _('{0} not a dir or not a geonetwork dir...').format(geonetdir), _('try again.') )
 
 
oldwar=geonetdir + '.war'
while True:
  oldwar = question( oldwar,
                     _('current war file')
                   )
  if iswar(oldwar):
    break
  error( '{0} Not a valid/redeable war file.'.format(oldwar), 'try again.' )
 
 
newwar='geonetwork-3.2.1.war'
while 1:
  newwar=question( newwar,
                   _('New war file')
                 )
  if iswar(newwar):
    break
  error( '{0} Not a valid/redeable war file.'.format(newwar), 'try again.' )
 
 
geonetdir=os.path.abspath(geonetdir)
oldwar=os.path.abspath(oldwar)
newwar=os.path.abspath(newwar)
 
print ''
print _('geonetwork dir : '), geonetdir
print _('current war file :  '), oldwar
print _('new war file : '), newwar
 
oldcontent=os.path.join(tmpdir,'oldcontent')
os.mkdir(oldcontent)
os.chdir(oldcontent)
 
print _('Extracting current war in temp dir')
zipfile.ZipFile(oldwar, 'r').extractall()
 
# Can't easily get what I want with filecmp.dircmp. It does not have
# recursive capabilities. too bad. Let's do it the barbarian way :
# get 2 full lists and compare.
# diff_cur_old=filecmp.dircmp(geonetdir, oldcontent)
# diff_cur_old.report_full_closure()
#for file in diff_cur_old.diff_files:
#  print file
 
#print '+++++++++++++++++++++++++++++++++++++++++++++'
print _('Finding all files in current war')
os.chdir(oldcontent)
oldtree=set()
for root, dirs, files in os.walk('.'):
  for name in files:
    oldtree.add(os.path.join(root,name)[2:])
#print '=============================================='
#for x in oldtree:
#  print x
 
print _('Finding all files in current installation')
os.chdir(geonetdir)
currenttree=set()
for root, dirs, files in os.walk('.'):
  for name in files:
    currenttree.add(os.path.join(root,name)[2:])
  #for name in dirs:
  #  currenttree.add(os.path.join(root,name))
#for x in currenttree:
#  print x
 
print _('Computing file diff between current war and current deployed app')
currenttreeenplus=currenttree.copy()
currenttreeenplus -= oldtree
print _('{0} more files installed than files in current war').format(len(currenttreeenplus))
'''
for x in currenttreeenplus:
  print x
  '''
 
communstree=currenttree.copy()
communstree -= currenttreeenplus
print _('computed {0} files in common (should be {1})').format( len(communstree), len(oldtree))
 
print _('searching among {0} files, those that have been modified').format(len(communstree))
modifiedtree=set()
for file in sorted(communstree):
  if not filecmp.cmp( os.path.join(geonetdir,file), os.path.join(oldcontent, file)):
    modifiedtree.add(file)
 
print _('Removing temporary files')
rmr(oldcontent)
 
print _('saving {0} modified files for later use').format(len(modifiedtree))
modifiedfiles=os.path.join( tmpdir, 'modified' )
os.mkdir(modifiedfiles)
for filename in modifiedtree:
  dir=os.path.join( modifiedfiles, os.path.dirname( filename ))
  if not os.path.exists( dir ):
    os.makedirs( dir )
  shutil.copyfile(os.path.join(geonetdir, filename), os.path.join( modifiedfiles, filename ))
 
print _('Analyzing new war')
newzip=zipfile.ZipFile(newwar, 'r')
newwartree=set()
for filename in newzip.namelist():
  newwartree.add(filename)
 
print _('Finding obsolete files')
obsoletetree=oldtree.copy()
obsoletetree -= newwartree
print _('Found {0} obsolete files').format(len(obsoletetree))
if yn( _('all checks are done. Start modifying ?') ):
  print _("Let's go!")
else:
  print _('Bye bye.')
  exit(0)
 
print _('extracting new war into {0}').format(geonetdir)
os.chdir(geonetdir)
newzip.extractall()
 
print _('removing obsolete files')
for filename in obsoletetree:
  os.remove(filename)
 
print _('removing cache')
rmr(*glob.glob('WEB-INF/data/data/resources/htmlcache/formatter-cache/public/*' ))
rmr(*glob.glob('WEB-INF/data/data/resources/htmlcache/formatter-cache/private/*'))
rmr(*glob.glob('WEB-INF/data/data/resources/htmlcache/formatter-cache/info-store.*.db'))
rmr(*glob.glob('WEB-INF/data/wro4j-cache.*'))
 
rep='all'
while True:
  rep=question( rep, _('''
There are {0} modified files, what do you want to do ?
 
0 : restore all
1 : stop now and examine yourself the diffs
2 : ask for each file
''').format(len(modifiedtree))
  ).strip().lower()
  if re.match( '\A[012]\Z', rep ):
    break
  error( _('did not understand, sorry...') )
 
os.chdir( modifiedfiles )
if '1' == rep:
  print _("OK. You have to examine the situation.\nmodified files are in {0}").format(modifiedfiles)
elif '0' == rep:
  for filename in sorted(modifiedtree):
    print filename
    shutil.copyfile(filename, os.path.join(geonetdir,filename))
else:
  for filename in sorted(modifiedtree):
    if yn( _("reinstall modified this modified file to that location ?\n{0}\n{1}\n ?").
           format(os.path.join(modifiedfiles,filename),os.path.join(geonetdir,filename))):
      shutil.copyfile(filename, os.path.join(geonetdir,filename))
      os.remove(filename)
  for root, dirs, files in os.walk(modifiedfiles, False):
    for dn in dirs:
      try:
        os.rmdir(os.path.join(root,dn))
      except:
        pass
  print _('you will find remaining files (if any) in {0}').format(modifiedfiles)
 
print 'don des fichiers à tomcat8.'
subprocess.call(['chown', '-hR', 'tomcat8:', geonetdir])
 
# __DATA__
#3hIElQAAAAAlAAAAHAAAAEQBAAA1AAAAbAIAAAAAAABAAwAAiwAAAEEDAAAKAAAAzQMAABUAAADY
#AwAAEQAAAO4DAABAAAAAAAQAACcAAABBBAAAKQAAAGkEAAAiAAAAkwQAACkAAAC2BAAAIAAAAOAE
#AAAWAAAAAQUAABgAAAAYBQAACQAAADEFAAAMAAAAOwUAAEAAAABIBQAACwAAAIkFAAAYAAAAlQUA
#ABoAAACuBQAAJgAAAMkFAAAsAAAA8AUAABAAAAAdBgAAFAAAAC4GAAAcAAAAQwYAABsAAABgBgAA
#EQAAAHwGAAAIAAAAjgYAAA8AAACXBgAAQwAAAKcGAAAOAAAA6wYAABcAAAD6BgAAJwAAABIHAAA4
#AAAAOgcAAAoAAABzBwAALQAAAH4HAAAyAAAArAcAACgAAADfBwAAewEAAAgIAAC+AAAAhAkAAAUA
#AABDCgAAHAAAAEkKAAAZAAAAZgoAAGQAAACACgAAMwAAAOUKAAApAAAAGQsAADYAAABDCwAAPAAA
#AHoLAAAvAAAAtwsAACQAAADnCwAAIAAAAAwMAAAJAAAALQwAABMAAAA3DAAATwAAAEsMAAAOAAAA
#mwwAACcAAACqDAAAIAAAANIMAAA3AAAA8wwAAEEAAAArDQAAEgAAAG0NAAAVAAAAgA0AABkAAACW
#DQAAIgAAALANAAAVAAAA0w0AAAcAAADpDQAAFgAAAPENAABBAAAACA4AABcAAABKDgAAJgAAAGIO
#AABIAAAAiQ4AADoAAADSDgAADgAAAA0PAAA6AAAAHA8AAEoAAABXDwAAOAAAAKIPAAABAAAAIAAA
#AAwAAAAlAAAAHgAAAAcAAAAcAAAAHQAAAAAAAAARAAAAAAAAABcAAAAbAAAAJAAAAAAAAAALAAAA
#AAAAABYAAAACAAAAIwAAAAAAAAAGAAAACQAAAA0AAAASAAAAIgAAAAAAAAAIAAAADgAAAAAAAAAA
#AAAABAAAAAAAAAAhAAAAGAAAAAAAAAAFAAAAAAAAAAMAAAAfAAAAFAAAAAAAAAAKAAAAFQAAABMA
#AAAZAAAAGgAAAAAAAAAAAAAAAAAAAAAAAAAQAAAADwAAAAAKVGhlcmUgYXJlIHswfSBtb2RpZmll
#ZCBmaWxlcywgd2hhdCBkbyB5b3Ugd2FudCB0byBkbyA/CgowIDogcmVzdG9yZSBhbGwKMSA6IHN0
#b3Agbm93IGFuZCBleGFtaW5lIHlvdXJzZWxmIHRoZSBkaWZmcwoyIDogYXNrIGZvciBlYWNoIGZp
#bGUKACAgIGJ5ZSBieWUALVBTcGFjZSBmb3IgbmV4dCBwYWdlAEFuYWx5emluZyBuZXcgd2FyAENv
#bXB1dGluZyBmaWxlIGRpZmYgYmV0d2VlbiBjdXJyZW50IHdhciBhbmQgY3VycmVudCBkZXBsb3ll
#ZCBhcHAARGlyZWN0b3J5IGNvbnRhaW5pbmcgY3VycmVudCBnZW9uZXR3b3JrAERvIHlvdSBhY2Nl
#cHQgdGhlIHJpc2tzIGFuZCB0aGUgbGljZW5zZSA/AEV4dHJhY3RpbmcgY3VycmVudCB3YXIgaW4g
#dGVtcCBkaXIARmluZGluZyBhbGwgZmlsZXMgaW4gY3VycmVudCBpbnN0YWxsYXRpb24ARmluZGlu
#ZyBhbGwgZmlsZXMgaW4gY3VycmVudCB3YXIARmluZGluZyBvYnNvbGV0ZSBmaWxlcwBGb3VuZCB7
#MH0gb2Jzb2xldGUgZmlsZXMATGV0J3MgZ28hAE5ldyB3YXIgZmlsZQBPSy4gWW91IGhhdmUgdG8g
#ZXhhbWluZSB0aGUgc2l0dWF0aW9uLgptb2RpZmllZCBmaWxlcyBhcmUgaW4gezB9AE9rLiBleGl0
#aW5nAFJlbW92aW5nIHRlbXBvcmFyeSBmaWxlcwBUaGVyZSBhcmUgbWlzc2luZyBjb21tYW5kcwBh
#bGwgY2hlY2tzIGFyZSBkb25lLiBTdGFydCBtb2RpZnlpbmcgPwBjb21wdXRlZCB7MH0gZmlsZXMg
#aW4gY29tbW9uIChzaG91bGQgYmUgezF9KQBjdXJyZW50IHdhciBmaWxlAGN1cnJlbnQgd2FyIGZp
#bGUgOiAgAGRpZCBub3QgdW5kZXJzdGFuZCwgc29ycnkuLi4AZXh0cmFjdGluZyBuZXcgd2FyIGlu
#dG8gezB9AGdlb25ldHdvcmsgZGlyIDogAG1pc3NpbmcgAG5ldyB3YXIgZmlsZSA6IAByZWluc3Rh
#bGwgbW9kaWZpZWQgdGhpcyBtb2RpZmllZCBmaWxlIHRvIHRoYXQgbG9jYXRpb24gPwp7MH0KezF9
#CiA/AHJlbW92aW5nIGNhY2hlAHJlbW92aW5nIG9ic29sZXRlIGZpbGVzAHNhdmluZyB7MH0gbW9k
#aWZpZWQgZmlsZXMgZm9yIGxhdGVyIHVzZQBzZWFyY2hpbmcgYW1vbmcgezB9IGZpbGVzLCB0aG9z
#ZSB0aGF0IGhhdmUgYmVlbiBtb2RpZmllZAB0cnkgYWdhaW4uAHlvdSB3aWxsIGZpbmQgcmVtYWlu
#aW5nIGZpbGVzIChpZiBhbnkpIGluIHswfQB7MH0gbW9yZSBmaWxlcyBpbnN0YWxsZWQgdGhhbiBm
#aWxlcyBpbiBjdXJyZW50IHdhcgB7MH0gbm90IGEgZGlyIG9yIG5vdCBhIGdlb25ldHdvcmsgZGly
#Li4uAFByb2plY3QtSWQtVmVyc2lvbjogClJlcG9ydC1Nc2dpZC1CdWdzLVRvOiAKUE9ULUNyZWF0
#aW9uLURhdGU6IDIwMTctMDUtMjcgMDg6MTQrMDIwMApQTy1SZXZpc2lvbi1EYXRlOiAyMDE3LTA1
#LTI3IDA5OjExKzAyMDAKTGFzdC1UcmFuc2xhdG9yOiBDaHJpc3RvcGhlIE1hcnRpbiA8c2NocGx1
#cnR6QGxhcG9zdGUubmV0PgpMYW5ndWFnZS1UZWFtOiBGcmVuY2gKTGFuZ3VhZ2U6IGZyCk1JTUUt
#VmVyc2lvbjogMS4wCkNvbnRlbnQtVHlwZTogdGV4dC9wbGFpbjsgY2hhcnNldD1VVEYtOApDb250
#ZW50LVRyYW5zZmVyLUVuY29kaW5nOiA4Yml0ClBsdXJhbC1Gb3JtczogbnBsdXJhbHM9MjsgcGx1
#cmFsPShuID4gMSk7ClgtR2VuZXJhdG9yOiBQb2VkaXQgMS44LjcuMQoACklsIHkgYSB7MH0gZmlj
#aGllcnMgbW9kaWZpw6lzLCBxdWUgdm91bGV6IHZvdXMgZmFpcmUgPwoKMCA6IExlcyByZXN0YXVy
#ZXIgdG91cyBkJ3VuIGNvdXAuCjEgOiBBcnLDqnRlciBtYWludGVuYW50IGV0IGV4YW1pbmVyIHZv
#dXMgbcOqbWUgbGVzIGRpZmbDqXJlbmNlcy4KMiA6IERlbWFuZGVyIHBvdXIgY2hhcXVlIGZpY2hp
#ZXIuCgBTYWx1dAAtUEVzcGFjZSBwb3VyIGZhaXJlIGTDqWZpbGVyAEFuYWxpc2UgZHUgbm91dmVh
#dSB3YXIuLi4AQ2FsY3VsIGRlIGxhIGRpZmbDqXJlbmNlIGRlIGZpY2hpZXJzIGVudHJlIGxlIHdh
#ciBhY3R1ZWwgZXQgbCdhcHBsaWNhdGlvbiBhY3R1ZWxsZW1lbnQgZMOpcGxvecOpZS4uLgBEb3Nz
#aWVyIGNvbnRlbmFudCBsYSB2ZXJzaW9uIGFjdHVlbGxlIGRlIGdlb25ldHdvcmsAQWNjZXB0ZXog
#dm91cyBsZXMgcmlzcXVlcyBldCBsYSBsaWNlbmNlID8ARXh0cmFjdGlvbiBkdSB3YXIgYWN0dWVs
#IGRhbnMgdW4gZG9zc2llciB0ZW1wb3JhaXJlLi4uAFJlY2hlcmNoZSBkZSB0b3VzIGxlcyBmaWNo
#aWVycyBkZSBsJ2luc3RhbGxhdGlvbiBhY3R1ZWxsZS4uLgBSZWNoZXJjaGUgZGUgdG91cyBsZXMg
#ZmljaGllcnMgZHUgd2FyIGFjdHVlbC4uLgBSZWNoZXJjaGUgZGVzIGZpY2hpZXJzIG9ic29sw6h0
#ZXMuLi4AVHJvdXbDqSB7MH0gZmljaGllcnMgb2Jzb2zDqHRlcy4AT24geSB2YSAhAE5vdXZlYXUg
#ZmljaGllciB3YXIARCdhY2NvcmQuIFZvdXMgZGV2ZXogZXhhbWluZXIgbGEgc2l0dWF0aW9uCmxl
#cyBmaWNoaWVycyBtb2RpZmnDqXMgc29udCBkYW5zIHswfQBEJ2FjY29yZC4gRmluLgBTdXBwcmVz
#c2lvbiBkZXMgZmljaGllcnMgdGVtcG9yYWlyZXMuLi4ASWwgbWFucXVlIGRlcyBjb21tYW5kZXMg
#ZXh0ZXJuZXMAVG91cyBsZXMgdGVzdHMgc29udCByw6lhbGlzw6lzLiBDb21tZW5jZXIgw6AgbW9k
#aWZpZXIgPwBOb21icmUgZGUgZmljaGllcnMgZW4gY29tbXVuIGNhbGN1bMOpIDogezB9LiAoRGV2
#cmFpdCDDqnRyZSB7MX0uKQBGaWNoaWVyIHdhciBhY3R1ZWwARmljaGllciB3YXIgYWN0dWVsIDog
#AFJpZW4gY29tcHJpcywgZMOpc29sw6kuLi4ARXh0cmFjdGlvbiBkdSBub3V2ZWF1IHdhciBkYW5z
#IHswfQBEb3NzaWVyIGdlb25ldHdvcmsgOiAAbWFucXVlIABOb3V2ZWF1IGZpY2hpZXIgd2FyIDog
#AFLDqWluc3RhbGxlciBjZSBmaWNoaWVyIG1vZGlmaWVyIMOgIGNldCBlbXBsYWNlbWVudCA6IAp7
#MH0KezF9CiA/AFN1cHByZXNzaW9uIGR1IGNhY2hlLi4uAFN1cHByZXNzaW9uIGRlcyBmaWNoaWVy
#cyBvYnNvbMOodGVzLi4uAFNhdXZlZ2FyZGUgZGVzIHswfSBmaWNoaWVycyBtb2RpZmnDqXMgcG91
#ciByw6l1dGlsaXNhdGlvbiB1bHTDqXJpZXVyZS4uLgBSZWNoZXJjaGUgZGVzIGZpY2hpZXJzIG1v
#ZGlmacOpcyBwYXJtaSBjZXMgezB9IGZpY2hpZXJzLi4uAEVzc2F5ZSBlbmNvcmUuAFZvdXMgdHJv
#dXZlcmV6IGxlcyBmaWNoaWVycyByZXN0YW50LCBzJ2lsIHkgZW4gYSwgZGFucyB7MH0ASWwgeSBh
#IHswfSBmaWNoaWVycyBkZSBwbHVzIGRhbnMgbCdhcHBsaWNhdGlvbiBkw6lwbG95w6llIHF1ZSBk
#YW5zIGxlIHdhci4AIHswfSBuJ2VzdCBwYXMgdW4gZG9zc2llciBvdSBuZSBjb250aWVudCBwYXMg
#Z2VvbmV0d29yay4A