From afeeb2b6430875cc3979ae4ad690d2a3efc0ac68 Mon Sep 17 00:00:00 2001 From: "Chris St. Pierre" Date: Fri, 11 May 2012 13:27:07 -0400 Subject: moved plugin-specific configs to main config file; propagate "setup" object to server Core --- tools/README | 16 ++--- tools/nagiosgen-convert.py | 75 --------------------- tools/packages-convert.py | 117 --------------------------------- tools/posixunified.py | 115 -------------------------------- tools/upgrade/1.1/posixunified.py | 115 ++++++++++++++++++++++++++++++++ tools/upgrade/1.2/nagiosgen-convert.py | 75 +++++++++++++++++++++ tools/upgrade/1.2/packages-convert.py | 117 +++++++++++++++++++++++++++++++++ tools/upgrade/1.3/migrate_configs.py | 54 +++++++++++++++ 8 files changed, 366 insertions(+), 318 deletions(-) delete mode 100755 tools/nagiosgen-convert.py delete mode 100755 tools/packages-convert.py delete mode 100644 tools/posixunified.py create mode 100644 tools/upgrade/1.1/posixunified.py create mode 100755 tools/upgrade/1.2/nagiosgen-convert.py create mode 100755 tools/upgrade/1.2/packages-convert.py create mode 100755 tools/upgrade/1.3/migrate_configs.py (limited to 'tools') diff --git a/tools/README b/tools/README index deb04bf3f..738ec1731 100644 --- a/tools/README +++ b/tools/README @@ -66,12 +66,6 @@ hostbase.py {-l|-c} hostinfo.py {-q |--showfields} - Query the hostbase databse -nagios-convert.py - - Convert 1.1.x-style NagiosGen config to 1.2.x-style - -packages-convert.py - - Convert 1.1.x-style Packages config to 1.2.x-style - pkgmgr_gen.py - Generate Pkgmgr XML files from a list of directories that contain RPMS @@ -80,14 +74,14 @@ pkgmgr_update.py - Update Pkgmgr XML files from a list of directories that contain RPMS -posixunified.py - - Convert 1.0.x-style Rules, Bundler, and Base configs to 1.1.x - POSIX unified-style configs (i.e., ConfigFile, Directory, - Permissions, and SymLink all become Path entries) - rpmlisting.py - ??? +upgrade + - This directory contains scripts used to upgrade to the specified + version. E.g., upgrade/1.2 has scripts needed to upgrade to + Bcfg2 1.2.x from bcfg2 1.1.x + yum-listpkgs.xml.py - Produces a list of all packages installed and available in a format suitable for use by Packages or Pkgmgr diff --git a/tools/nagiosgen-convert.py b/tools/nagiosgen-convert.py deleted file mode 100755 index 2c2142735..000000000 --- a/tools/nagiosgen-convert.py +++ /dev/null @@ -1,75 +0,0 @@ -#!/usr/bin/env python - -import os -import sys -import lxml.etree - -import Bcfg2.Options - -def main(): - opts = {'repo': Bcfg2.Options.SERVER_REPOSITORY} - setup = Bcfg2.Options.OptionParser(opts) - setup.parse(sys.argv[1:]) - repo = setup['repo'] - oldconfigfile = os.path.join(repo, 'Properties', 'NagiosGen.xml') - newconfigpath = os.path.join(repo, 'NagiosGen') - newconfigfile = os.path.join(newconfigpath, 'config.xml') - parentsfile = os.path.join(newconfigpath, 'parents.xml') - - if not os.path.exists(oldconfigfile): - print("%s does not exist, nothing to do" % oldconfigfile) - return 1 - - if not os.path.exists(newconfigpath): - print("%s does not exist, cannot write %s" % - (newconfigpath, newconfigfile)) - return 2 - - newconfig = lxml.etree.XML("") - - oldconfig = lxml.etree.parse(oldconfigfile) - for host in oldconfig.getroot().getchildren(): - if host.tag == lxml.etree.Comment: - # skip comments - continue - - if host.tag == 'default': - print("default tag will not be converted; use a suitable Group tag instead") - continue - - newhost = lxml.etree.Element("Client", name=host.tag) - for opt in host: - newopt = lxml.etree.Element("Option", name=opt.tag) - newopt.text = opt.text - newhost.append(newopt) - newconfig.append(newhost) - - # parse the parents config, if it exists - if os.path.exists(parentsfile): - parentsconfig = lxml.etree.parse(parentsfile) - for el in parentsconfig.xpath("//Depend"): - newhost = newconfig.find("Client[@name='%s']" % el.get("name")) - if newhost is not None: - newparents = newhost.find("Option[@name='parents']") - if newparents is not None: - newparents.text += "," + el.get("on") - else: - newparents = lxml.etree.Element("Option", name="parents") - newparents.text = el.get("on") - newhost.append(newparents) - else: - newhost = lxml.etree.Element("Client", name=el.get("name")) - newparents = lxml.etree.Element("Option", name="parents") - newparents.text = el.get("on") - newhost.append(newparents) - newconfig.append(newhost) - - try: - open(newconfigfile, 'w').write(lxml.etree.tostring(newconfig, - pretty_print=True)) - print("%s written" % newconfigfile) - except IOError: - print("Failed to write %s" % newconfigfile) - -if __name__ == '__main__': - sys.exit(main()) diff --git a/tools/packages-convert.py b/tools/packages-convert.py deleted file mode 100755 index c7b43279f..000000000 --- a/tools/packages-convert.py +++ /dev/null @@ -1,117 +0,0 @@ -#!/usr/bin/env python - -import os -import sys -import lxml.etree -from Bcfg2.Bcfg2Py3k import ConfigParser -import Bcfg2.Options - -XI_NAMESPACE = "http://www.w3.org/2001/XInclude" -XI = "{%s}" % XI_NAMESPACE - -def place_source(xdata, source, groups): - """ given a source's group memberships, place it appropriately - within the given XML document tree """ - if not groups: - xdata.append(source) - else: - for group in groups: - match = xdata.xpath("Group[@name='%s']" % group) - if match: - groups.remove(group) - xdata.replace(match[0], place_source(match[0], source, groups)) - return xdata - - # no group found to put this source into - group = groups.pop() - xdata.append(place_source(lxml.etree.Element("Group", name=group), - source, groups)) - - return xdata - -def main(): - opts = {'repo': Bcfg2.Options.SERVER_REPOSITORY} - setup = Bcfg2.Options.OptionParser(opts) - setup.parse(sys.argv[1:]) - repo = setup['repo'] - configpath = os.path.join(repo, 'Packages') - oldconfigfile = os.path.join(configpath, 'config.xml') - newconfigfile = os.path.join(configpath, 'packages.conf') - newsourcesfile = os.path.join(configpath, 'sources.xml') - - if not os.path.exists(oldconfigfile): - print("%s does not exist, nothing to do" % oldconfigfile) - return 1 - - if not os.path.exists(configpath): - print("%s does not exist, cannot write %s" % (configpath, - newconfigfile)) - return 2 - - newconfig = ConfigParser.SafeConfigParser() - newconfig.add_section("global") - - oldconfig = lxml.etree.parse(oldconfigfile).getroot() - - config = oldconfig.xpath('//Sources/Config') - if config: - if config[0].get("resolver", "enabled").lower() == "disabled": - newconfig.add_option("global", "resolver", "disabled") - if config[0].get("metadata", "enabled").lower() == "disabled": - newconfig.add_option("global", "metadata", "disabled") - newconfig.write(open(newconfigfile, "w")) - print("%s written" % newconfigfile) - - oldsources = [oldconfigfile] - while oldsources: - oldfile = oldsources.pop() - oldsource = lxml.etree.parse(oldfile).getroot() - - if oldfile == oldconfigfile: - newfile = newsourcesfile - else: - newfile = os.path.join(configpath, - oldfile.replace("%s/" % configpath, '')) - newsource = lxml.etree.Element("Sources", nsmap=oldsource.nsmap) - - for el in oldsource.getchildren(): - if el.tag == lxml.etree.Comment or el.tag == 'Config': - # skip comments and Config - continue - - if el.tag == XI + 'include': - oldsources.append(os.path.join(configpath, el.get('href'))) - newsource.append(el) - continue - - # element must be a *Source - newel = lxml.etree.Element("Source", - type=el.tag.replace("Source", - "").lower()) - try: - newel.set('recommended', el.find('Recommended').text.lower()) - except AttributeError: - pass - - for tag in ['RawURL', 'URL', 'Version']: - try: - newel.set(tag.lower(), el.find(tag).text) - except AttributeError: - pass - - for child in el.getchildren(): - if child.tag in ['Component', 'Blacklist', 'Whitelist', 'Arch']: - newel.append(child) - - groups = [e.text for e in el.findall("Group")] - newsource = place_source(newsource, newel, groups) - - try: - open(newfile, 'w').write(lxml.etree.tostring(newsource, - pretty_print=True)) - print("%s written" % newfile) - except IOError: - print("Failed to write %s" % newfile) - -if __name__ == '__main__': - sys.exit(main()) diff --git a/tools/posixunified.py b/tools/posixunified.py deleted file mode 100644 index 8eb4ed734..000000000 --- a/tools/posixunified.py +++ /dev/null @@ -1,115 +0,0 @@ -#!/usr/bin/env python - -from copy import deepcopy -import lxml.etree -import os -import sys - -import Bcfg2.Options - -""" -NOTE: This script takes a conservative approach when it comes to - updating your Rules. It creates a new unified-rules.xml file - without the attributes you have defined in your current rules. The - reason for this is to keep this script simple so we don't have - to go through and determine the priorities associated with your - current rules definitions. -""" - -if __name__ == '__main__': - opts = { - 'repo': Bcfg2.Options.SERVER_REPOSITORY, - } - setup = Bcfg2.Options.OptionParser(opts) - setup.parse(sys.argv[1:]) - repo = setup['repo'] - unifiedposixrules = "%s/Rules/unified-rules.xml" % repo - rulesroot = lxml.etree.Element("Rules") - - for plug in ['Base', 'Bundler']: - for root, dirs, files in os.walk('%s/%s' % (repo, plug)): - if '.svn' in dirs: - dirs.remove('.svn') - for filename in files: - if filename.startswith('new'): - continue - xdata = lxml.etree.parse(os.path.join(root, filename)) - # replace ConfigFile elements - for c in xdata.findall('//ConfigFile'): - parent = c.getparent() - oldc = c - c.tag = 'Path' - parent.replace(oldc, c) - # replace Directory elements - for d in xdata.findall('//Directory'): - parent = d.getparent() - oldd = d - d.tag = 'Path' - parent.replace(oldd, d) - # Create new-style Rules entry - newd = deepcopy(d) - newd.set('type', 'directory') - rulesroot.append(newd) - # replace BoundDirectory elements - for d in xdata.findall('//BoundDirectory'): - parent = d.getparent() - oldd = d - d.tag = 'BoundPath' - parent.replace(oldd, d) - # Create new-style entry - newd = deepcopy(d) - newd.set('type', 'directory') - # replace Permissions elements - for p in xdata.findall('//Permissions'): - parent = p.getparent() - oldp = p - p.tag = 'Path' - parent.replace(oldp, p) - # Create new-style Rules entry - newp = deepcopy(p) - newp.set('type', 'permissions') - rulesroot.append(newp) - # replace BoundPermissions elements - for p in xdata.findall('//BoundPermissions'): - parent = p.getparent() - oldp = p - p.tag = 'BoundPath' - parent.replace(oldp, p) - # Create new-style entry - newp = deepcopy(p) - newp.set('type', 'permissions') - # replace SymLink elements - for s in xdata.findall('//SymLink'): - parent = s.getparent() - olds = s - s.tag = 'Path' - parent.replace(olds, s) - # Create new-style Rules entry - news = deepcopy(s) - news.set('type', 'symlink') - rulesroot.append(news) - # replace BoundSymLink elements - for s in xdata.findall('//BoundSymLink'): - parent = s.getparent() - olds = s - s.tag = 'BoundPath' - parent.replace(olds, s) - # Create new-style entry - news = deepcopy(s) - news.set('type', 'symlink') - # write out the new bundle - try: - newbundle = open("%s/%s/new%s" % (repo, plug, filename), 'w') - except IOError: - print("Failed to write %s" % filename) - continue - newbundle.write(lxml.etree.tostring(xdata, pretty_print=True)) - newbundle.close() - - try: - newrules = open(unifiedposixrules, 'w') - rulesroot.set('priority', '1') - newrules.write(lxml.etree.tostring(rulesroot, pretty_print=True)) - newrules.close() - except IOError: - print("Failed to write %s" % unifiedposixrules) diff --git a/tools/upgrade/1.1/posixunified.py b/tools/upgrade/1.1/posixunified.py new file mode 100644 index 000000000..8eb4ed734 --- /dev/null +++ b/tools/upgrade/1.1/posixunified.py @@ -0,0 +1,115 @@ +#!/usr/bin/env python + +from copy import deepcopy +import lxml.etree +import os +import sys + +import Bcfg2.Options + +""" +NOTE: This script takes a conservative approach when it comes to + updating your Rules. It creates a new unified-rules.xml file + without the attributes you have defined in your current rules. The + reason for this is to keep this script simple so we don't have + to go through and determine the priorities associated with your + current rules definitions. +""" + +if __name__ == '__main__': + opts = { + 'repo': Bcfg2.Options.SERVER_REPOSITORY, + } + setup = Bcfg2.Options.OptionParser(opts) + setup.parse(sys.argv[1:]) + repo = setup['repo'] + unifiedposixrules = "%s/Rules/unified-rules.xml" % repo + rulesroot = lxml.etree.Element("Rules") + + for plug in ['Base', 'Bundler']: + for root, dirs, files in os.walk('%s/%s' % (repo, plug)): + if '.svn' in dirs: + dirs.remove('.svn') + for filename in files: + if filename.startswith('new'): + continue + xdata = lxml.etree.parse(os.path.join(root, filename)) + # replace ConfigFile elements + for c in xdata.findall('//ConfigFile'): + parent = c.getparent() + oldc = c + c.tag = 'Path' + parent.replace(oldc, c) + # replace Directory elements + for d in xdata.findall('//Directory'): + parent = d.getparent() + oldd = d + d.tag = 'Path' + parent.replace(oldd, d) + # Create new-style Rules entry + newd = deepcopy(d) + newd.set('type', 'directory') + rulesroot.append(newd) + # replace BoundDirectory elements + for d in xdata.findall('//BoundDirectory'): + parent = d.getparent() + oldd = d + d.tag = 'BoundPath' + parent.replace(oldd, d) + # Create new-style entry + newd = deepcopy(d) + newd.set('type', 'directory') + # replace Permissions elements + for p in xdata.findall('//Permissions'): + parent = p.getparent() + oldp = p + p.tag = 'Path' + parent.replace(oldp, p) + # Create new-style Rules entry + newp = deepcopy(p) + newp.set('type', 'permissions') + rulesroot.append(newp) + # replace BoundPermissions elements + for p in xdata.findall('//BoundPermissions'): + parent = p.getparent() + oldp = p + p.tag = 'BoundPath' + parent.replace(oldp, p) + # Create new-style entry + newp = deepcopy(p) + newp.set('type', 'permissions') + # replace SymLink elements + for s in xdata.findall('//SymLink'): + parent = s.getparent() + olds = s + s.tag = 'Path' + parent.replace(olds, s) + # Create new-style Rules entry + news = deepcopy(s) + news.set('type', 'symlink') + rulesroot.append(news) + # replace BoundSymLink elements + for s in xdata.findall('//BoundSymLink'): + parent = s.getparent() + olds = s + s.tag = 'BoundPath' + parent.replace(olds, s) + # Create new-style entry + news = deepcopy(s) + news.set('type', 'symlink') + # write out the new bundle + try: + newbundle = open("%s/%s/new%s" % (repo, plug, filename), 'w') + except IOError: + print("Failed to write %s" % filename) + continue + newbundle.write(lxml.etree.tostring(xdata, pretty_print=True)) + newbundle.close() + + try: + newrules = open(unifiedposixrules, 'w') + rulesroot.set('priority', '1') + newrules.write(lxml.etree.tostring(rulesroot, pretty_print=True)) + newrules.close() + except IOError: + print("Failed to write %s" % unifiedposixrules) diff --git a/tools/upgrade/1.2/nagiosgen-convert.py b/tools/upgrade/1.2/nagiosgen-convert.py new file mode 100755 index 000000000..2c2142735 --- /dev/null +++ b/tools/upgrade/1.2/nagiosgen-convert.py @@ -0,0 +1,75 @@ +#!/usr/bin/env python + +import os +import sys +import lxml.etree + +import Bcfg2.Options + +def main(): + opts = {'repo': Bcfg2.Options.SERVER_REPOSITORY} + setup = Bcfg2.Options.OptionParser(opts) + setup.parse(sys.argv[1:]) + repo = setup['repo'] + oldconfigfile = os.path.join(repo, 'Properties', 'NagiosGen.xml') + newconfigpath = os.path.join(repo, 'NagiosGen') + newconfigfile = os.path.join(newconfigpath, 'config.xml') + parentsfile = os.path.join(newconfigpath, 'parents.xml') + + if not os.path.exists(oldconfigfile): + print("%s does not exist, nothing to do" % oldconfigfile) + return 1 + + if not os.path.exists(newconfigpath): + print("%s does not exist, cannot write %s" % + (newconfigpath, newconfigfile)) + return 2 + + newconfig = lxml.etree.XML("") + + oldconfig = lxml.etree.parse(oldconfigfile) + for host in oldconfig.getroot().getchildren(): + if host.tag == lxml.etree.Comment: + # skip comments + continue + + if host.tag == 'default': + print("default tag will not be converted; use a suitable Group tag instead") + continue + + newhost = lxml.etree.Element("Client", name=host.tag) + for opt in host: + newopt = lxml.etree.Element("Option", name=opt.tag) + newopt.text = opt.text + newhost.append(newopt) + newconfig.append(newhost) + + # parse the parents config, if it exists + if os.path.exists(parentsfile): + parentsconfig = lxml.etree.parse(parentsfile) + for el in parentsconfig.xpath("//Depend"): + newhost = newconfig.find("Client[@name='%s']" % el.get("name")) + if newhost is not None: + newparents = newhost.find("Option[@name='parents']") + if newparents is not None: + newparents.text += "," + el.get("on") + else: + newparents = lxml.etree.Element("Option", name="parents") + newparents.text = el.get("on") + newhost.append(newparents) + else: + newhost = lxml.etree.Element("Client", name=el.get("name")) + newparents = lxml.etree.Element("Option", name="parents") + newparents.text = el.get("on") + newhost.append(newparents) + newconfig.append(newhost) + + try: + open(newconfigfile, 'w').write(lxml.etree.tostring(newconfig, + pretty_print=True)) + print("%s written" % newconfigfile) + except IOError: + print("Failed to write %s" % newconfigfile) + +if __name__ == '__main__': + sys.exit(main()) diff --git a/tools/upgrade/1.2/packages-convert.py b/tools/upgrade/1.2/packages-convert.py new file mode 100755 index 000000000..c7b43279f --- /dev/null +++ b/tools/upgrade/1.2/packages-convert.py @@ -0,0 +1,117 @@ +#!/usr/bin/env python + +import os +import sys +import lxml.etree +from Bcfg2.Bcfg2Py3k import ConfigParser +import Bcfg2.Options + +XI_NAMESPACE = "http://www.w3.org/2001/XInclude" +XI = "{%s}" % XI_NAMESPACE + +def place_source(xdata, source, groups): + """ given a source's group memberships, place it appropriately + within the given XML document tree """ + if not groups: + xdata.append(source) + else: + for group in groups: + match = xdata.xpath("Group[@name='%s']" % group) + if match: + groups.remove(group) + xdata.replace(match[0], place_source(match[0], source, groups)) + return xdata + + # no group found to put this source into + group = groups.pop() + xdata.append(place_source(lxml.etree.Element("Group", name=group), + source, groups)) + + return xdata + +def main(): + opts = {'repo': Bcfg2.Options.SERVER_REPOSITORY} + setup = Bcfg2.Options.OptionParser(opts) + setup.parse(sys.argv[1:]) + repo = setup['repo'] + configpath = os.path.join(repo, 'Packages') + oldconfigfile = os.path.join(configpath, 'config.xml') + newconfigfile = os.path.join(configpath, 'packages.conf') + newsourcesfile = os.path.join(configpath, 'sources.xml') + + if not os.path.exists(oldconfigfile): + print("%s does not exist, nothing to do" % oldconfigfile) + return 1 + + if not os.path.exists(configpath): + print("%s does not exist, cannot write %s" % (configpath, + newconfigfile)) + return 2 + + newconfig = ConfigParser.SafeConfigParser() + newconfig.add_section("global") + + oldconfig = lxml.etree.parse(oldconfigfile).getroot() + + config = oldconfig.xpath('//Sources/Config') + if config: + if config[0].get("resolver", "enabled").lower() == "disabled": + newconfig.add_option("global", "resolver", "disabled") + if config[0].get("metadata", "enabled").lower() == "disabled": + newconfig.add_option("global", "metadata", "disabled") + newconfig.write(open(newconfigfile, "w")) + print("%s written" % newconfigfile) + + oldsources = [oldconfigfile] + while oldsources: + oldfile = oldsources.pop() + oldsource = lxml.etree.parse(oldfile).getroot() + + if oldfile == oldconfigfile: + newfile = newsourcesfile + else: + newfile = os.path.join(configpath, + oldfile.replace("%s/" % configpath, '')) + newsource = lxml.etree.Element("Sources", nsmap=oldsource.nsmap) + + for el in oldsource.getchildren(): + if el.tag == lxml.etree.Comment or el.tag == 'Config': + # skip comments and Config + continue + + if el.tag == XI + 'include': + oldsources.append(os.path.join(configpath, el.get('href'))) + newsource.append(el) + continue + + # element must be a *Source + newel = lxml.etree.Element("Source", + type=el.tag.replace("Source", + "").lower()) + try: + newel.set('recommended', el.find('Recommended').text.lower()) + except AttributeError: + pass + + for tag in ['RawURL', 'URL', 'Version']: + try: + newel.set(tag.lower(), el.find(tag).text) + except AttributeError: + pass + + for child in el.getchildren(): + if child.tag in ['Component', 'Blacklist', 'Whitelist', 'Arch']: + newel.append(child) + + groups = [e.text for e in el.findall("Group")] + newsource = place_source(newsource, newel, groups) + + try: + open(newfile, 'w').write(lxml.etree.tostring(newsource, + pretty_print=True)) + print("%s written" % newfile) + except IOError: + print("Failed to write %s" % newfile) + +if __name__ == '__main__': + sys.exit(main()) diff --git a/tools/upgrade/1.3/migrate_configs.py b/tools/upgrade/1.3/migrate_configs.py new file mode 100755 index 000000000..c6e6cd2c3 --- /dev/null +++ b/tools/upgrade/1.3/migrate_configs.py @@ -0,0 +1,54 @@ +#!/usr/bin/env python + +import os +import sys +from Bcfg2.Bcfg2Py3k import ConfigParser +import Bcfg2.Options + +def copy_section(src_file, tgt_cfg, section, newsection=None): + if newsection is None: + newsection = section + + cfg = ConfigParser.ConfigParser() + if len(cfg.read(src_file)) == 1: + if cfg.has_section(section): + try: + tgt_cfg.add_section(newsection) + except ConfigParser.DuplicateSectionError: + print("[%s] section already exists in %s, adding options" % + (newsection, setup['cfile'])) + for opt in cfg.options(section): + val = cfg.get(section, opt) + if tgt_cfg.has_option(newsection, opt): + print("%s in [%s] already populated in %s, skipping" % + (opt, newsection, setup['cfile'])) + print(" %s: %s" % (setup['cfile'], + tgt_cfg.get(newsection, opt))) + print(" %s: %s" % (src_file, val)) + else: + print("Set %s in [%s] to %s" % (opt, newsection, val)) + tgt_cfg.set(newsection, opt, val) + +def main(): + opts = dict(repo=Bcfg2.Options.SERVER_REPOSITORY, + configfile=Bcfg2.Options.CFILE) + setup = Bcfg2.Options.OptionParser(opts) + setup.parse(sys.argv[1:]) + + copy_section(os.path.join(setup['repo'], 'Rules', 'rules.conf'), setup.cfp, + "rules") + pkgs_conf = os.path.join(setup['repo'], 'Packages', 'packages.conf') + copy_section(pkgs_conf, setup.cfp, "global", newsection="packages") + for section in ["apt", "yum", "pulp"]: + copy_section(pkgs_conf, setup.cfp, section, + newsection="packages:" + section) + + print("Writing %s" % setup['configfile']) + try: + setup.cfp.write(open(setup['configfile'], "w")) + except IOError: + err = sys.exc_info()[1] + print("Could not write %s: %s" % (setup['configfile'], err)) + +if __name__ == '__main__': + sys.exit(main()) -- cgit v1.2.3-1-g7c22