From johnsonm at rpath.com Wed Aug 19 17:38:25 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:38:25 +0000 Subject: mirrorball: Put the recipe creation code into its own class. Message-ID: <200908192138.n7JLcPcs012601@scc.eng.rpath.com> changeset: 9e98a6458e22 user: Xiaowen Xin date: Thu, 16 Nov 2006 11:00:49 +0100 Put the recipe creation code into its own class. committer: Xiaowen Xin diff --git a/rpmimport.py b/rpmimport.py --- a/rpmimport.py +++ b/rpmimport.py @@ -191,6 +191,40 @@ a('') return '\n'.join(l) +class RecipeMaker: + def __init__(self, cvc, cfg, rpmSource): + self.cvc = cvc + self.cfg = cfg + self.rpmSource = rpmSource + + def create(self, pkgname, recipeContents): + print 'creating initial template for', pkgname + try: + shutil.rmtree(pkgname) + except OSError, e: + pass + self.cvc.sourceCommand(self.cfg, [ "newpkg", pkgname], {}) + cwd = os.getcwd() + os.chdir(pkgname) + try: + recipe = pkgname + '.recipe' + f = open(recipe, 'w') + f.write(recipeContents) + f.close() + addfiles = [ 'add', recipe ] + # copy all the binaries to the cwd + for path, fn in self.rpmSource.rpmMap[src].iteritems(): + shutil.copy(fn, path) + addfiles.append(path) + self.cvc.sourceCommand(self.cfg, addfiles, {}) + self.cvc.sourceCommand(self.cfg, + [ 'commit' ], + { 'message': + 'Automated initial commit of ' + recipe }) + finally: + os.chdir(cwd) + + if __name__ == '__main__': from conary import conaryclient, conarycfg, versions, errors, cvc from conary import deps @@ -227,41 +261,9 @@ d = repos.getTroveVersionsByLabel(srccomps) # Iterate over foo:source. + recipeMaker = RecipeMaker(cvc, cfg, rpmSource) for srccomp in srccomps.iterkeys(): if srccomp not in d: src = srcmap[srccomp] pkgname = srccomp.split(':')[0] - print 'creating initial template for', pkgname - try: - shutil.rmtree(pkgname) - except OSError, e: - pass - cvc.sourceCommand(cfg, [ "newpkg", pkgname], {}) - cwd = os.getcwd() - os.chdir(pkgname) - try: - template = rpmSource.createTemplate(src) - recipe = pkgname + '.recipe' - f = open(recipe, 'w') - f.write(template) - f.close() - addfiles = [ 'add', recipe ] - # copy all the binaries to the cwd - for path, fn in rpmSource.rpmMap[src].iteritems(): - shutil.copy(fn, path) - addfiles.append(path) - cvc.sourceCommand(cfg, addfiles, {}) - cvc.sourceCommand(cfg, - [ 'commit' ], - { 'message': - 'Automated initial commit of ' + recipe }) - archs = rpmSource.getArchs(src) - finally: - os.chdir(cwd) -## except -## from conary.lib import epdb -## epdb.st() -## rpmSource.createTemplate(src) - #print rpmSource.rpmMap - #print rpmSource.revMap - #print rpmSource.srcPath + recipeMaker.create(pkgname, rpmSource.createTemplate(src)) From johnsonm at rpath.com Wed Aug 19 17:38:25 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:38:25 +0000 Subject: mirrorball: Added some comments for readability. Message-ID: <200908192138.n7JLcPXr012583@scc.eng.rpath.com> changeset: 65d7d1adb335 user: Xiaowen Xin date: Thu, 16 Nov 2006 10:10:08 +0100 Added some comments for readability. committer: Xiaowen Xin diff --git a/rpmimport.py b/rpmimport.py --- a/rpmimport.py +++ b/rpmimport.py @@ -28,9 +28,16 @@ class RpmSource: def __init__(self): + # {srpm: {rpm: path} self.rpmMap = dict() + + # {name: srpm} self.revMap = dict() + + # {srpm: path} self.srcPath = dict() + + # {rpmfile: header} self.headers = dict() def getHeader(self, f): @@ -105,12 +112,24 @@ return srpms def transformName(self, name): + """ + In name, - => _, + => _plus. + """ + return name.replace('-', '_').replace('+', '_plus') def quoteSequence(self, seq): + """ + [a, b] => 'a', 'b' + """ + return ', '.join("'%s'" % x for x in sorted(seq)) def getArchs(self, src): + """ + @return list that goes into the archs line in the recipe. + """ + hdrs = [ self.getHeader(x) for x in self.rpmMap[src].itervalues() ] archs = set(h[ARCH] for h in hdrs) if 'i586' in archs and 'i686' in archs: @@ -121,17 +140,27 @@ return archs def getNames(self, src): + """ + @return list that goes into the rpms line in the recipe. + """ + hdrs = [ self.getHeader(x) for x in self.rpmMap[src].itervalues() ] names = set(h[NAME] for h in hdrs) return names def getExtraArchs(self, src): + """ + For the special case of RPMs that have components optimized for the + i686 architecture while other components are at i586, then return + ('i686', set(rpms that are i686 only)), otherwise return (None, None). + """ + hdrs = [ self.getHeader(x) for x in self.rpmMap[src].itervalues() ] archMap = {} for h in hdrs: arch = h[ARCH] name = h[NAME] - if h[ARCH] in archMap: + if arch in archMap: archMap[arch].add(name) else: archMap[arch] = set((name,)) @@ -141,6 +170,10 @@ return None, None def createTemplate(self, src): + """ + @return the content of the new recipe. + """ + srchdr = self.getHeader(self.srcPath[src]) l = [] a = l.append @@ -180,7 +213,11 @@ rpmSource = RpmSource() for root in roots: rpmSource.walk(root) + + # {foo:source: {cfg.buildLabel: None}} srccomps = {} + + # {foo:source: foo-1.0-1.1.src.rpm} srcmap = {} for src in rpmSource.getSrpms(): h = rpmSource.getHeader(rpmSource.srcPath[src]) @@ -188,6 +225,8 @@ srcmap[srccomp] = src srccomps[srccomp] = {cfg.buildLabel: None} d = repos.getTroveVersionsByLabel(srccomps) + + # Iterate over foo:source. for srccomp in srccomps.iterkeys(): if srccomp not in d: src = srcmap[srccomp] From johnsonm at rpath.com Wed Aug 19 17:38:26 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:38:26 +0000 Subject: mirrorball: Automatically create info-* packages for all RPMs found. Message-ID: <200908192138.n7JLcQSo012618@scc.eng.rpath.com> changeset: e3b6b856d743 user: Xiaowen Xin date: Thu, 16 Nov 2006 14:53:23 +0100 Automatically create info-* packages for all RPMs found. committer: Xiaowen Xin diff --git a/rpmimport.py b/rpmimport.py --- a/rpmimport.py +++ b/rpmimport.py @@ -15,14 +15,17 @@ # +import grp import os +import pwd import sys import shutil from conary import rpmhelper # make local copies of tags for convenience -for tag in ('NAME', 'VERSION', 'RELEASE', 'SOURCERPM'): +for tag in ('NAME', 'VERSION', 'RELEASE', 'SOURCERPM', 'FILEUSERNAME', + 'FILEGROUPNAME'): sys.modules[__name__].__dict__[tag] = getattr(rpmhelper, tag) ARCH = 1022 @@ -192,12 +195,113 @@ return '\n'.join(l) class RecipeMaker: - def __init__(self, cvc, cfg, rpmSource): + def __init__(self, cvc, cfg, repos, rpmSource): self.cvc = cvc self.cfg = cfg + self.repos = repos self.rpmSource = rpmSource - def create(self, pkgname, recipeContents): + def walkUsers(self): + # Get all users and groups used in this run. + users = set() + groups = set() + for header in rpmSource.headers.itervalues(): + users = users.union(header[FILEUSERNAME]) + groups = groups.union(header[FILEGROUPNAME]) + + # Remove the root user and group. + users = users.difference(['root']) + groups = groups.difference(['root']) + + # If there are groups named the same as the user, then the user must + # have this group as its primary group. + groups = groups.difference(users) + + # All the packages we might create. + srccomps = {} + for account in users.union(groups): + srccomps['info-%s:source' % (account)] = {cfg.buildLabel: None} + + # Get current repository contents. + repoContents = self.repos.getTroveVersionsByLabel(srccomps) + + # Map username to groups it belongs to. + ugMap = dict() + for (g, a, b, us) in grp.getgrall(): + for u in us: + if u in ugMap: + ugMap[u].add(g) + else: + ugMap[u] = set([g]) + + # Create groups. + for group in groups: + # If it already exists, then move on. + if group in repoContents: + continue + + # If there is a user with this as its primary group, then create + # the user instead. + try: + gprop = grp.getgrnam(group) + except KeyError, e: + print "The group %s does not exist on the build system." \ + " Please create it first before running this." % (group) + continue + gid = gprop[2] + primaries = gprop[3] + [group] + for u in primaries: + try: + uprop = pwd.getpwnam(u) + if uprop[3] == gid: + users.add(uprop[0]) + groups.remove(gprop[0]) + except KeyError, e: + # No such user, oh well, ignore. + pass + + # Create the group recipe. + self.create('info-%s' % (group), + "class info_%(group)s(GroupInfoRecipe):\n" + " name = 'info-%(group)s'\n" + " version = '1'\n" + "\n" + " def setup(r):\n" + " r.Group('%(group)s', %(gid)s)\n" + % dict(group=group, gid=gid)) + + # Create users. + for user in users: + # If it already exists, then move on. + if user in repoContents: + continue + + try: + uprop = pwd.getpwnam(user) + except KeyError, e: + print "The user %s does not exist on the build system." \ + " Please create it before running this." % (user) + continue + + (uid, gid, comment, homedir, shell) = uprop[2:] + gprop = grp.getgrgid(gid) + group = gprop[0] + supgroups = ugMap.get(user, set()).difference(set([group])) + + self.create('info-%s' % (user), + "class info_%(user)s(UserInfoRecipe):\n" + " name = 'info-%(user)s'\n" + " version = '1'\n" + "\n" + " def setup(r):\n" + " r.User('%(user)s', %(uid)s, group='%(group)s', groupid=%(gid)s,\n" + " homedir='%(homedir)s', comment='%(comment)s', shell='%(shell)s'\n" + " supplemental=[%(supgroups)s])\n" + % dict(user=user, uid=uid, group=group, gid=gid, + homedir=homedir, comment=comment, shell=shell, + supgroups=', '.join(list(supgroups)))) + + def create(self, pkgname, recipeContents, srpm = None): print 'creating initial template for', pkgname try: shutil.rmtree(pkgname) @@ -212,19 +316,20 @@ f.write(recipeContents) f.close() addfiles = [ 'add', recipe ] + # copy all the binaries to the cwd - for path, fn in self.rpmSource.rpmMap[src].iteritems(): - shutil.copy(fn, path) - addfiles.append(path) + if srpm: + for path, fn in self.rpmSource.rpmMap[src].iteritems(): + shutil.copy(fn, path) + addfiles.append(path) self.cvc.sourceCommand(self.cfg, addfiles, {}) - self.cvc.sourceCommand(self.cfg, - [ 'commit' ], - { 'message': - 'Automated initial commit of ' + recipe }) + #self.cvc.sourceCommand(self.cfg, + # [ 'commit' ], + # { 'message': + # 'Automated initial commit of ' + recipe }) finally: os.chdir(cwd) - if __name__ == '__main__': from conary import conaryclient, conarycfg, versions, errors, cvc from conary import deps @@ -247,6 +352,7 @@ rpmSource = RpmSource() for root in roots: rpmSource.walk(root) + recipeMaker = RecipeMaker(cvc, cfg, repos, rpmSource) # {foo:source: {cfg.buildLabel: None}} srccomps = {} @@ -261,9 +367,10 @@ d = repos.getTroveVersionsByLabel(srccomps) # Iterate over foo:source. - recipeMaker = RecipeMaker(cvc, cfg, rpmSource) for srccomp in srccomps.iterkeys(): if srccomp not in d: src = srcmap[srccomp] pkgname = srccomp.split(':')[0] - recipeMaker.create(pkgname, rpmSource.createTemplate(src)) + recipeMaker.create(pkgname, rpmSource.createTemplate(src), src) + + recipeMaker.walkUsers() From johnsonm at rpath.com Wed Aug 19 17:38:28 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:38:28 +0000 Subject: mirrorball: Added an extra missing parameter to the InfoImport constructor. Message-ID: <200908192138.n7JLcSqV012733@scc.eng.rpath.com> changeset: ead5b2f85a9c user: Xiaowen Xin date: Thu, 16 Nov 2006 17:46:49 +0100 Added an extra missing parameter to the InfoImport constructor. committer: Xiaowen Xin diff --git a/infoimport.py b/infoimport.py --- a/infoimport.py +++ b/infoimport.py @@ -1,5 +1,26 @@ +#!/usr/bin/python +# +# Copyright (c) 2006 rPath, Inc. +# +# +# This program is distributed under the terms of the Common Public License, +# version 1.0. A copy of this license should have been distributed with this +# source file in a file called LICENSE. If it is not present, the license +# is always available at http://www.rpath.com/permanent/licenses/CPL-1.0. +# +# This program is distributed in the hope that it will be useful, but +# without any warranty; without even the implied warranty of merchantability +# or fitness for a particular purpose. See the Common Public License for +# full details. +# + + +import grp +import pwd + class InfoMaker: - def __init__(self, repos, recipeMaker): + def __init__(self, cfg, repos, recipeMaker): + self.cfg = cfg self.repos = repos self.recipeMaker = recipeMaker @@ -83,7 +104,7 @@ # All the packages we might create. srccomps = {} for account in users.union(groups): - srccomps['info-%s:source' % (account)] = {cfg.buildLabel: None} + srccomps['info-%s:source' % (account)] = {self.cfg.buildLabel: None} # Get current repository contents. repoContents = self.repos.getTroveVersionsByLabel(srccomps) @@ -156,4 +177,4 @@ rpmSource.walk(root) recipeMaker = RecipeMaker(cvc, cfg, repos, rpmSource) - infoMaker = InfoMaker(repos, recipeMaker) + infoMaker = InfoMaker(cfg, repos, recipeMaker) diff --git a/rpmimport.py b/rpmimport.py --- a/rpmimport.py +++ b/rpmimport.py @@ -15,9 +15,7 @@ # -import grp import os -import pwd import sys import shutil @@ -286,5 +284,5 @@ groups = groups.union(header[FILEGROUPNAME]) import infoimport - infoMaker = infoimport.InfoMaker(repos, recipeMaker) + infoMaker = infoimport.InfoMaker(cfg, repos, recipeMaker) infoMaker.makeInfo(users, groups) From johnsonm at rpath.com Wed Aug 19 17:38:27 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:38:27 +0000 Subject: mirrorball: Do a full closure on dependencies between users and groups when creating info-* packages. Message-ID: <200908192138.n7JLcRYk012674@scc.eng.rpath.com> changeset: c08977748d2f user: Xiaowen Xin date: Thu, 16 Nov 2006 17:01:43 +0100 Do a full closure on dependencies between users and groups when creating info-* packages. committer: Xiaowen Xin diff --git a/rpmimport.py b/rpmimport.py --- a/rpmimport.py +++ b/rpmimport.py @@ -201,7 +201,62 @@ self.repos = repos self.rpmSource = rpmSource + def closeUser(self, user, users, groups, ustaging, gstaging, ugMap): + """ + If a user has supplemental groups, then create those. + """ + try: + uprop = pwd.getpwnam(user) + except KeyError, e: + print "The user %s does not exist on the build system." \ + " Please create it before running this." % (user) + return + + (uid, gid, comment, homedir, shell) = uprop[2:] + gprop = grp.getgrgid(gid) + group = gprop[0] + supgroups = ugMap.get(user, set()).difference(set([group])) + for g in supgroups: + if g not in groups: + gstaging.add(g) + users.add(user) + + def closeGroup(self, group, users, groups, ustaging, gstaging, ugMap): + """ + If there is a user with this as its primary group, then create the user + instead. + """ + + try: + gprop = grp.getgrnam(group) + except KeyError, e: + print "The group %s does not exist on the build system." \ + " Please create it first before running this." % (group) + return + + gid = gprop[2] + primaries = gprop[3] + [group] + for u in primaries: + try: + uprop = pwd.getpwnam(u) + if uprop[3] == gid: + ustaging.add(uprop[0]) + return + except KeyError, e: + # No such user, oh well, ignore. + pass + groups.add(group) + def walkUsers(self): + # Map username to groups it belongs to. + ugMap = dict() + for (g, a, b, us) in grp.getgrall(): + for u in us: + if u in ugMap: + ugMap[u].add(g) + else: + ugMap[u] = set([g]) + # Get all users and groups used in this run. users = set() groups = set() @@ -211,6 +266,19 @@ users = users.union(header[FILEUSERNAME]) groups = groups.union(header[FILEGROUPNAME]) + # Groups and users depend on each other, so do their closure. + ustaging = users + gstaging = groups + users = set() + groups = set() + while ustaging or gstaging: + for user in ustaging: + self.closeUser(user, users, groups, ustaging, gstaging, ugMap) + ustaging = set() + for group in gstaging: + self.closeGroup(group, users, groups, ustaging, gstaging, ugMap) + gstaging = set() + # Remove the root user and group. users = users.difference(['root']) groups = groups.difference(['root']) @@ -227,64 +295,13 @@ # Get current repository contents. repoContents = self.repos.getTroveVersionsByLabel(srccomps) - # Map username to groups it belongs to. - ugMap = dict() - for (g, a, b, us) in grp.getgrall(): - for u in us: - if u in ugMap: - ugMap[u].add(g) - else: - ugMap[u] = set([g]) - - # Create groups. - for group in groups: - # If it already exists, then move on. - if group in repoContents: - continue - - # If there is a user with this as its primary group, then create - # the user instead. - try: - gprop = grp.getgrnam(group) - except KeyError, e: - print "The group %s does not exist on the build system." \ - " Please create it first before running this." % (group) - continue - gid = gprop[2] - primaries = gprop[3] + [group] - for u in primaries: - try: - uprop = pwd.getpwnam(u) - if uprop[3] == gid: - users.add(uprop[0]) - groups.remove(gprop[0]) - except KeyError, e: - # No such user, oh well, ignore. - pass - - # Create the group recipe. - self.create('info-%s' % (group), - "class info_%(group)s(GroupInfoRecipe):\n" - " name = 'info-%(group)s'\n" - " version = '1'\n" - "\n" - " def setup(r):\n" - " r.Group('%(group)s', %(gid)s)\n" - % dict(group=group, gid=gid)) - # Create users. for user in users: # If it already exists, then move on. - if user in repoContents: + if 'info-%s:source' %user in repoContents: continue - try: - uprop = pwd.getpwnam(user) - except KeyError, e: - print "The user %s does not exist on the build system." \ - " Please create it before running this." % (user) - continue - + uprop = pwd.getpwnam(user) (uid, gid, comment, homedir, shell) = uprop[2:] gprop = grp.getgrgid(gid) group = gprop[0] @@ -303,6 +320,25 @@ homedir=homedir, comment=comment, shell=shell, supgroups=', '.join("'%s'" % g for g in supgroups))) + # Create groups. + for group in groups: + # If it already exists, then move on. + if 'info-%s:source' %group in repoContents: + continue + + gprop = grp.getgrnam(group) + gid = gprop[2] + + # Create the group recipe. + self.create('info-%s' % (group), + "class info_%(group)s(GroupInfoRecipe):\n" + " name = 'info-%(group)s'\n" + " version = '1'\n" + "\n" + " def setup(r):\n" + " r.Group('%(group)s', %(gid)s)\n" + % dict(group=group, gid=gid)) + def create(self, pkgname, recipeContents, srpm = None): print 'creating initial template for', pkgname try: @@ -330,6 +366,7 @@ [ 'commit' ], { 'message': 'Automated initial commit of ' + recipe }) + self.cvc.sourceCommand(self.cfg, ['cook', pkgname], {}) finally: os.chdir(cwd) From johnsonm at rpath.com Wed Aug 19 17:38:28 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:38:28 +0000 Subject: mirrorball: Broke info-* creation into its own class. Message-ID: <200908192138.n7JLcSaV012714@scc.eng.rpath.com> changeset: 34d42f4f1ff6 user: Xiaowen Xin date: Thu, 16 Nov 2006 17:44:59 +0100 Broke info-* creation into its own class. committer: Xiaowen Xin diff --git a/infoimport.py b/infoimport.py new file mode 100755 --- /dev/null +++ b/infoimport.py @@ -0,0 +1,159 @@ +class InfoMaker: + def __init__(self, repos, recipeMaker): + self.repos = repos + self.recipeMaker = recipeMaker + + def closeUser(self, user, users, groups, ustaging, gstaging, ugMap): + """ + If a user has supplemental groups, then create those. + """ + try: + uprop = pwd.getpwnam(user) + except KeyError, e: + print "The user %s does not exist on the build system." \ + " Please create it before running this." % (user) + return + + (uid, gid, comment, homedir, shell) = uprop[2:] + gprop = grp.getgrgid(gid) + group = gprop[0] + supgroups = ugMap.get(user, set()).difference(set([group])) + for g in supgroups: + if g not in groups: + gstaging.add(g) + users.add(user) + + def closeGroup(self, group, users, groups, ustaging, gstaging, ugMap): + """ + If there is a user with this as its primary group, then create the user + instead. + """ + + try: + gprop = grp.getgrnam(group) + except KeyError, e: + print "The group %s does not exist on the build system." \ + " Please create it first before running this." % (group) + return + + gid = gprop[2] + primaries = gprop[3] + [group] + for u in primaries: + try: + uprop = pwd.getpwnam(u) + if uprop[3] == gid: + ustaging.add(uprop[0]) + return + except KeyError, e: + # No such user, oh well, ignore. + pass + groups.add(group) + + def makeInfo(self, users, groups): + # Map username to groups it belongs to. + ugMap = dict() + for (g, a, b, us) in grp.getgrall(): + for u in us: + if u in ugMap: + ugMap[u].add(g) + else: + ugMap[u] = set([g]) + + # Groups and users depend on each other, so do their closure. + ustaging = users + gstaging = groups + users = set() + groups = set() + while ustaging or gstaging: + for user in ustaging: + self.closeUser(user, users, groups, ustaging, gstaging, ugMap) + ustaging = set() + for group in gstaging: + self.closeGroup(group, users, groups, ustaging, gstaging, ugMap) + gstaging = set() + + # Remove the root user and group. + users = users.difference(['root']) + groups = groups.difference(['root']) + + # If there are groups named the same as the user, then the user must + # have this group as its primary group. + groups = groups.difference(users) + + # All the packages we might create. + srccomps = {} + for account in users.union(groups): + srccomps['info-%s:source' % (account)] = {cfg.buildLabel: None} + + # Get current repository contents. + repoContents = self.repos.getTroveVersionsByLabel(srccomps) + + # Create users. + for user in users: + # If it already exists, then move on. + if 'info-%s:source' %user in repoContents: + continue + + uprop = pwd.getpwnam(user) + (uid, gid, comment, homedir, shell) = uprop[2:] + gprop = grp.getgrgid(gid) + group = gprop[0] + supgroups = ugMap.get(user, set()).difference(set([group])) + + self.recipeMaker.create('info-%s' % (user), + "class info_%(user)s(UserInfoRecipe):\n" + " name = 'info-%(user)s'\n" + " version = '1'\n" + "\n" + " def setup(r):\n" + " r.User('%(user)s', %(uid)s, group='%(group)s', groupid=%(gid)s,\n" + " homedir='%(homedir)s', comment='%(comment)s', shell='%(shell)s',\n" + " supplemental=[%(supgroups)s])\n" + % dict(user=user, uid=uid, group=group, gid=gid, + homedir=homedir, comment=comment, shell=shell, + supgroups=', '.join("'%s'" % g for g in supgroups))) + + # Create groups. + for group in groups: + # If it already exists, then move on. + if 'info-%s:source' %group in repoContents: + continue + + gprop = grp.getgrnam(group) + gid = gprop[2] + + # Create the group recipe. + self.recipeMaker.create('info-%s' % (group), + "class info_%(group)s(GroupInfoRecipe):\n" + " name = 'info-%(group)s'\n" + " version = '1'\n" + "\n" + " def setup(r):\n" + " r.Group('%(group)s', %(gid)s)\n" + % dict(group=group, gid=gid)) + +if __name__ == '__main__': + from conary import conaryclient, conarycfg, versions, errors, cvc + from conary import deps + from conary.lib import util + from conary.build import use + + sys.excepthook = util.genExcepthook(debug=True) + + cfg = conarycfg.ConaryConfiguration(readConfigFiles=True) + cfg.initializeFlavors() + + buildFlavor = deps.deps.parseFlavor('is:x86(i586,!i686)') + cfg.buildFlavor = deps.deps.overrideFlavor(cfg.buildFlavor, + buildFlavor) + use.setBuildFlagsFromFlavor(None, cfg.buildFlavor, error=False) + + client = conaryclient.ConaryClient(cfg) + repos = client.getRepos() + roots = sys.argv[1:] + rpmSource = RpmSource() + for root in roots: + rpmSource.walk(root) + recipeMaker = RecipeMaker(cvc, cfg, repos, rpmSource) + + infoMaker = InfoMaker(repos, recipeMaker) From johnsonm at rpath.com Wed Aug 19 17:38:29 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:38:29 +0000 Subject: mirrorball: add patch and make Message-ID: <200908192138.n7JLcTKl012787@scc.eng.rpath.com> changeset: 05d4f9f1140e user: Matt Wilson date: Fri, 17 Nov 2006 09:30:53 -0500 add patch and make committer: Matt Wilson diff --git a/rpmimport.py b/rpmimport.py --- a/rpmimport.py +++ b/rpmimport.py @@ -95,20 +95,21 @@ 'gzip', 'insserv', 'iproute2', 'iptables', 'iputils', 'kbd', 'klogd', 'krb5', 'ksh', 'libaio', 'libattr', 'libelf', 'libgcc', 'libjpeg', 'libnscd', 'libpng', - 'libstdc++', 'libxcrypt', 'mdadm', 'mingetty', + 'libstdc++', 'libxcrypt', 'make', 'mdadm', 'mingetty', 'mkinitrd', 'mktemp', 'module-init-tools', 'ncurses', 'net-tools', 'netcfg', 'openldap2', 'openldap2-client', 'openslp', 'openssl', 'pam', - 'pam-modules', 'pciutils', 'pcre', 'perl', 'perl-Bootloader', - 'perl-Compress-Zlib', 'perl-DBD-SQLite', 'perl-DBI', - 'perl-Net-Daemon', 'perl-PlRPC', 'perl-TermReadKey', - 'perl-URI', 'perl-gettext', 'procps', 'procps', - 'psmisc', 'pwdutils', 'pwdutils', 'python', 'resmgr', - 'sed', 'slang', 'sles-release', 'sysconfig', - 'sysfsutils', 'syslog-ng', 'sysvinit', 'sysvinit', - 'tar', 'tcl', 'tcsh', 'termcap', 'timezone', 'udev', - 'unixODBC', 'util-linux', 'vim', 'wget', - 'wireless-tools', 'xorg-x11', 'zlib']: + 'pam-modules', 'patch', 'pciutils', 'pcre', 'perl', + 'perl-Bootloader', 'perl-Compress-Zlib', + 'perl-DBD-SQLite', 'perl-DBI', 'perl-Net-Daemon', + 'perl-PlRPC', 'perl-TermReadKey', 'perl-URI', + 'perl-gettext', 'procps', 'procps', 'psmisc', + 'pwdutils', 'pwdutils', 'python', 'resmgr', 'sed', + 'slang', 'sles-release', 'sysconfig', 'sysfsutils', + 'syslog-ng', 'sysvinit', 'sysvinit', 'tar', 'tcl', + 'tcsh', 'termcap', 'timezone', 'udev', 'unixODBC', + 'util-linux', 'vim', 'wget', 'wireless-tools', + 'xorg-x11', 'zlib']: srpms = list() for b in bins: From johnsonm at rpath.com Wed Aug 19 17:38:28 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:38:28 +0000 Subject: mirrorball: update the package list Message-ID: <200908192138.n7JLcSmk012751@scc.eng.rpath.com> changeset: be3359393f31 user: Matt Wilson date: Fri, 17 Nov 2006 05:40:24 -0500 update the package list committer: Matt Wilson diff --git a/rpmimport.py b/rpmimport.py --- a/rpmimport.py +++ b/rpmimport.py @@ -85,28 +85,31 @@ Get all sources we think we need now. """ - bins = ['acl', 'alsa', 'attr', 'libattr', 'ash', 'bzip2', - 'insserv', 'coreutils', 'cpio', 'cracklib', - 'cyrus-sasl', 'dhcpcd', 'diffutils', - 'sles-release', 'e2fsprogs', 'expat', 'expect', + bins = ['aaa_base', 'acl', 'alsa', 'apache2', 'ash', 'attr', + 'bash', 'bzip2', 'compat-libstdc++', 'coreutils', + 'cpio', 'cracklib', 'cron', 'cyrus-sasl', 'db', + 'dhcpcd', 'diffutils', 'e2fsprogs', 'expat', 'expect', 'file', 'filesystem', 'findutils', 'findutils-locate', - 'fontconfig', 'freetype', 'gawk', 'gdbm', 'glib2', - 'glibc', 'glibc-locale', 'glibc-i18ndata', - 'glibc-devel', 'gmp', 'gpm', 'grep', 'grub', 'gzip', - 'aaa_base', 'sysconfig', 'procps', 'iproute2', - 'iptables', 'iputils', 'kbd', 'krb5', 'libaio', - 'libelf', 'libgcc', 'compat-libstdc++', 'libstdc++', - 'mdadm', 'mingetty', 'mkinitrd', 'mktemp', - 'module-init-tools', 'ncurses', 'net-tools', - 'openssl', 'pam', 'pam-modules', 'perl-Bootloader', - 'pwdutils', 'pcre', 'perl', 'perl-DBI', 'procps', - 'psmisc', 'python', 'sed', 'netcfg', 'pwdutils', - 'slang', 'sqlite', 'sysfsutils', 'klogd', 'syslog-ng', - 'sysvinit', 'tar', 'tcl', 'termcap', 'timezone', - 'udev', 'unixODBC', 'sysvinit', 'util-linux', 'vim', - 'cron', 'wget', 'wireless-tools', 'xorg-x11', 'zlib', - 'perl-Bootloader' ] - # add rpm, popt, readline, bash, db, apache2 + 'fontconfig', 'freetype', 'freetype2', 'gawk', 'gdbm', + 'glib2', 'glibc', 'gmp', 'gpm', 'grep', 'grub', + 'gzip', 'insserv', 'iproute2', 'iptables', 'iputils', + 'kbd', 'klogd', 'krb5', 'ksh', 'libaio', 'libattr', + 'libelf', 'libgcc', 'libjpeg', 'libnscd', 'libpng', + 'libstdc++', 'libxcrypt', 'mdadm', 'mingetty', + 'mkinitrd', 'mktemp', 'module-init-tools', 'ncurses', + 'net-tools', 'netcfg', 'openldap2', + 'openldap2-client', 'openslp', 'openssl', 'pam', + 'pam-modules', 'pcre', 'perl', 'perl-Bootloader', + 'perl-Compress-Zlib', 'perl-DBD-SQLite', 'perl-DBI', + 'perl-Net-Daemon', 'perl-PlRPC', 'perl-TermReadKey', + 'perl-URI', 'perl-gettext', 'procps', 'procps', + 'psmisc', 'pwdutils', 'pwdutils', 'python', 'resmgr', + 'sed', 'slang', 'sles-release', 'sysconfig', + 'sysfsutils', 'syslog-ng', 'sysvinit', 'sysvinit', + 'tar', 'tcl', 'tcsh', 'termcap', 'timezone', 'udev', + 'unixODBC', 'util-linux', 'vim', 'wget', + 'wireless-tools', 'xorg-x11', 'zlib']: + srpms = list() for b in bins: srpms.append(self.revMap[b]) From johnsonm at rpath.com Wed Aug 19 17:38:26 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:38:26 +0000 Subject: mirrorball: Added missing commas and quotes. Message-ID: <200908192138.n7JLcQeu012657@scc.eng.rpath.com> changeset: 18096cffe465 user: Xiaowen Xin date: Thu, 16 Nov 2006 15:45:10 +0100 Added missing commas and quotes. committer: Xiaowen Xin diff --git a/rpmimport.py b/rpmimport.py --- a/rpmimport.py +++ b/rpmimport.py @@ -297,11 +297,11 @@ "\n" " def setup(r):\n" " r.User('%(user)s', %(uid)s, group='%(group)s', groupid=%(gid)s,\n" - " homedir='%(homedir)s', comment='%(comment)s', shell='%(shell)s'\n" + " homedir='%(homedir)s', comment='%(comment)s', shell='%(shell)s',\n" " supplemental=[%(supgroups)s])\n" % dict(user=user, uid=uid, group=group, gid=gid, homedir=homedir, comment=comment, shell=shell, - supgroups=', '.join(list(supgroups)))) + supgroups=', '.join("'%s'" % g for g in supgroups))) def create(self, pkgname, recipeContents, srpm = None): print 'creating initial template for', pkgname @@ -325,6 +325,7 @@ shutil.copy(fn, path) addfiles.append(path) self.cvc.sourceCommand(self.cfg, addfiles, {}) + self.cvc.sourceCommand(self.cfg, ['cook', recipe], {}) self.cvc.sourceCommand(self.cfg, [ 'commit' ], { 'message': From johnsonm at rpath.com Wed Aug 19 17:38:26 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:38:26 +0000 Subject: mirrorball: Only get the headers from the RPMs that we actually use. Message-ID: <200908192138.n7JLcQgx012637@scc.eng.rpath.com> changeset: 9e19cf9caa01 user: Xiaowen Xin date: Thu, 16 Nov 2006 15:26:15 +0100 Only get the headers from the RPMs that we actually use. committer: Xiaowen Xin diff --git a/rpmimport.py b/rpmimport.py --- a/rpmimport.py +++ b/rpmimport.py @@ -205,9 +205,11 @@ # Get all users and groups used in this run. users = set() groups = set() - for header in rpmSource.headers.itervalues(): - users = users.union(header[FILEUSERNAME]) - groups = groups.union(header[FILEGROUPNAME]) + for src in self.rpmSource.getSrpms(): + for rpm in self.rpmSource.rpmMap[src].values(): + header = self.rpmSource.getHeader(rpm) + users = users.union(header[FILEUSERNAME]) + groups = groups.union(header[FILEGROUPNAME]) # Remove the root user and group. users = users.difference(['root']) @@ -323,10 +325,10 @@ shutil.copy(fn, path) addfiles.append(path) self.cvc.sourceCommand(self.cfg, addfiles, {}) - #self.cvc.sourceCommand(self.cfg, - # [ 'commit' ], - # { 'message': - # 'Automated initial commit of ' + recipe }) + self.cvc.sourceCommand(self.cfg, + [ 'commit' ], + { 'message': + 'Automated initial commit of ' + recipe }) finally: os.chdir(cwd) From johnsonm at rpath.com Wed Aug 19 17:38:29 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:38:29 +0000 Subject: mirrorball: Added pciutils to list of packages we need. Message-ID: <200908192138.n7JLcTaB012769@scc.eng.rpath.com> changeset: b4b253e42b86 user: Xiaowen Xin date: Fri, 17 Nov 2006 11:50:01 +0100 Added pciutils to list of packages we need. committer: Xiaowen Xin diff --git a/rpmimport.py b/rpmimport.py --- a/rpmimport.py +++ b/rpmimport.py @@ -99,7 +99,7 @@ 'mkinitrd', 'mktemp', 'module-init-tools', 'ncurses', 'net-tools', 'netcfg', 'openldap2', 'openldap2-client', 'openslp', 'openssl', 'pam', - 'pam-modules', 'pcre', 'perl', 'perl-Bootloader', + 'pam-modules', 'pciutils', 'pcre', 'perl', 'perl-Bootloader', 'perl-Compress-Zlib', 'perl-DBD-SQLite', 'perl-DBI', 'perl-Net-Daemon', 'perl-PlRPC', 'perl-TermReadKey', 'perl-URI', 'perl-gettext', 'procps', 'procps', From johnsonm at rpath.com Wed Aug 19 17:38:30 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:38:30 +0000 Subject: mirrorball: correct syntax Message-ID: <200908192138.n7JLcUFt012822@scc.eng.rpath.com> changeset: 037cb0cc8ca1 user: Matt Wilson date: Fri, 17 Nov 2006 09:31:57 -0500 correct syntax committer: Matt Wilson diff --git a/rpmimport.py b/rpmimport.py --- a/rpmimport.py +++ b/rpmimport.py @@ -109,7 +109,7 @@ 'syslog-ng', 'sysvinit', 'sysvinit', 'tar', 'tcl', 'tcsh', 'termcap', 'timezone', 'udev', 'unixODBC', 'util-linux', 'vim', 'wget', 'wireless-tools', - 'xorg-x11', 'zlib']: + 'xorg-x11', 'zlib'] srpms = list() for b in bins: From johnsonm at rpath.com Wed Aug 19 17:38:27 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:38:27 +0000 Subject: mirrorball: Broke info-* creation into its own class. Message-ID: <200908192138.n7JLcRNs012695@scc.eng.rpath.com> changeset: 277d1cb5b770 user: Xiaowen Xin date: Thu, 16 Nov 2006 17:31:13 +0100 Broke info-* creation into its own class. committer: Xiaowen Xin diff --git a/rpmimport.py b/rpmimport.py --- a/rpmimport.py +++ b/rpmimport.py @@ -201,144 +201,6 @@ self.repos = repos self.rpmSource = rpmSource - def closeUser(self, user, users, groups, ustaging, gstaging, ugMap): - """ - If a user has supplemental groups, then create those. - """ - try: - uprop = pwd.getpwnam(user) - except KeyError, e: - print "The user %s does not exist on the build system." \ - " Please create it before running this." % (user) - return - - (uid, gid, comment, homedir, shell) = uprop[2:] - gprop = grp.getgrgid(gid) - group = gprop[0] - supgroups = ugMap.get(user, set()).difference(set([group])) - for g in supgroups: - if g not in groups: - gstaging.add(g) - users.add(user) - - def closeGroup(self, group, users, groups, ustaging, gstaging, ugMap): - """ - If there is a user with this as its primary group, then create the user - instead. - """ - - try: - gprop = grp.getgrnam(group) - except KeyError, e: - print "The group %s does not exist on the build system." \ - " Please create it first before running this." % (group) - return - - gid = gprop[2] - primaries = gprop[3] + [group] - for u in primaries: - try: - uprop = pwd.getpwnam(u) - if uprop[3] == gid: - ustaging.add(uprop[0]) - return - except KeyError, e: - # No such user, oh well, ignore. - pass - groups.add(group) - - def walkUsers(self): - # Map username to groups it belongs to. - ugMap = dict() - for (g, a, b, us) in grp.getgrall(): - for u in us: - if u in ugMap: - ugMap[u].add(g) - else: - ugMap[u] = set([g]) - - # Get all users and groups used in this run. - users = set() - groups = set() - for src in self.rpmSource.getSrpms(): - for rpm in self.rpmSource.rpmMap[src].values(): - header = self.rpmSource.getHeader(rpm) - users = users.union(header[FILEUSERNAME]) - groups = groups.union(header[FILEGROUPNAME]) - - # Groups and users depend on each other, so do their closure. - ustaging = users - gstaging = groups - users = set() - groups = set() - while ustaging or gstaging: - for user in ustaging: - self.closeUser(user, users, groups, ustaging, gstaging, ugMap) - ustaging = set() - for group in gstaging: - self.closeGroup(group, users, groups, ustaging, gstaging, ugMap) - gstaging = set() - - # Remove the root user and group. - users = users.difference(['root']) - groups = groups.difference(['root']) - - # If there are groups named the same as the user, then the user must - # have this group as its primary group. - groups = groups.difference(users) - - # All the packages we might create. - srccomps = {} - for account in users.union(groups): - srccomps['info-%s:source' % (account)] = {cfg.buildLabel: None} - - # Get current repository contents. - repoContents = self.repos.getTroveVersionsByLabel(srccomps) - - # Create users. - for user in users: - # If it already exists, then move on. - if 'info-%s:source' %user in repoContents: - continue - - uprop = pwd.getpwnam(user) - (uid, gid, comment, homedir, shell) = uprop[2:] - gprop = grp.getgrgid(gid) - group = gprop[0] - supgroups = ugMap.get(user, set()).difference(set([group])) - - self.create('info-%s' % (user), - "class info_%(user)s(UserInfoRecipe):\n" - " name = 'info-%(user)s'\n" - " version = '1'\n" - "\n" - " def setup(r):\n" - " r.User('%(user)s', %(uid)s, group='%(group)s', groupid=%(gid)s,\n" - " homedir='%(homedir)s', comment='%(comment)s', shell='%(shell)s',\n" - " supplemental=[%(supgroups)s])\n" - % dict(user=user, uid=uid, group=group, gid=gid, - homedir=homedir, comment=comment, shell=shell, - supgroups=', '.join("'%s'" % g for g in supgroups))) - - # Create groups. - for group in groups: - # If it already exists, then move on. - if 'info-%s:source' %group in repoContents: - continue - - gprop = grp.getgrnam(group) - gid = gprop[2] - - # Create the group recipe. - self.create('info-%s' % (group), - "class info_%(group)s(GroupInfoRecipe):\n" - " name = 'info-%(group)s'\n" - " version = '1'\n" - "\n" - " def setup(r):\n" - " r.Group('%(group)s', %(gid)s)\n" - % dict(group=group, gid=gid)) - def create(self, pkgname, recipeContents, srpm = None): print 'creating initial template for', pkgname try: @@ -413,4 +275,16 @@ pkgname = srccomp.split(':')[0] recipeMaker.create(pkgname, rpmSource.createTemplate(src), src) - recipeMaker.walkUsers() + + # Get all users and groups used in this run. + users = set() + groups = set() + for src in rpmSource.getSrpms(): + for rpm in rpmSource.rpmMap[src].values(): + header = rpmSource.getHeader(rpm) + users = users.union(header[FILEUSERNAME]) + groups = groups.union(header[FILEGROUPNAME]) + + import infoimport + infoMaker = infoimport.InfoMaker(repos, recipeMaker) + infoMaker.makeInfo(users, groups) From johnsonm at rpath.com Wed Aug 19 17:38:34 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:38:34 +0000 Subject: mirrorball: add openssh Message-ID: <200908192138.n7JLcYaI013021@scc.eng.rpath.com> changeset: d1a35020843e user: Matt Wilson date: Fri, 01 Dec 2006 12:52:04 -0500 add openssh committer: Matt Wilson diff --git a/rpmimport/rpmsource.py b/rpmimport/rpmsource.py --- a/rpmimport/rpmsource.py +++ b/rpmimport/rpmsource.py @@ -101,7 +101,7 @@ 'libstdc++', 'libusb', 'libxcrypt', 'make', 'mdadm', 'mingetty', 'mkinitrd', 'mktemp', 'module-init-tools', 'ncurses', 'net-tools', 'netcfg', 'openldap2', - 'openldap2-client', 'openslp', 'openssl', 'pam', + 'openldap2-client', 'openslp', 'openssh', 'openssl', 'pam', 'pam-modules', 'patch', 'pciutils', 'pcre', 'perl', 'perl-Bootloader', 'perl-Compress-Zlib', 'perl-DBD-SQLite', 'perl-DBI', 'perl-Digest-SHA1', From johnsonm at rpath.com Wed Aug 19 17:38:36 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:38:36 +0000 Subject: mirrorball: remove breakpoint Message-ID: <200908192138.n7JLcaNP013115@scc.eng.rpath.com> changeset: 1d3f42078291 user: Matt Wilson date: Fri, 01 Dec 2006 13:45:57 -0500 remove breakpoint committer: Matt Wilson diff --git a/pkgwiz.py b/pkgwiz.py --- a/pkgwiz.py +++ b/pkgwiz.py @@ -109,8 +109,6 @@ recipeFile.close() # Remove the original RPMs and add the new ones. - import epdb - epdb.st() cwd = os.getcwd() os.chdir(pkgname) from conary.state import ConaryStateFromFile From johnsonm at rpath.com Wed Aug 19 17:38:33 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:38:33 +0000 Subject: mirrorball: make that php5 Message-ID: <200908192138.n7JLcX1B012999@scc.eng.rpath.com> changeset: 9258f428e606 user: Matt Wilson date: Fri, 01 Dec 2006 12:51:49 -0500 make that php5 committer: Matt Wilson diff --git a/rpmimport/rpmsource.py b/rpmimport/rpmsource.py --- a/rpmimport/rpmsource.py +++ b/rpmimport/rpmsource.py @@ -106,7 +106,7 @@ 'perl-Bootloader', 'perl-Compress-Zlib', 'perl-DBD-SQLite', 'perl-DBI', 'perl-Digest-SHA1', 'perl-Net-Daemon', 'perl-PlRPC', 'perl-TermReadKey', - 'perl-URI', 'perl-gettext', 'php', 'procps', 'psmisc', + 'perl-URI', 'perl-gettext', 'php5', 'procps', 'psmisc', 'pwdutils', 'python', 'python-xml', 'resmgr', 'sed', 'slang', 'sles-release', 'sysconfig', 'sysfsutils', 'syslog-ng', 'sysvinit', 'tar', 'tcl', 'tcsh', From johnsonm at rpath.com Wed Aug 19 17:38:35 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:38:35 +0000 Subject: mirrorball: All on individual lines ... Message-ID: <200908192138.n7JLcZ2J013076@scc.eng.rpath.com> changeset: e4424b7d73da user: Xiaowen Xin date: Sat, 02 Dec 2006 01:58:39 +0800 All on individual lines ... committer: Xiaowen Xin diff --git a/rpmimport/rpmsource.py b/rpmimport/rpmsource.py --- a/rpmimport/rpmsource.py +++ b/rpmimport/rpmsource.py @@ -85,34 +85,137 @@ Get all sources we think we need now. """ - bins = ['aaa_base', 'acl', 'alsa', 'apache2', 'ash', 'attr', - 'bash', 'binutils', 'bzip2', 'compat-libstdc++', - 'coreutils', 'cpio', 'cpp', 'cracklib', 'cron', - 'cyrus-sasl', 'db', 'dhcpcd', 'diffutils', - 'e2fsprogs', 'expat', 'expect', 'file', 'filesystem', - 'fillup', 'findutils', 'findutils-locate', - 'fontconfig', 'freetype', 'freetype2', 'gawk', 'gcc', - 'gdbm', 'glib2', 'glibc', 'gmp', 'gpm', 'grep', - 'grub', 'gzip', 'insserv', 'iproute2', 'iptables', - 'iputils', 'jpeg', 'kbd', 'klogd', 'krb5', 'ksh', - 'less', 'libaio', 'libapr1', 'libapr-util1', - 'libattr', 'libcom_err', 'libelf', 'libgcc', - 'libjpeg', 'libnscd', 'libpcap', 'libpng', - 'libstdc++', 'libusb', 'libxcrypt', 'make', 'mdadm', - 'mingetty', 'mkinitrd', 'mktemp', 'module-init-tools', - 'ncurses', 'net-tools', 'netcfg', 'openldap2', - 'openldap2-client', 'openslp', 'openssh', 'openssl', - 'pam', 'pam-modules', 'patch', 'pciutils', 'pcre', - 'perl', 'perl-Bootloader', 'perl-Compress-Zlib', - 'perl-DBD-SQLite', 'perl-DBI', 'perl-Digest-SHA1', - 'perl-Net-Daemon', 'perl-PlRPC', 'perl-TermReadKey', - 'perl-URI', 'perl-gettext', 'php5', 'procps', - 'psmisc', 'pwdutils', 'python', 'python-xml', - 'resmgr', 'sed', 'slang', 'sles-release', 'sysconfig', - 'sysfsutils', 'syslog-ng', 'sysvinit', 'tar', 'tcl', - 'tcsh', 'tcpdump', 'termcap', 'timezone', 'udev', - 'unixODBC', 'util-linux', 'vim', 'wget', - 'wireless-tools', 'xorg-x11', 'zlib'] + bins = [ + 'aaa_base', + 'acl', + 'alsa', + 'apache2', + 'ash', + 'attr', + 'bash', + 'binutils', + 'bzip2', + 'compat-libstdc++', + 'coreutils', + 'cpio', + 'cpp', + 'cracklib', + 'cron', + 'cyrus-sasl', + 'db', + 'dhcpcd', + 'diffutils', + 'e2fsprogs', + 'expat', + 'expect', + 'file', + 'filesystem', + 'fillup', + 'findutils', + 'findutils-locate', + 'fontconfig', + 'freetype', + 'freetype2', + 'gawk', + 'gcc', + 'gdbm', + 'glib2', + 'glibc', + 'gmp', + 'gpm', + 'grep', + 'grub', + 'gzip', + 'insserv', + 'iproute2', + 'iptables', + 'iputils', + #'java-1_4_2-sun', + 'jpeg', + 'kbd', + 'klogd', + 'krb5', + 'ksh', + 'less', + 'libaio', + 'libapr1', + 'libapr-util1', + 'libattr', + 'libcom_err', + 'libelf', + 'libgcc', + 'libjpeg', + 'libnscd', + 'libpcap', + 'libpng', + 'libstdc++', + 'libtool', + 'libusb', + 'libxcrypt', + 'libxml2', + 'make', + 'mdadm', + 'mingetty', + 'mkinitrd', + 'mktemp', + 'mm', + 'module-init-tools', + 'ncurses', + 'net-tools', + 'netcfg', + 'openct', + 'openldap2', + 'openldap2-client', + 'opensc', + 'openslp', + 'openssh', + 'openssl', + 'pam', + 'pam-modules', + 'patch', + 'pciutils', + 'pcre', + 'perl', + 'perl-Bootloader', + 'perl-Compress-Zlib', + 'perl-DBD-SQLite', + 'perl-DBI', + 'perl-Digest-SHA1', + 'perl-Net-Daemon', + 'perl-PlRPC', + 'perl-TermReadKey', + 'perl-URI', + 'perl-gettext', + 'php5', + 'procps', + 'psmisc', + 'pwdutils', + 'python', + 'python-xml', + 'resmgr', + 'sed', + 'slang', + 'sles-release', + 'sysconfig', + 'sysfsutils', + 'syslog-ng', + 'sysvinit', + 'tar', + 'tcl', + 'tcpd', + 'tcsh', + 'tcpdump', + 'termcap', + 'timezone', + 'udev', + 'unixODBC', + 'util-linux', + 'vim', + 'wget', + 'wireless-tools', + 'xorg-x11', + 'zlib' + ] srpms = list() for b in bins: From johnsonm at rpath.com Wed Aug 19 17:38:33 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:38:33 +0000 Subject: mirrorball: add php Message-ID: <200908192138.n7JLcX2A012982@scc.eng.rpath.com> changeset: 30bc51c083a8 user: Matt Wilson date: Fri, 01 Dec 2006 12:51:13 -0500 add php committer: Matt Wilson diff --git a/rpmimport/rpmsource.py b/rpmimport/rpmsource.py --- a/rpmimport/rpmsource.py +++ b/rpmimport/rpmsource.py @@ -106,7 +106,7 @@ 'perl-Bootloader', 'perl-Compress-Zlib', 'perl-DBD-SQLite', 'perl-DBI', 'perl-Digest-SHA1', 'perl-Net-Daemon', 'perl-PlRPC', 'perl-TermReadKey', - 'perl-URI', 'perl-gettext', 'procps', 'psmisc', + 'perl-URI', 'perl-gettext', 'php', 'procps', 'psmisc', 'pwdutils', 'python', 'python-xml', 'resmgr', 'sed', 'slang', 'sles-release', 'sysconfig', 'sysfsutils', 'syslog-ng', 'sysvinit', 'tar', 'tcl', 'tcsh', From johnsonm at rpath.com Wed Aug 19 17:38:42 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:38:42 +0000 Subject: mirrorball: don't cook when creating initial :source component, set up buildreqs for rapa, add packages Message-ID: <200908192138.n7JLcgSH013411@scc.eng.rpath.com> changeset: a617517d394b user: Matt Wilson date: Fri, 11 Apr 2008 14:57:56 -0400 don't cook when creating initial :source component, set up buildreqs for rapa, add packages committer: Matt Wilson diff --git a/pkgwiz.py b/pkgwiz.py --- a/pkgwiz.py +++ b/pkgwiz.py @@ -92,25 +92,30 @@ 'krb5', 'ksh', 'less', - 'lvm2', 'libaio', + 'libapr-util1', 'libapr1', - 'libapr-util1', 'libattr', 'libcap', 'libcom_err', 'libelf', + 'libevent', 'libgcc', 'libgssapi', + 'libiniparser', 'libjpeg', 'libnscd', 'libpcap', 'libpng', + 'librpcsecgss', 'libstdc++', 'libtool', 'libusb', 'libxcrypt', 'libxml2', + 'libxml2-python', + 'logrotate', + 'lvm2', 'make', 'mdadm', 'mingetty', @@ -121,6 +126,8 @@ 'ncurses', 'net-tools', 'netcfg', + 'nfs-utils', + 'nfsidmap', 'openct', 'openldap2', 'openldap2-client', @@ -147,15 +154,18 @@ 'php5', 'pkgconfig', 'popt', + 'portmap', 'procps', 'psmisc', 'pwdutils', 'python', 'python-xml', 'resmgr', + 'samba', 'sed', 'slang', 'sles-release', + 'strace', 'sysconfig', 'sysfsutils', 'syslog-ng', diff --git a/rpmimport/recipemaker.py b/rpmimport/recipemaker.py --- a/rpmimport/recipemaker.py +++ b/rpmimport/recipemaker.py @@ -60,12 +60,12 @@ [ 'commit' ], { 'message': 'Automated initial commit of %s:source' %pkgname}) - cvc.sourceCommand(self.cfg, ['cook', pkgname], {'no-deps': None}) - cfg = copy.copy(self.cfg) - buildFlavor = deps.deps.parseFlavor('is:x86_64') - cfg.buildFlavor = deps.deps.overrideFlavor( - cfg.buildFlavor, buildFlavor) - cvc.sourceCommand(cfg, ['cook', pkgname], {'no-deps': None}) + #cvc.sourceCommand(self.cfg, ['cook', pkgname], {'no-deps': None}) + #cfg = copy.copy(self.cfg) + #buildFlavor = deps.deps.parseFlavor('is:x86_64') + #cfg.buildFlavor = deps.deps.overrideFlavor( + # cfg.buildFlavor, buildFlavor) + #cvc.sourceCommand(cfg, ['cook', pkgname], {'no-deps': None}) finally: os.chdir(cwd) From johnsonm at rpath.com Wed Aug 19 17:38:32 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:38:32 +0000 Subject: mirrorball: Merge Message-ID: <200908192138.n7JLcWjU012944@scc.eng.rpath.com> changeset: 74dbcfea5d39 user: Xiaowen Xin date: Wed, 29 Nov 2006 02:06:01 +0800 Merge committer: Xiaowen Xin diff --git a/rpmimport.recipe b/rpmimport.recipe new file mode 100644 --- /dev/null +++ b/rpmimport.recipe @@ -0,0 +1,225 @@ +#!/usr/bin/python +# +# Copyright (c) 2006 rPath, Inc. +# +# + +from conary import rpmhelper + +class RPMImportRecipe(PackageRecipe): + name = 'rpmimport' + version = '1.0_1' + rpms = None + archs = [ 'noarch' ] + extraArch = {} + + def __init__(r, *args, **kw): + r.usedrpms = [] + r.headers = [] + PackageRecipe.__init__(r, *args, **kw) + + def unpack(r): + if not r.rpms: + r.rpms = [ '%(name)s' ] + for rpm in r.rpms: + # if we have a tuple, it's name, version + if isinstance(rpm, tuple): + rpmname, verrel = rpm + rpm = rpmname + '-%s' %verrel + else: + rpmname = rpm + rpm += '-%(version)s-%(release)s' + for rpmarch in r.archs: + pkgarch = rpmarch + if rpmarch == 'noarch': + arch = True + elif rpmarch.startswith('i') and rpmarch.endswith('86'): + # this is i586, i686 - most likely + primaryArch = Arch.x86 + arch = primaryArch[rpmarch] + if (rpmarch in r.extraArch + and rpmname not in r.extraArch[rpmarch] + and rpmarch == 'i686'): + # handle cases like glibc, glibc-devel=i686, + # but the rest are i586 + pkgarch = 'i586' + elif rpmarch == 'x86_64': + arch = Arch[rpmarch] + else: + raise RPMImportError('unsupported arch: %s' %rpmarch) + rpmfile = rpm + '.%s.rpm' %pkgarch + if arch: + r.usedrpms.append(rpmfile %r.macros) + # follow the packaging splitting from the RPMs we're importing + packagename = rpmname + # collapse libfoo -> foo when the main package name is foo + # but don't collapse libbar -> bar when it's produced by the + # foo src rpm + if (not r.name.startswith('lib') + and packagename.startswith('lib') + and packagename[3:].startswith(r.name)): + packagename = packagename[3:] + # collapse suffixes + for suffix in ('-devel', '-docs', '-doc', '-info', + '-lib', '-libs', '-locale', '-man', + '-i18ndata', '-html'): + # collapse foo-devel -> foo + if rpmname.endswith(suffix): + packagename = packagename[:-len(suffix)] + break + r.addArchive(rpmfile, dir='/', use=arch, package=packagename) + + def disableBuildRequirementsPolicy(r): + r.EnforceSonameBuildRequirements(exceptions='.*') + r.EnforceJavaBuildRequirements(exceptions='.*') + r.EnforceCILBuildRequirements(exceptions='.*') + r.EnforceConfigLogBuildRequirements(exceptions='.*') + # Note that perl and python runtime requirement CANNOT + # be discovered without listing them as runtime requirements, + # so it is important not to disable + # r.Enforce{Perl,Python}BuildRequirements + + def disablePolicy(r): + r.BadInterpreterPaths(exceptions='.*') + r.CheckDesktopFiles(exceptions='.*') + r.CheckDestDir(exceptions='.*') + r.CheckSonames(exceptions='.*') + r.DanglingSymlinks(exceptions='.*') + r.ExecutableLibraries(exceptions='.*') + r.FilesForDirectories(exceptions='.*') + r.FilesInMandir(exceptions='.*') + r.FixDirModes(exceptions='.*') + r.FixupMultilibPaths(exceptions='.*') + r.LinkCount(exceptions='.*') + r.IgnoredSetuid(exceptions='.*') + r.ImproperlyShared(exceptions='.*') + r.NonBinariesInBindirs(exceptions='.*') + r.NonMultilibComponent(exceptions='.*') + r.NonMultilibDirectories(exceptions='.*') + r.NormalizeCompression(exceptions='.*') + r.NormalizeInterpreterPaths(exceptions='.*') + r.RemoveNonPackageFiles(exceptions='.*') + r.WarnWriteable(exceptions='.*') + r.WorldWriteableExecutables(exceptions='.*') + # ObsoletePaths does not honor exceptions + del r.ObsoletePaths + + def disableStrip(r): + """ + Default to not stripping; allow override in subclasses with: + + def disableStrip(r): pass + """ + r.Strip(exceptions='.*') + + def policy(r): + """ + hook for adding additional policy in subclasses. + """ + pass + + def preprocess(r): + "hook for adding sources/policy calls before anything else" + pass + + def postprocess(r): + "hook for adding sources/policy calls after anything else" + pass + + def readHeaders(r): + sourceList = r.fetchAllSources() + for rpmname in r.usedrpms: + rpmfiles = [ x for x in sourceList if os.path.basename(x) == rpmname ] + if len(rpmfiles) != 1: + raise RPMImportError('more than one source object matches ' + 'the "%s" rpm filename' %rpmname) + rpmfile = rpmfiles[0] + r.headers.append(rpmhelper.readHeader(file(rpmfile))) + + def processFiles(r): + # given the set of RPM headers used, set permissions, config + # flags, etc appropriately + for header in r.headers: + for path, mode, rdev, flags, username, groupname in zip( + header.paths(), + header[rpmhelper.FILEMODES], + header[rpmhelper.FILERDEVS], + header[rpmhelper.FILEFLAGS], + header[rpmhelper.FILEUSERNAME], + header[rpmhelper.FILEGROUPNAME]): + path = util.normpath(path) + escaped_path = util.literalRegex(path) + escaped_path = escaped_path.replace('\\/', '/') + # handle non-root ownership + if username != 'root' or groupname != 'root': + r.Ownership(username, groupname, escaped_path) + if stat.S_ISDIR(mode): + # hande directories with permissions other than + # root:root 755 (new conary should handle this for + # us automatically) + r.MakeDirs(path) + if (mode & 07777 != 0755 or username != 'root' + or groupname != 'root'): + r.ExcludeDirectories(exceptions=escaped_path) + elif stat.S_ISCHR(mode) or stat.S_ISBLK(mode): + if stat.S_ISCHR(mode): + type='c' + else: + type='b' + # this is correct for 32-bit device number + # RPM does not provide 64-bit device number + minor = rdev & 0xff | (rdev >> 12) & 0xffffff00 + major = (rdev >> 8) & 0xfff + r.MakeDevices(escaped_path, type, major, minor, + username, groupname, mode&0777) + continue + + if flags & (1 << 0): + # CONFIG + r.Config(escaped_path) + if (flags & (1 << 1) or + flags & (1 << 7) or + flags & (1 << 8)): + # DOC, LICENSE, README + r.ComponentSpec('doc', escaped_path) + if flags & (1 << 4): + # NOREPLACE + r.InitialContents(escaped_path) + if flags & (1 << 6): + # GHOST. We only handle ghost files, not ghost dirs + if stat.S_ISREG(mode): + r.Create(path) + r.InitialContents(escaped_path) + elif stat.S_ISDIR(mode): + r.ExcludeDirectories(exceptions=escaped_path) + + # handle "special" permissions (although un-cpio'ing + # the payload should have set them all correctly, payload + # doesn't include things like %gost) + if not stat.S_ISLNK(mode): + r.SetModes(path, mode & 07777) + + def processRequires(r): + # FIXME: implement + pass + + def processProvides(r): + # FIXME: implement + pass + + def setup(r): + if r.__class__.__name__ == 'RPMImportRecipe': + return + r.macros.version, r.macros.release = r.version.rsplit('_', 1) + r.preprocess() + r.unpack() + r.policy() + r.readHeaders() + r.processFiles() + r.disableBuildRequirementsPolicy() + r.disableStrip() + r.disablePolicy() + r.postprocess() + +class RPMImportError(Exception): + pass diff --git a/rpmimport/rpmsource.py b/rpmimport/rpmsource.py --- a/rpmimport/rpmsource.py +++ b/rpmimport/rpmsource.py @@ -104,7 +104,7 @@ 'perl-Bootloader', 'perl-Compress-Zlib', 'perl-DBD-SQLite', 'perl-DBI', 'perl-Digest-SHA1', 'perl-Net-Daemon', 'perl-PlRPC', 'perl-TermReadKey', 'perl-URI', - 'perl-gettext', 'procps', 'procps', 'psmisc', + 'perl-gettext', 'procps', 'psmisc', 'pwdutils', 'python', 'python-xml', 'resmgr', 'sed', 'slang', 'sles-release', 'sysconfig', 'sysfsutils', 'syslog-ng', 'sysvinit', 'tar', 'tcl', From johnsonm at rpath.com Wed Aug 19 17:38:30 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:38:30 +0000 Subject: mirrorball: add more packages, skip cooking Message-ID: <200908192138.n7JLcUow012845@scc.eng.rpath.com> changeset: adf826a59fc7 user: Matt Wilson date: Fri, 17 Nov 2006 10:04:18 -0500 add more packages, skip cooking committer: Matt Wilson diff --git a/rpmimport.py b/rpmimport.py --- a/rpmimport.py +++ b/rpmimport.py @@ -86,15 +86,16 @@ """ bins = ['aaa_base', 'acl', 'alsa', 'apache2', 'ash', 'attr', - 'bash', 'bzip2', 'compat-libstdc++', 'coreutils', - 'cpio', 'cracklib', 'cron', 'cyrus-sasl', 'db', - 'dhcpcd', 'diffutils', 'e2fsprogs', 'expat', 'expect', - 'file', 'filesystem', 'findutils', 'findutils-locate', - 'fontconfig', 'freetype', 'freetype2', 'gawk', 'gdbm', - 'glib2', 'glibc', 'gmp', 'gpm', 'grep', 'grub', - 'gzip', 'insserv', 'iproute2', 'iptables', 'iputils', - 'kbd', 'klogd', 'krb5', 'ksh', 'libaio', 'libattr', - 'libelf', 'libgcc', 'libjpeg', 'libnscd', 'libpng', + 'bash', 'binutils', 'bzip2', 'compat-libstdc++', + 'coreutils', 'cpio', 'cracklib', 'cron', 'cyrus-sasl', + 'db', 'dhcpcd', 'diffutils', 'e2fsprogs', 'expat', + 'expect', 'file', 'filesystem', 'findutils', + 'findutils-locate', 'fontconfig', 'freetype', + 'freetype2', 'gawk', 'gdbm', 'glib2', 'glibc', 'gmp', + 'gpm', 'grep', 'grub', 'gzip', 'insserv', 'iproute2', + 'iptables', 'iputils', 'kbd', 'klogd', 'krb5', 'ksh', + 'less', 'libaio', 'libattr', 'libelf', 'libgcc', + 'libjpeg', 'libnscd', 'libpcap', 'libpng', 'libstdc++', 'libxcrypt', 'make', 'mdadm', 'mingetty', 'mkinitrd', 'mktemp', 'module-init-tools', 'ncurses', 'net-tools', 'netcfg', 'openldap2', @@ -107,9 +108,9 @@ 'pwdutils', 'pwdutils', 'python', 'resmgr', 'sed', 'slang', 'sles-release', 'sysconfig', 'sysfsutils', 'syslog-ng', 'sysvinit', 'sysvinit', 'tar', 'tcl', - 'tcsh', 'termcap', 'timezone', 'udev', 'unixODBC', - 'util-linux', 'vim', 'wget', 'wireless-tools', - 'xorg-x11', 'zlib'] + 'tcsh', 'tcpdump', 'termcap', 'timezone', 'udev', + 'unixODBC', 'util-linux', 'vim', 'wget', + 'wireless-tools', 'xorg-x11', 'zlib'] srpms = list() for b in bins: @@ -225,12 +226,12 @@ shutil.copy(fn, path) addfiles.append(path) self.cvc.sourceCommand(self.cfg, addfiles, {}) - self.cvc.sourceCommand(self.cfg, ['cook', recipe], {}) + #self.cvc.sourceCommand(self.cfg, ['cook', recipe], {}) self.cvc.sourceCommand(self.cfg, [ 'commit' ], { 'message': 'Automated initial commit of ' + recipe }) - self.cvc.sourceCommand(self.cfg, ['cook', pkgname], {}) + #self.cvc.sourceCommand(self.cfg, ['cook', pkgname], {}) finally: os.chdir(cwd) From johnsonm at rpath.com Wed Aug 19 17:38:34 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:38:34 +0000 Subject: mirrorball: re-wrap pkg list Message-ID: <200908192138.n7JLcY53013040@scc.eng.rpath.com> changeset: 2627b9a91242 user: Matt Wilson date: Fri, 01 Dec 2006 12:52:13 -0500 re-wrap pkg list committer: Matt Wilson diff --git a/rpmimport/rpmsource.py b/rpmimport/rpmsource.py --- a/rpmimport/rpmsource.py +++ b/rpmimport/rpmsource.py @@ -101,18 +101,18 @@ 'libstdc++', 'libusb', 'libxcrypt', 'make', 'mdadm', 'mingetty', 'mkinitrd', 'mktemp', 'module-init-tools', 'ncurses', 'net-tools', 'netcfg', 'openldap2', - 'openldap2-client', 'openslp', 'openssh', 'openssl', 'pam', - 'pam-modules', 'patch', 'pciutils', 'pcre', 'perl', - 'perl-Bootloader', 'perl-Compress-Zlib', + 'openldap2-client', 'openslp', 'openssh', 'openssl', + 'pam', 'pam-modules', 'patch', 'pciutils', 'pcre', + 'perl', 'perl-Bootloader', 'perl-Compress-Zlib', 'perl-DBD-SQLite', 'perl-DBI', 'perl-Digest-SHA1', 'perl-Net-Daemon', 'perl-PlRPC', 'perl-TermReadKey', - 'perl-URI', 'perl-gettext', 'php5', 'procps', 'psmisc', - 'pwdutils', 'python', 'python-xml', 'resmgr', 'sed', - 'slang', 'sles-release', 'sysconfig', 'sysfsutils', - 'syslog-ng', 'sysvinit', 'tar', 'tcl', 'tcsh', - 'tcpdump', 'termcap', 'timezone', 'udev', 'unixODBC', - 'util-linux', 'vim', 'wget', 'wireless-tools', - 'xorg-x11', 'zlib'] + 'perl-URI', 'perl-gettext', 'php5', 'procps', + 'psmisc', 'pwdutils', 'python', 'python-xml', + 'resmgr', 'sed', 'slang', 'sles-release', 'sysconfig', + 'sysfsutils', 'syslog-ng', 'sysvinit', 'tar', 'tcl', + 'tcsh', 'tcpdump', 'termcap', 'timezone', 'udev', + 'unixODBC', 'util-linux', 'vim', 'wget', + 'wireless-tools', 'xorg-x11', 'zlib'] srpms = list() for b in bins: From johnsonm at rpath.com Wed Aug 19 17:38:40 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:38:40 +0000 Subject: mirrorball: add sync-repos script and hardlink.py helper Message-ID: <200908192138.n7JLceJB013331@scc.eng.rpath.com> changeset: 0126c753add1 user: Matt Wilson date: Tue, 18 Mar 2008 20:52:09 -0400 add sync-repos script and hardlink.py helper hardlink.py is from http://hardlinkpy.googlecode.com/svn/trunk/hardlink.py committer: Matt Wilson diff --git a/hardlink.py b/hardlink.py new file mode 100755 --- /dev/null +++ b/hardlink.py @@ -0,0 +1,497 @@ +#!/usr/bin/python + +# hardlink - Goes through a directory structure and creates hardlinks for +# files which are identical. +# +# Copyright (C) 2003 - 2007 John L. Villalovos, Hillsboro, Oregon +# +# This program is free software; you can redistribute it and/or modify it under +# the terms of the GNU General Public License as published by the Free Software +# Foundation; either version 2 of the License, or (at your option) any later +# version. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for +# more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., 59 +# Temple Place, Suite 330, Boston, MA 02111-1307, USA. +# +# +# ------------------------------------------------------------------------ +# John Villalovos +# email: john at sodarock.com +# http://www.sodarock.com/ +# +# Inspiration for this program came from the hardlink.c code. I liked what it +# did but did not like the code itself, to me it was very unmaintainable. So I +# rewrote in C++ and then I rewrote it in python. In reality this code is +# nothing like the original hardlink.c, since I do things quite differently. +# Even though this code is written in python the performance of the python +# version is much faster than the hardlink.c code, in my limited testing. This +# is mainly due to use of different algorithms. +# +# Original inspirational hardlink.c code was written by: Jakub Jelinek +# +# +# ------------------------------------------------------------------------ +# +# TODO: +# * Thinking it might make sense to walk the entire tree first and collect +# up all the file information before starting to do comparisons. Thought +# here is we could find all the files which are hardlinked to each other +# and then do a comparison. If they are identical then hardlink +# everything at once. + +import getopt +import os +import re +import stat +import sys +import time + +from optparse import OptionParser + +# Hash functions +# Create a hash from a file's size and time values +def hash_size_time(size, time): + return (size ^ time) & (MAX_HASHES - 1); + +def hash_size(size): + return (size) & (MAX_HASHES - 1); + +def hash_value(size, time, notimestamp): + if notimestamp: + return hash_size(size) + else: + return hash_size_time(size,time) + +# If two files have the same inode and are on the same device then they are +# already hardlinked. +def isAlreadyHardlinked( + st1, # first file's status + st2 ): # second file's status + result = ( + (st1[stat.ST_INO] == st2[stat.ST_INO]) and # Inodes equal + (st1[stat.ST_DEV] == st2[stat.ST_DEV]) # Devices equal + ); + return result + +# if a file is eligibile for hardlinking. Files will only be considered for +# hardlinking if this function returns true. +def eligibleForHardlink( + st1, # first file's status + st2, # second file's status + options): + + result = ( + # Must meet the following + # criteria: + (not isAlreadyHardlinked(st1, st2)) and # NOT already hard linked + (st1[stat.ST_SIZE] == st2[stat.ST_SIZE]) and # size is the same + (st1[stat.ST_SIZE] != 0 ) and # size is not zero + (st1[stat.ST_MODE] == st2[stat.ST_MODE]) and # file mode is the same + (st1[stat.ST_UID] == st2[stat.ST_UID]) and # owner user id is the same + (st1[stat.ST_GID] == st2[stat.ST_GID]) and # owner group id is the same + ((st1[stat.ST_MTIME] == st2[stat.ST_MTIME]) or # modified time is the same + (options.notimestamp)) and # OR date hashing is off + (st1[stat.ST_DEV] == st2[stat.ST_DEV]) # device is the same + ) + if None: + # if not result: + print "\n***\n", st1 + print st2 + print "Already hardlinked: %s" % (not isAlreadyHardlinked(st1, st2)) + print "Modes:", st1[stat.ST_MODE], st2[stat.ST_MODE] + print "UIDs:", st1[stat.ST_UID], st2[stat.ST_UID] + print "GIDs:", st1[stat.ST_GID], st2[stat.ST_GID] + print "SIZE:", st1[stat.ST_SIZE], st2[stat.ST_SIZE] + print "MTIME:", st1[stat.ST_MTIME], st2[stat.ST_MTIME] + print "Ignore date:", options.notimestamp + print "Device:", st1[stat.ST_DEV], st2[stat.ST_DEV] + return result + + +def areFileContentsEqual(filename1, filename2, options): + """Determine if the contents of two files are equal. + **!! This function assumes that the file sizes of the two files are + equal.""" + # Open our two files + file1 = open(filename1,'rb') + file2 = open(filename2,'rb') + # Make sure open succeeded + if not (file1 and file2): + print "Error opening file in areFileContentsEqual" + print "Was attempting to open:" + print "file1: %s" % filename1 + print "file2: %s" % filename2 + result = False + else: + if options.verbose >= 1: + print "Comparing: %s" % filename1 + print " to : %s" % filename2 + buffer_size = 1024*1024 + while 1: + buffer1 = file1.read(buffer_size) + buffer2 = file2.read(buffer_size) + if buffer1 != buffer2: + result = False + break + if not buffer1: + result = True + break + gStats.didComparison() + return result + +# Determines if two files should be hard linked together. +def areFilesHardlinkable(file_info_1, file_info_2, options): + filename1 = file_info_1[0] + stat_info_1 = file_info_1[1] + filename2 = file_info_2[0] + stat_info_2 = file_info_2[1] + # See if the files are eligible for hardlinking + if eligibleForHardlink(stat_info_1, stat_info_2, options): + # Now see if the contents of the file are the same. If they are then + # these two files should be hardlinked. + if not options.samename: + # By default we don't care if the filenames are equal + result = areFileContentsEqual(filename1, filename2, options) + else: + # Make sure the filenames are the same, if so then compare content + basename1 = os.path.basename(filename1) + basename2 = os.path.basename(filename2) + if basename1 == basename2: + result = areFileContentsEqual(filename1, filename2, options) + else: + result = False + else: + result = False + return result + +# Hardlink two files together +def hardlinkfiles(sourcefile, destfile, stat_info, options): + # rename the destination file to save it + temp_name = destfile + ".$$$___cleanit___$$$" + try: + if not options.dryrun: + os.rename(destfile, temp_name) + except OSError, error: + print "Failed to rename: %s to %s" % (destfile, temp_name) + print error + result = False + else: + # Now link the sourcefile to the destination file + try: + if not options.dryrun: + os.link(sourcefile, destfile) + except: + print "Failed to hardlink: %s to %s" % (sourcefile, destfile) + # Try to recover + try: + os.rename(temp_name, destfile) + except: + print "BAD BAD - failed to rename back %s to %s" (temp_name, destfile) + result = False + else: + # hard link succeeded + # Delete the renamed version since we don't need it. + if not options.dryrun: + os.unlink ( temp_name) + # update our stats + gStats.didHardlink(sourcefile, destfile, stat_info) + if options.verbose >= 1: + if options.dryrun: + print "Did NOT link. Dry run" + size = stat_info[stat.ST_SIZE] + print "Linked: %s" % sourcefile + print" to: %s, saved %s" % (destfile, size) + result = True + return result + +def hardlink_identical_files(directories, filename, options): + """ + The purpose of this function is to hardlink files together if the files are + the same. To be considered the same they must be equal in the following + criteria: + * file mode + * owner user id + * owner group id + * file size + * modified time (optional) + * file contents + + Also, files will only be hardlinked if they are on the same device. This + is because hardlink does not allow you to hardlink across file systems. + + The basic idea on how this is done is as follows: + + Walk the directory tree building up a list of the files. + + For each file, generate a simple hash based on the size and modified time. + + For any other files which share this hash make sure that they are not + identical to this file. If they are identical then hardlink the files. + + Add the file info to the list of files that have the same hash value.""" + + for exclude in options.excludes: + if re.search(exclude, filename): + return + try: + stat_info = os.stat(filename) + except OSError: + # Python 1.5.2 doesn't handle 2GB+ files well :( + print "Unable to get stat info for: %s" % filename + print "If running Python 1.5 this could be because the file is greater than 2 Gibibytes" + return + if not stat_info: + # We didn't get the file status info :( + return + + # Is it a directory? + if stat.S_ISDIR(stat_info[stat.ST_MODE]): + # If it is a directory then add it to the list of directories. + directories.append(filename) + # Is it a regular file? + elif stat.S_ISREG(stat_info[stat.ST_MODE]): + # Create the hash for the file. + file_hash = hash_value(stat_info[stat.ST_SIZE], stat_info[stat.ST_MTIME], + options.notimestamp) + # Bump statistics count of regular files found. + gStats.foundRegularFile() + if options.verbose >= 2: + print "File: %s" % filename + work_file_info = (filename, stat_info) + if file_hashes.has_key(file_hash): + # We have file(s) that have the same hash as our current file. + # Let's go through the list of files with the same hash and see if + # we are already hardlinked to any of them. + for (temp_filename,temp_stat_info) in file_hashes[file_hash]: + if isAlreadyHardlinked(stat_info,temp_stat_info): + gStats.foundHardlink(temp_filename,filename, + temp_stat_info) + break + else: + # We did not find this file as hardlinked to any other file + # yet. So now lets see if our file should be hardlinked to any + # of the other files with the same hash. + for (temp_filename,temp_stat_info) in file_hashes[file_hash]: + if areFilesHardlinkable(work_file_info, (temp_filename, temp_stat_info), + options): + hardlinkfiles(temp_filename, filename, temp_stat_info, options) + break + else: + # The file should NOT be hardlinked to any of the other + # files with the same hash. So we will add it to the list + # of files. + file_hashes[file_hash].append(work_file_info) + else: + # There weren't any other files with the same hash value so we will + # create a new entry and store our file. + file_hashes[file_hash] = [work_file_info] + + +class cStatistics: + def __init__(self): + self.dircount = 0L # how many directories we find + self.regularfiles = 0L # how many regular files we find + self.comparisons = 0L # how many file content comparisons + self.hardlinked_thisrun = 0L # hardlinks done this run + self.hardlinked_previously = 0L; # hardlinks that are already existing + self.bytes_saved_thisrun = 0L # bytes saved by hardlinking this run + self.bytes_saved_previously = 0L # bytes saved by previous hardlinks + self.hardlinkstats = [] # list of files hardlinked this run + self.starttime = time.time() # track how long it takes + self.previouslyhardlinked = {} # list of files hardlinked previously + + def foundDirectory(self): + self.dircount = self.dircount + 1 + def foundRegularFile(self): + self.regularfiles = self.regularfiles + 1 + def didComparison(self): + self.comparisons = self.comparisons + 1 + def foundHardlink(self,sourcefile, destfile, stat_info): + filesize = stat_info[stat.ST_SIZE] + self.hardlinked_previously = self.hardlinked_previously + 1 + self.bytes_saved_previously = self.bytes_saved_previously + filesize + if not self.previouslyhardlinked.has_key(sourcefile): + self.previouslyhardlinked[sourcefile] = (stat_info,[destfile]) + else: + self.previouslyhardlinked[sourcefile][1].append(destfile) + def didHardlink(self,sourcefile,destfile,stat_info): + filesize = stat_info[stat.ST_SIZE] + self.hardlinked_thisrun = self.hardlinked_thisrun + 1 + self.bytes_saved_thisrun = self.bytes_saved_thisrun + filesize + self.hardlinkstats.append((sourcefile, destfile)) + def printStats(self, options): + print "\n" + print "Hard linking Statistics:" + # Print out the stats for the files we hardlinked, if any + if self.previouslyhardlinked and options.printprevious: + keys = self.previouslyhardlinked.keys() + keys.sort() + print "Files Previously Hardlinked:" + for key in keys: + stat_info, file_list = self.previouslyhardlinked[key] + size = stat_info[stat.ST_SIZE] + print "Hardlinked together: %s" % key + for filename in file_list: + print " : %s" % filename + print "Size per file: %s Total saved: %s" % (size, + size * len(file_list)) + print + if self.hardlinkstats: + if options.dryrun: + print "Statistics reflect what would have happened if not a dry run" + print "Files Hardlinked this run:" + for (source,dest) in self.hardlinkstats: + print"Hardlinked: %s" % source + print" to: %s" % dest + print + print "Directories : %s" % self.dircount + print "Regular files : %s" % self.regularfiles + print "Comparisons : %s" % self.comparisons + print "Hardlinked this run : %s" % self.hardlinked_thisrun + print "Total hardlinks : %s" % (self.hardlinked_previously + self.hardlinked_thisrun) + print "Bytes saved this run : %s (%s)" % (self.bytes_saved_thisrun, humanize_number(self.bytes_saved_thisrun)) + totalbytes = self.bytes_saved_thisrun + self.bytes_saved_previously; + print "Total bytes saved : %s (%s)" % (totalbytes, humanize_number(totalbytes)) + print "Total run time : %s seconds" % (time.time() - self.starttime) + + + +def humanize_number( number ): + if number > 1024 * 1024 * 1024: + return ("%.3f gibibytes" % (number / (1024.0 * 1024 * 1024))) + if number > 1024 * 1024: + return ("%.3f mibibytes" % (number / (1024.0 * 1024))) + if number > 1024: + return ("%.3f kibibytes" % (number / 1024.0)) + return ("%d bytes" % number) + + + +def printversion(self): + print "hardlink.py, Version %s" % VERSION + print "Copyright (C) 2003 - 2006 John L. Villalovos." + print "email: software at sodarock.com" + print "web: http://www.sodarock.com/" + print """ +This program is free software; you can redistribute it and/or modify it under +the terms of the GNU General Public License as published by the Free Software +Foundation; version 2 of the License. + +This program is distributed in the hope that it will be useful, but WITHOUT ANY +WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A +PARTICULAR PURPOSE. See the GNU General Public License for more details. + +You should have received a copy of the GNU General Public License along with +this program; if not, write to the Free Software Foundation, Inc., 59 Temple +Place, Suite 330, Boston, MA 02111-1307, USA. +""" + + +def parseCommandLine(): + usage = "usage: %prog [options] directory [ directory ... ]" + version = "%prog: " + VERSION + parser = OptionParser(usage=usage, version=version) + parser.add_option("-f", "--filenames-equal", help="Filenames have to be identical", + action="store_true", dest="samename", default=False,) + + parser.add_option("-n", "--dry-run", help="Do NOT actually hardlink files", + action="store_true", dest="dryrun", default=False,) + + parser.add_option("-p", "--print-previous", help="Print previously created hardlinks", + action="store_true", dest="printprevious", default=False,) + + parser.add_option("-q", "--no-stats", help="Do not print the statistics", + action="store_false", dest="printstats", default=True,) + + parser.add_option("-t", "--timestamp-ignore", + help="File modification times do NOT have to be identical", + action="store_true", dest="notimestamp", default=False,) + + parser.add_option("-v", "--verbose", + help="Verbosity level (default: %default)", metavar="LEVEL", + action="store", dest="verbose", default=1,) + + parser.add_option("-x", "--exclude", + help="Regular expression used to exclude files/dirs (may specify multiple times)", metavar="REGEX", + action="append", dest="excludes", default=[],) + + (options, args) = parser.parse_args() + if not args: + parser.print_help() + print + print "Error: Must supply one or more directories" + sys.exit(1) + args = [os.path.abspath(os.path.expanduser(dirname)) for dirname in args] + for dirname in args: + if not os.path.isdir(dirname): + parser.print_help() + print + print "Error: %s is NOT a directory" % dirname + sys.exit(1) + return options, args + + +# Start of global declarations +debug = None +debug1 = None + +MAX_HASHES = 128 * 1024 + +gStats = cStatistics() + +file_hashes = {} + +VERSION = "0.04 - 2007-11-14 (14-Nov-2007)" + +def main(): + # Parse our argument list and get our list of directories + options, directories = parseCommandLine() + # Compile up our regexes ahead of time + MIRROR_PL_REGEX = re.compile(r'^\.in\.') + RSYNC_TEMP_REGEX = re.compile((r'^\..*\.\?{6,6}$')) + # Now go through all the directories that have been added. + # NOTE: hardlink_identical_files() will add more directories to the + # directories list as it finds them. + while directories: + # Get the last directory in the list + directory = directories[-1] + '/' + del directories[-1] + if not os.path.isdir(directory): + print "%s is NOT a directory!" % directory + else: + gStats.foundDirectory() + # Loop through all the files in the directory + try: + dir_entries = os.listdir(directory) + except OSError: + print "Error: Unable to do an os.listdir on: %s Skipping..." % directory + continue + for entry in dir_entries: + pathname = os.path.normpath(os.path.join(directory,entry)) + # Look at files/dirs beginning with "." + if entry[0] == ".": + # Ignore any mirror.pl files. These are the files that + # start with ".in." + if MIRROR_PL_REGEX.match(entry): + continue + # Ignore any RSYNC files. These are files that have the + # format .FILENAME.?????? + if RSYNC_TEMP_REGEX.match(entry): + continue + if os.path.islink(pathname): + if debug1: print "%s: is a symbolic link, ignoring" % pathname + continue + if debug1 and os.path.isdir(pathname): + print "%s is a directory!" % pathname + hardlink_identical_files(directories, pathname, options) + if options.printstats: + gStats.printStats(options) + +if __name__ == '__main__': + main() From johnsonm at rpath.com Wed Aug 19 17:38:35 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:38:35 +0000 Subject: mirrorball: add #!/usr/bin/python/ Message-ID: <200908192138.n7JLcaJ8013097@scc.eng.rpath.com> changeset: 703d1a72028b user: Matt Wilson date: Fri, 01 Dec 2006 13:35:54 -0500 add #!/usr/bin/python/ committer: Matt Wilson diff --git a/pkgwiz.py b/pkgwiz.py old mode 100644 new mode 100755 --- a/pkgwiz.py +++ b/pkgwiz.py @@ -1,3 +1,4 @@ +#!/usr/bin/python from conary import cvc, rpmhelper import conary.lib.util import os From johnsonm at rpath.com Wed Aug 19 17:38:39 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:38:39 +0000 Subject: mirrorball: correct name of dbus Message-ID: <200908192138.n7JLcd1W013232@scc.eng.rpath.com> changeset: c0b7db7298c8 user: Matt Wilson date: Fri, 01 Dec 2006 15:13:09 -0500 correct name of dbus committer: Matt Wilson diff --git a/rpmimport/rpmsource.py b/rpmimport/rpmsource.py --- a/rpmimport/rpmsource.py +++ b/rpmimport/rpmsource.py @@ -103,7 +103,7 @@ 'cron', 'cyrus-sasl', 'db', - 'dbus', + 'dbus-1', 'dhcpcd', 'diffutils', 'e2fsprogs', From johnsonm at rpath.com Wed Aug 19 17:38:38 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:38:38 +0000 Subject: mirrorball: branch merge Message-ID: <200908192138.n7JLcchI013215@scc.eng.rpath.com> changeset: 1f2465693f1e user: Matt Wilson date: Fri, 01 Dec 2006 15:11:59 -0500 branch merge committer: Matt Wilson diff --git a/pkgwiz.py b/pkgwiz.py --- a/pkgwiz.py +++ b/pkgwiz.py @@ -111,8 +111,6 @@ recipeFile.close() # Remove the original RPMs and add the new ones. - import epdb - epdb.st() cwd = os.getcwd() os.chdir(pkgname) from conary.state import ConaryStateFromFile From johnsonm at rpath.com Wed Aug 19 17:38:41 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:38:41 +0000 Subject: mirrorball: add packages to import Message-ID: <200908192138.n7JLcf9C013370@scc.eng.rpath.com> changeset: 0993e24c7175 user: Matt Wilson date: Wed, 02 Apr 2008 11:41:39 -0400 add packages to import committer: Matt Wilson diff --git a/pkgwiz.py b/pkgwiz.py --- a/pkgwiz.py +++ b/pkgwiz.py @@ -43,8 +43,10 @@ 'audit', 'bash', 'binutils', + 'busybox', 'bzip2', 'compat-libstdc++', + 'compat-openssl097g', 'coreutils', 'cpio', 'cpp', @@ -95,6 +97,7 @@ 'libapr1', 'libapr-util1', 'libattr', + 'libcap', 'libcom_err', 'libelf', 'libgcc', @@ -170,7 +173,7 @@ 'vim', 'wget', 'wireless-tools', - 'xorg-x11', + #'xorg-x11', 'zlib' ) From johnsonm at rpath.com Wed Aug 19 17:38:42 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:38:42 +0000 Subject: mirrorball: add some more packages Message-ID: <200908192138.n7JLch6w013428@scc.eng.rpath.com> changeset: 49556b9027c8 user: Matt Wilson date: Tue, 13 May 2008 10:10:15 -0400 add some more packages committer: Matt Wilson diff --git a/pkgwiz.py b/pkgwiz.py --- a/pkgwiz.py +++ b/pkgwiz.py @@ -38,6 +38,8 @@ 'aaa_base', 'acl', 'apache2', + 'apache2-mod_php', + 'apache2-mod_python', 'ash', 'attr', 'audit', @@ -169,6 +171,7 @@ 'sysconfig', 'sysfsutils', 'syslog-ng', + 'sysstat', 'sysvinit', 'tar', 'tcl', @@ -184,6 +187,7 @@ 'vim', 'wget', 'wireless-tools', + 'xntp', #'xorg-x11', 'zlib', 'zip', From johnsonm at rpath.com Wed Aug 19 17:38:39 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:38:39 +0000 Subject: mirrorball: add popt to list Message-ID: <200908192138.n7JLcdwK013256@scc.eng.rpath.com> changeset: 3b211a40d8dc user: Matt Wilson date: Fri, 01 Dec 2006 15:41:10 -0500 add popt to list committer: Matt Wilson diff --git a/rpmimport/rpmsource.py b/rpmimport/rpmsource.py --- a/rpmimport/rpmsource.py +++ b/rpmimport/rpmsource.py @@ -190,6 +190,7 @@ 'perl-URI', 'perl-gettext', 'php5', + 'popt', 'procps', 'psmisc', 'pwdutils', From johnsonm at rpath.com Wed Aug 19 17:38:45 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:38:45 +0000 Subject: mirrorball: add initial pylint integration (SLE-87) Message-ID: <200908192138.n7JLcjeI013546@scc.eng.rpath.com> changeset: 290ddc393f12 user: Elliot Peele date: Tue, 27 May 2008 17:38:41 -0400 add initial pylint integration (SLE-87) committer: Elliot Peele diff --git a/pylint/Makefile b/pylint/Makefile new file mode 100644 --- /dev/null +++ b/pylint/Makefile @@ -0,0 +1,31 @@ +# +# Copyright (c) 2008 rPath, Inc. +# +# This program is distributed under the terms of the Common Public License, +# version 1.0. A copy of this license should have been distributed with this +# source file in a file called LICENSE. If it is not present, the license +# is always available at http://www.rpath.com/permanent/licenses/CPL-1.0. +# +# This program is distributed in the hope that it will be useful, but +# without any warranty; without even the implied warranty of merchantability +# or fitness for a particular purpose. See the Common Public License for +# full details. +# + +dist_files = init_pylint.py Makefile pylintrc run_pylint + +all: pylint +install: + +dist: default-dist + +pylint: + @echo running pylint... + @./run_pylint + @echo Done. Error output is in the reports/ directory. + +clean: + rm -rf reports *,cover + +include ../Make.rules +include ../Make.defs diff --git a/pylint/init_pylint.py b/pylint/init_pylint.py new file mode 100644 --- /dev/null +++ b/pylint/init_pylint.py @@ -0,0 +1,45 @@ +# +# Copyright (c) 2008 rPath, Inc. +# +# This program is distributed under the terms of the Common Public License, +# version 1.0. A copy of this license should have been distributed with this +# source file in a file called LICENSE. If it is not present, the license +# is always available at http://www.rpath.com/permanent/licenses/CPL-1.0. +# +# This program is distributed in the hope that it will be useful, but +# without any warranty; without even the implied warranty of merchantability +# or fitness for a particular purpose. See the Common Public License for +# full details. +# + +import os +import sys + +import epdb +epdb.st() + +# set default SLEESTACK_PATH, if it was not set. +parDir = '/'.join(os.path.realpath(__file__).split('/')[:-2]) +parDir = os.path.dirname(parDir) +mirrorballPath = os.getenv('SLEESTACK_PATH', parDir) +os.environ['SLEESTACK_PATH'] = mirrorballPath + +def setPathFromEnv(variable, directory): + parDir = '/'.join(os.path.realpath(__file__).split('/')[:-3]) + parDir = os.path.dirname(parDir) + '/' + directory + thisPath = os.getenv(variable, parDir) + os.environ[variable] = thisPath + if thisPath not in sys.path: + sys.path.insert(0, thisPath) + return thisPath + +# set default CONARY_PATH, if it was not set. +conaryPath = setPathFromEnv('CONARY_PATH', 'conary') + +# set default RMAKE_PATH, if it was not set. +rmakePath = setPathFromEnv('RMAKE_PATH', 'rmake') + +for path in rmakePath, conaryPath, mirrorballPath: + if path in sys.path: + sys.path.remove(path) + sys.path.insert(0, path) diff --git a/pylint/pylint_commit b/pylint/pylint_commit new file mode 100755 --- /dev/null +++ b/pylint/pylint_commit @@ -0,0 +1,30 @@ +#!/bin/sh +curdir=`pwd` +files=`hg diff | diffstat -l -p1 | grep -v pylint | sed s,^,${curdir}/,g` +if [ -z "$files" ] ; then + exit 0; +fi +echo "Running pylint...." +cd pylint; +./run_pylint $files --files-output=n | tee pylint_errors.txt +if [ -s pylint_errors.txt ]; then + while true; do + echo "Pylint errors were found." + echo -n "Commit anyway? [y/N] " + read yn + if [ -z "$yn" ]; then + echo "Not committing." + exit 1 + fi + case $yn in + y | Y ) echo "Committing" + exit 0 ;; + [nN] ) echo "Errors for your files stored in `pwd`/pylint_errors.txt" + exit 1 ;; + * ) echo "Answer y or n" + esac + done +else + echo "Pylint passed." + exit 1 +fi diff --git a/pylint/pylintrc b/pylint/pylintrc new file mode 100644 --- /dev/null +++ b/pylint/pylintrc @@ -0,0 +1,35 @@ +[BASIC] +# Regular expression which should only match correct class names +class-rgx=[A-Z][a-zA-Z0-9]+$ + +# Regular expression which should only match correct function names +function-rgx=[a-z_][a-zA-Z0-9]*$ + +# Regular expression which should only match correct function names +name-rgx=[a-z_][a-zA-Z0-9]*$ + +# Regular expression which should only match correct method names +method-rgx=([a-z_][a-zA-Z0-9]*)|(__[a-z][a-zA-Z0-9]+__)$ + +# Regular expression which should only match correct argument names +argument-rgx=[a-z][a-zA-Z0-9]*$ + +# Regular expression which should only match correct variable names +variable-rgx=[a-z][a-zA-Z0-9]*$ + +# Attributes can be camel case too +attr-rgx=[a-z_][a-zA-Z0-9_]{2,30}$ + +# Good variable names which should always be accepted, separated by a comma +good-names=i,j,k,n,v,f,ex,Run,_,db,cu,__ + + + + +[REPORTS] +files-output=yes +reports=yes +include-ids=yes + +[MESSAGES CONTROL] +disable-msg=W0142,I0011 diff --git a/pylint/run_pylint b/pylint/run_pylint new file mode 100755 --- /dev/null +++ b/pylint/run_pylint @@ -0,0 +1,37 @@ +#!/bin/sh +# +# Copyright (c) 2008 rPath, Inc. +# +# This program is distributed under the terms of the Common Public License, +# version 1.0. A copy of this license should have been distributed with this +# source file in a file called LICENSE. If it is not present, the license +# is always available at http://www.rpath.com/permanent/licenses/CPL-1.0. +# +# This program is distributed in the hope that it will be useful, but +# without any warranty; without even the implied warranty of merchantability +# or fitness for a particular purpose. See the Common Public License for +# full details. +# + +if [ -x reports ]; then + rm -rf reports; +fi +mkdir reports +cp init_pylint.py reports +cd reports + +if [ -z "$*" ]; then + files="updateBot repomd rpmimport" +else + files=$@ +fi + +pylint --init-hook='import sys; sys.path.append("."); import init_pylint' --rcfile='../pylintrc' $files +rc=$? +rm init_pylint*; +for file in `ls`; do + if [ ! -s $file ]; then + rm $file; + fi +done +exit $rc From johnsonm at rpath.com Wed Aug 19 17:38:31 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:38:31 +0000 Subject: mirrorball: Reorg. Message-ID: <200908192138.n7JLcVEB012882@scc.eng.rpath.com> changeset: c994dad9d44e user: Xiaowen Xin date: Fri, 24 Nov 2006 18:30:48 -0500 Reorg. committer: Xiaowen Xin diff --git a/cook.py b/cook.py new file mode 100644 --- /dev/null +++ b/cook.py @@ -0,0 +1,32 @@ + def cook(self, group): + """ + Go through all the packages in a group that's on the current label and cook them. + """ + + self.repo() + from conary import display, queryrep, trove + + troveTups = queryrep.getTrovesToDisplay( + self.repos, [group], [], + versionFilter=queryrep.VERSION_FILTER_LATEST, + flavorFilter=queryrep.FLAVOR_FILTER_BEST, + labelPath=self.cfg.buildLabel, defaultFlavor=self.cfg.flavor, + affinityDb=None) + + dcfg = display.DisplayConfig(self.repos, None) + troveSource = dcfg.getTroveSource() + troves = troveSource.getTroves(troveTups, withFiles=False) + childTups = list(troves[0].iterTroveList(strongRefs=True)) + + import epdb + epdb.st() + + troveSpecs = [(group, None, None)] + self.repos.findTroves(self.cfg.buildLabel, troveSpecs) + + troveTups = [(group, None, None)] + + #t = troveSource.getTroves(troveTups) + ttup = self.repos.findTroves(self.cfg.buildLabel, troveTups)[troveTups[0]] + + diff --git a/infoimport.py b/infoimport.py deleted file mode 100755 --- a/infoimport.py +++ /dev/null @@ -1,180 +0,0 @@ -#!/usr/bin/python -# -# Copyright (c) 2006 rPath, Inc. -# -# -# This program is distributed under the terms of the Common Public License, -# version 1.0. A copy of this license should have been distributed with this -# source file in a file called LICENSE. If it is not present, the license -# is always available at http://www.rpath.com/permanent/licenses/CPL-1.0. -# -# This program is distributed in the hope that it will be useful, but -# without any warranty; without even the implied warranty of merchantability -# or fitness for a particular purpose. See the Common Public License for -# full details. -# - - -import grp -import pwd - -class InfoMaker: - def __init__(self, cfg, repos, recipeMaker): - self.cfg = cfg - self.repos = repos - self.recipeMaker = recipeMaker - - def closeUser(self, user, users, groups, ustaging, gstaging, ugMap): - """ - If a user has supplemental groups, then create those. - """ - try: - uprop = pwd.getpwnam(user) - except KeyError, e: - print "The user %s does not exist on the build system." \ - " Please create it before running this." % (user) - return - - (uid, gid, comment, homedir, shell) = uprop[2:] - gprop = grp.getgrgid(gid) - group = gprop[0] - supgroups = ugMap.get(user, set()).difference(set([group])) - for g in supgroups: - if g not in groups: - gstaging.add(g) - users.add(user) - - def closeGroup(self, group, users, groups, ustaging, gstaging, ugMap): - """ - If there is a user with this as its primary group, then create the user - instead. - """ - - try: - gprop = grp.getgrnam(group) - except KeyError, e: - print "The group %s does not exist on the build system." \ - " Please create it first before running this." % (group) - return - - gid = gprop[2] - primaries = gprop[3] + [group] - for u in primaries: - try: - uprop = pwd.getpwnam(u) - if uprop[3] == gid: - ustaging.add(uprop[0]) - return - except KeyError, e: - # No such user, oh well, ignore. - pass - groups.add(group) - - def makeInfo(self, users, groups): - # Map username to groups it belongs to. - ugMap = dict() - for (g, a, b, us) in grp.getgrall(): - for u in us: - if u in ugMap: - ugMap[u].add(g) - else: - ugMap[u] = set([g]) - - # Groups and users depend on each other, so do their closure. - ustaging = users - gstaging = groups - users = set() - groups = set() - while ustaging or gstaging: - for user in ustaging: - self.closeUser(user, users, groups, ustaging, gstaging, ugMap) - ustaging = set() - for group in gstaging: - self.closeGroup(group, users, groups, ustaging, gstaging, ugMap) - gstaging = set() - - # Remove the root user and group. - users = users.difference(['root']) - groups = groups.difference(['root']) - - # If there are groups named the same as the user, then the user must - # have this group as its primary group. - groups = groups.difference(users) - - # All the packages we might create. - srccomps = {} - for account in users.union(groups): - srccomps['info-%s:source' % (account)] = {self.cfg.buildLabel: None} - - # Get current repository contents. - repoContents = self.repos.getTroveVersionsByLabel(srccomps) - - # Create users. - for user in users: - # If it already exists, then move on. - if 'info-%s:source' %user in repoContents: - continue - - uprop = pwd.getpwnam(user) - (uid, gid, comment, homedir, shell) = uprop[2:] - gprop = grp.getgrgid(gid) - group = gprop[0] - supgroups = ugMap.get(user, set()).difference(set([group])) - - self.recipeMaker.create('info-%s' % (user), - "class info_%(user)s(UserInfoRecipe):\n" - " name = 'info-%(user)s'\n" - " version = '1'\n" - "\n" - " def setup(r):\n" - " r.User('%(user)s', %(uid)s, group='%(group)s', groupid=%(gid)s,\n" - " homedir='%(homedir)s', comment='%(comment)s', shell='%(shell)s',\n" - " supplemental=[%(supgroups)s])\n" - % dict(user=user, uid=uid, group=group, gid=gid, - homedir=homedir, comment=comment, shell=shell, - supgroups=', '.join("'%s'" % g for g in supgroups))) - - # Create groups. - for group in groups: - # If it already exists, then move on. - if 'info-%s:source' %group in repoContents: - continue - - gprop = grp.getgrnam(group) - gid = gprop[2] - - # Create the group recipe. - self.recipeMaker.create('info-%s' % (group), - "class info_%(group)s(GroupInfoRecipe):\n" - " name = 'info-%(group)s'\n" - " version = '1'\n" - "\n" - " def setup(r):\n" - " r.Group('%(group)s', %(gid)s)\n" - % dict(group=group, gid=gid)) - -if __name__ == '__main__': - from conary import conaryclient, conarycfg, versions, errors, cvc - from conary import deps - from conary.lib import util - from conary.build import use - - sys.excepthook = util.genExcepthook(debug=True) - - cfg = conarycfg.ConaryConfiguration(readConfigFiles=True) - cfg.initializeFlavors() - - buildFlavor = deps.deps.parseFlavor('is:x86(i586,!i686)') - cfg.buildFlavor = deps.deps.overrideFlavor(cfg.buildFlavor, - buildFlavor) - use.setBuildFlagsFromFlavor(None, cfg.buildFlavor, error=False) - - client = conaryclient.ConaryClient(cfg) - repos = client.getRepos() - roots = sys.argv[1:] - rpmSource = RpmSource() - for root in roots: - rpmSource.walk(root) - recipeMaker = RecipeMaker(cvc, cfg, repos, rpmSource) - - infoMaker = InfoMaker(cfg, repos, recipeMaker) diff --git a/pkgwiz.py b/pkgwiz.py new file mode 100644 --- /dev/null +++ b/pkgwiz.py @@ -0,0 +1,172 @@ +import conary.lib.util +import infoimport +import os +import rpmimport +import shutil +import sys + +HELP_TEXT = """ +pkgs | [| ...] +accounts | [| ...] + +pkgs can be given individual RPMs or the root of a directory tree to walk. + +When packages are imported, they will be checked against what's in the repository. If newer, then check new one in. + +accounts will create user and/or group info- packages, using information from the build system. +""" + +class PkgWiz: + def __init__(self): + self.cfg = None + self.client = None + self.repos = None + self.rpmSource = rpmimport.RpmSource() + self.recipeMaker = None + + def help(self): + print HELP_TEXT + + def repo(self): + from conary import conaryclient, conarycfg, versions, errors + from conary import deps + from conary.build import use + + self.cfg = conarycfg.ConaryConfiguration(readConfigFiles=True) + self.cfg.initializeFlavors() + + buildFlavor = deps.deps.parseFlavor('is:x86(i586,!i686)') + self.cfg.buildFlavor = deps.deps.overrideFlavor( + self.cfg.buildFlavor, buildFlavor) + use.setBuildFlagsFromFlavor(None, self.cfg.buildFlavor, error=False) + + self.client = conaryclient.ConaryClient(self.cfg) + self.repos = self.client.getRepos() + self.recipeMaker = rpmimport.RecipeMaker(self.cfg, self.repos, self.rpmSource) + + def createPkgs(self, dirs): + for dir in dirs: + self.rpmSource.walk(dir) + + # {foo:source: {cfg.buildLabel: None}} + srccomps = {} + + # {foo:source: foo-1.0-1.1.src.rpm} + srcmap = {} + for src in self.rpmSource.getSrpms(): + h = self.rpmSource.getHeader(self.rpmSource.srcPath[src]) + srccomp = h[NAME] + ':source' + srcmap[srccomp] = src + srccomps[srccomp] = {cfg.buildLabel: None} + d = self.repos.getTroveVersionsByLabel(srccomps) + + # Iterate over foo:source. + for srccomp in srccomps.iterkeys(): + src = srcmap[srccomp] + pkgname = srccomp.split(':')[0] + if srccomp not in d: + self.recipeMaker.create(pkgname, self.rpmSource.createTemplate(src), src) + else: + # The package already exists in the repository, so query its + # version. + oldVersion = 1# FIXME + # Get the version + srchdr = self.rpmSource.getHeader(self.rpmSource.srcPath(src)) + newVersion = '%s_%s' % (srchdr[VERSION], srchdr[RELEASE]) + if newVersion == oldVersion: + continue + + # Check it out. + from conary import cvc + cvc.sourceCommand(self.cfg, ['checkout', pkgname]) + + # Look for the version string and replace it with the new. + import re + pattern = re.compile("""(?P\\s+)version\\s*=\\s*['"]([^'"\\s]+_[^'"\\s]+)['"]\\s*$""") + filename = os.path.join(pkgname, pkgname + ".recipe") + recipeFile = open(filename) + lines = recipeFile.readlines() + recipeFile.close() + spot = None + for (index, line) in lines: + match = pattern.match(line) + if match: + spot = index + break + if not spot: + raise Exception( + "The version string was not found in the %s recipe." + " Is this correct?" %pkgname) + lines = lines[:spot] + [match.group('lead') + + "version = '%s'\n" %newVersion] + lines[spot+1:] + recipeFile = open(filename, 'w') + recipeFile.write(''.join(lines)) + recipeFile.close() + + # Remove the original RPMs and add the new ones. + import epdb + epdb.st() + cwd = os.getcwd() + os.chdir(pkgname) + from conary.state import ConaryStateFromFile + conaryState = ConaryStateFromFile("CONARY", repos) + state = conaryState.getSourceState() + rpms = [] + for (theId, path, fileId, version) in state.iterFileList(): + if path.endswith('rpm'): + rpms.append(path) + for rpm in rpms: + cvc.sourceCommand(self.cfg, ['remove', rpm]) + addfiles = ['add'] + for path, fn in self.rpmSource.rpmMap[src].iteritems(): + shutil.copy(fn, path) + addfiles.append(path) + cvc.sourceCommand(self.cfg, addfiles, {}) + cvc.sourceCommand(self.cfg, + [ 'commit' ], {'message': 'Automated update of ' + pkgname}) + + def test(self): + import epdb + epdb.st() + from conary.state import ConaryStateFromFile + conaryState = ConaryStateFromFile("/home/xiaowen/nuernberg/pwdutils/CONARY", self.repos) + + def createUsers(self, users): + # Get all users and groups used in this run. + users = set() + groups = set() + for src in self.rpmSource.getSrpms(): + for rpm in self.rpmSource.rpmMap[src].values(): + header = self.rpmSource.getHeader(rpm) + users = users.union(header[FILEUSERNAME]) + groups = groups.union(header[FILEGROUPNAME]) + + import infoimport + infoMaker = infoimport.InfoMaker(cfg, repos, self.recipeMaker) + infoMaker.makeInfo(users, groups) + + def main(self, argv): + if '--debug' in argv: + argv.remove('--debug') + sys.excepthook = conary.lib.util.genExcepthook(debug=True) + + if len(argv) < 2: + self.help() + return + category = argv[1] + if 'pkgs' == category: + dirs = argv[2:] + if not dirs: + dirs = ['.'] + self.test() + self.createPkgs(dirs) + elif 'accounts' == category: + users = argv[2:] + self.createUsers(users) + else: + self.help() + return + +if __name__ == '__main__': + pkgWiz = PkgWiz() + pkgWiz.main(sys.argv) diff --git a/rpmimport.py b/rpmimport.py deleted file mode 100755 --- a/rpmimport.py +++ /dev/null @@ -1,293 +0,0 @@ -#!/usr/bin/python -# -# Copyright (c) 2006 rPath, Inc. -# -# -# This program is distributed under the terms of the Common Public License, -# version 1.0. A copy of this license should have been distributed with this -# source file in a file called LICENSE. If it is not present, the license -# is always available at http://www.rpath.com/permanent/licenses/CPL-1.0. -# -# This program is distributed in the hope that it will be useful, but -# without any warranty; without even the implied warranty of merchantability -# or fitness for a particular purpose. See the Common Public License for -# full details. -# - - -import os -import sys -import shutil - -from conary import rpmhelper - -# make local copies of tags for convenience -for tag in ('NAME', 'VERSION', 'RELEASE', 'SOURCERPM', 'FILEUSERNAME', - 'FILEGROUPNAME'): - sys.modules[__name__].__dict__[tag] = getattr(rpmhelper, tag) -ARCH = 1022 - -class RpmSource: - def __init__(self): - # {srpm: {rpm: path} - self.rpmMap = dict() - - # {name: srpm} - self.revMap = dict() - - # {srpm: path} - self.srcPath = dict() - - # {rpmfile: header} - self.headers = dict() - - def getHeader(self, f): - if f in self.headers: - return self.headers[f] - header = rpmhelper.readHeader(file(f)) - self.headers[f] = header - return header - - def procBin(self, f, rpm): - header = self.getHeader(f) - self.headers[f] = header - if SOURCERPM in header: - srpm = header[SOURCERPM] - if self.rpmMap.has_key(srpm): - self.rpmMap[srpm][rpm] = f - else: - self.rpmMap[srpm] = {rpm: f} - self.revMap[header[NAME]] = srpm - - def procSrc(self, f, rpm): - self.srcPath[rpm] = f - - def walk(self, root): - """ - Walk the tree rooted at root and collect information about rpms found. - """ - - for dirpath, dirnames, filenames in os.walk(root): - for f in filenames: - # ignore the 32-bit compatibility libs - we will - # simply use the 32-bit components from the repository - if '32bit' in f: - continue - if f.endswith(".rpm"): - fullpath = os.path.join(dirpath, f) - if f.endswith(".src.rpm") or f.endswith('.nosrc.rpm'): - self.procSrc(fullpath, f) - else: - self.procBin(fullpath, f) - - def getSrpms(self): - """ - Get all sources we think we need now. - """ - - bins = ['aaa_base', 'acl', 'alsa', 'apache2', 'ash', 'attr', - 'bash', 'binutils', 'bzip2', 'compat-libstdc++', - 'coreutils', 'cpio', 'cracklib', 'cron', 'cyrus-sasl', - 'db', 'dhcpcd', 'diffutils', 'e2fsprogs', 'expat', - 'expect', 'file', 'filesystem', 'findutils', - 'findutils-locate', 'fontconfig', 'freetype', - 'freetype2', 'gawk', 'gdbm', 'glib2', 'glibc', 'gmp', - 'gpm', 'grep', 'grub', 'gzip', 'insserv', 'iproute2', - 'iptables', 'iputils', 'kbd', 'klogd', 'krb5', 'ksh', - 'less', 'libaio', 'libattr', 'libelf', 'libgcc', - 'libjpeg', 'libnscd', 'libpcap', 'libpng', - 'libstdc++', 'libxcrypt', 'make', 'mdadm', 'mingetty', - 'mkinitrd', 'mktemp', 'module-init-tools', 'ncurses', - 'net-tools', 'netcfg', 'openldap2', - 'openldap2-client', 'openslp', 'openssl', 'pam', - 'pam-modules', 'patch', 'pciutils', 'pcre', 'perl', - 'perl-Bootloader', 'perl-Compress-Zlib', - 'perl-DBD-SQLite', 'perl-DBI', 'perl-Net-Daemon', - 'perl-PlRPC', 'perl-TermReadKey', 'perl-URI', - 'perl-gettext', 'procps', 'procps', 'psmisc', - 'pwdutils', 'pwdutils', 'python', 'resmgr', 'sed', - 'slang', 'sles-release', 'sysconfig', 'sysfsutils', - 'syslog-ng', 'sysvinit', 'sysvinit', 'tar', 'tcl', - 'tcsh', 'tcpdump', 'termcap', 'timezone', 'udev', - 'unixODBC', 'util-linux', 'vim', 'wget', - 'wireless-tools', 'xorg-x11', 'zlib'] - - srpms = list() - for b in bins: - srpms.append(self.revMap[b]) - return srpms - - def transformName(self, name): - """ - In name, - => _, + => _plus. - """ - - return name.replace('-', '_').replace('+', '_plus') - - def quoteSequence(self, seq): - """ - [a, b] => 'a', 'b' - """ - - return ', '.join("'%s'" % x for x in sorted(seq)) - - def getArchs(self, src): - """ - @return list that goes into the archs line in the recipe. - """ - - hdrs = [ self.getHeader(x) for x in self.rpmMap[src].itervalues() ] - archs = set(h[ARCH] for h in hdrs) - if 'i586' in archs and 'i686' in archs: - # remove the base arch if we have an extra arch - arch, extra = self.getExtraArchs(src) - if arch == 'i686': - archs.remove('i586') - return archs - - def getNames(self, src): - """ - @return list that goes into the rpms line in the recipe. - """ - - hdrs = [ self.getHeader(x) for x in self.rpmMap[src].itervalues() ] - names = set(h[NAME] for h in hdrs) - return names - - def getExtraArchs(self, src): - """ - For the special case of RPMs that have components optimized for the - i686 architecture while other components are at i586, then return - ('i686', set(rpms that are i686 only)), otherwise return (None, None). - """ - - hdrs = [ self.getHeader(x) for x in self.rpmMap[src].itervalues() ] - archMap = {} - for h in hdrs: - arch = h[ARCH] - name = h[NAME] - if arch in archMap: - archMap[arch].add(name) - else: - archMap[arch] = set((name,)) - if 'i586' in archMap and 'i686' in archMap: - if archMap['i586'] != archMap['i686']: - return 'i686', archMap['i686'] - return None, None - - def createTemplate(self, src): - """ - @return the content of the new recipe. - """ - - srchdr = self.getHeader(self.srcPath[src]) - l = [] - a = l.append - a("loadSuperClass('rpmimport.recipe')") - a('class %s(RPMImportRecipe):' %(self.transformName(srchdr[NAME]))) - a(" name = '%s'" %(srchdr[NAME])) - a(" version = '%s_%s'" %(srchdr[VERSION], srchdr[RELEASE])) - archs = self.getArchs(src) - names = self.getNames(src) - extras = self.getExtraArchs(src)[1] - a(' rpms = [ %s ]' % self.quoteSequence(names)) - a(' archs = [ %s ]' % self.quoteSequence(archs)) - if extras: - a(" extraArch = { 'i686': [ %s ] }" %self.quoteSequence(extras)) - a('') - return '\n'.join(l) - -class RecipeMaker: - def __init__(self, cvc, cfg, repos, rpmSource): - self.cvc = cvc - self.cfg = cfg - self.repos = repos - self.rpmSource = rpmSource - - def create(self, pkgname, recipeContents, srpm = None): - print 'creating initial template for', pkgname - try: - shutil.rmtree(pkgname) - except OSError, e: - pass - self.cvc.sourceCommand(self.cfg, [ "newpkg", pkgname], {}) - cwd = os.getcwd() - os.chdir(pkgname) - try: - recipe = pkgname + '.recipe' - f = open(recipe, 'w') - f.write(recipeContents) - f.close() - addfiles = [ 'add', recipe ] - - # copy all the binaries to the cwd - if srpm: - for path, fn in self.rpmSource.rpmMap[src].iteritems(): - shutil.copy(fn, path) - addfiles.append(path) - self.cvc.sourceCommand(self.cfg, addfiles, {}) - #self.cvc.sourceCommand(self.cfg, ['cook', recipe], {}) - self.cvc.sourceCommand(self.cfg, - [ 'commit' ], - { 'message': - 'Automated initial commit of ' + recipe }) - #self.cvc.sourceCommand(self.cfg, ['cook', pkgname], {}) - finally: - os.chdir(cwd) - -if __name__ == '__main__': - from conary import conaryclient, conarycfg, versions, errors, cvc - from conary import deps - from conary.lib import util - from conary.build import use - - sys.excepthook = util.genExcepthook(debug=True) - - cfg = conarycfg.ConaryConfiguration(readConfigFiles=True) - cfg.initializeFlavors() - - buildFlavor = deps.deps.parseFlavor('is:x86(i586,!i686)') - cfg.buildFlavor = deps.deps.overrideFlavor(cfg.buildFlavor, - buildFlavor) - use.setBuildFlagsFromFlavor(None, cfg.buildFlavor, error=False) - - client = conaryclient.ConaryClient(cfg) - repos = client.getRepos() - roots = sys.argv[1:] - rpmSource = RpmSource() - for root in roots: - rpmSource.walk(root) - recipeMaker = RecipeMaker(cvc, cfg, repos, rpmSource) - - # {foo:source: {cfg.buildLabel: None}} - srccomps = {} - - # {foo:source: foo-1.0-1.1.src.rpm} - srcmap = {} - for src in rpmSource.getSrpms(): - h = rpmSource.getHeader(rpmSource.srcPath[src]) - srccomp = h[NAME] + ':source' - srcmap[srccomp] = src - srccomps[srccomp] = {cfg.buildLabel: None} - d = repos.getTroveVersionsByLabel(srccomps) - - # Iterate over foo:source. - for srccomp in srccomps.iterkeys(): - if srccomp not in d: - src = srcmap[srccomp] - pkgname = srccomp.split(':')[0] - recipeMaker.create(pkgname, rpmSource.createTemplate(src), src) - - - # Get all users and groups used in this run. - users = set() - groups = set() - for src in rpmSource.getSrpms(): - for rpm in rpmSource.rpmMap[src].values(): - header = rpmSource.getHeader(rpm) - users = users.union(header[FILEUSERNAME]) - groups = groups.union(header[FILEGROUPNAME]) - - import infoimport - infoMaker = infoimport.InfoMaker(cfg, repos, recipeMaker) - infoMaker.makeInfo(users, groups) diff --git a/rpmimport/infomaker.py b/rpmimport/infomaker.py new file mode 100755 --- /dev/null +++ b/rpmimport/infomaker.py @@ -0,0 +1,183 @@ +#!/usr/bin/python +# +# Copyright (c) 2006 rPath, Inc. +# +# +# This program is distributed under the terms of the Common Public License, +# version 1.0. A copy of this license should have been distributed with this +# source file in a file called LICENSE. If it is not present, the license +# is always available at http://www.rpath.com/permanent/licenses/CPL-1.0. +# +# This program is distributed in the hope that it will be useful, but +# without any warranty; without even the implied warranty of merchantability +# or fitness for a particular purpose. See the Common Public License for +# full details. +# + + +import grp +import pwd +import sys + +class InfoMaker: + def __init__(self, cfg, repos, recipeMaker): + self.cfg = cfg + self.repos = repos + self.recipeMaker = recipeMaker + + def closeUser(self, user, users, groups, ustaging, gstaging, ugMap): + """ + If a user has supplemental groups, then create those. + """ + try: + uprop = pwd.getpwnam(user) + except KeyError, e: + print "The user %s does not exist on the build system." \ + " Please create it before running this." % (user) + return + + (uid, gid, comment, homedir, shell) = uprop[2:] + gprop = grp.getgrgid(gid) + group = gprop[0] + supgroups = ugMap.get(user, set()).difference(set([group])) + for g in supgroups: + if g not in groups: + gstaging.add(g) + users.add(user) + + def closeGroup(self, group, users, groups, ustaging, gstaging, ugMap): + """ + If there is a user with this as its primary group, then create the user + instead. + """ + + try: + gprop = grp.getgrnam(group) + except KeyError, e: + print "The group %s does not exist on the build system." \ + " Please create it first before running this." % (group) + return + + gid = gprop[2] + primaries = gprop[3] + [group] + for u in primaries: + try: + uprop = pwd.getpwnam(u) + if uprop[3] == gid: + ustaging.add(uprop[0]) + return + except KeyError, e: + # No such user, oh well, ignore. + pass + groups.add(group) + + def makeInfo(self, users, groups): + # Map username to groups it belongs to. + ugMap = dict() + for (g, a, b, us) in grp.getgrall(): + for u in us: + if u in ugMap: + ugMap[u].add(g) + else: + ugMap[u] = set([g]) + + # Groups and users depend on each other, so do their closure. + ustaging = users + gstaging = groups + users = set() + groups = set() + while ustaging or gstaging: + for user in ustaging: + self.closeUser(user, users, groups, ustaging, gstaging, ugMap) + ustaging = set() + for group in gstaging: + self.closeGroup(group, users, groups, ustaging, gstaging, ugMap) + gstaging = set() + + # Remove the root user and group. + users = users.difference(['root']) + groups = groups.difference(['root']) + + # If there are groups named the same as the user, then the user must + # have this group as its primary group. + groups = groups.difference(users) + + # All the packages we might create. + srccomps = {} + for account in users.union(groups): + srccomps['info-%s:source' % (account)] = {self.cfg.buildLabel: None} + + # Get current repository contents. + repoContents = self.repos.getTroveVersionsByLabel(srccomps) + + # Create users. + for user in users: + # If it already exists, then move on. + if 'info-%s:source' %user in repoContents: + continue + + uprop = pwd.getpwnam(user) + (uid, gid, comment, homedir, shell) = uprop[2:] + gprop = grp.getgrgid(gid) + group = gprop[0] + supgroups = ugMap.get(user, set()).difference(set([group])) + + self.recipeMaker.create('info-%s' % (user), + "class info_%(user)s(UserInfoRecipe):\n" + " name = 'info-%(user)s'\n" + " version = '1'\n" + "\n" + " def setup(r):\n" + " r.User('%(user)s', %(uid)s, group='%(group)s', groupid=%(gid)s,\n" + " homedir='%(homedir)s', comment='%(comment)s', shell='%(shell)s',\n" + " supplemental=[%(supgroups)s])\n" + % dict(user=user, uid=uid, group=group, gid=gid, + homedir=homedir, comment=comment, shell=shell, + supgroups=', '.join("'%s'" % g for g in supgroups))) + + # Create groups. + for group in groups: + # If it already exists, then move on. + if 'info-%s:source' %group in repoContents: + continue + + gprop = grp.getgrnam(group) + gid = gprop[2] + + # Create the group recipe. + self.recipeMaker.create('info-%s' % (group), + "class info_%(group)s(GroupInfoRecipe):\n" + " name = 'info-%(group)s'\n" + " version = '1'\n" + "\n" + " def setup(r):\n" + " r.Group('%(group)s', %(gid)s)\n" + % dict(group=group, gid=gid)) + +if __name__ == '__main__': + from conary import conaryclient, conarycfg, versions, errors, cvc + from conary import deps + from conary.lib import util + from conary.build import use + from rpmimport import RecipeMaker, RpmSource + + sys.excepthook = util.genExcepthook(debug=True) + + cfg = conarycfg.ConaryConfiguration(readConfigFiles=True) + cfg.initializeFlavors() + + buildFlavor = deps.deps.parseFlavor('is:x86(i586,!i686)') + cfg.buildFlavor = deps.deps.overrideFlavor(cfg.buildFlavor, + buildFlavor) + use.setBuildFlagsFromFlavor(None, cfg.buildFlavor, error=False) + + client = conaryclient.ConaryClient(cfg) + repos = client.getRepos() + roots = sys.argv[1:] + rpmSource = RpmSource() + for root in roots: + rpmSource.walk(root) + recipeMaker = RecipeMaker(cvc, cfg, repos, rpmSource) + + infoMaker = InfoMaker(cfg, repos, recipeMaker) + infoMaker.makeInfo(set(['video']), set()) diff --git a/rpmimport/recipemaker.py b/rpmimport/recipemaker.py new file mode 100644 --- /dev/null +++ b/rpmimport/recipemaker.py @@ -0,0 +1,38 @@ +class RecipeMaker: + def __init__(self, cfg, repos, rpmSource): + self.cfg = cfg + self.repos = repos + self.rpmSource = rpmSource + + def create(self, pkgname, recipeContents, srpm = None): + from conary import cvc + + print 'creating initial template for', pkgname + try: + shutil.rmtree(pkgname) + except OSError, e: + pass + cvc.sourceCommand(self.cfg, [ "newpkg", pkgname], {}) + cwd = os.getcwd() + os.chdir(pkgname) + try: + recipe = pkgname + '.recipe' + f = open(recipe, 'w') + f.write(recipeContents) + f.close() + addfiles = [ 'add', recipe ] + + # copy all the binaries to the cwd + if srpm: + for path, fn in self.rpmSource.rpmMap[src].iteritems(): + shutil.copy(fn, path) + addfiles.append(path) + cvc.sourceCommand(self.cfg, addfiles, {}) + cvc.sourceCommand(self.cfg, ['cook', recipe], {'no-deps': None}) + cvc.sourceCommand(self.cfg, + [ 'commit' ], + { 'message': + 'Automated initial commit of ' + recipe }) + cvc.sourceCommand(self.cfg, ['cook', pkgname], {'no-deps': None}) + finally: + os.chdir(cwd) diff --git a/rpmimport/rpmsource.py b/rpmimport/rpmsource.py new file mode 100755 --- /dev/null +++ b/rpmimport/rpmsource.py @@ -0,0 +1,198 @@ +#!/usr/bin/python +# +# Copyright (c) 2006 rPath, Inc. +# +# +# This program is distributed under the terms of the Common Public License, +# version 1.0. A copy of this license should have been distributed with this +# source file in a file called LICENSE. If it is not present, the license +# is always available at http://www.rpath.com/permanent/licenses/CPL-1.0. +# +# This program is distributed in the hope that it will be useful, but +# without any warranty; without even the implied warranty of merchantability +# or fitness for a particular purpose. See the Common Public License for +# full details. +# + + +import os +import sys +import shutil + +from conary import rpmhelper + +# make local copies of tags for convenience +for tag in ('NAME', 'VERSION', 'RELEASE', 'SOURCERPM', 'FILEUSERNAME', + 'FILEGROUPNAME'): + sys.modules[__name__].__dict__[tag] = getattr(rpmhelper, tag) +ARCH = 1022 + +class RpmSource: + def __init__(self): + # {srpm: {rpm: path} + self.rpmMap = dict() + + # {name: srpm} + self.revMap = dict() + + # {srpm: path} + self.srcPath = dict() + + # {rpmfile: header} + self.headers = dict() + + def getHeader(self, f): + if f in self.headers: + return self.headers[f] + header = rpmhelper.readHeader(file(f)) + self.headers[f] = header + return header + + def procBin(self, f, rpm): + header = self.getHeader(f) + self.headers[f] = header + if SOURCERPM in header: + srpm = header[SOURCERPM] + if self.rpmMap.has_key(srpm): + self.rpmMap[srpm][rpm] = f + else: + self.rpmMap[srpm] = {rpm: f} + self.revMap[header[NAME]] = srpm + + def procSrc(self, f, rpm): + self.srcPath[rpm] = f + + def walk(self, root): + """ + Walk the tree rooted at root and collect information about rpms found. + """ + + for dirpath, dirnames, filenames in os.walk(root): + for f in filenames: + # ignore the 32-bit compatibility libs - we will + # simply use the 32-bit components from the repository + if '32bit' in f: + continue + if f.endswith(".rpm"): + fullpath = os.path.join(dirpath, f) + if f.endswith(".src.rpm") or f.endswith('.nosrc.rpm'): + self.procSrc(fullpath, f) + else: + self.procBin(fullpath, f) + + def getSrpms(self): + """ + Get all sources we think we need now. + """ + + bins = ['aaa_base', 'acl', 'alsa', 'apache2', 'ash', 'attr', + 'bash', 'binutils', 'bzip2', 'compat-libstdc++', + 'coreutils', 'cpio', 'cracklib', 'cron', 'cyrus-sasl', + 'db', 'dhcpcd', 'diffutils', 'e2fsprogs', 'expat', + 'expect', 'file', 'filesystem', 'findutils', + 'findutils-locate', 'fontconfig', 'freetype', + 'freetype2', 'gawk', 'gdbm', 'glib2', 'glibc', 'gmp', + 'gpm', 'grep', 'grub', 'gzip', 'insserv', 'iproute2', + 'iptables', 'iputils', 'kbd', 'klogd', 'krb5', 'ksh', + 'less', 'libaio', 'libattr', 'libelf', 'libgcc', + 'libjpeg', 'libnscd', 'libpcap', 'libpng', + 'libstdc++', 'libxcrypt', 'make', 'mdadm', 'mingetty', + 'mkinitrd', 'mktemp', 'module-init-tools', 'ncurses', + 'net-tools', 'netcfg', 'openldap2', + 'openldap2-client', 'openslp', 'openssl', 'pam', + 'pam-modules', 'patch', 'pciutils', 'pcre', 'perl', + 'perl-Bootloader', 'perl-Compress-Zlib', + 'perl-DBD-SQLite', 'perl-DBI', 'perl-Net-Daemon', + 'perl-PlRPC', 'perl-TermReadKey', 'perl-URI', + 'perl-gettext', 'procps', 'procps', 'psmisc', + 'pwdutils', 'pwdutils', 'python', 'resmgr', 'sed', + 'slang', 'sles-release', 'sysconfig', 'sysfsutils', + 'syslog-ng', 'sysvinit', 'sysvinit', 'tar', 'tcl', + 'tcsh', 'tcpdump', 'termcap', 'timezone', 'udev', + 'unixODBC', 'util-linux', 'vim', 'wget', + 'wireless-tools', 'xorg-x11', 'zlib'] + + srpms = list() + for b in bins: + srpms.append(self.revMap[b]) + return srpms + + def transformName(self, name): + """ + In name, - => _, + => _plus. + """ + + return name.replace('-', '_').replace('+', '_plus') + + def quoteSequence(self, seq): + """ + [a, b] => 'a', 'b' + """ + + return ', '.join("'%s'" % x for x in sorted(seq)) + + def getArchs(self, src): + """ + @return list that goes into the archs line in the recipe. + """ + + hdrs = [ self.getHeader(x) for x in self.rpmMap[src].itervalues() ] + archs = set(h[ARCH] for h in hdrs) + if 'i586' in archs and 'i686' in archs: + # remove the base arch if we have an extra arch + arch, extra = self.getExtraArchs(src) + if arch == 'i686': + archs.remove('i586') + return archs + + def getNames(self, src): + """ + @return list that goes into the rpms line in the recipe. + """ + + hdrs = [ self.getHeader(x) for x in self.rpmMap[src].itervalues() ] + names = set(h[NAME] for h in hdrs) + return names + + def getExtraArchs(self, src): + """ + For the special case of RPMs that have components optimized for the + i686 architecture while other components are at i586, then return + ('i686', set(rpms that are i686 only)), otherwise return (None, None). + """ + + hdrs = [ self.getHeader(x) for x in self.rpmMap[src].itervalues() ] + archMap = {} + for h in hdrs: + arch = h[ARCH] + name = h[NAME] + if arch in archMap: + archMap[arch].add(name) + else: + archMap[arch] = set((name,)) + if 'i586' in archMap and 'i686' in archMap: + if archMap['i586'] != archMap['i686']: + return 'i686', archMap['i686'] + return None, None + + def createTemplate(self, src): + """ + @return the content of the new recipe. + """ + + srchdr = self.getHeader(self.srcPath[src]) + l = [] + a = l.append + a("loadSuperClass('rpmimport.recipe')") + a('class %s(RPMImportRecipe):' %(self.transformName(srchdr[NAME]))) + a(" name = '%s'" %(srchdr[NAME])) + a(" version = '%s_%s'" %(srchdr[VERSION], srchdr[RELEASE])) + archs = self.getArchs(src) + names = self.getNames(src) + extras = self.getExtraArchs(src)[1] + a(' rpms = [ %s ]' % self.quoteSequence(names)) + a(' archs = [ %s ]' % self.quoteSequence(archs)) + if extras: + a(" extraArch = { 'i686': [ %s ] }" %self.quoteSequence(extras)) + a('') + return '\n'.join(l) From johnsonm at rpath.com Wed Aug 19 17:38:32 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:38:32 +0000 Subject: mirrorball: Fixed some imports after the split. Message-ID: <200908192138.n7JLcWNB012924@scc.eng.rpath.com> changeset: 913f93bbec15 user: Xiaowen Xin date: Tue, 28 Nov 2006 08:40:04 +0800 Fixed some imports after the split. committer: Xiaowen Xin diff --git a/pkgwiz.py b/pkgwiz.py --- a/pkgwiz.py +++ b/pkgwiz.py @@ -1,7 +1,7 @@ +from conary import cvc, rpmhelper import conary.lib.util -import infoimport import os -import rpmimport +from rpmimport import infomaker, recipemaker, rpmsource import shutil import sys @@ -21,7 +21,7 @@ self.cfg = None self.client = None self.repos = None - self.rpmSource = rpmimport.RpmSource() + self.rpmSource = rpmsource.RpmSource() self.recipeMaker = None def help(self): @@ -34,6 +34,10 @@ self.cfg = conarycfg.ConaryConfiguration(readConfigFiles=True) self.cfg.initializeFlavors() + cvcCommand = cvc.CvcCommand() + cvcCommand.setContext(self.cfg, dict()) + if not self.cfg.buildLabel and self.cfg.installLabelPath: + self.cfg.buildLabel = self.cfg.installLabelPath[0] buildFlavor = deps.deps.parseFlavor('is:x86(i586,!i686)') self.cfg.buildFlavor = deps.deps.overrideFlavor( @@ -42,7 +46,7 @@ self.client = conaryclient.ConaryClient(self.cfg) self.repos = self.client.getRepos() - self.recipeMaker = rpmimport.RecipeMaker(self.cfg, self.repos, self.rpmSource) + self.recipeMaker = recipemaker.RecipeMaker(self.cfg, self.repos, self.rpmSource) def createPkgs(self, dirs): for dir in dirs: @@ -55,9 +59,9 @@ srcmap = {} for src in self.rpmSource.getSrpms(): h = self.rpmSource.getHeader(self.rpmSource.srcPath[src]) - srccomp = h[NAME] + ':source' + srccomp = h[rpmhelper.NAME] + ':source' srcmap[srccomp] = src - srccomps[srccomp] = {cfg.buildLabel: None} + srccomps[srccomp] = {self.cfg.buildLabel: None} d = self.repos.getTroveVersionsByLabel(srccomps) # Iterate over foo:source. @@ -69,10 +73,10 @@ else: # The package already exists in the repository, so query its # version. - oldVersion = 1# FIXME + oldVersion = d[srccomp].keys()[0].trailingRevision().getVersion() # Get the version - srchdr = self.rpmSource.getHeader(self.rpmSource.srcPath(src)) - newVersion = '%s_%s' % (srchdr[VERSION], srchdr[RELEASE]) + srchdr = self.rpmSource.getHeader(self.rpmSource.srcPath[src]) + newVersion = '%s_%s' % (srchdr[rpmhelper.VERSION], srchdr[rpmhelper.RELEASE]) if newVersion == oldVersion: continue @@ -125,12 +129,6 @@ cvc.sourceCommand(self.cfg, [ 'commit' ], {'message': 'Automated update of ' + pkgname}) - def test(self): - import epdb - epdb.st() - from conary.state import ConaryStateFromFile - conaryState = ConaryStateFromFile("/home/xiaowen/nuernberg/pwdutils/CONARY", self.repos) - def createUsers(self, users): # Get all users and groups used in this run. users = set() @@ -141,8 +139,7 @@ users = users.union(header[FILEUSERNAME]) groups = groups.union(header[FILEGROUPNAME]) - import infoimport - infoMaker = infoimport.InfoMaker(cfg, repos, self.recipeMaker) + infoMaker = infomaker.InfoMaker(cfg, repos, self.recipeMaker) infoMaker.makeInfo(users, groups) def main(self, argv): @@ -158,7 +155,7 @@ dirs = argv[2:] if not dirs: dirs = ['.'] - self.test() + self.repo() self.createPkgs(dirs) elif 'accounts' == category: users = argv[2:] diff --git a/rpmimport/recipemaker.py b/rpmimport/recipemaker.py --- a/rpmimport/recipemaker.py +++ b/rpmimport/recipemaker.py @@ -1,3 +1,7 @@ +import os +import shutil + + class RecipeMaker: def __init__(self, cfg, repos, rpmSource): self.cfg = cfg @@ -24,7 +28,7 @@ # copy all the binaries to the cwd if srpm: - for path, fn in self.rpmSource.rpmMap[src].iteritems(): + for path, fn in self.rpmSource.rpmMap[srpm].iteritems(): shutil.copy(fn, path) addfiles.append(path) cvc.sourceCommand(self.cfg, addfiles, {}) @@ -33,6 +37,6 @@ [ 'commit' ], { 'message': 'Automated initial commit of ' + recipe }) - cvc.sourceCommand(self.cfg, ['cook', pkgname], {'no-deps': None}) + #cvc.sourceCommand(self.cfg, ['cook', pkgname], {'no-deps': None}) finally: os.chdir(cwd) diff --git a/rpmimport/rpmsource.py b/rpmimport/rpmsource.py --- a/rpmimport/rpmsource.py +++ b/rpmimport/rpmsource.py @@ -87,27 +87,27 @@ bins = ['aaa_base', 'acl', 'alsa', 'apache2', 'ash', 'attr', 'bash', 'binutils', 'bzip2', 'compat-libstdc++', - 'coreutils', 'cpio', 'cracklib', 'cron', 'cyrus-sasl', + 'coreutils', 'cpio', 'cpp', 'cracklib', 'cron', 'cyrus-sasl', 'db', 'dhcpcd', 'diffutils', 'e2fsprogs', 'expat', - 'expect', 'file', 'filesystem', 'findutils', + 'expect', 'file', 'filesystem', 'fillup', 'findutils', 'findutils-locate', 'fontconfig', 'freetype', - 'freetype2', 'gawk', 'gdbm', 'glib2', 'glibc', 'gmp', + 'freetype2', 'gawk', 'gcc', 'gdbm', 'glib2', 'glibc', 'gmp', 'gpm', 'grep', 'grub', 'gzip', 'insserv', 'iproute2', - 'iptables', 'iputils', 'kbd', 'klogd', 'krb5', 'ksh', - 'less', 'libaio', 'libattr', 'libelf', 'libgcc', + 'iptables', 'iputils', 'jpeg', 'kbd', 'klogd', 'krb5', 'ksh', + 'less', 'libaio', 'libapr1', 'libapr-util1', 'libattr', 'libcom_err', 'libelf', 'libgcc', 'libjpeg', 'libnscd', 'libpcap', 'libpng', - 'libstdc++', 'libxcrypt', 'make', 'mdadm', 'mingetty', + 'libstdc++', 'libusb', 'libxcrypt', 'make', 'mdadm', 'mingetty', 'mkinitrd', 'mktemp', 'module-init-tools', 'ncurses', 'net-tools', 'netcfg', 'openldap2', 'openldap2-client', 'openslp', 'openssl', 'pam', 'pam-modules', 'patch', 'pciutils', 'pcre', 'perl', 'perl-Bootloader', 'perl-Compress-Zlib', - 'perl-DBD-SQLite', 'perl-DBI', 'perl-Net-Daemon', + 'perl-DBD-SQLite', 'perl-DBI', 'perl-Digest-SHA1', 'perl-Net-Daemon', 'perl-PlRPC', 'perl-TermReadKey', 'perl-URI', 'perl-gettext', 'procps', 'procps', 'psmisc', - 'pwdutils', 'pwdutils', 'python', 'resmgr', 'sed', + 'pwdutils', 'python', 'python-xml', 'resmgr', 'sed', 'slang', 'sles-release', 'sysconfig', 'sysfsutils', - 'syslog-ng', 'sysvinit', 'sysvinit', 'tar', 'tcl', + 'syslog-ng', 'sysvinit', 'tar', 'tcl', 'tcsh', 'tcpdump', 'termcap', 'timezone', 'udev', 'unixODBC', 'util-linux', 'vim', 'wget', 'wireless-tools', 'xorg-x11', 'zlib'] From johnsonm at rpath.com Wed Aug 19 17:38:31 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:38:31 +0000 Subject: mirrorball: Add __init__.py Message-ID: <200908192138.n7JLcVJS012903@scc.eng.rpath.com> changeset: 5bdc3f042e8b user: Xiaowen Xin date: Fri, 24 Nov 2006 18:32:24 -0500 Add __init__.py committer: Xiaowen Xin diff --git a/rpmimport/__init__.py b/rpmimport/__init__.py new file mode 100644 From johnsonm at rpath.com Wed Aug 19 17:38:33 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:38:33 +0000 Subject: mirrorball: word wrap package list Message-ID: <200908192138.n7JLcXcQ012964@scc.eng.rpath.com> changeset: 39c3d71d8197 user: Matt Wilson date: Fri, 01 Dec 2006 12:43:22 -0500 word wrap package list committer: Matt Wilson diff --git a/rpmimport/rpmsource.py b/rpmimport/rpmsource.py --- a/rpmimport/rpmsource.py +++ b/rpmimport/rpmsource.py @@ -87,30 +87,32 @@ bins = ['aaa_base', 'acl', 'alsa', 'apache2', 'ash', 'attr', 'bash', 'binutils', 'bzip2', 'compat-libstdc++', - 'coreutils', 'cpio', 'cpp', 'cracklib', 'cron', 'cyrus-sasl', - 'db', 'dhcpcd', 'diffutils', 'e2fsprogs', 'expat', - 'expect', 'file', 'filesystem', 'fillup', 'findutils', - 'findutils-locate', 'fontconfig', 'freetype', - 'freetype2', 'gawk', 'gcc', 'gdbm', 'glib2', 'glibc', 'gmp', - 'gpm', 'grep', 'grub', 'gzip', 'insserv', 'iproute2', - 'iptables', 'iputils', 'jpeg', 'kbd', 'klogd', 'krb5', 'ksh', - 'less', 'libaio', 'libapr1', 'libapr-util1', 'libattr', 'libcom_err', 'libelf', 'libgcc', + 'coreutils', 'cpio', 'cpp', 'cracklib', 'cron', + 'cyrus-sasl', 'db', 'dhcpcd', 'diffutils', + 'e2fsprogs', 'expat', 'expect', 'file', 'filesystem', + 'fillup', 'findutils', 'findutils-locate', + 'fontconfig', 'freetype', 'freetype2', 'gawk', 'gcc', + 'gdbm', 'glib2', 'glibc', 'gmp', 'gpm', 'grep', + 'grub', 'gzip', 'insserv', 'iproute2', 'iptables', + 'iputils', 'jpeg', 'kbd', 'klogd', 'krb5', 'ksh', + 'less', 'libaio', 'libapr1', 'libapr-util1', + 'libattr', 'libcom_err', 'libelf', 'libgcc', 'libjpeg', 'libnscd', 'libpcap', 'libpng', - 'libstdc++', 'libusb', 'libxcrypt', 'make', 'mdadm', 'mingetty', - 'mkinitrd', 'mktemp', 'module-init-tools', 'ncurses', - 'net-tools', 'netcfg', 'openldap2', + 'libstdc++', 'libusb', 'libxcrypt', 'make', 'mdadm', + 'mingetty', 'mkinitrd', 'mktemp', 'module-init-tools', + 'ncurses', 'net-tools', 'netcfg', 'openldap2', 'openldap2-client', 'openslp', 'openssl', 'pam', 'pam-modules', 'patch', 'pciutils', 'pcre', 'perl', 'perl-Bootloader', 'perl-Compress-Zlib', - 'perl-DBD-SQLite', 'perl-DBI', 'perl-Digest-SHA1', 'perl-Net-Daemon', - 'perl-PlRPC', 'perl-TermReadKey', 'perl-URI', - 'perl-gettext', 'procps', 'psmisc', + 'perl-DBD-SQLite', 'perl-DBI', 'perl-Digest-SHA1', + 'perl-Net-Daemon', 'perl-PlRPC', 'perl-TermReadKey', + 'perl-URI', 'perl-gettext', 'procps', 'psmisc', 'pwdutils', 'python', 'python-xml', 'resmgr', 'sed', 'slang', 'sles-release', 'sysconfig', 'sysfsutils', - 'syslog-ng', 'sysvinit', 'tar', 'tcl', - 'tcsh', 'tcpdump', 'termcap', 'timezone', 'udev', - 'unixODBC', 'util-linux', 'vim', 'wget', - 'wireless-tools', 'xorg-x11', 'zlib'] + 'syslog-ng', 'sysvinit', 'tar', 'tcl', 'tcsh', + 'tcpdump', 'termcap', 'timezone', 'udev', 'unixODBC', + 'util-linux', 'vim', 'wget', 'wireless-tools', + 'xorg-x11', 'zlib'] srpms = list() for b in bins: From johnsonm at rpath.com Wed Aug 19 17:38:42 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:38:42 +0000 Subject: mirrorball: add zip and unzip Message-ID: <200908192138.n7JLcgrd013389@scc.eng.rpath.com> changeset: 4609d13dbba7 user: Matt Wilson date: Fri, 04 Apr 2008 14:58:53 -0400 add zip and unzip committer: Matt Wilson diff --git a/pkgwiz.py b/pkgwiz.py --- a/pkgwiz.py +++ b/pkgwiz.py @@ -169,12 +169,14 @@ 'timezone', 'udev', 'unixODBC', + 'unzip', 'util-linux', 'vim', 'wget', 'wireless-tools', #'xorg-x11', - 'zlib' + 'zlib', + 'zip', ) class PkgWiz: From johnsonm at rpath.com Wed Aug 19 17:38:40 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:38:40 +0000 Subject: mirrorball: add pkgconfig Message-ID: <200908192138.n7JLcepW013295@scc.eng.rpath.com> changeset: d8cff544d4ab user: Matt Wilson date: Mon, 04 Dec 2006 23:01:44 -0500 add pkgconfig committer: Matt Wilson diff --git a/rpmimport/rpmsource.py b/rpmimport/rpmsource.py --- a/rpmimport/rpmsource.py +++ b/rpmimport/rpmsource.py @@ -192,6 +192,7 @@ 'perl-URI', 'perl-gettext', 'php5', + 'pkgconfig', 'popt', 'procps', 'psmisc', From johnsonm at rpath.com Wed Aug 19 17:38:39 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:38:39 +0000 Subject: mirrorball: add device-mapper and lvm2 Message-ID: <200908192138.n7JLce8N013277@scc.eng.rpath.com> changeset: a718db148ec6 user: Matt Wilson date: Mon, 04 Dec 2006 14:33:57 -0500 add device-mapper and lvm2 committer: Matt Wilson diff --git a/rpmimport/rpmsource.py b/rpmimport/rpmsource.py --- a/rpmimport/rpmsource.py +++ b/rpmimport/rpmsource.py @@ -102,6 +102,7 @@ 'cracklib', 'cron', 'cyrus-sasl', + 'device-mapper', 'db', 'dbus-1', 'dhcpcd', @@ -140,6 +141,7 @@ 'krb5', 'ksh', 'less', + 'lvm2', 'libaio', 'libapr1', 'libapr-util1', From johnsonm at rpath.com Wed Aug 19 17:38:35 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:38:35 +0000 Subject: mirrorball: Put it all on the same line and added a few sources. Message-ID: <200908192138.n7JLcZ4u013057@scc.eng.rpath.com> changeset: 1a3617784a03 user: Xiaowen Xin date: Sat, 02 Dec 2006 01:53:25 +0800 Put it all on the same line and added a few sources. committer: Xiaowen Xin diff --git a/rpmimport/rpmsource.py b/rpmimport/rpmsource.py --- a/rpmimport/rpmsource.py +++ b/rpmimport/rpmsource.py @@ -85,34 +85,137 @@ Get all sources we think we need now. """ - bins = ['aaa_base', 'acl', 'alsa', 'apache2', 'ash', 'attr', - 'bash', 'binutils', 'bzip2', 'compat-libstdc++', - 'coreutils', 'cpio', 'cpp', 'cracklib', 'cron', - 'cyrus-sasl', 'db', 'dhcpcd', 'diffutils', - 'e2fsprogs', 'expat', 'expect', 'file', 'filesystem', - 'fillup', 'findutils', 'findutils-locate', - 'fontconfig', 'freetype', 'freetype2', 'gawk', 'gcc', - 'gdbm', 'glib2', 'glibc', 'gmp', 'gpm', 'grep', - 'grub', 'gzip', 'insserv', 'iproute2', 'iptables', - 'iputils', 'jpeg', 'kbd', 'klogd', 'krb5', 'ksh', - 'less', 'libaio', 'libapr1', 'libapr-util1', - 'libattr', 'libcom_err', 'libelf', 'libgcc', - 'libjpeg', 'libnscd', 'libpcap', 'libpng', - 'libstdc++', 'libusb', 'libxcrypt', 'make', 'mdadm', - 'mingetty', 'mkinitrd', 'mktemp', 'module-init-tools', - 'ncurses', 'net-tools', 'netcfg', 'openldap2', - 'openldap2-client', 'openslp', 'openssl', 'pam', - 'pam-modules', 'patch', 'pciutils', 'pcre', 'perl', - 'perl-Bootloader', 'perl-Compress-Zlib', - 'perl-DBD-SQLite', 'perl-DBI', 'perl-Digest-SHA1', - 'perl-Net-Daemon', 'perl-PlRPC', 'perl-TermReadKey', - 'perl-URI', 'perl-gettext', 'procps', 'psmisc', - 'pwdutils', 'python', 'python-xml', 'resmgr', 'sed', - 'slang', 'sles-release', 'sysconfig', 'sysfsutils', - 'syslog-ng', 'sysvinit', 'tar', 'tcl', 'tcsh', - 'tcpdump', 'termcap', 'timezone', 'udev', 'unixODBC', - 'util-linux', 'vim', 'wget', 'wireless-tools', - 'xorg-x11', 'zlib'] + bins = [ + 'aaa_base', + 'acl', + 'alsa', + 'apache2', + 'ash', + 'attr', + 'bash', + 'binutils', + 'bzip2', + 'compat-libstdc++', + 'coreutils', + 'cpio', + 'cpp', + 'cracklib', + 'cron', + 'cyrus-sasl', + 'db', + 'dhcpcd', + 'diffutils', + 'e2fsprogs', + 'expat', + 'expect', + 'file', + 'filesystem', + 'fillup', + 'findutils', + 'findutils-locate', + 'fontconfig', + 'freetype', + 'freetype2', + 'gawk', + 'gcc', + 'gdbm', + 'glib2', + 'glibc', + 'gmp', + 'gpm', + 'grep', + 'grub', + 'gzip', + 'insserv', + 'iproute2', + 'iptables', + 'iputils', + #'java-1_4_2-sun', + 'jpeg', + 'kbd', + 'klogd', + 'krb5', + 'ksh', + 'less', + 'libaio', + 'libapr1', + 'libapr-util1', + 'libattr', + 'libcom_err', + 'libelf', + 'libgcc', + 'libjpeg', + 'libnscd', + 'libpcap', + 'libpng', + 'libstdc++', + 'libtool', + 'libusb', + 'libxcrypt', + 'libxml2', + 'make', + 'mdadm', + 'mingetty', + 'mkinitrd', + 'mktemp', + 'mm', + 'module-init-tools', + 'ncurses', + 'net-tools', + 'netcfg', + 'openct', + 'openldap2', + 'openldap2-client', + 'opensc', + 'openslp', + 'openssh', + 'openssl', + 'pam', + 'pam-modules', + 'patch', + 'pciutils', + 'pcre', + 'perl', + 'perl-Bootloader', + 'perl-Compress-Zlib', + 'perl-DBD-SQLite', + 'perl-DBI', + 'perl-Digest-SHA1', + 'perl-Net-Daemon', + 'perl-PlRPC', + 'perl-TermReadKey', + 'perl-URI', + 'perl-gettext', + 'php5', + 'procps', + 'psmisc', + 'pwdutils', + 'python', + 'python-xml', + 'resmgr', + 'sed', + 'slang', + 'sles-release', + 'sysconfig', + 'sysfsutils', + 'syslog-ng', + 'sysvinit', + 'tar', + 'tcl', + 'tcpd', + 'tcsh', + 'tcpdump', + 'termcap', + 'timezone', + 'udev', + 'unixODBC', + 'util-linux', + 'vim', + 'wget', + 'wireless-tools', + 'xorg-x11', + 'zlib' + ] srpms = list() for b in bins: From johnsonm at rpath.com Wed Aug 19 17:38:37 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:38:37 +0000 Subject: mirrorball: add hwinfo Message-ID: <200908192138.n7JLcbPN013156@scc.eng.rpath.com> changeset: c68e596e1b74 user: Matt Wilson date: Fri, 01 Dec 2006 14:50:42 -0500 add hwinfo committer: Matt Wilson diff --git a/rpmimport/rpmsource.py b/rpmimport/rpmsource.py --- a/rpmimport/rpmsource.py +++ b/rpmimport/rpmsource.py @@ -126,6 +126,7 @@ 'grep', 'grub', 'gzip', + 'hwinfo', 'insserv', 'iproute2', 'iptables', From johnsonm at rpath.com Wed Aug 19 17:38:37 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:38:37 +0000 Subject: mirrorball: add dbus and hal Message-ID: <200908192138.n7JLcbSC013175@scc.eng.rpath.com> changeset: 811c053be975 user: Matt Wilson date: Fri, 01 Dec 2006 15:11:43 -0500 add dbus and hal committer: Matt Wilson diff --git a/rpmimport/rpmsource.py b/rpmimport/rpmsource.py --- a/rpmimport/rpmsource.py +++ b/rpmimport/rpmsource.py @@ -103,6 +103,7 @@ 'cron', 'cyrus-sasl', 'db', + 'dbus', 'dhcpcd', 'diffutils', 'e2fsprogs', @@ -126,6 +127,7 @@ 'grep', 'grub', 'gzip', + 'hal', 'hwinfo', 'insserv', 'iproute2', From johnsonm at rpath.com Wed Aug 19 17:38:36 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:38:36 +0000 Subject: mirrorball: wordwrap, use a tuple instead of a list Message-ID: <200908192138.n7JLcaCD013135@scc.eng.rpath.com> changeset: 753f75357f5b user: Matt Wilson date: Fri, 01 Dec 2006 13:41:57 -0500 wordwrap, use a tuple instead of a list committer: Matt Wilson diff --git a/pkgwiz.py b/pkgwiz.py --- a/pkgwiz.py +++ b/pkgwiz.py @@ -12,9 +12,11 @@ pkgs can be given individual RPMs or the root of a directory tree to walk. -When packages are imported, they will be checked against what's in the repository. If newer, then check new one in. +When packages are imported, they will be checked against what's in the +repository. If newer, then check new one in. -accounts will create user and/or group info- packages, using information from the build system. +accounts will create user and/or group info- packages, using +information from the build system. """ class PkgWiz: diff --git a/rpmimport/rpmsource.py b/rpmimport/rpmsource.py --- a/rpmimport/rpmsource.py +++ b/rpmimport/rpmsource.py @@ -85,7 +85,7 @@ Get all sources we think we need now. """ - bins = [ + bins = ( 'aaa_base', 'acl', 'alsa', @@ -215,7 +215,7 @@ 'wireless-tools', 'xorg-x11', 'zlib' - ] + ) srpms = list() for b in bins: From johnsonm at rpath.com Wed Aug 19 17:38:43 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:38:43 +0000 Subject: mirrorball: add more packages Message-ID: <200908192138.n7JLchE6013468@scc.eng.rpath.com> changeset: 43eb98ac6e9d user: Matt Wilson date: Tue, 27 May 2008 16:49:35 -0400 add more packages committer: Matt Wilson diff --git a/pkgwiz.py b/pkgwiz.py --- a/pkgwiz.py +++ b/pkgwiz.py @@ -53,6 +53,7 @@ 'cpp', 'cracklib', 'cron', + 'curl', 'cyrus-sasl', 'device-mapper', 'db', @@ -76,6 +77,7 @@ 'glib2', 'glibc', 'gmp', + 'gnutls', 'gpm', 'grep', 'grub', @@ -102,10 +104,14 @@ 'libelf', 'libevent', 'libgcc', + 'libgpg-error', + 'libgcrypt', 'libgssapi', + 'libidn', 'libiniparser', 'libjpeg', 'libnscd', + 'libopencdk', 'libpcap', 'libpng', 'librpcsecgss', @@ -115,8 +121,10 @@ 'libxcrypt', 'libxml2', 'libxml2-python', + 'libxslt', 'logrotate', 'lvm2', + 'lzo', 'make', 'mdadm', 'mingetty', From johnsonm at rpath.com Wed Aug 19 17:38:44 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:38:44 +0000 Subject: mirrorball: add initial repomd parsing implementation Message-ID: <200908192138.n7JLci2w013490@scc.eng.rpath.com> changeset: 0dd4a2b2bdb2 user: Elliot Peele date: Wed, 21 May 2008 16:11:41 -0400 add initial repomd parsing implementation committer: Elliot Peele diff --git a/repomd/__init__.py b/repomd/__init__.py new file mode 100644 --- /dev/null +++ b/repomd/__init__.py @@ -0,0 +1,39 @@ +# +# Copyright (c) 2008 rPath, Inc. +# +# This program is distributed under the terms of the Common Public License, +# version 1.0. A copy of this license should have been distributed with this +# source file in a file called LICENSE. If it is not present, the license +# is always available at http://www.rpath.com/permanent/licenses/CPL-1.0. +# +# This program is distributed in the hope that it will be useful, but +# without any warranty; without even the implied warranty of merchantability +# or fitness for a particular purpose. See the Common Public License for +# full details. +# + + +from repomdxml import RepoMdXml +from repository import Repository +from errors import * + +__all__ = ('Client', ) + public_errors + +class Client(object): + def __init__(self, repoUrl): + self._repoUrl = repoUrl + + self._baseMdPath = '/repodata/repomd.xml' + self._repo = Repository(self._repoUrl) + self._repomd = RepoMdXml(self._repo, self._baseMdPath).parse() + + def getRepos(self): + return self._repo + + def getPatchDetail(self): + node = self._repomd.getRepoData('patches') + return [ x.parseChildren() for x in node.parseChildren().getPatches() ] + + def getPackageDetail(self): + node = self._repomd.getRepoData('primary') + return node.parseChildren().getPackages() diff --git a/repomd/errors.py b/repomd/errors.py new file mode 100644 --- /dev/null +++ b/repomd/errors.py @@ -0,0 +1,37 @@ +# +# Copyright (c) 2008 rPath, Inc. +# +# This program is distributed under the terms of the Common Public License, +# version 1.0. A copy of this license should have been distributed with this +# source file in a file called LICENSE. If it is not present, the license +# is always available at http://www.rpath.com/permanent/licenses/CPL-1.0. +# +# This program is distributed in the hope that it will be useful, but +# without any warranty; without even the implied warranty of merchantability +# or fitness for a particular purpose. See the Common Public License for +# full details. +# + +public_errors = ('RepoMdError', 'ParseError', 'UnknownElementError') + +__all__ = public_errors + ('public_errors', ) + +class RepoMdError(Exception): + pass + +class ParseError(RepoMdError): + pass + +class UnknownElementError(ParseError): + def __init__(self, element): + self._element = element + self._error = 'Element %s is not supported by this parser.' + + def __str__(self): + return self._error % (self._element.getAbsoluteName(), ) + +class UnknownAttributeError(UnknownElementError): + def __init__(self, element, attribute): + UnknownElementError.__init__(self, element) + self._attribute = attribute + self._error = 'Attribute %s of %%s is not supported by this parser.' % (attribute, ) diff --git a/repomd/packagexml.py b/repomd/packagexml.py new file mode 100644 --- /dev/null +++ b/repomd/packagexml.py @@ -0,0 +1,179 @@ +# +# Copyright (c) 2008 rPath, Inc. +# +# This program is distributed under the terms of the Common Public License, +# version 1.0. A copy of this license should have been distributed with this +# source file in a file called LICENSE. If it is not present, the license +# is always available at http://www.rpath.com/permanent/licenses/CPL-1.0. +# +# This program is distributed in the hope that it will be useful, but +# without any warranty; without even the implied warranty of merchantability +# or fitness for a particular purpose. See the Common Public License for +# full details. +# + +__all__ = ('PackageXmlMixIn', ) + +from rpath_common.xmllib import api1 as xmllib + +from errors import UnknownElementError, UnknownAttributeError + +class _Package(xmllib.BaseNode): + name = None + arch = None + epoch = None + version = None + release = None + checksum = None + checksumType = None + summary = None + description = None + packager = None + url = None + fileTimestamp = None + buildTimestamp = None + packageSize = None + installedSize = None + archiveSize = None + location = None + license = None + vendor = None + group = None + buildhost = None + sourcerpm = None + headerStart = None + headerEnd = None + + def addChild(self, child): + if child.getName() == 'name': + self.name = child.finalize() + elif child.getName() == 'arch': + self.arch = child.finalize() + elif child.getName() == 'version': + self.epoch = child.getAttribute('epoch') + self.version = child.getAttribute('ver') + self.release = child.getAttribute('rel') + elif child.getName() == 'checksum': + self.checksum = child.finalize() + self.checksumType = child.getAttribute('type') + elif child.getName() == 'summary': + self.summary = child.finalize() + elif child.getName() == 'description': + self.description = child.finalize() + elif child.getName() == 'packager': + self.packager = child.finalize() + elif child.getName() == 'url': + self.url = child.finalize() + elif child.getName() == 'time': + self.fileTimestamp = child.getAttribute('file') + self.buildTimestamp = child.getAttribute('build') + elif child.getName() == 'size': + self.packageSize = child.getAttribute('package') + self.installedSize = child.getAttribute('installed') + self.archiveSize = child.getAttribute('archive') + elif child.getName() == 'location': + self.location = child.getAttribute('href') + elif child.getName() == 'format': + self.format = [] + for node in child.iterChildren(): + if node.getName() == 'rpm:license': + self.license = node.getText() + elif node.getName() == 'rpm:vendor': + self.vendor = node.getText() + elif node.getName() == 'rpm:group': + self.group = node.getText() + elif node.getName() == 'rpm:buildhost': + self.buildhost = node.getText() + elif node.getName() == 'rpm:sourcerpm': + self.sourcerpm = node.getText() + elif node.getName() == 'rpm:header-range': + self.headerStart = node.getAttribute('start') + self.headerEnd = node.getAttribute('end') + elif node.getName() in ('rpm:provides', 'rpm:requires', + 'rpm:obsoletes', 'rpm:recommends', + 'rpm:conflicts', 'suse:freshens'): + self.format.append(node) + elif node.getName() == 'file': + pass + else: + raise UnknownElementError(node) + elif child.getName() == 'pkgfiles': + pass + else: + raise UnknownElementError(child) + + +class _RpmRequires(xmllib.BaseNode): + def addChild(self, child): + if child.getName() in ('rpm:entry', 'suse:entry'): + for attr, value in child.iterAttributes(): + child.kind = None + child.name = None + child.epoch = None + child.version = None + child.release = None + child.flags = None + child.pre = None + + if attr == 'kind': + child.kind = value + elif attr == 'name': + child.name = value + elif attr == 'epoch': + child.epoch = value + elif attr == 'ver': + child.version = value + elif attr == 'rel': + child.release = value + elif attr == 'flags': + child.flags = value + elif attr == 'pre': + child.pre = value + else: + raise UnknownAttributeError(child, attr) + xmllib.BaseNode.addChild(self, child) + else: + raise UnknownElementError(child) + + +class _RpmRecommends(_RpmRequires): + pass + + +class _RpmProvides(_RpmRequires): + pass + + +class _RpmObsoletes(_RpmRequires): + pass + + +class _RpmConflicts(_RpmRequires): + pass + + +class _SuseFreshens(_RpmRequires): + pass + + +class PackageXmlMixIn(object): + def _registerTypes(self): + self._databinder.registerType(_Package, name='package') + self._databinder.registerType(xmllib.StringNode, name='name') + self._databinder.registerType(xmllib.StringNode, name='arch') + self._databinder.registerType(xmllib.StringNode, name='checksum') + self._databinder.registerType(xmllib.StringNode, name='summary') + self._databinder.registerType(xmllib.StringNode, name='description') + self._databinder.registerType(xmllib.StringNode, name='url') + # FIXME: really shouldn't need to comment these out + #self._databinder.registerType(xmllib.StringNode, name='license', namespace='rpm') + #self._databinder.registerType(xmllib.StringNode, name='vendor', namespace='rpm') + #self._databinder.registerType(xmllib.StringNode, name='group', namespace='rpm') + #self._databinder.registerType(xmllib.StringNode, name='buildhost', namespace='rpm') + #self._databinder.registerType(xmllib.StringNode, name='sourcerpm', namespace='rpm') + self._databinder.registerType(_RpmRequires, name='requires', namespace='rpm') + self._databinder.registerType(_RpmRecommends, name='recommends', namespace='rpm') + self._databinder.registerType(_RpmProvides, name='provides', namespace='rpm') + self._databinder.registerType(_RpmObsoletes, name='obsoletes', namespace='rpm') + self._databinder.registerType(_RpmConflicts, name='conflicts', namespace='rpm') + self._databinder.registerType(_SuseFreshens, name='freshens', namespace='suse') diff --git a/repomd/patchesxml.py b/repomd/patchesxml.py new file mode 100644 --- /dev/null +++ b/repomd/patchesxml.py @@ -0,0 +1,58 @@ +# +# Copyright (c) 2008 rPath, Inc. +# +# This program is distributed under the terms of the Common Public License, +# version 1.0. A copy of this license should have been distributed with this +# source file in a file called LICENSE. If it is not present, the license +# is always available at http://www.rpath.com/permanent/licenses/CPL-1.0. +# +# This program is distributed in the hope that it will be useful, but +# without any warranty; without even the implied warranty of merchantability +# or fitness for a particular purpose. See the Common Public License for +# full details. +# + +__all__ = ('PatchesXml', ) + +# import stable api +from rpath_common.xmllib import api1 as xmllib + +from patchxml import PatchXml +from xmlcommon import XmlFileParser +from errors import UnknownElementError + +class _Patches(xmllib.BaseNode): + def addChild(self, child): + if child.getName() == 'patch': + child.id = child.getAttribute('id') + child._parser = PatchXml(None, child.location) + child.parseChildren = child._parser.parse + xmllib.BaseNode.addChild(self, child) + else: + raise UnknownElementError(child) + + def getPatches(self): + return self.getChildren('patch') + + +class _PatchElement(xmllib.BaseNode): + id = None + checksum = '' + checksumType = 'sha' + location = '' + + def addChild(self, child): + if child.getName() == 'checksum': + self.checksum = child.finalize() + self.checksumType = child.getAttribute('type') + elif child.getName() == 'location': + self.location = child.getAttribute('href') + else: + raise UnkownElementError(child) + + +class PatchesXml(XmlFileParser): + def _registerTypes(self): + self._databinder.registerType(_Patches, name='patches') + self._databinder.registerType(_PatchElement, name='patch') + self._databinder.registerType(xmllib.StringNode, name='checksum') diff --git a/repomd/patchxml.py b/repomd/patchxml.py new file mode 100644 --- /dev/null +++ b/repomd/patchxml.py @@ -0,0 +1,88 @@ +# +# Copryright (c) 2008 rPath, Inc. +# + +__all__ = ('PatchXml', ) + +from rpath_common.xmllib import api1 as xmllib + +from packagexml import * +from xmlcommon import XmlFileParser +from errors import UnknownElementError + +class _Patch(xmllib.BaseNode): + name = None + summary = None + description = None + version = None + release = None + requires = None + recommends = None + rebootNeeded = False + licenseToConfirm = None + packageManager = False + category = None + + def addChild(self, child): + if child.getName() == 'yum:name': + self.name = child.finalize() + elif child.getName() == 'summary': + if child.getAttribute('lang') == 'en': + self.summary = child.finalize() + elif child.getName() == 'description': + if child.getAttribute('lang') == 'en': + self.description = child.finalize() + elif child.getName() == 'yum:version': + self.version = child.getAttribute('ver') + self.release = child.getAttribute('rel') + elif child.getName() == 'rpm:requires': + self.requires = child.getChildren('entry', namespace='rpm') + elif child.getName() == 'rpm:recommends': + self.recommneds = child.getChildren('entry', namespace='rpm') + elif child.getName() == 'reboot-needed': + self.rebootNeeded = True + elif child.getName() == 'license-to-confirm': + self.licenseToConfirm = child.finalize() + elif child.getName() == 'package-manager': + self.packageManager = True + elif child.getName() == 'category': + self.category = child.finalize() + elif child.getName() == 'atoms': + self.packages = child.getChildren('package') + else: + raise UnknownElementError(child) + + def __cmp__(self, other): + if self.version > other.version: + return 1 + elif self.version < other.version: + return -1 + elif self.release > other.release: + return 1 + elif self.release < other.release: + return -1 + else: + return 0 + + +class _Atoms(xmllib.BaseNode): + def addChild(self, child): + if child.getName() == 'package': + child.type = child.getAttribute('type') + xmllib.BaseNode.addChild(self, child) + elif child.getName() == 'message': + pass + elif child.getName() == 'script': + pass + else: + raise UnknownElementError(child) + + +class PatchXml(XmlFileParser, PackageXmlMixIn): + def _registerTypes(self): + PackageXmlMixIn._registerTypes(self) + self._databinder.registerType(_Patch, name='patch') + self._databinder.registerType(xmllib.StringNode, name='name', namespace='yum') + self._databinder.registerType(xmllib.StringNode, name='category') + self._databinder.registerType(_Atoms, name='atoms') + self._databinder.registerType(xmllib.StringNode, name='license-to-confirm') diff --git a/repomd/primaryxml.py b/repomd/primaryxml.py new file mode 100644 --- /dev/null +++ b/repomd/primaryxml.py @@ -0,0 +1,28 @@ +# +# Copryright (c) 2008 rPath, Inc. +# + +__all__ = ('PrimaryXml', ) + +from rpath_common.xmllib import api1 as xmllib + +from packagexml import * +from xmlcommon import XmlFileParser +from errors import UnknownElementError + +class _Metadata(xmllib.BaseNode): + def addChild(self, child): + if child.getName() == 'package': + child.type = child.getAttribute('type') + xmllib.BaseNode.addChild(self, child) + else: + raise UnknownElementError(child) + + def getPackages(self): + return self.getChildren('package') + + +class PrimaryXml(XmlFileParser, PackageXmlMixIn): + def _registerTypes(self): + PackageXmlMixIn._registerTypes(self) + self._databinder.registerType(_Metadata, name='metadata') diff --git a/repomd/repomdxml.py b/repomd/repomdxml.py new file mode 100644 --- /dev/null +++ b/repomd/repomdxml.py @@ -0,0 +1,79 @@ +# +# Copyright (c) 2008 rPath, Inc. +# +# This program is distributed under the terms of the Common Public License, +# version 1.0. A copy of this license should have been distributed with this +# source file in a file called LICENSE. If it is not present, the license +# is always available at http://www.rpath.com/permanent/licenses/CPL-1.0. +# +# This program is distributed in the hope that it will be useful, but +# without any warranty; without even the implied warranty of merchantability +# or fitness for a particular purpose. See the Common Public License for +# full details. +# + +__all__ = ('RepoMdXml', ) + +# use stable api +from rpath_common.xmllib import api1 as xmllib + +from primaryxml import PrimaryXml +from patchesxml import PatchesXml +from xmlcommon import XmlFileParser +from errors import UnknownElementError + +class _RepoMd(xmllib.BaseNode): + def addChild(self, child): + if child.getName() == 'data': + child.type = child.getAttribute('type') + if child.type == 'patches': + child._parser = PatchesXml(None, child.location) + child.parseChildren = child._parser.parse + elif child.type == 'primary': + child._parser = PrimaryXml(None, child.location) + child.parseChildren = child._parser.parse + xmllib.BaseNode.addChild(self, child) + else: + raise UnknownElementError(child) + + def getRepoData(self, name=None): + if not name: + return self.getChildren('data') + + for node in self.getChildren('data'): + if node.type == name: + return node + + return None + + +class _RepoMdDataElement(xmllib.BaseNode): + location = '' + checksum = '' + checksumType = 'sha' + timestamp = '' + openChecksum = '' + openChecksumType = 'sha' + + def addChild(self, child): + if child.getName() == 'location': + self.location = child.getAttribute('href') + elif child.getName() == 'checksum': + self.checksum = child.finalize() + self.checksumType = child.getAttribute('type') + elif child.getName() == 'timestamp': + self.timestamp = child.finalize() + elif child.getName() == 'open-checksum': + self.openChecksum = child.finalize() + self.openChecksumType = child.getAttribute('type') + else: + raise UnknownElementError(child) + + +class RepoMdXml(XmlFileParser): + def _registerTypes(self): + self._databinder.registerType(_RepoMd, name='repomd') + self._databinder.registerType(_RepoMdDataElement, name='data') + self._databinder.registerType(xmllib.StringNode, name='checksum') + self._databinder.registerType(xmllib.IntegerNode, name='timestamp') + self._databinder.registerType(xmllib.StringNode, name='open-checksum') diff --git a/repomd/repository.py b/repomd/repository.py new file mode 100644 --- /dev/null +++ b/repomd/repository.py @@ -0,0 +1,40 @@ +# +# Copyright (c) 2008 rPath, Inc. +# +# This program is distributed under the terms of the Common Public License, +# version 1.0. A copy of this license should have been distributed with this +# source file in a file called LICENSE. If it is not present, the license +# is always available at http://www.rpath.com/permanent/licenses/CPL-1.0. +# +# This program is distributed in the hope that it will be useful, but +# without any warranty; without even the implied warranty of merchantability +# or fitness for a particular purpose. See the Common Public License for +# full details. +# + +__all__ = ('Repository', ) + +import os +import gzip +import tempfile +import urlgrabber + +class Repository(object): + def __init__(self, repoUrl): + self._repoUrl = repoUrl + + def get(self, fileName): + fn = self._getTempFile() + realUrl = self._getRealUrl(fileName) + dlFile = urlgrabber.urlgrab(realUrl, filename=fn) + + if os.path.basename(fileName).endswith('.gz'): + return gzip.open(dlFile) + else: + return open(dlFile) + + def _getTempFile(self): + return tempfile.mktemp(prefix='mdparse') + + def _getRealUrl(self, path): + return self._repoUrl + '/' + path diff --git a/repomd/xmlcommon.py b/repomd/xmlcommon.py new file mode 100644 --- /dev/null +++ b/repomd/xmlcommon.py @@ -0,0 +1,41 @@ +# +# Copyright (c) 2008 rPath, Inc. +# +# This program is distributed under the terms of the Common Public License, +# version 1.0. A copy of this license should have been distributed with this +# source file in a file called LICENSE. If it is not present, the license +# is always available at http://www.rpath.com/permanent/licenses/CPL-1.0. +# +# This program is distributed in the hope that it will be useful, but +# without any warranty; without even the implied warranty of merchantability +# or fitness for a particular purpose. See the Common Public License for +# full details. +# + +__all__ = ('XmlFileParser', ) + +from rpath_common.xmllib import api1 as xmllib + +class XmlFileParser(object): + def __init__(self, repository, path): + self._repository = repository + self._path = path + + self._databinder = xmllib.DataBinder() + self._registerTypes() + + self._data = None + + def _registerTypes(self): + pass + + def parse(self, refresh=False): + if not self._data or refresh: + fn = self._repository.get(self._path) + self._data = self._databinder.parseFile(fn) + + for child in self._data.iterChildren(): + if hasattr(child, '_parser'): + child._parser._repository = self._repository + + return self._data From johnsonm at rpath.com Wed Aug 19 17:38:43 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:38:43 +0000 Subject: mirrorball: already packaged Message-ID: <200908192138.n7JLchYq013447@scc.eng.rpath.com> changeset: ce1bfbb02e67 user: Matt Wilson date: Tue, 13 May 2008 10:15:57 -0400 already packaged committer: Matt Wilson diff --git a/pkgwiz.py b/pkgwiz.py --- a/pkgwiz.py +++ b/pkgwiz.py @@ -38,7 +38,6 @@ 'aaa_base', 'acl', 'apache2', - 'apache2-mod_php', 'apache2-mod_python', 'ash', 'attr', From johnsonm at rpath.com Wed Aug 19 17:38:45 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:38:45 +0000 Subject: mirrorball: initial ignore file Message-ID: <200908192138.n7JLcjxe013529@scc.eng.rpath.com> changeset: 15d615966c20 user: Elliot Peele date: Tue, 27 May 2008 16:09:31 -0400 initial ignore file committer: Elliot Peele diff --git a/.hgignore b/.hgignore new file mode 100644 --- /dev/null +++ b/.hgignore @@ -0,0 +1,3 @@ +(^|/)\.hg($|/) +.*\.pyc$ +.*\~$ From johnsonm at rpath.com Wed Aug 19 17:38:38 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:38:38 +0000 Subject: mirrorball: branch merge Message-ID: <200908192138.n7JLccxk013193@scc.eng.rpath.com> changeset: eac03fbab8db user: Matt Wilson date: Fri, 01 Dec 2006 14:48:13 -0500 branch merge committer: Matt Wilson diff --git a/pkgwiz.py b/pkgwiz.py --- a/pkgwiz.py +++ b/pkgwiz.py @@ -111,8 +111,6 @@ recipeFile.close() # Remove the original RPMs and add the new ones. - import epdb - epdb.st() cwd = os.getcwd() os.chdir(pkgname) from conary.state import ConaryStateFromFile From johnsonm at rpath.com Wed Aug 19 17:38:31 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:38:31 +0000 Subject: mirrorball: add rpmimport.recipe for safekeeping Message-ID: <200908192138.n7JLcVCI012864@scc.eng.rpath.com> changeset: c99db35b1b6f user: Matt Wilson date: Tue, 21 Nov 2006 04:21:10 -0500 add rpmimport.recipe for safekeeping committer: Matt Wilson diff --git a/rpmimport.py b/rpmimport.py --- a/rpmimport.py +++ b/rpmimport.py @@ -94,23 +94,24 @@ 'freetype2', 'gawk', 'gdbm', 'glib2', 'glibc', 'gmp', 'gpm', 'grep', 'grub', 'gzip', 'insserv', 'iproute2', 'iptables', 'iputils', 'kbd', 'klogd', 'krb5', 'ksh', - 'less', 'libaio', 'libattr', 'libelf', 'libgcc', - 'libjpeg', 'libnscd', 'libpcap', 'libpng', - 'libstdc++', 'libxcrypt', 'make', 'mdadm', 'mingetty', - 'mkinitrd', 'mktemp', 'module-init-tools', 'ncurses', - 'net-tools', 'netcfg', 'openldap2', - 'openldap2-client', 'openslp', 'openssl', 'pam', - 'pam-modules', 'patch', 'pciutils', 'pcre', 'perl', - 'perl-Bootloader', 'perl-Compress-Zlib', - 'perl-DBD-SQLite', 'perl-DBI', 'perl-Net-Daemon', - 'perl-PlRPC', 'perl-TermReadKey', 'perl-URI', - 'perl-gettext', 'procps', 'procps', 'psmisc', - 'pwdutils', 'pwdutils', 'python', 'resmgr', 'sed', - 'slang', 'sles-release', 'sysconfig', 'sysfsutils', - 'syslog-ng', 'sysvinit', 'sysvinit', 'tar', 'tcl', - 'tcsh', 'tcpdump', 'termcap', 'timezone', 'udev', - 'unixODBC', 'util-linux', 'vim', 'wget', - 'wireless-tools', 'xorg-x11', 'zlib'] + 'less', 'libaio', 'libapr-util1', 'libapr1', + 'libattr', 'libelf', 'libgcc', 'libjpeg', 'libnscd', + 'libpcap', 'libpng', 'libstdc++', 'libusb', + 'libxcrypt', 'make', 'mdadm', 'mingetty', 'mkinitrd', + 'mktemp', 'module-init-tools', 'ncurses', 'net-tools', + 'netcfg', 'openldap2', 'openldap2-client', 'openslp', + 'openssl', 'pam', 'pam-modules', 'patch', 'pciutils', + 'pcre', 'perl', 'perl-Bootloader', + 'perl-Compress-Zlib', 'perl-DBD-SQLite', 'perl-DBI', + 'perl-Digest-SHA1', 'perl-Net-Daemon', 'perl-PlRPC', + 'perl-TermReadKey', 'perl-URI', 'perl-gettext', + 'procps', 'procps', 'psmisc', 'pwdutils', 'pwdutils', + 'python', 'resmgr', 'sed', 'slang', 'sles-release', + 'sysconfig', 'sysfsutils', 'syslog-ng', 'sysvinit', + 'sysvinit', 'tar', 'tcl', 'tcsh', 'tcpdump', + 'termcap', 'timezone', 'udev', 'unixODBC', + 'util-linux', 'vim', 'wget', 'wireless-tools', + 'xorg-x11', 'zlib'] srpms = list() for b in bins: diff --git a/rpmimport.recipe b/rpmimport.recipe new file mode 100644 --- /dev/null +++ b/rpmimport.recipe @@ -0,0 +1,225 @@ +#!/usr/bin/python +# +# Copyright (c) 2006 rPath, Inc. +# +# + +from conary import rpmhelper + +class RPMImportRecipe(PackageRecipe): + name = 'rpmimport' + version = '1.0_1' + rpms = None + archs = [ 'noarch' ] + extraArch = {} + + def __init__(r, *args, **kw): + r.usedrpms = [] + r.headers = [] + PackageRecipe.__init__(r, *args, **kw) + + def unpack(r): + if not r.rpms: + r.rpms = [ '%(name)s' ] + for rpm in r.rpms: + # if we have a tuple, it's name, version + if isinstance(rpm, tuple): + rpmname, verrel = rpm + rpm = rpmname + '-%s' %verrel + else: + rpmname = rpm + rpm += '-%(version)s-%(release)s' + for rpmarch in r.archs: + pkgarch = rpmarch + if rpmarch == 'noarch': + arch = True + elif rpmarch.startswith('i') and rpmarch.endswith('86'): + # this is i586, i686 - most likely + primaryArch = Arch.x86 + arch = primaryArch[rpmarch] + if (rpmarch in r.extraArch + and rpmname not in r.extraArch[rpmarch] + and rpmarch == 'i686'): + # handle cases like glibc, glibc-devel=i686, + # but the rest are i586 + pkgarch = 'i586' + elif rpmarch == 'x86_64': + arch = Arch[rpmarch] + else: + raise RPMImportError('unsupported arch: %s' %rpmarch) + rpmfile = rpm + '.%s.rpm' %pkgarch + if arch: + r.usedrpms.append(rpmfile %r.macros) + # follow the packaging splitting from the RPMs we're importing + packagename = rpmname + # collapse libfoo -> foo when the main package name is foo + # but don't collapse libbar -> bar when it's produced by the + # foo src rpm + if (not r.name.startswith('lib') + and packagename.startswith('lib') + and packagename[3:].startswith(r.name)): + packagename = packagename[3:] + # collapse suffixes + for suffix in ('-devel', '-docs', '-doc', '-info', + '-lib', '-libs', '-locale', '-man', + '-i18ndata', '-html'): + # collapse foo-devel -> foo + if rpmname.endswith(suffix): + packagename = packagename[:-len(suffix)] + break + r.addArchive(rpmfile, dir='/', use=arch, package=packagename) + + def disableBuildRequirementsPolicy(r): + r.EnforceSonameBuildRequirements(exceptions='.*') + r.EnforceJavaBuildRequirements(exceptions='.*') + r.EnforceCILBuildRequirements(exceptions='.*') + r.EnforceConfigLogBuildRequirements(exceptions='.*') + # Note that perl and python runtime requirement CANNOT + # be discovered without listing them as runtime requirements, + # so it is important not to disable + # r.Enforce{Perl,Python}BuildRequirements + + def disablePolicy(r): + r.BadInterpreterPaths(exceptions='.*') + r.CheckDesktopFiles(exceptions='.*') + r.CheckDestDir(exceptions='.*') + r.CheckSonames(exceptions='.*') + r.DanglingSymlinks(exceptions='.*') + r.ExecutableLibraries(exceptions='.*') + r.FilesForDirectories(exceptions='.*') + r.FilesInMandir(exceptions='.*') + r.FixDirModes(exceptions='.*') + r.FixupMultilibPaths(exceptions='.*') + r.LinkCount(exceptions='.*') + r.IgnoredSetuid(exceptions='.*') + r.ImproperlyShared(exceptions='.*') + r.NonBinariesInBindirs(exceptions='.*') + r.NonMultilibComponent(exceptions='.*') + r.NonMultilibDirectories(exceptions='.*') + r.NormalizeCompression(exceptions='.*') + r.NormalizeInterpreterPaths(exceptions='.*') + r.RemoveNonPackageFiles(exceptions='.*') + r.WarnWriteable(exceptions='.*') + r.WorldWriteableExecutables(exceptions='.*') + # ObsoletePaths does not honor exceptions + del r.ObsoletePaths + + def disableStrip(r): + """ + Default to not stripping; allow override in subclasses with: + + def disableStrip(r): pass + """ + r.Strip(exceptions='.*') + + def policy(r): + """ + hook for adding additional policy in subclasses. + """ + pass + + def preprocess(r): + "hook for adding sources/policy calls before anything else" + pass + + def postprocess(r): + "hook for adding sources/policy calls after anything else" + pass + + def readHeaders(r): + sourceList = r.fetchAllSources() + for rpmname in r.usedrpms: + rpmfiles = [ x for x in sourceList if os.path.basename(x) == rpmname ] + if len(rpmfiles) != 1: + raise RPMImportError('more than one source object matches ' + 'the "%s" rpm filename' %rpmname) + rpmfile = rpmfiles[0] + r.headers.append(rpmhelper.readHeader(file(rpmfile))) + + def processFiles(r): + # given the set of RPM headers used, set permissions, config + # flags, etc appropriately + for header in r.headers: + for path, mode, rdev, flags, username, groupname in zip( + header.paths(), + header[rpmhelper.FILEMODES], + header[rpmhelper.FILERDEVS], + header[rpmhelper.FILEFLAGS], + header[rpmhelper.FILEUSERNAME], + header[rpmhelper.FILEGROUPNAME]): + path = util.normpath(path) + escaped_path = util.literalRegex(path) + escaped_path = escaped_path.replace('\\/', '/') + # handle non-root ownership + if username != 'root' or groupname != 'root': + r.Ownership(username, groupname, escaped_path) + if stat.S_ISDIR(mode): + # hande directories with permissions other than + # root:root 755 (new conary should handle this for + # us automatically) + r.MakeDirs(path) + if (mode & 07777 != 0755 or username != 'root' + or groupname != 'root'): + r.ExcludeDirectories(exceptions=escaped_path) + elif stat.S_ISCHR(mode) or stat.S_ISBLK(mode): + if stat.S_ISCHR(mode): + type='c' + else: + type='b' + # this is correct for 32-bit device number + # RPM does not provide 64-bit device number + minor = rdev & 0xff | (rdev >> 12) & 0xffffff00 + major = (rdev >> 8) & 0xfff + r.MakeDevices(escaped_path, type, major, minor, + username, groupname, mode&0777) + continue + + if flags & (1 << 0): + # CONFIG + r.Config(escaped_path) + if (flags & (1 << 1) or + flags & (1 << 7) or + flags & (1 << 8)): + # DOC, LICENSE, README + r.ComponentSpec('doc', escaped_path) + if flags & (1 << 4): + # NOREPLACE + r.InitialContents(escaped_path) + if flags & (1 << 6): + # GHOST. We only handle ghost files, not ghost dirs + if stat.S_ISREG(mode): + r.Create(path) + r.InitialContents(escaped_path) + elif stat.S_ISDIR(mode): + r.ExcludeDirectories(exceptions=escaped_path) + + # handle "special" permissions (although un-cpio'ing + # the payload should have set them all correctly, payload + # doesn't include things like %gost) + if not stat.S_ISLNK(mode): + r.SetModes(path, mode & 07777) + + def processRequires(r): + # FIXME: implement + pass + + def processProvides(r): + # FIXME: implement + pass + + def setup(r): + if r.__class__.__name__ == 'RPMImportRecipe': + return + r.macros.version, r.macros.release = r.version.rsplit('_', 1) + r.preprocess() + r.unpack() + r.policy() + r.readHeaders() + r.processFiles() + r.disableBuildRequirementsPolicy() + r.disableStrip() + r.disablePolicy() + r.postprocess() + +class RPMImportError(Exception): + pass From johnsonm at rpath.com Wed Aug 19 17:38:41 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:38:41 +0000 Subject: mirrorball: reorganize code, use factories Message-ID: <200908192138.n7JLcfXK013351@scc.eng.rpath.com> changeset: 311d7dd341ea user: Matt Wilson date: Tue, 25 Mar 2008 14:00:32 -0400 reorganize code, use factories committer: Matt Wilson diff --git a/pkgwiz.py b/pkgwiz.py --- a/pkgwiz.py +++ b/pkgwiz.py @@ -1,4 +1,19 @@ #!/usr/bin/python +# +# Copyright (c) 2006,2008 rPath, Inc. +# +# +# This program is distributed under the terms of the Common Public License, +# version 1.0. A copy of this license should have been distributed with this +# source file in a file called LICENSE. If it is not present, the license +# is always available at http://www.rpath.com/permanent/licenses/CPL-1.0. +# +# This program is distributed in the hope that it will be useful, but +# without any warranty; without even the implied warranty of merchantability +# or fitness for a particular purpose. See the Common Public License for +# full details. +# + from conary import cvc, rpmhelper import conary.lib.util import os @@ -18,6 +33,146 @@ accounts will create user and/or group info- packages, using information from the build system. """ +sles10sp1prefix = '/srv/www/html/sle/' +sles10sp1pkgs = ( + 'aaa_base', + 'acl', + 'apache2', + 'ash', + 'attr', + 'audit', + 'bash', + 'binutils', + 'bzip2', + 'compat-libstdc++', + 'coreutils', + 'cpio', + 'cpp', + 'cracklib', + 'cron', + 'cyrus-sasl', + 'device-mapper', + 'db', + 'dbus-1', + 'dhcpcd', + 'diffutils', + 'e2fsprogs', + 'expat', + 'expect', + 'file', + 'filesystem', + 'fillup', + 'findutils', + 'findutils-locate', + 'fontconfig', + 'freetype', + 'freetype2', + 'gawk', + 'gcc', + 'gdbm', + 'glib2', + 'glibc', + 'gmp', + 'gpm', + 'grep', + 'grub', + 'gzip', + 'hal', + 'hwinfo', + 'insserv', + 'iproute2', + 'iptables', + 'iputils', + #'java-1_4_2-sun', + 'jpeg', + 'kbd', + 'klogd', + 'krb5', + 'ksh', + 'less', + 'lvm2', + 'libaio', + 'libapr1', + 'libapr-util1', + 'libattr', + 'libcom_err', + 'libelf', + 'libgcc', + 'libgssapi', + 'libjpeg', + 'libnscd', + 'libpcap', + 'libpng', + 'libstdc++', + 'libtool', + 'libusb', + 'libxcrypt', + 'libxml2', + 'make', + 'mdadm', + 'mingetty', + 'mkinitrd', + 'mktemp', + 'mm', + 'module-init-tools', + 'ncurses', + 'net-tools', + 'netcfg', + 'openct', + 'openldap2', + 'openldap2-client', + 'opensc', + 'openslp', + 'openssh', + 'openssl', + 'pam', + 'pam-modules', + 'patch', + 'pciutils', + 'pcre', + 'perl', + 'perl-Bootloader', + 'perl-Compress-Zlib', + 'perl-DBD-SQLite', + 'perl-DBI', + 'perl-Digest-SHA1', + 'perl-Net-Daemon', + 'perl-PlRPC', + 'perl-TermReadKey', + 'perl-URI', + 'perl-gettext', + 'php5', + 'pkgconfig', + 'popt', + 'procps', + 'psmisc', + 'pwdutils', + 'python', + 'python-xml', + 'resmgr', + 'sed', + 'slang', + 'sles-release', + 'sysconfig', + 'sysfsutils', + 'syslog-ng', + 'sysvinit', + 'tar', + 'tcl', + 'tcpd', + 'tcsh', + 'tcpdump', + 'termcap', + 'timezone', + 'udev', + 'unixODBC', + 'util-linux', + 'vim', + 'wget', + 'wireless-tools', + 'xorg-x11', + 'zlib' + ) class PkgWiz: def __init__(self): @@ -30,12 +185,15 @@ def help(self): print HELP_TEXT - def repo(self): + def _setupRepo(self): + if self.cfg: + return from conary import conaryclient, conarycfg, versions, errors from conary import deps from conary.build import use self.cfg = conarycfg.ConaryConfiguration(readConfigFiles=True) + self.cfg.read(os.path.dirname(__file__) + '/conaryrc') self.cfg.initializeFlavors() cvcCommand = cvc.CvcCommand() cvcCommand.setContext(self.cfg, dict()) @@ -52,6 +210,7 @@ self.recipeMaker = recipemaker.RecipeMaker(self.cfg, self.repos, self.rpmSource) def createPkgs(self, dirs): + self._setupRepo() for dir in dirs: self.rpmSource.walk(dir) @@ -60,7 +219,7 @@ # {foo:source: foo-1.0-1.1.src.rpm} srcmap = {} - for src in self.rpmSource.getSrpms(): + for src in set(self.rpmSource.getSrpms(sles10sp1pkgs)): h = self.rpmSource.getHeader(self.rpmSource.srcPath[src]) srccomp = h[rpmhelper.NAME] + ':source' srcmap[srccomp] = src @@ -68,73 +227,21 @@ d = self.repos.getTroveVersionsByLabel(srccomps) # Iterate over foo:source. - for srccomp in srccomps.iterkeys(): - src = srcmap[srccomp] + for srccomp in set(srccomps.iterkeys()): + srpm = srcmap[srccomp] pkgname = srccomp.split(':')[0] if srccomp not in d: - self.recipeMaker.create(pkgname, self.rpmSource.createTemplate(src), src) + self.recipeMaker.createManifest(pkgname, srpm, sles10sp1prefix) else: - # The package already exists in the repository, so query its - # version. - oldVersion = d[srccomp].keys()[0].trailingRevision().getVersion() - # Get the version - srchdr = self.rpmSource.getHeader(self.rpmSource.srcPath[src]) - newVersion = '%s_%s' % (srchdr[rpmhelper.VERSION], srchdr[rpmhelper.RELEASE]) - if newVersion == oldVersion: - continue - - # Check it out. - from conary import cvc - cvc.sourceCommand(self.cfg, ['checkout', pkgname]) - - # Look for the version string and replace it with the new. - import re - pattern = re.compile("""(?P\\s+)version\\s*=\\s*['"]([^'"\\s]+_[^'"\\s]+)['"]\\s*$""") - filename = os.path.join(pkgname, pkgname + ".recipe") - recipeFile = open(filename) - lines = recipeFile.readlines() - recipeFile.close() - spot = None - for (index, line) in lines: - match = pattern.match(line) - if match: - spot = index - break - if not spot: - raise Exception( - "The version string was not found in the %s recipe." - " Is this correct?" %pkgname) - lines = lines[:spot] + [match.group('lead') - + "version = '%s'\n" %newVersion] + lines[spot+1:] - recipeFile = open(filename, 'w') - recipeFile.write(''.join(lines)) - recipeFile.close() - - # Remove the original RPMs and add the new ones. - cwd = os.getcwd() - os.chdir(pkgname) - from conary.state import ConaryStateFromFile - conaryState = ConaryStateFromFile("CONARY", repos) - state = conaryState.getSourceState() - rpms = [] - for (theId, path, fileId, version) in state.iterFileList(): - if path.endswith('rpm'): - rpms.append(path) - for rpm in rpms: - cvc.sourceCommand(self.cfg, ['remove', rpm]) - addfiles = ['add'] - for path, fn in self.rpmSource.rpmMap[src].iteritems(): - shutil.copy(fn, path) - addfiles.append(path) - cvc.sourceCommand(self.cfg, addfiles, {}) - cvc.sourceCommand(self.cfg, - [ 'commit' ], {'message': 'Automated update of ' + pkgname}) + continue + self.recipeMaker.updateManifest(pkgname, srpm, sles10sp1prefix) def createUsers(self, users): + self._setupRepo() # Get all users and groups used in this run. users = set() groups = set() - for src in self.rpmSource.getSrpms(): + for src in self.rpmSource.getSrpms(slessp1pkgs): for rpm in self.rpmSource.rpmMap[src].values(): header = self.rpmSource.getHeader(rpm) users = users.union(header[FILEUSERNAME]) @@ -156,7 +263,6 @@ dirs = argv[2:] if not dirs: dirs = ['.'] - self.repo() self.createPkgs(dirs) elif 'accounts' == category: users = argv[2:] diff --git a/rpmimport/recipemaker.py b/rpmimport/recipemaker.py --- a/rpmimport/recipemaker.py +++ b/rpmimport/recipemaker.py @@ -1,6 +1,23 @@ +#!/usr/bin/python +# +# Copyright (c) 2006,2008 rPath, Inc. +# +# +# This program is distributed under the terms of the Common Public License, +# version 1.0. A copy of this license should have been distributed with this +# source file in a file called LICENSE. If it is not present, the license +# is always available at http://www.rpath.com/permanent/licenses/CPL-1.0. +# +# This program is distributed in the hope that it will be useful, but +# without any warranty; without even the implied warranty of merchantability +# or fitness for a particular purpose. See the Common Public License for +# full details. +# + +import copy import os import shutil - +from conary import cvc, deps class RecipeMaker: def __init__(self, cfg, repos, rpmSource): @@ -8,35 +25,71 @@ self.repos = repos self.rpmSource = rpmSource - def create(self, pkgname, recipeContents, srpm = None): - from conary import cvc - + def _createSourceComponent(self, pkgname, recipeContents=None, + manifestContents=None, newPkgArgs={}): print 'creating initial template for', pkgname try: shutil.rmtree(pkgname) except OSError, e: pass - cvc.sourceCommand(self.cfg, [ "newpkg", pkgname], {}) + cvc.sourceCommand(self.cfg, [ "newpkg", pkgname ], newPkgArgs) cwd = os.getcwd() os.chdir(pkgname) try: - recipe = pkgname + '.recipe' - f = open(recipe, 'w') - f.write(recipeContents) - f.close() - addfiles = [ 'add', recipe ] + addfiles = [ 'add' ] + if recipeContents: + recipe = pkgname + '.recipe' + f = open(recipe, 'w') + f.write(recipeContents) + f.close() + addfiles.append(recipe) + if manifestContents: + manifest = 'manifest' + f = open(manifest, 'w') + f.write(manifestContents) + f.close() + addfiles.append(manifest) - # copy all the binaries to the cwd - if srpm: - for path, fn in self.rpmSource.rpmMap[srpm].iteritems(): - shutil.copy(fn, path) - addfiles.append(path) - cvc.sourceCommand(self.cfg, addfiles, {}) - cvc.sourceCommand(self.cfg, ['cook', recipe], {'no-deps': None}) + cvc.sourceCommand(self.cfg, addfiles, {'text':True}) + try: + cvc.sourceCommand(self.cfg, ['cook'], {'no-deps': None}) + except Exception, e: + print '++++++ error building', pkgname, str(e) + return cvc.sourceCommand(self.cfg, - [ 'commit' ], - { 'message': - 'Automated initial commit of ' + recipe }) - #cvc.sourceCommand(self.cfg, ['cook', pkgname], {'no-deps': None}) + [ 'commit' ], + { 'message': + 'Automated initial commit of %s:source' %pkgname}) + cvc.sourceCommand(self.cfg, ['cook', pkgname], {'no-deps': None}) + cfg = copy.copy(self.cfg) + buildFlavor = deps.deps.parseFlavor('is:x86_64') + cfg.buildFlavor = deps.deps.overrideFlavor( + cfg.buildFlavor, buildFlavor) + cvc.sourceCommand(cfg, ['cook', pkgname], {'no-deps': None}) finally: os.chdir(cwd) + + def _updateSourceComponent(self): + raise NotImplemented + + def _createOrUpdateManifest(self, pkgname, srpm, prefix, + create=False, update=False): + assert(create or update) + manifest = self.rpmSource.createManifest(srpm, prefix) + + if create: + fn = self._createSourceComponent + else: + fn = self._updateSourceComponent + + fn(pkgname, manifestContents=manifest, + newPkgArgs={'factory':'sle-rpm'}) + + def createManifest(self, pkgname, srpm, prefix): + self._createOrUpdateManifest(pkgname, srpm, prefix, create=True) + + def updateManifest(self, pkgname, srpm, prefix): + self._createOrUpdateManifest(pkgname, srpm, prefix, update=True) + + def createRecipe(self, pkgname, recipeContents): + self._createSourceComponent(pkgname, recipeContents=recipeContents) diff --git a/rpmimport/rpmsource.py b/rpmimport/rpmsource.py --- a/rpmimport/rpmsource.py +++ b/rpmimport/rpmsource.py @@ -1,6 +1,5 @@ -#!/usr/bin/python # -# Copyright (c) 2006 rPath, Inc. +# Copyright (c) 2006,2008 rPath, Inc. # # # This program is distributed under the terms of the Common Public License, @@ -23,7 +22,7 @@ # make local copies of tags for convenience for tag in ('NAME', 'VERSION', 'RELEASE', 'SOURCERPM', 'FILEUSERNAME', - 'FILEGROUPNAME'): + 'FILEGROUPNAME'): sys.modules[__name__].__dict__[tag] = getattr(rpmhelper, tag) ARCH = 1022 @@ -62,171 +61,56 @@ def procSrc(self, f, rpm): self.srcPath[rpm] = f + def getRPMS(self, src): + """ + @return list of binary RPMS built from this source. + """ + return [ x for x in self.rpmMap[src].itervalues() ] + def walk(self, root): """ Walk the tree rooted at root and collect information about rpms found. """ - + ignored = ('patch.rpm', 'delta.rpm') for dirpath, dirnames, filenames in os.walk(root): for f in filenames: # ignore the 32-bit compatibility libs - we will # simply use the 32-bit components from the repository + skip = False if '32bit' in f: + skip = True + for suffix in ignored: + if f.endswith(suffix): + skip = True + break + continue + if not f.endswith(".rpm"): + skip = True + if skip: continue - if f.endswith(".rpm"): - fullpath = os.path.join(dirpath, f) - if f.endswith(".src.rpm") or f.endswith('.nosrc.rpm'): - self.procSrc(fullpath, f) - else: - self.procBin(fullpath, f) + fullpath = os.path.join(dirpath, f) + try: + h = self.getHeader(fullpath) + except IOError: + print 'bad rpm:', fullpath + os.unlink(fullpath) + continue + if h[VERSION] not in f: + # ignore files like aaa_base.rpm. We only want + # one version, like aaa_base-10-0.8.x86_64.rpm + continue + if f.endswith(".src.rpm") or f.endswith('.nosrc.rpm'): + self.procSrc(fullpath, f) + else: + self.procBin(fullpath, f) - def getSrpms(self): + def getSrpms(self, pkglist): """ - Get all sources we think we need now. + Get source RPMS for a given list of binary RPM package names """ - - bins = ( - 'aaa_base', - 'acl', - 'alsa', - 'apache2', - 'ash', - 'attr', - 'bash', - 'binutils', - 'bzip2', - 'compat-libstdc++', - 'coreutils', - 'cpio', - 'cpp', - 'cracklib', - 'cron', - 'cyrus-sasl', - 'device-mapper', - 'db', - 'dbus-1', - 'dhcpcd', - 'diffutils', - 'e2fsprogs', - 'expat', - 'expect', - 'file', - 'filesystem', - 'fillup', - 'findutils', - 'findutils-locate', - 'fontconfig', - 'freetype', - 'freetype2', - 'gawk', - 'gcc', - 'gdbm', - 'glib2', - 'glibc', - 'gmp', - 'gpm', - 'grep', - 'grub', - 'gzip', - 'hal', - 'hwinfo', - 'insserv', - 'iproute2', - 'iptables', - 'iputils', - #'java-1_4_2-sun', - 'jpeg', - 'kbd', - 'klogd', - 'krb5', - 'ksh', - 'less', - 'lvm2', - 'libaio', - 'libapr1', - 'libapr-util1', - 'libattr', - 'libcom_err', - 'libelf', - 'libgcc', - 'libjpeg', - 'libnscd', - 'libpcap', - 'libpng', - 'libstdc++', - 'libtool', - 'libusb', - 'libxcrypt', - 'libxml2', - 'make', - 'mdadm', - 'mingetty', - 'mkinitrd', - 'mktemp', - 'mm', - 'module-init-tools', - 'ncurses', - 'net-tools', - 'netcfg', - 'openct', - 'openldap2', - 'openldap2-client', - 'opensc', - 'openslp', - 'openssh', - 'openssl', - 'pam', - 'pam-modules', - 'patch', - 'pciutils', - 'pcre', - 'perl', - 'perl-Bootloader', - 'perl-Compress-Zlib', - 'perl-DBD-SQLite', - 'perl-DBI', - 'perl-Digest-SHA1', - 'perl-Net-Daemon', - 'perl-PlRPC', - 'perl-TermReadKey', - 'perl-URI', - 'perl-gettext', - 'php5', - 'pkgconfig', - 'popt', - 'procps', - 'psmisc', - 'pwdutils', - 'python', - 'python-xml', - 'resmgr', - 'sed', - 'slang', - 'sles-release', - 'sysconfig', - 'sysfsutils', - 'syslog-ng', - 'sysvinit', - 'tar', - 'tcl', - 'tcpd', - 'tcsh', - 'tcpdump', - 'termcap', - 'timezone', - 'udev', - 'unixODBC', - 'util-linux', - 'vim', - 'wget', - 'wireless-tools', - 'xorg-x11', - 'zlib' - ) - srpms = list() - for b in bins: - srpms.append(self.revMap[b]) + for p in pkglist: + srpms.append(self.revMap[p]) return srpms def transformName(self, name): @@ -306,5 +190,18 @@ a(' archs = [ %s ]' % self.quoteSequence(archs)) if extras: a(" extraArch = { 'i686': [ %s ] }" %self.quoteSequence(extras)) + # add a trailing newline a('') return '\n'.join(l) + + def createManifest(self, srpm, prefix): + """ + @return the text for the manifest file. + """ + l = [] + l.append(self.srcPath[srpm]) + l.extend([x for x in self.getRPMS(srpm)]) + if prefix: + l = [ x[len(prefix):] for x in l] + # add a trailing newline + return '\n'.join(sorted(l) + ['']) From johnsonm at rpath.com Wed Aug 19 17:43:09 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:43:09 +0000 Subject: mirrorball: remove CVE handling, CVEs are now part of the description Message-ID: <200908192143.n7JLh9BA024260@scc.eng.rpath.com> changeset: 93fd53d9db49 user: Elliot Peele date: Thu, 30 Jul 2009 15:28:43 -0400 remove CVE handling, CVEs are now part of the description committer: Elliot Peele diff --git a/pmap/scientific.py b/pmap/scientific.py --- a/pmap/scientific.py +++ b/pmap/scientific.py @@ -24,7 +24,7 @@ Container class for Scientific Linux advisories. """ - _slots = ('cve', 'pkgs', ) + _slots = ('pkgs', ) def finalize(self): """ @@ -46,8 +46,6 @@ self._states.update({ 'synopsis' : self._synopsis, 'Issue' : None, - 'CVE' : self._cveNames, - 'MultiCVE' : self._cveMultiLine, 'SLVersion' : self._slVersion, 'Arch' : self._arch, 'End' : self._end, @@ -56,8 +54,6 @@ self._supportedArch = ('SRPM', 'SRPMS', 'i386', 'x86_64', ) self._filterLine('^Issue date:.*', 'Issue') - self._filterLine('^CVE Names:.*', 'CVE') - self._filter('^cve-[0-9]{4}-[0-9]{4}', 'MultiCVE') self._filter('^sl[0-9]\.x', 'SLVersion') self._filterLine('^SL [0-9]\.x', 'SLVersion') self._filterLine('^\s*(%s).*' % '|'.join(self._supportedArch), 'Arch') @@ -131,38 +127,6 @@ self._curObj.summary = self._getLine() - def _addCVE(self, cve): - """ - Add a cve to curObj. - """ - - if self._curObj.cve is None: - self._curObj.cve = {} - - splt = cve.split() - cve = splt[0].strip(',') - desc = '' - if len(splt) > 1: - desc = ' '.join(splt[1:]) - if cve not in self._curObj.cve: - self._curObj.cve[cve] = desc - - def _cveNames(self): - """ - Parse CVE Names line. - """ - - for cve in self._getLine().split(): - if cve.startswith('CVE-'): - self._addCVE(cve) - - def _cveMultiLine(self): - """ - Parse multiline CVEs. - """ - - self._addCVE(self._getFullLine()) - def _slVersion(self): """ Parse SL Version. From johnsonm at rpath.com Wed Aug 19 17:43:09 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:43:09 +0000 Subject: mirrorball: add scripts for testing advisories Message-ID: <200908192143.n7JLh9cN024277@scc.eng.rpath.com> changeset: 5b4a83496c5c user: Elliot Peele date: Thu, 30 Jul 2009 15:31:41 -0400 add scripts for testing advisories committer: Elliot Peele diff --git a/scripts/advise b/scripts/advise new file mode 100755 --- /dev/null +++ b/scripts/advise @@ -0,0 +1,50 @@ +#!/usr/bin/python +# +# Copyright (c) 2009 rPath, Inc. +# +# This program is distributed under the terms of the Common Public License, +# version 1.0. A copy of this license should have been distributed with this +# source file in a file called LICENSE. If it is not present, the license +# is always available at http://www.rpath.com/permanent/licenses/CPL-1.0. +# +# This program is distributed in the hope that it will be useful, but +# without any warranty; without even the implied warranty of merchantability +# or fitness for a particular purpose. See the Common Public License for +# full details. +# + +""" +Send out a single advisory if available for a given package on a +given platform. +""" + +import os +import sys + +from conary.lib import util +sys.excepthook = util.genExcepthook() + +platform = sys.argv[1] +mirrorballPath = os.environ['HOME'] + '/hg/mirrorball' + +sys.path.insert(0, mirrorballPath) + +from updatebot import log +from updatebot import bot +from updatebot import config + +log.addRootLogger() + +cfg = config.UpdateBotConfig() +cfg.read(mirrorballPath + '/config/%s/updatebotrc' % platform) + +obj = bot.Bot(cfg) + +obj._pkgSource.load() + +advisor = obj._advisor +updater = obj._updater + +advisor.load() + +import epdb ; epdb.st() diff --git a/scripts/pmapload b/scripts/pmapload new file mode 100755 --- /dev/null +++ b/scripts/pmapload @@ -0,0 +1,45 @@ +#!/usr/bin/python +# +# Copyright (c) 2009 rPath, Inc. +# +# This program is distributed under the terms of the Common Public License, +# version 1.0. A copy of this license should have been distributed with this +# source file in a file called LICENSE. If it is not present, the license +# is always available at http://www.rpath.com/permanent/licenses/CPL-1.0. +# +# This program is distributed in the hope that it will be useful, but +# without any warranty; without even the implied warranty of merchantability +# or fitness for a particular purpose. See the Common Public License for +# full details. +# + +""" +Load a single mail archive. +""" + +import os +import sys + +from conary.lib import util +sys.excepthook = util.genExcepthook() + +platform = sys.argv[1] +archivePath = sys.argv[2] +mirrorballPath = os.environ['HOME'] + '/hg/mirrorball' + +sys.path.insert(0, mirrorballPath) + +from updatebot import log +from updatebot import config + +import pmap + +log.addRootLogger() + +cfg = config.UpdateBotConfig() +cfg.read(mirrorballPath + '/config/%s/updatebotrc' % platform) + + +data = pmap.parse(archivePath, backend=cfg.platformName, productVersion=cfg.upstreamProductVersion) + +import epdb; epdb.st() From johnsonm at rpath.com Wed Aug 19 17:43:10 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:43:10 +0000 Subject: mirrorball: allow for a space between SL and the version Message-ID: <200908192143.n7JLhAHe024294@scc.eng.rpath.com> changeset: 53c8937be7d9 user: Elliot Peele date: Fri, 31 Jul 2009 10:56:27 -0400 allow for a space between SL and the version committer: Elliot Peele diff --git a/pmap/scientific.py b/pmap/scientific.py --- a/pmap/scientific.py +++ b/pmap/scientific.py @@ -62,7 +62,7 @@ self._productVersion = productVersion if productVersion: self._checkSubject = True - self._subjectVersionRE = re.compile('.*SL%s\.x.*' % productVersion) + self._subjectVersionRE = re.compile('.*SL[ ]{0,1}%s\.x.*' % productVersion) else: self._checkSubject = False From johnsonm at rpath.com Wed Aug 19 17:43:11 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:43:11 +0000 Subject: mirrorball: add profiling info Message-ID: <200908192143.n7JLhBqR024349@scc.eng.rpath.com> changeset: 1f528c8e2666 user: Elliot Peele date: Wed, 05 Aug 2009 14:03:49 -0400 add profiling info committer: Elliot Peele diff --git a/scripts/promote b/scripts/promote --- a/scripts/promote +++ b/scripts/promote @@ -15,6 +15,8 @@ from header import * +import tempfile + from updatebot import conaryhelper helper = conaryhelper.ConaryHelper(cfg) @@ -24,9 +26,14 @@ import logging slog = logging.getLogger('script') +#import cProfile +#prof = cProfile.Profile() +#prof.enable() + trvLst = helper._repos.findTrove(helper._ccfg.buildLabel, cfg.topGroup) trvLst = helper._findLatest(trvLst) +slog.info('creating changeset') cs, packages = helper.promote( trvLst, [], @@ -36,6 +43,11 @@ extraPromoteTroves=cfg.extraPromoteTroves, commit=False ) +slog.info('changeset created') + +#prof.disable() +#prof.dump_stats('promote.lsprof') +#prof.print_stats() newPkgs = set([ (x[0].split(':')[0], x[1].getSourceVersion(), x[2]) for x in packages]) @@ -56,6 +68,10 @@ continue slog.info('promoting %s=%s[%s]' % (pkg[0], pkg[1], flv)) +#csFileName = tempfile.mktemp() +#slog.info('writing changeset to %s' % csFileName) +#cs.writeToFile(csFileName) + slog.info('committing') helper._repos.commitChangeSet(cs) From johnsonm at rpath.com Wed Aug 19 17:43:12 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:43:12 +0000 Subject: mirrorball: handle comparing against strings or unicode objects Message-ID: <200908192143.n7JLhCRx024389@scc.eng.rpath.com> changeset: 1c270ae4dbfd user: Elliot Peele date: Wed, 05 Aug 2009 14:05:34 -0400 handle comparing against strings or unicode objects committer: Elliot Peele diff --git a/updatebot/lib/xobjects.py b/updatebot/lib/xobjects.py --- a/updatebot/lib/xobjects.py +++ b/updatebot/lib/xobjects.py @@ -83,7 +83,10 @@ return hash(self.key) def __cmp__(self, other): - return cmp(self.key, other.key) + if type(other) in (str, unicode): + return cmp(self.key, other) + else: + return cmp(self.key, other.key) class XDict(object): From johnsonm at rpath.com Wed Aug 19 17:43:12 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:43:12 +0000 Subject: mirrorball: add script for generating the package name map for factory-comps-group based Message-ID: <200908192143.n7JLhC2k024367@scc.eng.rpath.com> changeset: 261fb3e4de48 user: Elliot Peele date: Wed, 05 Aug 2009 14:04:23 -0400 add script for generating the package name map for factory-comps-group based groups committer: Elliot Peele diff --git a/scripts/genPackageNameMap b/scripts/genPackageNameMap new file mode 100755 --- /dev/null +++ b/scripts/genPackageNameMap @@ -0,0 +1,38 @@ +#!/usr/bin/python +# +# Copyright (c) 2009 rPath, Inc. +# +# This program is distributed under the terms of the Common Public License, +# version 1.0. A copy of this license should have been distributed with this +# source file in a file called LICENSE. If it is not present, the license +# is always available at http://www.rpath.com/permanent/licenses/CPL-1.0. +# +# This program is distributed in the hope that it will be useful, but +# without any warranty; without even the implied warranty of merchantability +# or fitness for a particular purpose. See the Common Public License for +# full details. +# + +from header import * + +from xobj import xobj + +from updatebot.bot import Bot +from updatebot.lib.xobjects import XDict + +pkgSource = Bot(cfg)._pkgSource +pkgSource.load() + +def getLatestBin(binPkgs): + lst = list(binPkgs) + lst.sort() + return lst[-1] + +d = XDict() +for binName, binPkgs in pkgSource.binNameMap.iteritems(): + latest = getLatestBin(binPkgs) + srcName = pkgSource.binPkgMap[latest].name + if binName not in d: + d[binName] = srcName + +print xobj.toxml(d, 'pkgNameMap') From johnsonm at rpath.com Wed Aug 19 17:43:11 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:43:11 +0000 Subject: mirrorball: add a basic freezable string dictionary implementation Message-ID: <200908192143.n7JLhBgk024330@scc.eng.rpath.com> changeset: fcdc8e9b9cd8 user: Elliot Peele date: Tue, 04 Aug 2009 15:53:57 -0400 add a basic freezable string dictionary implementation committer: Elliot Peele diff --git a/updatebot/lib/xobjects.py b/updatebot/lib/xobjects.py --- a/updatebot/lib/xobjects.py +++ b/updatebot/lib/xobjects.py @@ -65,3 +65,50 @@ self.data.sourcePackage = pkg else: self.data.binaryPackages.append(pkg) + + +class XDictItem(object): + """ + Object to represent key/value pairs. + """ + + key = str + value = str + + def __init__(self, key=None, value=None): + self.key = key + self.value = value + + def __hash__(self): + return hash(self.key) + + def __cmp__(self, other): + return cmp(self.key, other.key) + + +class XDict(object): + """ + String based xobj dict implementation. + """ + + items = [ XDictItem ] + + def __init__(self): + self.items = [] + + def __setitem__(self, key, value): + item = XDictItem(key, value) + if item in self.items: + idx = self.items.index(item) + self.items[idx] = item + else: + self.items.append(item) + + def __getitem__(self, key): + if key in self.items: + idx = self.items.index(key) + return self.items[idx].value + raise KeyError, key + + def __contains__(self, key): + return key in self.items From johnsonm at rpath.com Wed Aug 19 17:43:10 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:43:10 +0000 Subject: mirrorball: add promote callback Message-ID: <200908192143.n7JLhA8S024311@scc.eng.rpath.com> changeset: ae2668b7577c user: Elliot Peele date: Mon, 03 Aug 2009 17:08:31 -0400 add promote callback committer: Elliot Peele diff --git a/updatebot/conaryhelper.py b/updatebot/conaryhelper.py --- a/updatebot/conaryhelper.py +++ b/updatebot/conaryhelper.py @@ -41,6 +41,8 @@ from updatebot.errors import PromoteMismatchError from updatebot.errors import MirrorFailedError +from updatebot.lib.conarycallbacks import UpdateBotCloneCallback + log = logging.getLogger('updatebot.conaryhelper') class ConaryHelper(object): @@ -599,10 +601,13 @@ if version == latestVer: trvLst.append((name, version, flavor)) + callback = UpdateBotCloneCallback(self._ccfg, 'test', log=log) + success, cs = self._client.createSiblingCloneChangeSet( labelMap, trvLst, - cloneSources=True) + cloneSources=True, + callback=callback) log.info('changeset created in %s' % (time.time() - start, )) @@ -634,7 +639,7 @@ log.info('committing changeset') - self._repos.commitChangeSet(cs) + self._repos.commitChangeSet(cs, callback=callback) log.info('changeset committed') log.info('promote complete, elapsed time %s' % (time.time() - start, )) diff --git a/updatebot/lib/conarycallbacks.py b/updatebot/lib/conarycallbacks.py new file mode 100644 --- /dev/null +++ b/updatebot/lib/conarycallbacks.py @@ -0,0 +1,81 @@ +# +# Copyright (c) 2009 rPath, Inc. +# +# This program is distributed under the terms of the Common Public License, +# version 1.0. A copy of this license should have been distributed with this +# source file in a file called LICENSE. If it is not present, the license +# is always available at http://www.rpath.com/permanent/licenses/CPL-1.0. +# +# This program is distributed in the hope that it will be useful, but +# without any warranty; without even the implied warranty of merchantability +# or fitness for a particular purpose. See the Common Public License for +# full details. +# + +""" +Conary callbacks that have been wrapped to be more friendly with the +logging module. +""" + +import logging +log = logging.getLogger('updatebot.lib.conarycallbacks') + +from conary.conaryclient.callbacks import CloneCallback + +def callonce(func): + def wrapper(self, *args, **kwargs): + if self._last != func.__name__: + self._last = func.__name__ + return func(self, *args, **kwargs) + return wrapper + +class UpdateBotCloneCallback(CloneCallback): + def __init__(self, *args, **kwargs): + self._log = kwargs.pop('log', log) + CloneCallback.__init__(self, *args, **kwargs) + + self._last = None + + def _message(self, message): + self._log.info(message) + + @callonce + def determiningCloneTroves(self, current=0, total=0): + CloneCallback.determiningCloneTroves(self, current=0, total=0) + + @callonce + def determiningTargets(self): + CloneCallback.determiningTargets(self) + + @callonce + def targetSources(self, current=0, total=0): + CloneCallback.targetSources(self, current=0, total=0) + + @callonce + def targetBinaries(self, current=0, total=0): + CloneCallback.targetBinaries(self, current=0, total=0) + + @callonce + def checkNeedsFulfilled(self, current=0, total=0): + CloneCallback.checkNeedsFulfilled(self, current=0, total=0) + + @callonce + def rewriteTrove(self, current=0, total=0): + CloneCallback.rewriteTrove(self, current=0, total=0) + + @callonce + def buildingChangeset(self, current=0, total=0): + CloneCallback.buildingChangeset(self, current=0, total=0) + + @callonce + def requestingFiles(self, number): + CloneCallback.requestingFiles(self, number) + + @callonce + def requestingFileContentsWithCount(self, count): + CloneCallback.requestingFileContentsWithCount(self, count) + + @callonce + def gettingCloneData(self): + CloneCallback.gettingCloneData(self) + From johnsonm at rpath.com Wed Aug 19 17:38:45 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:38:45 +0000 Subject: mirrorball: add test framework Message-ID: <200908192138.n7JLcjc9013510@scc.eng.rpath.com> changeset: 24d911c85d90 user: Elliot Peele date: Tue, 27 May 2008 15:36:22 -0400 add test framework committer: Elliot Peele diff --git a/test/Makefile b/test/Makefile new file mode 100644 --- /dev/null +++ b/test/Makefile @@ -0,0 +1,10 @@ +all: + for dir in `find . -type d -name '*test'`; do \ +relativelink=`echo "$$dir" | sed s,/[^/]*,/..,g | sed 's,./,,'`; \ +ln -fs $${relativelink}/testsetup.py $$dir; \ +done + +clean: + for dir in `find . -type d -name '*test'`; do \ + rm -f $${dir}/testsetup.py*;\ + done diff --git a/test/__init__.py b/test/__init__.py new file mode 100644 diff --git a/test/archive/Makefile b/test/archive/Makefile new file mode 100644 --- /dev/null +++ b/test/archive/Makefile @@ -0,0 +1,43 @@ +# +# Copyright (c) 2006-2007 rPath, Inc. All Rights Reserved. +# +# All Rights Reserved +# +arch_files = arch/ppc arch/x86 arch/x86_64 + +use_files = use/bootstrap\ + use/builddocs\ + use/buildtests\ + use/desktop\ + use/dietlibc\ + use/emacs\ + use/gcj\ + use/gdbm\ + use/gnome\ + use/gtk\ + use/ipv6\ + use/kde\ + use/krb\ + use/ldap\ + use/pam\ + use/pcre\ + use/perl\ + use/python\ + use/qt\ + use/readline\ + use/sasl\ + use/ssl\ + use/tcl\ + use/tk\ + use/X + +dist_files = $(arch_files) $(use_files) Makefile + +all: default-all + +dist: default-dist + +clean: default-clean + +include ../../Make.rules +# vim: set sts=8 sw=8 noexpandtab : diff --git a/test/archive/arch/ppc b/test/archive/arch/ppc new file mode 100644 --- /dev/null +++ b/test/archive/arch/ppc @@ -0,0 +1,10 @@ +Name ppc +archProp LE True +archProp BE False +archProp bits32 False +archProp bits64 True +macro unamearch ppc +macro targetarch powerpc + +[ppc64] + diff --git a/test/archive/arch/x86 b/test/archive/arch/x86 new file mode 100644 --- /dev/null +++ b/test/archive/arch/x86 @@ -0,0 +1,46 @@ +Name x86 +archProp LE True +archProp BE False +archProp bits32 True +archProp bits64 False +macro targetarch i386 +macro unamearch i386 +macro optflags -O2 + +[i486] +macro targetarch i486 +macro unamearch i486 + +[i586] +macro targetarch i586 +macro unamearch i586 +subsumes i486 + +[i686] +macro targetarch i686 +macro unamearch i686 +subsumes i586, i486 + +[cmov] + +[sse] + +[sse2] +subsumes sse + +[sse3] +subsumes sse2,sse + +[mmx] + +[mmxext] +subsumes mmx + +[3dnow] +buildName threednow + +[3dnowext] +buildName threednowext +subsumes 3dnow + +[nx] diff --git a/test/archive/arch/x86_64 b/test/archive/arch/x86_64 new file mode 100644 --- /dev/null +++ b/test/archive/arch/x86_64 @@ -0,0 +1,17 @@ +archProp LE True +archProp BE False +archProp bits32 False +archProp bits64 True +macro lib lib64 + +[nx] + +[sse3] + +[3dnow] +buildName threednow + +[3dnowext] +buildName threednowext +subsumes 3dnow + diff --git a/test/archive/authdb b/test/archive/authdb new file mode 100644 index 0000000000000000000000000000000000000000..1636f3d45eccdb69b9d1ee30813973adb76f44a1 GIT binary patch literal 96256 zc%1EBdu$xXdEeO~Z$&+9>p`y>h2c>&BVI&NI#Sdl3N0VWqkPiwDDffMNz==bx1z4P z2X%Xttinxuq9QqM5)?%Xq(xD*f3!`|e~h*PiawAONzf)i3Zn&z0DZs>V6=a at K+?2! z>J)M3v5%R%-Mb?#iIVpF;cj;x-^_e7yR$R%ee;;Rn9Nm- at a0mukg0@6lr{w+<@s<} zQIsbY8E42J3wETmh<5Vdhku(Ie-{4)zm0#0-@^ZnzmNX~e-Hly{yY3F{0;n7{O9;9 z_%-}T_z&>!abf@<;CSgAxmOd4lq)Y#_Rvv z3i$y5fIwgy>F4i7v1C%Ay#R!NtdJi700;;)%Nkem-6jl^+C at 79r#D`t(CSpw;`YwX@;rM-|!TwEZDrzYbshHZ*4 zW!_~cQeAA*^!@wMdOOR(R6cXPR5qkvc2WOw6Q+bBx4gCz|HLNCqTA3ZBgYP7ratBZu2VdTc*ZSOJ;W at +7}fmRsAUMF`*!)Gpmk4^C%4B+){DK*`sfiX!j}t(drSC)k5w9RJv^3}TXoSK=9C(fqmlM4^ngpu%Ud at 4R0Pff(Jcl>{@2O^B{%@@pP#Onq(KN={iu3gcojLjiL7MZ>DAY(p(iPMvSj5?e9^4 at 4chNdWNEvmcXy+z&R*T| za;CU^#W3yrF6ouET?dw!!KbIoJ!w}ryjIMSb;J?6s zjK7S32mc2C9A3vikFVh?_>*`ZCkP<`0C1q~YeN)j$6jKh=_KYg4o*si6%c?Fnj75z=-s1Ab52Nex(iPuszW)%QBK(+E%-ji7^3sj9Y(MbM^d z8Y6E*T8J4)lC_!OzP9b$U+v)G+O}EW0_&TAL;Q}~IX&@U^lg~i z$ybEMhY-mB0f68F(IcD8<(&;NT at AwK{B5E!&$DgdDEzlKgL z=rqc!Ur at h}BlzR^Pq&)4M*>H7g!GASzd0_pLM9!xO;6nF*{SKhz36raQ|L at w$rM+N zWNF28O;1Wn9oJpfLKaI|!=2{hqhKs%3QZ at vESHLv%yOk6O~EkD%!<*FMo{m$Jf}IE z4WZ zrYv3~N1kl-wC#A)QQNfl#iumAs|!_kFt(DJB_nV0>pqDk?iL=%)4SgcHRfqeQHz}0 z9nyQc&{dW+4^9f18#=vok`4`>fF`9Ef%X3YKp=tXZa%#K5A`1a2pHP1f>tqPP-Xv zcG)y#&L))Dt6MYHh{xGDRs2oNZ=A^ox88m8JUXvNQzjisiOYJu;(dAE^i{GRdh<;g z_*x1{ z^n+d2mOI=n!9H-7rAyiyKq8>uS*!bDwZ-X9jn1H`?iFLRW7_XMg~Fn#i_G%_yT0kQn{3fFNR&`aBR-`IM0#RVq#wUyt5v~5ZkXPW3v8}++^5I z`jEBKn-Fo6OlCJ%L-s@$LT4FeLaX&1vNjGPWZdKsP0h|sOCfkx9H(n2K>iN^1Q*c$0|3DVwEqA=Z~^T<01#Yo{r|5k^!KpfPOgh>?Em>Flc}ak zS;J(P4 at uHg=G4-{SpN<2 at 7)Z9mL?Zeas^|qk}0g3-0roO_HwS6$;e?|Ku90c{U#3Zk7bnIda7OfHT{f^syXJ#9BG@!8~W8`Uqj|w9Hx!Q+<+B*Mn+9e7LFK#@$+HPY=4gNT(<;s!O6lnxNpgHkqHm z=!UFrSjMzEWP at 8ehk^4%yIv;mdJ#g`=0)pt!59gf^&q+U&wgOxeI`HfxHmZWfd|^$ z)CV56$;<~@pP>B*00Ie+{{sNQ1(*MQRiVE}5O<#Y5IZ>XbLaUPpheO at D=w2BvTb-) zJ`2=#u33jS$dnlJd zx!gVM3wT^kABN>nApZjZ0Ji+^n~L&HbWD8@|KV2ivndpMG`+tERr~mP+)gJT>zY68 zc`mm=Nk5W8|DaA at CNA!IA*^*CJc#aGq>?iy%%xh3BtesWnyvD&X@`WK36+&Owv9>D z?&ESFhgBb;myng_f-g~cuMS zhZQ{JQ^T2~&yK=1D&9POqT`7a at gDSu#PV{R>BR#fJE8&yfl~cJEXFt-<6-Z z1ow6-tiZ2btWCMK31vyTFc@|S-lsHu@!61muotbdVY{{evDg3JHb6lF~vSFho}ejtgipZkcW at 86H^TxEj5 z3uiB!H*T8NZA20PT8^^t at 2YWAoCVi>!PS~+l$);Cznm+s80FP+&RQ?eC(aEVI~u8( zV7IbLSaR)Nlkfy-mX(7v=b@?Cjgv_tw*}&4+8+0GRl3cako5-gHGxVHsvSNQ(qsGm zglcvQQOj`b*7TsJA3B6?U$me7lpC#CV?AE`E{L8jy%ViIV*htXE-sOQG)uGT6J=D= z1GQgj?z5RE>K6al;gEjuP=n%^IF8mhoPhp601#Y2{~rJdE};Df0D=pC{&!MQl4uTn zT}`X6sXyIpT({bWG`+tct at F+JCXH2t3_^;_Il~mqC#6B1=rfQtR^2m?!(@n3$dqr= zq1|BJAgLb9`HX2gM<}-tpqMEbbZuTMw3qXl6-wKjG*Uc52DtsG+Qo)oV*V0Y$@)>R zJ&9`Q*k4dvv1Ol3G4_+h5;gH_nZ5;zI&=1?@F~1mxr6^fOBGq3U zVMVvjg%PvHwY6N?Fzx$uQqkG2_oC=p43JtYY%s(`X{}hr^A2ri)g z2LOT#sQ&;!Z~^^)03f)4_8$NUF1Y at 0L?OT1%HN}2bqYV%B*@G$L8oIYMG{%Zct+B| z2n19CeL_+Z*{%Zx%h1Yc5dN@^%pfte8p7Q~bOV8$(`7dhh900h$4tm`3i-W at eoOrm^0`RzjyF8cD7&4j{@ioyn8)=Vrwq at QOKYp9YdT+7 zn%v!^NSTLKt}PWQ3($H|@PyM&$RAr>rdTT8ER@#Fe;WSco2O1LWrt6U9$Pwj{KTotr$&}XmX9ti zWwWCvjv2$z(UB->SGqf=3%O#2H2hco`4iWwCy$LDGe(z3qsNb*ICk{%^6|{c<4eOM zOUCh&OD9i7hiUt-;x`riCjQY at XBhwh+z0H!J;?cxs5>#6yvsfjh`0Zw{{M(v006+_ zhn*@`dYliy4uq9%`vU;&KLGHUqiOs9Ukd(TasdDUk2%^^jF76fBXUEm{$u6ait=ss zJ9uTQd3!%t|Mc^ke)urDdqmdtOdEwIOLZpNr at i$)mTzm?pG_Px;-&Jj(o74fv~)~} zMO$}P2v_Y$B=`1!UQf=q=xh^L|5fdZY5K at vR82FDS1pr?Y at O4Lq}}xhK$2@&om(4R zD*x)!ES>OTMwEQs}= zkN>}~;2)9;004N*uuIhtgQ_S;M!AqP>Aq8 at V7w@wJL7t6Mz)sEEE#!0xMokkPR-1R zQwzzYOl|F()gtwcGNBLGQOiWjnblRJnQ$+YE#!*LViM1jbqi&SgnyejGFaf*dMZu% zwdf>Xlhn<0yzU#1{9S0uA|=bo9P(Ysc=-Fr-`Yw#pU$;1)jyFPwCllOxR$NR=wwJw z^!c6xETk9}EVI!50|3E*fBRol$nXE|KL^zvaZQi(*=i8sLqyMnb8YipC*#S~KAI1D z#+ysm_~Us8O=+En4xwK;eLZK~ppB;xw8pAf99>Gp7sEAv;nstvJU43OWNdXEA*1&OWX=M+jOi at 4^CV}D*AUGpjdhhWTd5dXwqVgSfRLXdySF8V zOj?EK6_&TV23MRfELI~eJnH7vZH#>k|-@~Hi#qO^K^m?RY(!)w*dHp}2 zCPY=3#(5 at xsCDBk0S1h-Z(;$ zuYb|%Ul~HT=ugAnLP8Vsr?Ex>glNK{p>I%q-W-<=y+a#%bDF;|;#*QGw)8%%7hcfx zBS+BP4mPxl7c03+-Y6Kw3LjY1IV?)UygKii!Jh4IkItGdqqPIrcCTf$EsY?B?zh0? z1l>5;Fw;g2-wpcXHc6zF_-mK+zK_A(+J*;r{xdt=VFT>{0{{dUu>Kza2rhX4|1Cv% z3mw^NUhX$yiIBc<#7{(8-)oE3IlcIq)H&8Y{^j$v-Q$@XMbfdabNIMl_n3RP);*Sc znk5=iAlpE9mnWKblWE!;bxEbpXw!M8bwww}bWerpp4nrj7P2Rn<;F57bjMd7Shnn^ zCbHeaP=(68q?|(k9{>PA{~rJdGNAnj0D=o>{{evDg025YuPf+vbua##t>y;sei1w0 zqKwlLDn;v at KCvE6YI=V^y3=1P;}pf4=Fj*^Ha8{I6cq5xKAxNzZ$&nCFNB#>`ONiF nnMo*RndLp+HlgvngBIm at LI^>OcVarEpYLzsvCB`~{N(=wzx<7o diff --git a/test/archive/distcache-1.4.5-2.src.rpm b/test/archive/distcache-1.4.5-2.src.rpm new file mode 100644 index 0000000000000000000000000000000000000000..8e78a9e0c3366e6b555fadc1265fa858102f2f92 GIT binary patch literal 379067 zc$}2C1z23ck~Tb8aCaXpxVsYw?(Qi z|8MVi=IQBvtKX{XuG6RLoTKvN1t)i`Re(B%*9luxvK<&Qt&@T=AiVt7;4uAEb zj$U!VUmd8ESANeQez-6c3_v9FYgB`%)tDlk{Wv;gGXfyj&o8z(vdS=WC30gzOi^*S zMLU)PkWILdLGxP~yLoe$50%T$ttqL-KAV%5hnJJh+>FJH)!dxJl!t}YoP(PkWXfr3 z%FfEpW@^I4X~N6F%Km5dF#S-h&fhY6R8 at F-^y>M*EAQU|uoyUp^G at 72~~Pj=w!L{wx3eD<*iw zUay$u at BW~oU-!rJs-OJQzw7~;_mw~ViuwLxn1xp?@mC*C;#FVruRfgR>w2VKvCk`( z{;Llc|BCfq=hI)Y{_A|jD>nFx;Za`goBs6&kNS$uUh(VsgEoJ~_^;UeRiE&WJu$#P z^B3SB`RnDFzJHN8SsA;!n3)1qRpo%rX3ox5_IAL(EHz6r;6HVM4j>a7ki|>yWbbBW zYUT_C0o_1OR%R}qKznnbxvQOt%Zn-qZ1o~Aw|7D?vjZ7}t?Vpbcy?wk?)FYLjK(16 z7f1i<BGONj{ z{&z!60Dy at 1zP+hE80^Vp>}mxzWpXkzwFJ2^nb_O at iNJue%8LalkPA at G%FfjT at QN6K z(sm|H069q|x&JBx3Rt?hIPftu8 at pO~S%JZzf3f#ZnGitrAL|E-+qqde+1uHg*|{*k z2btO0+c^VFO$<%IRxc7JD?2NfKhW9C$<55^75>+H!sG&SVlwt(`|ol_XEPU9D^n&1 zkc){W;B`sv?(R(gCqsJQ1f}^FKWoh?Ucvi`|69l%3OL&C9~cX$;~qX6FI?|6f2SdwUna|I;4;@cchqfWNsP=`}&i zfLvYdZ9z6>fIkBhdpq-&pc}i`+k*j44z at 2t8ZmoYhnLg(a?B;Hz-9^{TQg_cm#2*z zz{bS+Qi0f+SeQ5%SpghO>`bhGV((+TD&kCIBq9#AD#AUvD#9qWDuN|oB!XPHD%{^| zB-~5>x3#1i3DYJR2}7x`3T;6d$=I6#6`fpO&c1-P{hwasKToC*@IT&8{v8%8KpYIV zdU?j=%*=oS&h`%ORxXzR$^%G%?VYSZc0f50P{saFA`p0$Fv)@bA@~orshQCKYV7~M zq?h0 at u`%-gTP&V`i^ctKvAF&%7U#dk;`p~%?Ee<)f4hZ$S*!p?;6D=$AJD=c_%~qy z!5|kiCm;<4E8Cx^i1vl%>R|dJdr6{y at BW`TR$f+iPFe&;pp~5o*wxew=}ra(raztG8q$@EqB zU+5rHQ=sS{t14!Wu2wHk%0HIC at sd^kEaA`WpQp;o;>DZY%jC;K{}=%}n>blHxcsN* zKSrHB?M#4w4%o}CUdn?R$d(W2>TCvNbhbCK at wRujGjn2aFmtkX_Ig9ZfInF9Y%t^QF at oc}xj)#o3votZn(>^~Fz%cQxL2axLJnRm7Ovsw at sZ0`GF?y z?CSjIpV3 at NTI|K8^oyz$2>h4E1o)q;E0C4tr9g18adUD1c?E?0|1{Q!{g-y|(hgtR z at k=}UzvS=#x4QXX>*N1gRGFL|%uE0P69+4Mz)J~t0Pz0P?LWQ`o}d4`1_RJYqh3D^ zpoW`Zw6aF(m?Cu at +~43WnmfQBACrIrAl_KK3BrUsR&Z!SOI*dEM9TueCqk#K5- at nI ziX>iE`=8WLx9uF)kNPSI{~j}xRo86Oztn)jfQSg_1jYlj)Q}J$mbjE_VmNmVxocLB zMrD-V-^@t;Ah@$ISXCT-F2TK1bmDrxscG~AZ@~rgwf_E|1v-{$ipfYA#n>LUfPXi; z#md#&TG=SR!+9N7V#;M?i6Qg`I8ggg9=ZNAN%TH8TU)6+?$hk75^j znu&g3z620AN2OgehiJH=XfXZcV$pR3po*wr0k%8%VeWLdDzk1 at CcDr8KdQ-y%=gw% zzC8K=8ucz8z&W{J&+ at gC%_6 at zG-^c&fof@&eF_3q8 at JukU z^LCZwr_XcGjvMlhTU2`#>>>g}&6z`E3p>s#T>S^J)h6oSPZ~%~>tD88yU!c%``<#N zpbWn;LRqc7Z3=@Tky#|$i-x+H at OW;^=3*ft(!_0Ur5w|g)Ey0w+-FRIR$|U6`_=HJ2;GRqT_SDDOPpD>*pNC39SLAdmKMt}!wG*7@@wV;1A?9&!8;HJO6qwxU(mFAHY z>zR`U7T|vZkb)Em2m@$n1hhtlr7WGCerscc75WgiWgGs0yy*Yqd;T$r!;U{q=hrqz zcib66A*1Id4_wWeXCr||sNr8{Od(I^+$dMVM-Nod!H>)g!TgVrC;m2b9YeIwhT1S8 zkcWB#5IJKDq(lhK$P$vJoXoM<&=4>z5M6SBS$*q=7C025LqkE7#t%K!yf5}unX3Fp zL>2|ni2UE3c|V*G-BCe?J*i%1=X8)deDt4w)_bN;crM~@QhFFLITX%(%HQX*dUj0y zgxHh0VUO#|{Z#*B;B3b$KrR$6{D=|ru?0WigqhLLo$m2zcKw>W^}(-o*8EhoYn$TRDi at dTzQShr45*Ok451 at ULj!eO7rE*2G~&10db4 zlri`tJ>zo1ob3!hJIUXEx_iFMJSRl$spiYC_V3phaMyfpVmpP6^w)j!gq7Km7AQ>q zj4JS4oqvZ5eq0dfB-Va at dawZ9^saT#&H2LyKhq*TOX5GEGCfx!JxT67zYTsqds=ya z`dp3l#7uN&Z*QNo_$rz4= zr5ad0W;^)RyR*VmpX^w}O-hPKhNarSz1IHouilIiVxt4dn>OUtwL=tVXc6#Yeuw7+ z>8&m7I{-s|lk`4(*w>Y}^vozIsHmu`!o+>+kG((v(MTw(TnMwzw>aOB{UjK*WBLio?%qT;RWB`gIQWBuJsxaQ9=Y>H z))oUgD0%GcNeOjV)Shc&=!v!u?YjN0V;q`7zuPyl;)|{RY?2=E{Rrb8uxJhS;Fkw^ zTnO^lirQ0v<>E?eMGT06>FV>vEopkc>6rEt&B9(W{2I!lxPz1~u-n<avBi_I zX2C;+kDQ(JP<}f*`s%^&?9f9W%6!l)4h(VMz9hsp8YYPNv8A at hmYV0)xR`@VQifQP zrp8)t9txwzpG|bc`dPNT$8K|?OZF5o*&EZFy1az+lDNcbXw#OwAbmh>pjfTR=Qj?27fM7=J1DoUU0Y0Mxkok!;O%O{7S=V9z`P2c)#_PYJ= zx0BfGF`-ImUUXV)WO+Wj1IWH_PJyZ^`kb&X{0T}TWfZHprI&=*c4vK_yn6O}v zYH;EpG8L}lgV{b|Y!h}F9yoqJZrLD+_t(6kllEuiT2u}Zlb{sCYIwt+BWW7PN8913 zC8lc~j1J(e-ft&_ at 1n%(vI#M-w!kvqrHxuchS5#ZiX^D!LzPM(K66~TK* z at j-GYjuTmB5IGCn-V)gXiR^y`HeAkWU$p^y4|KsO^qhT38A-d@&(OJ3pJ;R2=odIP zhXw||b88^gUh2tTm_7b27o|!?5UQP--|_omGQ1N>Fp_#RSmT7^u)(F7vQm|*Zuq#CS}&}%mBc(1NB^G^a?Qldpj4fUDLQidwVo)vOK*>0tXISvHAe(L!TEp1ss z3ZX?B*{o5$%f%qNA+XZ&>XAY6sD?{saE4N-lDwC-6|Lk at mjAbvqa`?UvZD20S~dO= z>qH^NGFs7~ftfu7Czyxdef)Mx`=h#0#!PfQe;r8xXZe#YMoB}}wMR02 z*I|9ObYy-Q4qbcW0+fVb$WV at 0-vO=i*@&L4*va>=MyjaYZ)BAXKa->H*({yft$G%p z;ZhsI#|}iCa4iV5CXCh~OGy)DsEkD&tdPf{Xzw54;C at A6IJSLHxLq|$DrGKY&yj0o z!=8aiSElA4aZ6bugK0JxyXBOS+vl(N0$o*0 z+$t-iFh_>FKY7%Pl$V1w3Y8Sw#+-uW{CpYQ?#>1n-u;d|NlUCQ8iB2M;4HWTPfelP zG8+gsdM&+`9BFXQ#Z)GcZ2*UjaU6J#Y#)mVm}uLNE6I)srw~k&yOFnl6

s{P|U= z+x%i{+`pl-=qc);*JwMc3_o)I7NvS}ch1V@=jK!!DQuQv)^~a&QvT}ck^6fg|FeDd zy}54M(w68VV!n4!*-#2liA7|JvSCvE7^UDYcMr$gB+hY%X*ZgW09>gac#tz~qgyT6 zu1ZFY*LmAfQOFLZ at 5}p=!QOOK-NO;i^NHQ)NI54j9coUjaP%r|Wz#O{M>A z{T$1EN;0=7M+*!G2TM!=0vMQ^*OlN&RXTp*$WPt9nc53EFIrO<9-mzK;mt#cz-qmM zeo>NCvm=v|`B5$NEa)3CmzR*!g|cnE;My4D4G~=Gjs!xpE*p zOQ#KHIeEWqeMZIW2GI!AIaVO_Z22$Ar_d!Yab{+EFG at dklbKP|&vwR(2w%EPUfbi^ zj;V`~@h6lD-}d0A)9TsO&Tv+x)vyX at F{u_b-AZ?C)UP5D6y`I65jFQAdM_TTM8$*@ zn at jp1ZyYAxUzwm;(LG++DPP|Tl~r_oBI|yspv_N zq-j5oz%qSqlG=h_a~IX{1t~gIiH-29u4IW3${T zN2n^60H-!fjK8*W>$Km5dCbM*d?|wXDyTuUz|rNSPle{nb!C1Qr5&(t*^WGwpzSkW z_T}Ra1+VX?z$+^2!dnSW^PZE=}!u8UhX5W8GcSl?>(_M%+DWgAZ~c`Q*IZ%6!rv6Zh-+ zf3gQAM^3;#25AHrqlR(cyslHG#EGwI=tCQoC-h(D2E+z!w zom at kNliJM#FGp%t$I`p)c~^bF=Z99Uyjeax?PUH_K0CbW+3A?`rQqm?T7CU9Cgf z>cR4Mjq9q`Wr at efqvONSVpe*KB#vQF+ka~CUuT(k>-f7jYJ}1Iqp8hYkOf|#RBn6? z%#_+ at dWt~>nzxOOk`*Ol>;_M2Upbu0r@^7u;_o$doCl{I{XY-cBdwytj+q zYMY%TafZCxR6mu)K=2b?4u>yfrlFU-Tf$SXHT at n`T3UKk+0XeoA%sG#?^3wyoIO&~ z&@|o>!JMxaMP-2!h>NvJfnREjj!{6C4&DH+p20%W9;~aNjy$yn7Ef>A~D zkgfU1PcA%@QuNDxp&4woaO~Q0;TYR1$^A9&l{VtFINxHgNwFEP?f~uJ+rIT~Tprd8 z(c9phC{wvGj2n`u9vVM|DW$g_d+`b&dj^iot!6dn0j;k!MEm_H43i z{L}kT_y-~(O2wbBX3(nh2|X)nYMnHBA8IQ^jjyl;H(Nxxj0>9r<#xTu+^j|NC8+Nq z$co{(RT#Ztm?XEIe~Yh)QcER_Gljmx5P+;sGMsLLKC;E`mWH&}hkpElCQfi6{ll9K ztJ`_Ku&~fHE_7j^75(@5VcjpOF$Ok$zBQ7r{L+c3ySUbzZ!nZ02IaJx8q)6o8kC6GxwP*=uYvNVPoK9;B&AW_3vTE%cJu(8!<8q^T zu>Ea6E;k-{ekz-$SF}sVlUcNeEXyDwl-g$kMpMe6_66>qVr<~C36T!hG*X*vZJvLN z#b)!$ro4wl&YsVbTzUx7k~#@|dj_~=5AHFy3$oQ}0HUYzubx39Nd)nf$g!+{80!)V ztfN%MwhgTODis>qK3-Wn#WXsHhO=5*bFhCg zE=yr7hjJj{C^Tpp?+D)tfC<4KbM-NDKn+FI>qn7*%||brq`fjVqSQ!qH_B#;>tpPs z>OM+Ga at vdwKrq!1riPT*q^NpYMgoSYjLFxdkU{pTkUy z at G({UKG%GXnFNMNi^m>UFUp2{42xt^rhq#!FwNEYxY;H7cYQDEWELs=vY9a4k at ZM1 zwYAAlW6bO3vDha{t5iP{b__ZfQ^sZNCu>@%T7QgL=bX8kv3JXf1h(th&TD-ZPtrRV z1V63WlOlUq`y|Xb`&{Soy&?^y45w)?ro)DcUTHexhC zuM{OEk>ondczI-7`q$JXihM`a=0Umkd4I(#dL8en#g`z(%qA+NtqJi|kFj$7=}Zv` znQ$wfMd%cf^s^p4`%e%9Ilp_1!eL%@MY*^lTiVKKqO1W4Z|YCRfz+g{6`>$Wm9T0{ zd!M&96tuKPIa(1bBIRv&cPJjPR}6?bhC|uXasC7!kYmL69MK)(_>s^izV=#q&$d&k zW$JGN+GZS~gQHfp)8xq!$w(GSFgJdpB_zP_p=;dF>pQWU1Dwh*NoX6+&au;yJewOBgNX1$e1QUsDZ51Hg}n6Nr>?%?;6 zZ7f(^bN#|iJeC~ab$dQt+>UF9NV7Ze_uc|;h!=BjXfY&3I4SY>gL!7H?7B*oa5~K0 z;wyuj at ka$y71K@)NY=6SB78QikZYc2dO^}cxd%s)%@`&nWBMz3g(RoXiS%nb zj0%iw_qTt<3b}mGP>6CfXLxrA$HeO<4E?qe*t`y9Hj3An_yO0qrjQJ;oss8^ZiBKu z=yEktGgXs;w^=$EDjQ2JD#Rp%vhKU7%Gt*ZTNJl#jkt2Z_c_wFNf$>^lR%Q@{2lpm z^O3tv`Fo$vd0egI{TiMOp*8qZqH11xf at J;931{4dIzG2_L}0$Z(%=G}rOfH_pqH41 zvmDwx?g?H?#$P}5j+cs12Qun0M}1e5lh0xWvvX6nO)_m)kv7 zAX9JY$kXB-n`YSz<6%y=*Rd*$M=FeY?8bvC_&x`m6pgMd=S9MhGa`h0Pe z_ol9sInhtlak4ujcTvkWZEuY69X z&pur(v{=q4LahZoQO^4bZyOgGTv^1Sr}8b+><#1=3yEQ`Q#$`}`Rq{kDM%D+qO`Fw z(2EcCTL*<{|0bV-`f_38Hw;p{AEhBda+VwpDdX`Oy)Ip*bG?vOZtz48Z#jElA`1w7j;<^1^Uvo(X6AQe*;f%3hY|)+-J+b$`R}D6AouXhiF#FCd&$Wiv4F6`c zZs)`pXmqVO3iH`$=R{-S-X#nVg|?ys|Fq|)fmXebmq5T>@g^ZUF82|}wSl>hSr#gL zn$2y~ePhO`(ii{3PRHgM3PY8gU)gIoE*@d3ErIEJ at NpfA@XKH*Xf=mw!%T2~xp+j1Go0{GJAYI>bmz}Ovr`8WwITw~6 z?RUNdAG83zIq?k>)8mr(cnoDCTN%C_r~P(^v088oMBl*SKTFAAyX-sN+bsAyb|vHv z78A?!9j0mHVbr1Yj%HpfB|F|s`&{e|*ynP3%KSrFUCCQJIMRmh at yF)!I5CrrUHp?z z6I{Rjh>z=PCaPjnydLBP-~|zGnc3SprO8Z}F?Hj%4Y*YV3l4p9yq;X_f|rt*X4gC< z`494+nh4fTzq(;Phj`p=+PP>)UZVJAadVFEqzf|d-03yvLMlT=v*pkW9P4C0_ACY) zf}lbI*OY?z3ZwFs><67)!aXi(#=i%C==0=H8kV}72~bogllmTQB+OH?(gSM#@76H(I=Bg zMmuY2Az1RsZttM8KaO?A{tRP{qOg+u-^DDHw?36AJ6JNtsU#gF<|+J;{=GEq~B9{#L|X@ zg9pm z$&)mRD4RQEUB03{gv!l&>4nfp}+h?I6ErlZs%JEn(6 z`FQV{wDcohyF_)H{>){ueV<^1EWFCpIr)d*0=l-7=*~(34ic&=pha!B1$Vl8 z!a!%h7=9~(Sm#3}AvtBu_1ys5ZHegsvydxf9b=t8B_?QNQb%UQK? ztb(OE!sNi!^#>r=e57XHXEx74P3`3B!X*+}IRjtj<{yz0&S+e&CP*9Tp*XnqGUD+c zh{;V=Q8rP24 at Ke4Eh8t4HB(*l#NGCttr4Fxv`<5MTzN}>;bsS*VQq9x7^+R8ZY-nL zs8nfJp(eMV6#Meh(LdGspVCnvYQd$|qk4zUolZJEhzmoXY~STgB*zwE zF|d}qyr{YRj!L at U2!PP~j_SSedtIGq at 6vtRW^wQB^YIYA4%e$r&73j;>;4SN8#0bl1 zmRFG$!onL$<4Mt9zQ*{isw`^eu76O`>@Y?5hVKpHLVZ`-!wnO-;ax$w){xglDl~`V zCT_FVdBx+fpc<~q8OMA4jdCr#ltsuGyNmDN3qS0M7QQRU-qN5G9Q zqEZlJBE$iG5|jifpi&5&?x at sajuKgm`$ImQo at g4;%lNQM!$g}U72aYQD(>4^lUL+P zG=gkWWlZXxe%h}Gj!vg;;O%AnR;fY_ErT%l%C*Gx%R}`9fj1*vu3vBI6a|hX?I6Ca z0)=gu1|{*mlq_BVHK&4Gmiij{hvKOs?^vORLdF<3ep%&uXEPZeRbA?fRE)3rQ0s0{ zJ>z<-cXe_PY-5M1W!OpYISZ4^uYH{^!R)lWViJ|A%H~8>R^~p40#z8xi)g~OoMSy9 zM(`QLkfS!Y?vy{E^P#;U6fI;61WyrnqmU+r$S?U&(cAeGiH;4AK zh9mElXb*TOzKv;~`_T}dH`sXTcsXQ+=DOX+S|oAZu|qCVs^=xL;TUn~z(K+GUX>9^a^R at i zrumr5(NYk|+Wms`D~x97>0IR}N;^3R)d%5WG|2eA)YquIWha(QJ#2ws$%p+yEQ4zj z2Ji2 at KVo`964SH!KGlPQ#=EZiVE0GuY%O>}#zwzq_D0=`uzLOgFSsbs`S;-aQ!1`( z0Zs+ZFOS$xXMBp!V>UWmgMP%3XqP-+Ka{1gO_jCeT?~5FBW)pxHec`0d}#6UlX*^0 zYc^HvZZy_3d;hN5u7y8y(BxMe3npY$0kefua-z5|#}MAvD;lWwWQ=41iajm`pS*~S zNSTvU1o32SoF__eZG>L$JgnB4gVGIU>i~njx%S;5q-iFei0VPnBgGo#A)>CEfhqo+ zJ9mD^%CQy;b~)M)D~oI-#TH)#`aaH+EYk_&06vAi5jbh>QN`u*I`5JOk`FML&`I;M zvCLwFVK&e8=$U6qhG}IsqZ3(F`KY^8{UB(teBXWgb=W$#<{_hFP7N5X6Y)UBxik1Q zy>t>~zu*r8os>{ekM}W_B$l$Z&A+e&WJRBD4JF+TN5|j)R=TjTAs|y|kzh#7V^&s| zSjd;HfHUaCazXG3FZgHl0%f6vz}tC$xf-v!ZlWaNcJ3f{gP=R7(J0XH-G-yvm54fz znDMkHdokQeFlW0{NW9Vkb_D}?9Lt9fm;;iDj>XgZP`j5Di~3H^?8(Y4&bH`7mdZ$% z5~et09QLOsEz>h~EDfhOZ|KcD#R=6BB at Hm|C*K)Qy(Pp{fy)YHPkiz%M;k(`RFteeOb;B*=w%mOSlRwS44-l+ ztsu0e at q61T43%X2?I-RBc at 8^2ECE)}YvZSeJTSXvH{zmCkvLS96*qyC?=+ at z%-6o^ zvJd9y?@{9Y74DH>O=T%lbn|iE#%ib4e)h~g%ZbIYx#!@z at By^~uEduWM900ng09iw5Mqf$m9a9>dS0J%W5>-z50w~EtcAF) z6ztfDsuM9gByfTXqBfoJsDc*4D)T=;j5X`TWk473CxR*$+Qv<4l^mc?-3?Y3#%M)i ztIRg&@0=aeFSbWVu8ofqj1B%hSkPM~)tRlYA1$BKzgcvWbYc_60TO>7>ybi*Ax~oA z|6xnoziqUIs1`%!D$7Z2iUo-{#L34>3RP(Un_)t{PGhdoVoooO9-HJrgN*rkQ6*tA zSvj0QnBwQX3ZaS3$DQ*+Sn3^4qx&1(ul=#pFL{smO-Z3;A1I4{sx4wddQGI*r?q{I zOlnF}3D;}JSu|jXE}M9tTL}J(HlcN~IJQ$PP$RligjX8AxfRvd+1K&6r-pka at 4i=A z;s~@FO_%F at NnbcYzgvu|0GDYcS3z)EAOB|%d^QO`U63{Kv|c49A@*wv_Ll{p5Fw~r zT`_!i>azAz#D~ocbI<@gsv>JwO7cXSbmewXKj$2}ldSBmksZ1ZlnsfD6Ert%s7d1u zJq)tlz+w23s22mk#~n$PLEJoM^n8w8=l2|ux%sc?MT-w=BAsM4CrK#;qc*(vAoUPtm%r`hdYa}+$EMQ2U9W54+JQmHq7M0f+ z(b#I0pm2G378R3yHY3lG0NSb?h`<91=nGC7lh$oUt*M8W(Hr+geXG9|?c9_rtY0%P3E{tchGzkac(IRQmCPJXG&dZcU4wH&eQuEHWALzn9vrPMY} zph3^W!@x9ySL6dQB{>Ng#9nl0T2ux`9e*uGa3PhltxeoKi=s4EpxATLeYb%WbKOMz zZC?ET=ZEi-SqYOrma7hGC6+%IP^2YnZaV?zX^?b~;@Cd6)R;($14!i0Ih$K60VSRfo$(T zxw$sawiy5TEqHyR>1zMuysI+LXN9uED!+#x6abzT?XTLg4>g8 at D#Eb)DRs1W#r8H) zW5&|QWO?nL$pWNB{xL;{a;o*Bh6}YW-5ykQm8yHoH at 1S*Hm!}mGM|K3_(Ts0W3NV2 zj1>bJP!p9^jEA^{XXr~AMI#i1m?AaWRhlS|@OwEyyKUL9|?#lV`uEOlc^*-!L%6`MtU7jz9hh^Tza26r4 zrPJnZ5jD`>Xh>Zd537QKt at M;aEXn1ZHfam*+1FdsU*kw;0&5z9d0WzPd6a=)5pJp{ zTo+ng{ym#evLXRK?om;`3ep9FmakHXSu<8RJf**ilbOmH7REsXkBmwW4uZX6BvY>R zSgy}ut~s{p2Ye{lV%en9H?*bk)!W?uIOq){*Q(O_>M`Ftp~a6PuL{atP;R$`6!^WY zO-noKfx0Z{=CKmKUXMQPnj?x%KNmU1l$E;l18QnUz<1~JKj0rf+{AD=_+aSnym#7d zcK*oPaRwYgB*%a!HEN2kK#&$SuZ+#BOc%x`m6D)t>Q>xGhO}?w5{_ntJz8!3P(1;fS4dSt3~Q)}YBdeI3Wk z-G0yNwWA#uym+*I5#Uj&{M2rOcQI8ve6zsVmrlT>Dbf`PfywSlwQ;f4eH19CrU_Qg z at Wrl#g|fb5M?AS(5*H!45Jk$@r-tF{dU&ry9gC6mge4sh%E?cAi^sVnT2{89XQ1JS z=^UIFOs2oj0QA?G*C(1N5HaAgltOU8VOZFFr$kNL`i`ZmlP0j%W$vqJZimof1YQW& z^o at I{& zW->t at x`sCwBD9RMvr5g(ONXgfDBI`ZSxSH~TGMnG at qn2vfKLnDfU#d^ zahVuuwNthwc)EDg8>nBc;|oK(-qL7#Zt#hqUV|L2b*5W~)ZQ1_AAl*1(pJ(ceMNZz zF)KvQ0-=aje>pD4#(cmq>elamck#ycsHFQ`^p+{%yr_yJQifz^ikl$4Ii$n47n%8E z6nbfs-%>%yb+lN(rhZ~xZi&R<#N$VTx)Q`p8PDmBx)df0RJ at A&moLa&;YXk1Q2}Qt zL1jBN4kO`<{250L6A4R`aV0&aPX~;Ly}0rVHfnkn3v@(pXZH_dwgPHb&Poi4^`)yH zhS5C?#Omn2JX*{CesaIuU1D$kf+aJ^Mx6qnS9|1Ta}fEcgd7Qkg%j!bUPad z>z%m7VXyD_ZXbPQzs)YaNb<#{0Tg62S2*hLeqxBZ%QNl!_UAjuGiW`Fh1dP zF`OErcSF`mrv1^BTPNts-0Ac)Yv4tC(E249j!v)$j5p4Q at twD`)I$R?M|PY|teGPf zt0qR9lz}x at Hh`gs_`(*e4UTx=&D=bWsBLk2u~+`8)Z`sQ+qa9H(8zqxdfjITv4^gd zwP{UN%-e@}vK%2<;)e^w`(OIv6IEtm?GbXMTWfLGPTG=9%s<&ak|yu at c@ru+EA+FR z(F|&-a}-u~k=40ghR`B3wk5mb7~RQiEytYw;Pe!iE};cANMR7bk8C16{yrhvR%Vu? zL1vb6x71DS at QYZH^KSAW_72GK>b2$ptJ!-o>DXId&5u0+p0I`%%3Y^QFE5)$}-`-$q2SgqHj1nZp=et_w~ zKR?k>H?LV-WFOW4xD?d1JfyC^FI~@3hgci1HKGdS%)5iM1 zOvUFf;>v(Uy2slnIwjmQenIJ$5dsKf;G(}dmDTp|2isZt(CtJ!&HAV}es}C|u z=6tTVw*umzc+ zbkY`9zGb6nWmI~}sKKBU{_bpwSAM_0f=%A}i9{t|iYksS9B3wTTWW|OTlpz}BeOCc zo!T#>Y3ozWH^oPx&(@;dsknI?n$fL|7Y3ltP)Nco6m&Ubd=<38N1UYKc}E%r#vDiC zx0IX7*YEtH;qrL!9>EQ9xxzlezC3GkoIDQe5WGx`+Cy#OGc;_Dk-mv-T0WSg)0nAm zQyl6gry&#?xGB^gsi8;BK8An4*{uGK^IrX1~YlpCRSD#eQXxYpF&Np z>A@5E^V&6JXMZU*mLtw_s;;g5~PG`r6YL=CeR3 z%8jzJfkwaqjJvwoWNp95-2rB036Li^>BG<65ml&!_TuSN(SJ) zUkj=1jMC>KaCN1wyE16GTBe26U>yF%!#ksuS5`5RBwlP}Pjk*x7WK&`7 zUL2S?zFjT!Y}w2O->erhLLzupVSvBl>2#GBbJ^|N-C#m!*9xSRU1E%6f~>uu at ZX9A zj(3ih*2&)`lPc7`G1=-~Dg?5oObRsUJU+Ol@^SUdYhgB+#ILD at Db37bvJ)^n at UuC!?tpo+i2Dslv{++$T12SIeF~jWee< z&hdBjm`~{`0TA48&SkHXDEVX4bPuH;AIXNpJk55Z!eF`|4kFsj_6J+AYthZ==jJ?n3x}5n{Y?g77 at g>u3 zszUJ at qTn0!YIEf4HX$xYxqLi2B(&O8nCM*oz^HSv%>!21p+cQ>_b_z$s_ZOsQ&iEe zuc8=Y39{dJ`z%-YNl)+Q>PHJs*22q at qCw?j at kJNK_E`@+okmf(q74#YFc>oZyLtkA z!fJ3mX}W>H at s&u5f;Z(q!!vBR z?|)j{ri2X#JJR4q52Me$E4_`6yG_{oI&|ISY5?aFZmWgN=M-p zAQ&OXCTA=+gxfe*Htn;{9G3Te;PYpz{3WNg#3kO^3t3LKt{E{{QZD#%=|ViSG$lyP zM};31m at 5o?ZD{Q}rOe|wT6lyf3PeNK?npt%h+G>eCJ~>@+&D;8LuGB;DGS at ek5Y6c zGF_?GDeBCK^W_xaVCeSS!{AxGFISr2<+-xO^DfEzz=a%6-FJc)T`6+YkQ|J2CnGt; zA#xLS)> zt*QOhY);h+N0d;C?SK`Phe-Rzq{8+TeCBXYS~x+ at E^+zGa^BdO2=DWz7_A!!uPoda(2-RqPaW at ws=DwNW7=!FxHklwNYcMl~*-+ z<9A;%e+O#a(xi>BxLgb{$}xO{?TtoixjJ4mI@{&glpOi^_Jh|X0&LWWzD?2XHcF?K zM^dG=p`Gmu#Hawd>E*g{2uaG*IChhp$o{;n at 1Lv6zmYc>6O?g)vakY&jH#z99LR?4HvPvkY3Rx?}5ga)!y8<1MVUW!@%Tq+~QV5?}hqj-6(1S8F^$9SD> zhhF*B$h2m=iSF6sPKd8FV%FKruRbqt#+E4vQ2|ElscnV=^}{^Yudsft>u{zcG%CCe z;WgCF&M<{xm8!GS7II7PWoa(w`df~ncjx8x&Y6k zk12$7Im&)0bLVrOsVl(mv(^&x)+S>Zt>Vr>Y9pq at P(C5>R~S$ENW^ehH9^D`l}{I@ zFzwv|bsKEuiVl)sJfQ~7+oc6~+Cc4(F2-_X7=9{3AzS4z~^o90~-wXFE#M>x1 zEJunM=t=jv541l1eoFDAq}OrR#Lq6ZHl3!$L{hh?0xOVMjR-Z}kt`npQ zlj^pl0VL~*!1%W4Y1-E2XJA;S!pt*0GQi*sD?!0=8`-k(6(_s857Uq6EKXmDnjN|< zOCzET#ukeATLh~F4K|Uym(&kdDNfZ0Mnq)H)NqNrPXpM)-|)h|z8zA^eprX at ED9Nv z-#TVnSXKLh!@y3yuD|%*uKWDF+vjJ+0Cxds?AAY_QvwX}6RTqn6HgOPE- zroiCsn%wXHE0}i(UCj3Fh^-h+IX at kbeD*l+Dh!f*qluB_pg&T2XR~CQPgo z|6)YkA_8`*u2s!i#a%T|X}yf;rI=d)MXD|%ReS@}lQNf!8n8e`fq6`{Dr(uaaRXi? zos)7W&zmEiMPETpurDuEn{Z&c3F0>C6P_RU^qa9=b#JvFX?S0kw1R8%Ec at IY%UoM$ z$x^L*Et4Ol&`_w%W2Gp52JI?kbg at HOC$- at lr_(VDgvi0*%Or}7QeQp at UPo7_R{Zvd~sd*9V*nQ zh%eGjT3I4sd+asNd35OA*K5xIfXU^+?s}5_O>1mZS)CSP=FN3&Q90Y#`+63lbE?|7 z^RT6ei;!CtayZ<>(6)$)VTp>G&7DQo2+3~rHM*-Rzi;Njb`W_ZZH at U57v6c;$AlRx zID_jeLX+&~6R9Cqp{&}4HF(@4yk0JzLWzm0=8w_HS8EZau1=}{2N^)-zo#*tNB|}q z8*oWO`NvBd(mvBK9x$-F>a1F<#B6%jOV41d?4O#cQ3zjJYQcT at ed-%?ZQsI|)y+}+ zo)uLS(@g=|M-8ye>@r(pWh1(+?@yZt*oeg{TB=KO4p7qDQdrL%Cs-xOtl~hj3S&HS zDtyu;Co!x{8c-{rQCwC05+{$x at 4Z&u?>^ivU$R#Ohu`IoUS|}{ngL+EyCf--+y6Q9ogIaL(_vl_21SjH_0XqmLY=QYaqXiW z?>V!3x%LvT#>|ao-YRW=)@Noti$%wY*ShhIUC9~{*hO5wYTWQ55Hm<+!PO`30QF;V zj+ at j2!dk=0SR#?sPUTn4KFFs)LfvNdFn|?h_d}b4`a?nn!34>k-TuR8BVe> zX({83Y)-YraFq6?4CZWdR#x~SNMUju*FHrh^p~0vBH_;C_ZKPWnkI|`run8%MOcWv z#B-e0>{mFR4X_m0$0lc7s8|GJGc=-~4ax7TiDL<2(KqEdCe|)d7QgXN9Izr2t)X_I z3?@u%eP&%ysK3WB8eC1lyp=Aw^+fmL^Qe5Yeu=U==C$^<^i_Mv)Ry!We|qiD2|rm@ z>>~E at e93bef_dblKvCq%*27eSAx$SF=?c at E1>2EHOauVVoM9xG5N_WYCeO at xL0)K?{-HjCC0^7nt=s3* zQ-+^R*y8DK6(Tca3H9f6AIlaOQM)yWMLSBDbx)DNL4ysJOR)+QNsqk|A{R8%lu at uwOOkNlQlPD9Bs?Z00({Q at 1kngd99Amo#0M(iSd;W3`;O|KN;~`9G zM{f$JLiwvf?3P#>VArlp0wNYdT}iD?N&MlDBHGoxoUXaLQ&XkkOP*YpW^e>W^D6jI zTmMP&fHgsOtr;*EtF$wapmO4=C4Br0qtluYJcw%}7=~!QR<^h<8#=w8dRT4vD5)Uc zVc=2lx?kV3HZ{w-S6NAQ72^uA!tQ>tF}`@#sz_-)q5`-ILvK~^^TnEBB7n=zE_iBb zQgUVa)c^(n4E9#xRwAK?%TV=G##aVBXU;71^{iZ+I$|%OtoSZ6Yg#-)SF+3zhw^Rl z>|^;FD+!hcX3F#%!<__;vUBIY73VdAZp9FJeTj?JD-zYB&i-0XBD(TOS?S#SxUv1X zqIh8&Dtmr?eVBkPS+(yzwFB?&NocK)eS3V?!jO29gTrFP32Vt(*Syal-(=_0>=S at I z{HRB~bYNv8OB#ILSn?p7)036xnCt3AZSYUdADB16Uk at 0lDyOY$hu4jA9ZMXt$^$J? ziD)MonTkX;x2*lwL92`?fyDqZK=xr;2UDiS++M8#Q;w{`+?%*AvO_Crx6*=r*S_^C zPh;Tg_!Ge{6-jOV;n9ZF@?$^A8PH2>S6#^4Z+&G)np%=!qF&PTnPQ%;T*}IH!|1l@ z<^BqailVq6eejr!wCT__K4}{+mOVj4dB=vb+hJ}J>pgaSJtSK!N(1$TZs5Ht2$zl% zNPyalHyiSRJVzdotK4u3hM8WXZOpzvi)qIc(X z=V at Du?Vh-JaHeMZxmKT2fjK_`Bhlxba at 2H-ef{BeIkL5im=ZKUtCzwibSstdBKWL@ zhT%vqi~*1X5GGcv82Clhs*GEpJuV}4+Oxp8R-z6gN~$o!9nFVF0@`{_t{(o=W3bV9 zk!7##cP1pMI2~;dEp%XX_`PlhqJC528mpKzB$Px(i9Z6>>y{;_mj!9peXNl(aO>9j z?k5h?R)7soG=*{K-=|k&%92Cq+o`-_kS`1U^$qCxz7BLTdv_Tlb5JOtex9~alG9{y zJ9r)zcta8n`#Sl^tKFOx+}v?*;eekODV>r3O+{X%rLReDQLl_uoTY*#U5T%1EN0Py z_uvCB$QPFEgHdHyDxZ$N6n4ji5rn(`Qd5AVhV*HH5!Q?9 at e@r4=}vo%IV83s*m)dP#tu z4(*6Vp4kgb>`9w6((R?pPbe&lS)7|>xg1D_cCKos3$dc1$1FCEC$_X&6LZN-f(%CE zmH<&|8+i;h^Ix}FM`<`>lT*T^wBBH^i<6_=U{_yIKJleZXa at w7&l{&&Hk0m)_IG7( zps05?Yp;kn9+T-LdQW?mmQTCZ#o&@Ep}tEti27j&d(dPe%LIvI9{C>t96;m0UPviN zgk$_Wi#aR!|2#rq?KKCdK at _KND$Mz=b=nh-gk$i8Jjaak|0*Zo!^nQ&x6W;5xt`4c z8iZw{T8dwL^jD7wP2q0RNo3Ptywc=RLPJDup29gwKjkGlb{|{Wn*w`RI5Y7%57Tei zi6{DECnwV7_Fr2uWowY(JfwxNOriZonL#FG8Z7&sclepkXhJR5z)YY_Bcx0IkD`)4 z-Tw>yc8)VzLx~CExz5myaL-u$TqwUD0*?k&-l?{BJo%{`V`YWINA<(Y3O)&>ovCj>nYN z)_WK^gVHb5`3a<6VsV}`kmU3n$CZSf;OJhGppIVduqvy0D?t at 401Ue7 zyn(<`N-!8UOBlI9v_R?%E|X~Hk%cgSR&yAEksxxLO_nD9US7X`;o_$WX4LzYqqnx| z=4uYi?uSACR(i(gI|a}L#QI0_hvC>pZ6uG{F}9v$k|Ds+V3VovK6zODaE`yMt#>tvYyp2Y7sM=G5yxk_h!9c+SE;OH@{e8jXuB5XxHp5 z&i(N`FHZE)(AO5y{@J@@?PMF-;=jBi zUJ5d$sVx%HUgTsq^6>WTj($0t6^NYuZ(C#SJ5B-PxWRG;kZ4}X?93eHn0*K3BlAAv zZQ^tF3`EN#r=CseJ_%pyDjZoBUee=%2XpIghphTL8pD}9$T?La=yx6DcY=f%KzeLV zzJvE47%^bY?i~ERRvrW-llQB<7;<*5*CO z0knmtA+Y5YXE_sjVXH_N97dwz!!aeCHkcg46qg{l-WzHv6Nwa+kS`&y#>2GW6ocS2 zKe;fLV_eDtQDTV$4ib61hGWG#JtGI$ls**|Fu3cPG7E7q4Rs z<@sAkY}sAMEgj48w~(7eiiSJZHt6{oJ|Wzb+N7l8fZ~|A7}--caJ*tCAn(;MC(qkb>`w|7{Tz_de2bcr(0QHB?0XMfril%<=k_S9a z7J<$ViH2iD5`@FZ9Es#+Vhtt+7SoHztY2t;8-9UvpAWY+Lq!y{ub#UynKF<`43aHj zUNOvGaS%^9rZ_-8?~ikM+vQunZbs{fG|%E6>iV6$Mt)S^Im~`=JJ=|umu$u$#=}b# zgPNT<$25{cf%|3NS{(fbvjBcCc~Il87z3^wNy>yCe1#>=SiIZvo_-QV6d6L}f}~mj zQ6HQby~p{Mrg;3>6yb3wHdNp at K;W3qf~A{dJ(!M5a!8Pw5&Niko at cPmqqnG-@u3Z! zfUgDe|B1xH(SeMG at sk-vxiVen= zG6wN-Iv?GTbo~(~C*cbZCS!SjSj!QHIM8ShC-fe3_+OA|SmGqv_*O)dvYsD%BN<^? z4IRvh_d<{0pW=U0Qb&Wqa!2pE$4LqY5n{P$4%b%)Y{uObnsppRczrug7 at g(0QaCW@ zNqK=FkhuV}u*Ae~rn@|Z=gI-Bfl=TwHik-#KH!7^GAjv<t5Yi3lBQ0GQP!e~<; zHvzP$q{wH^%}v?gMwmt^MM1`53iYuzg?n09tr)2LNfYN2PD+-Zm6N(zC#Bw)GYWO! zU%vF12n-MhA?9C{wgzfRA2G0jQ5RXvgFy!QK!%YOn0$Ryj`tW9m5c&!7~=}2^$2Gm z<m2yP0&#*6f~Bb>cwuJ4=>Z7J zR)WpA0Ai;mki};Yl;yQiK|Be{TyXszL`Sg5KvGwJ&6l!++Y8x&x4nIF-!vLiv}Qdu zl~FcnxZx4fr6MBl>hbHQRb1Ct(uSpkoAsdZp&Ay4_gsD_;(vwB-JT!N`&@pb{ufN} zY|rsKJm1pp?D8vxyj#{QI)J+q0*d2KFW8wEFsIqXV?b9Wx%J|O>pw~spV~eX!X6X2 z9`%^1fUJj?djtG`&{O-mTZNl0S(RT3tc$Rlm#jeNUGXGEA>ka0C# zCSw at gW#gBGOGhG7Kw<`xvI5;M(h(2~HF;HLZPduPrgyghq|0%pf)QrX8q3m4{1`zh zWw^C74T1**AsD#~+=zqypWp(o at Vi`=IUeQCU=9q(^<(1H>|{T{pFtZ~b}ca at vSRQ8 z!}>k0&d;HFCV5NJnS9^U61Z04Bf z&9}+BZy#gJOVOKhkS3?DODBqIBqO=miPM_DW|;EB#L{xn zr#GoPhQWRGDG$#DnV|WKiUhAZ#|%aiC}Ln^BAlF*Xs<*a|Ia;bn?UrWlF1~_ZcQbYA z+N?%L`ITB#Wr5*R7p>eJ@*Z&*qKf zWh1QvR7e6Ms9<5tOoT%N7 at w7=3F?0A6ROp(-{HufM~5}~HS!+dvr@~sZ++3;%0TJxxeEU72MvY%1TC}B0nO@ z&JtKhNaJHcaa)dyqyy07y=X z!{0B9-Y%_Xe6DyePsMtszL$2H*xjdDP_$f4y=`l?^lJ4ve00-g?AW!YNICC)mNacf z;r=tofxhs;?2zVGSd9inXEHKVz||Lk%ZItc0z3c^4dr4ptVoEM2}FSWz#Qin1_h=i zX)_#uiD>EsL65XJ2%}NPK%|(C(;>8hOh67Wn~0UBg7M5PxG(^)tgZC`U_#7k-f>^6 zuc)lh6|Q3~1RS7UWrSi at 6b>1K?LJbUqe8L?CJLD$goYXpb(DlcMdqz%2uAL|L^4N% zG{ltBaD+8Mm7=_d&y$NujF!7jAdz`7Uj#TH0L8fvumX?{avTK4ISTL(wtFn_zdM_Q z>ur%OMY11tk&>*U`P_bb(4^YUtL4TH%uIyLlO?1{Rw&y;aMxjg0hpM;8K*sm->d1E zA}N;-c$o>R$Dzzd+Z*q1YAgu%hvIzZg(Qs5IffVMH4YgJ^b{31;|vT-)GRT^Mg;~2 zOHl+7+Vn}M9O=`M21+c5J`On|97d73xklul0S|G>WJC^4L8I7g-c#eo(SKO6JU|&~ z6b3COiCJQ>-deJd4=rdWx|&fm4>Q~j6XcWAShlJ2D_uQJ{ZAu at cS1U)MxWaMgWLU6 z?cLhc?8%B{XNw7pc6^^?z}OsN*-&=C_=Di>=Dg?-dqK|v=GRt4a0}LC%+tP2d1z;HxUU5$szIR83v at K2s zC+F9ao?+Wfen%KZp5k%_#W5)oXV3ykJA#FuK$WII{HCYvwV>tq+aHb1x8DiT$mDjL z2Q?ffn!+QU#-8Rk6 at -Ojpcr6A<00-iB-n8E%!Cd?NfJogh%982f^6Q$9}M_ at J=69I zO6sfh)5-3==_OBt8*NNiI7hu&$qdW~d!9BF8JZ>(K9oouL|bagmdT&3w!cs&+%A*l zUnRyJeOvo at ZsDofUa)H$ z0S?7t3=$|Jl at 2ja%EvQ~5>naiAw0v{l6^tbpW9xiMi-FIuLmaSHv2beToWoQF_^|} zuaJ<&oyzN9dCi-w_IkTxZxDJ3=Dv0*XsZ;*gZ>WZ#QPW#G!Z(;o?=6sL=1(d at S2T8 zs!d5~{bNT&QDc4gN=J7XFdORRi15ha0v_La`3NMT2uDX8juGS5 ziY3Pk!>y!&+;Iq}7_(x8iHX6y2WN*03q at G+WTSKyLUG7PD9Notbmj^kr9VcfT z=c{N%2u1qA1 at O$*>{yd)Ih$aUmi>Co2^}uQ^GNm%b-BzsSSU~!=yW9MXD}pl=gxt$ z$19L*%{~J`lWwJcZsA5GZ+Qj{IP8RC=0dVWh>pXi7-5c(3IpW(+}K~B-2;HsXfZMG zJ=3d&kzk3<+!%3$FNA at ZCQQuABSt#6_ZXG}Dkd6=D3z~(B^)Ry) z7-F|o)+N6FYcX-(QI@@wnF_QUv?5|!C&P*qsMug^7;?=;zsz+01K3;1JL6a8^V?cC*z(t1>9!BhM6LGUp69~M%1Dck at StBt|c16fc08~y? zih=<|gJ}I at P+$VV*$Z%%whwLV9K`EAA`xuvg<0V)UJLl4|g%H;Kn28Ls=D!!HDthO>Ewa z at r7yhFm(;uR<=2$mNQ93Wq!I?7{n8}dI+BWY^+cJKj_?$YnetQRJgsqadA{F33 zA?Ho*>BdJfE|~p4N?*!;k-8Y6lMFL)9d96S!y-;~8_8NZPI7GK_nQn7zQm=FV5k9x zVK0J@u(a+NVU8ei&c7i&XL$KiS_bJ|1J3A>#klo* zRK&-bbvjO)e>JDSz2x{#;5RwK;UYmqg#^$*G&>payY&bh2S>}+2ZO)$e!RbKa$JCB zUeVjjf_)lWQ^XSdm<0DvHdooP=LLQ*wuW-`bE^qau82utlQ?fI4l)7i7_fm71B3 at 0 z at SH@C%W~*KEjUU|K*k4z)i`DN(J}`;M_Ip={r7Wa)oFanYNA$G?ET_N;6N!Y9PPPtolk(%N3_`#*f+$2( zqcC at zr2XF7Kteg{M<8X$d4o}Dry)yBax8@;L)6o0{Elrg=B#^Pc|W{A;vb+s at O;7h z!THnghoC4w(jRkWPo{iN9cu-(Kp;qkXC-k(V3i`49z-;wBfL|! zm-x8eF41jXz9OH_?e!^CspCpd8#}?()r@>+HuWLn>GQ#I_)ce=$ zpRHM at bZpbCs9G*2+P1aY`Zao-zB%c#_H0_yBpkQC%NjPLS~XkepBR0MV;UU9R|4Xm znpZs4>dLc{CC*V=#93KA?WwmZSGud}-8QQx&N<|_7OdjYF{{MdqA+6wa?o1(ccbe~ z(F1E|a z;|BXeg3 at W;G`%H*WHP&qd2GYkmiiyXKi$2v$0A{`i#w_QFb-f203ZkgeWC}}e83+9 z_+QKi$t)2PhtG at L6ZP)x-}vXku+DCdAV+0?@I2Czt zL0&}*n*Gt%Q{pEvnAS`HP&QL^5t!&|D93of7XdiNdx&t*5ys}ybBpJ5Y>#|sRTQhM>!w3t^sfha3oynSag#h<~gIe zMH8Mazru~#7e-74uoBLTZu`d33psNJay*!MpQ?*AYdpSkj!mucy)&Wm`HmtLw!B`h zL9%NQM+CGYj6tA~y2*iyHHO1;jtosPjk~KHoc+5yH)kd>YOV~BIEY5Wlw6Z*4&96c zN7!D2B)RIuk{ZVE1>0vJLD8Csv2hMyo?#?d$K^L>3~9rTXKuBXB<$quX|QR#4sFTy{<%qw0R?}7APDVa#lVXc$q(!&z zxdj{;6O4=<3h_5*8L(Jns4XcR9KfjE at Ze)GPGl*pn2lwC6N4Ru<<3oe9W)k%5 at 0kU z$Dz>BiECXBC)|oUWbQ%1gj&Qw!6alTLx>>AS+^XiZvLRt^Se>VxyJ;Q34?<#t{8&= z(m)yz(1#!q!NDd7E@&+!&@WKs?Kt{Dz%-b%A$o*IWH{0d!W!bpJJylHW3~z>rbLJu zLC?eI*dSF6sW`|?^^Nnm&k-JPo!8^P!v`mfQJ{RaJ}(c5G(G*fA;RD|in%KlQj*%Q zx$t9dOgLjrR~cuZ7=6bH z!d6g?&v=JCjT;|Q=UM+}!{A!D$o?is9D}&NB4dP6fff^6!NQo(@sS)E9Qsca`G^ka z*j7Oo=yDB_xd#L_$}BhH!6u6agc(GKLB0Pk0!m<+ajYS{gm7cR1UaJ^a!j+IxWQG+ zser-8Su#rZb4MctN0YGUQ2-zUN=xY{B`j{R{e(qEVsXL-=q^ehWQt438I+8s1=Q#? zPPj$RFvfNmVVFnnI|5QLCPijiJ!i%G_ddUefd&qy(}4R-mmu6m#m*tV1LVRI8v59Q zcW1zaHv?GonGfwZtMW8p@)HS<1;xc>1tmlR2v8Vy5Wv?CY}!s^v}&!kib$CV2H9pH zd?dy?u7SX27QxB~8CpnSV9;W(krKEZr#a*@<1PCFgJD#ifz2i6$LD>Qrh^%O9=*hTGY1M!`N2ch#Dna^`Q4+GkHkYa%^Cxs8AUC1)l1YCAHGBy#h zV`4lDXGR2WTVWO at q@qhR1 at K^!`4D8>Eke!91LP^N#>803Cf9d&jf at SNf(UOj+FAfG zUiJgpd6(V|ej+jap38SBBXfU|DIWFkR)NxFz4IK2C_sS>%4oAvU`}Kv7W%#&gl at 2+ z8o(yxt5HUSA@`P*eJo{5P40$b47}tHSc at U|IRhuiK-zH|iUKSqVPDrgyL?Za-gbFD zKj;sOcsWVGJd0Yew5^?imUe4I<0%`%Xp)?OLADbda~8lh!)fs_1Ql`5voc%}W0|8N zGA3BD1PL~eVHpC&%M9moOcGiP5O46`BNw3?p8{k1rq98s;yal6y__}cbY$$C10;@V zE*SjhARt3z_Yivt9Khvzx}I?*79az<-HB-gVsnxrNNh0I1E`tGZya*81X$`CB4a_0 zD~8U~koKPo)yc`orOT%5`eS;1lxV6cZhL0l6Uum^C# z?M1dj13ikv5=vx(Bw!e@;1*AJyF69HbH(=q*!^cNM}IDRIC`1FZlwsw?lm3m9%P*k zrc5405rEk2K_Jv*rm+2=1`NJ-7e^?QXWtVuGeRk at Y&mx*<%5}HnR1JW$*VI5%r`J| zCh5*Wyb;LQLuf|lB8njOfZ>AW5D01{(C$D-0?Cs`zcF_9vM^E88#7Vb)Iqq0*heA; zai=Dfu5Fw=-hG~l4gv{KrN93T&LW<# zLt33zD=QMJenl}yI?7DiPiI1H`tKIVe19I3;qE#+Z1}00ZG(_&sH{E!uZTE`Aj2L< zG`CX)LB4;Az9_~Q^4llgb+TagO5fWb}j(pOwa^;YYWVr#@qJhB< zbT%k8 at _Ia=r6c#q!cU=$#WeIkTdTjnuY#)jvb%9rqQx?VK0@?K!%VbFoQBWX*xZ|E*zCpR&*1knC-o`5r zM66xp(Wcur*qcJqq8-9;1u|lW6Y@>i;4w}JCPIi4LE%IMs}L$XiJaVV#88HO2ob at 7 z!v%%dMT?UOK?R8(#32&nkT{@2lqP9uC{Wgdn$$+(EKTYNky*$(6g&YmK_KFC)gz93 zJ&;L4LM-81X{L=7{TL*Sv<;$VqT)BBT zBbroXhIc*#lhDWLgQ#EqM_)!vB21Ru%Xo)}q2f+F45kG9m(ETRn8b+= z at tv?Oe)G&Q!yw={=sBAnXQG3|vkiOfU&v$AUPLtsZ_4HzV z3UIDed>P<7OO{f@@LcMx#HoI6^ciS;>n8Up at 6~|Vk{0aks-w+WykB at Z5AhFPq@DRQo??R zQ}g&q-{B~t1YE%69}cjCPR%1GN5sJgHkde9W7ur?o4PAcYPmYaTPIaS5fXffOnlRh zDdBcWN_n7B4igZK9tYDKU9bWf2!Rb_2us!4|4%1(#YlUiAR_U3t;0k|@)#X}>?x*1 z4+NYJZ7ti)tA;H0b=jnqUn9F3dg)ljo`x_Edk{cUS%^7TFii9+AvQ#N&QO_Jln=iIe5CaW2;TJVUAvPrj^DV`cy;WSt4}zE6dd8 zj;`h&yizF>#i>hhRiC7xHX(8%24XgmEWq^^ACE)KxXEZI(GbYIJMb z*JraQvapKBhG{9RWK-i|EyiTTQB<^+*klAm2~tDeJhUBZ5xEB$g|=(CJW!~l11r8v zOVQ|;^e84X6c!#3=O!Xo9S}5V$~48qiq7HO1WYmz7#C at 28R;-LGC4saIf#)5n8@@K zqUW%49-!Qg44H;62{Uv)ke|hVA#p(g7#Yw#EYTU7#axhh2oa)9LKfp3U at l35#&pNZ`WW{)oSRPvCu;AGZ&Hocs$<1^ zKhKK3D(BCXmbEGRbo-ie-fy9jq42OB46HfC<+PaKZe(VPsK{PeJp_o1XK})hI3R|Q z*m8o!hSka+?8{zT!3Klr{?Kv)lCwDzv}(N} zL}>RGPSqDimwwN_aG}2~#IYWkkr^at{#Ri;(__bwG7C7YEi2aEzAX81lnjRv1dybJ zE0RKVkgrHfCHw?S&cZ at +!%-k%sB=^nfr#UdIMblOf}Gef18Kx?9A|)HjDs9Q9EC&( zh=|S=j%N!s8$$OStmGy)lol0{HMb*i2C>UZ;o$LoA7{eiN2c0%^C^!8le44nJ270B z{vDj+M;NhVTP8 at Bf(jsk5G|7IIlziIC>+ at HHaLegbSxJTjT||t9MR-CcvE zit1Js9 at U)ANBVzFyJ{b77~Gf#cI&rE5=9g^f&&%aOqjyUJIT!C1ra3UjApbl5Du=G zO%}>r4#>+v=7 at SSgA9av9-|^0>0vRrSD`3#tY2f1bHMS=B8o??(zS=1vC$_owp~tB zQg(OuMxpovuLTnZxustB4+ic`Pjb$-TI5p+yBMY{^L>fgk;yBcT^2Ck zzH1~mtC;(zD?D!1XFl-_@&v=Acxdt$Whp_cDC(0?;xZPQ3&^Yps1Gm}ibJ>-Wc at +S zvv;`Q9J%`%cR4Y!*7nj-4Clj{^?UroCJT;SKh#=1g%nsETmmc*ZX(9xolF=FjAXt| z5=e+VT(Kq%j1)jN7h!DdRNz}ejB*}; zs-+Q`Yw;<;lh|R_M&X7XXEFIr!1V=HQB~;k=BnOsr-hNR at -j} zYO$Y(;HJWA%c9OR3^2nigd{%91*BmCn7v|BIi^BRNYGRf2{L*eMuX7byj;j~$<+I( zOnqlKJo|;JS4i>+x at F6eZP~+rX?Dw80nZe6S4M}ICA>=%*RX7V4UbWx8|W+>E<6G1B4! zLqZ_%0D^=Z-O1J%5iT_batpX~x6m?(4FQKUvm0mSxWI*GB;uflYDpNI3Xh5q8Du!e z4mq5tvBRSt=e;7Vk1IYWhOzS_;|+tSH=CG at e2L&k)q_F+LTm^;gZEp)NT>;qy!!?uOsqE_IcRJUdCn5>4~v6wcw}Od*Gq6P<0l+*d9y|h8!EFr$v6Goq>@yX;H~K z$oU&O@^WZBL&$bA2H|0u{o!-C<_n3oNhG=sc-nGLm%}hfED){=2##nA*kU7?gBXfc zpFdc5yp{Q-F;Yp=qPww`Qkzc89nPxmxmH!_r=iL7y!&Z=%u=eQTJ|m6L^K2rKtb4t zD|4ugsA(K+hLeM^=EnztZzLBeMT^zT2eow>nY_z`o`y{BVh%LFjbeL za`OW7A!t*0SF at z+0ObIL)sBm9?^Kurgf5&(UQamb-#G&2^KR$T=f}?8xaR6OY{|{$ z;QDqLu^!}z$4x&^Q`YySeCi~xp9Q4$@=x}jn5bhxmI1?9i<@%@;BIqen at hwpNMRKs zvr)uem7+L8K}j&Pj3Zd#NjVfq+EP$ZO+V%E!3SL6!I8==4#Bzz0uxAa_a&MvNs-Ks zZaJeTHgBCtzK`$nwx}ykIdRRu34>!OOm7~H#D^vypOo)OiA-Nu*$?IxWKdBPC*nzl z4e2sSS^=aY?6NXRWeKvA=rc at bIDjCs>?0qb^%pH+b}gd}c(ntZj}gX3M8lLg?Q at aj zN!pHs9wHe&2QoH7O^lRuJjfkqw8|8em=+FJWhMy)Nwe!7Xb+^NO##YS z^CtS$YN#ECLN!E6rb&hyQkn&$4n)5paWRv&WzDj&>s5NVb7|uyeR%Wy%r;Za+4i)P zzs;B`Cn8pB%?@H?nL2RmKbN%rbD~FrX93;8-Nmep9Yg?_m~vyJVq#Kk!_n_3yvhzC zh at 1xm3QnXRp$upY6mEQqBr4M8o6QzEx8KeP=^K&^X$7SxG+g*?hX;UO#|Z$pbdgy} zA_nNTnKkD at fq6l=z?gtyfh#9wbBN|rBnu#TZGc=t-0mqi{GFk2GNJY%(`8WcZgiBdv^j%wR5Hv4BS|8hq}KbE`T# zY-7^v{ujYjEmQGNHZ-o|Zn6O}l#puzV`H;k7nc?08CER5fyfw`mXQAD_zqozA{^jy z=R-t>nOyY+#|gOjW6&I$b=kSZJ_iK)Nzr4D9NdylT19mhjsQTAa1 at oE0CRB(jN-VQ zw5ZX?$5A6f3>93c+|ABHAi==J4m1KcJPJ9t`yR1~8x86QHXs46L6QUE$Dxb}?P|IA z9)z7lZf;zH;9q6cF1)diQE=r1=m`#3A&ZO5QoMD}^W{4~o4+JMY;RA at tMH0lDm_(9M2hIT at aLDBM z5ubTt(8UO$cTeZ}2yahHKcArG2LiJ-x#;*s&+1i-v&78G6|D^m=eWLp1M5L7c@`XO zXQ|ASoo-p=;DQ8 at grM8r;iE!h(8)OU<{nR`!M&_x&CenK5z1hR$0PbffXF{E=W&D# z!;V0x_zeW%F={}-jI$JpjuFPB%mIo!20n||89iVXRG4EZ<*t)EH+yzE@|`REA9JD2 zIOra_6Y3AcfLLsZ;N*ucgTqV^cViG>qDyW2q&ve z#&bau&cVv*>L(~NXYMMcaJpfy7{Hu09k`hk<%9+`p8~!X$79gs)bBSVysy5mnN>Y+ zC$Z76VK;{5j^|n+>JDf}exI~K`pPVGJ-*8jBA*j$EU)c&w>Nqhz|mLM#=h;7=;Y+n zhmcXor6Q8fSds+D|6T*Zu2FFaF4c)BqlP&#qKpW0$n^#RsG|(y$@lEo8wSSU4p5{{ z8;BWV at t9`{gPiq-T}8^DG(d1;7^;GxL;h zBbGl|sv*y*Ypj%D at me=9L#~h(xQQr$5V at Z7MQUfwl3^(#(rEDxU`!JUk~|A-uU-#c z9WI9bO|*U&Ac4d zn{AI{-p62mhewj8?vdio9GLG`B;mUxyM8sqEEWE zFD`Q>jZMvmLzNW(eas`q=LP}7+P9p-);c_k)NW{ooV=QT&64K57JD2SGzdp`KalJJ z#b8H_9Lmc#E$)plu6tJO+}en$5LRp2S+*FHSNrLiax2IU#NpxD`Cf&5v3uA z>B0V1<$&K!!HEqLN9h4D84)K_oDUM2_(DOhDxVmVuE85(31PN7 zokTh$?Z8OeXm{_dC6fa;Ub_JpMcDNic;kV_g|6# z000MFzz1Ex9d~tIzyJUM0DUiCo9z92tJUi3W}4QAE9rjx$t9uVAzeB-zNnRnH1PXXzR$F- zPImN#dCKHiP--BE(@56WLR`F=kHE*VNA-HU`<32#_Uvxi&2(1|822ogm}Hoo%9qEu zB1m2WyrIfu16a7tG;r9o=8bsdMa0uwT{%IOVCw+ECnPahGqTD+bC{7Jpo(l}F(62D zDLGkKP7{(RHPgyXyxFpXCc=t{FkUgGlrhkJ6+V__fjiDXf at z39xsF`xTNz6ng{_yE zJMQmC at QD8bx!6PJ{ zw4MmuMoV7L=;>$$>I$nu-?-q zayenBBuzGP at FPFRhJ$l+P+_!%g3MfoAVkpO2rSaOx~980o1C$`Th}%zx7?r19{$cr z&|g_;AcM=w=Ecl=PXi={9ET)wPqgQZ{KH`I7!olfkhR7;hagBwV=++^DCElGeC1P} z=+S~PNIM%2gd>O7HxiR~;DHjgYB~THK+ltyRsm$`h(a}{bJq(bR)V0NM}+Y&zFZ-+%$_uGMO+8f*>d>MkI+62^SwuBPo?d zDn>_y^n^(VIn+>2q;CL5;1WiciSnAT!3>KLqI1MriK6`Ugu zC=?t=S)|HjBxyC!VzZF2CXEc7)WF_$jg+2JvHUZ3onIux>1PyfOaqIV?mh*} z6=ikEL?S(-oe|WzXM*ANMeOEd#pox*^g22nqKtiiZfPF2>}i7+5GI9jQW|O zoMV7Wj|7B6P$z4j#XYD$De1A;lhjO|h;x!N^weJy5z5R02eimJ zBLLcKFC45zBQ-r=7RjbQNsS$ww`|#s)X6IwHOZzu3h0_+qYOd{H z_=DDX7ASM5+^}~%?PKWpAuOV*BkUj%ueIp`>%ll#x~=jCA-TBRE_@ z@$#LJ7S)W1bl98;`ILHkLwWHCUc`JnLyJvzJ8ey`;X_(~b zhTl;X6d_1rM;WltOjLHJSf03-og%L)@Q=ICBTkiBv8wP^9H$>6+J|p(;;67; zjInzP0naCjMuTZ#kRPX?l;wp4?rL(%j0gh27>f*sHsc5mF;2rt6I+ at Ijl~w3PEJXH zzf+EcXbK9>5m-5-NSv>i%@gq$@t96G3#t+|Eg%;ZBdK6H+Cfk%vN~)=_^~2SX8}0h z!1bzhl*Lot!R6JSO8Zq0jiy6f!EVJ8p!b;|h!L>kjp_{KEKlg*@-C&GDsvKx9q at u$#Nx@?Tx8)HAVxf;W+ at Xfagh|V<|x_2Ow-BwFMG;43;cp_z)Kw4agi)S(%`r$uvni zG}5$}yLZm?x;YcCJdR=09*0FTcN<5jYfdqNii#D39my%70qzFUMivoIBT}~N^(PUu z^L}PdRP{RH#ih2D at usmVw6GA^R!pMG0Te8ufPRLBVObE+Ib#tZfN{bKL=?_R*Rjwf zIfYVdQhPyHSJ684z1%w(U1(6z*2%3|myH=$O-vt=-l0QL<$8jgZZ` zlAw}A27%yXY%(GdbC6_rY*kM5of}l>dfgLefOOY0hUPjQ)1<9`bDo<=ocTimuu$y% z_vCvd{C3%oHf^xWlXbS=%y`(dHPQ1+zhVkDD9wt2(qxkeR7 z1yK^0GKhyA(iU|kj{~8S0L*e3@)(rmhLWZyA{SpV0b)*hax+{L9C9k^F%1*rc!cX1 zMAcMHA_?X= zX)<&zMRGD>f(3*%gftF1J)x;0fJPAnk~suFAr&$su!+d#dpRjyg5ZWjdWuSnjNl^okAR}T*_K$ML!Z$piU??b{3IWO28%)GYjsMjv&~|*y4u_m?_PQFkt3UM>F1XX}IDsgP1{+t%I!v z^+lPSg5?lwcQe?ddxj|6ngj+UR8647ix4INq+AE2##09o%bObWf(RYOhY)cJA-1tn zOfbU*h6YkQOHOIY6Nt!QvHYDVHZTro5PAB2rcCFVa7ITza7M?#_ at 95i-zH}bDs)q# zrj=fnGuHVA?@DqeqJlgnBA6k-I_p8RhEloaGBH6g<%5ts ze@!{?mvDP{4_d6wb_W^hq?+=OC?GJQS;S-`BEtiY2;d}0W^U3Jvs=Lz zI|P;pFjtlc2UU-)^eN3)QN2ea?BVTGuSb`in_ROaba*@8LYphl`LX1 at o1ECjpLdcG z1&(JSzj+jm(>71Ab0R!fk4#p{kDWx%S5~H#Wy)yeb_ at Rw0?LNRe{32&44e;gNuWpE zSaX3EI6FDfN-Qs}cpiINK=ISewmb|JL8Cw)eE(Gr*b_*hxIUkvPcwn&MX+p|Bxj1|Je*n3`#Xlt?Hv z$BMZt&AZ1&mFRBAx7%&KvpMf0vmp%DMj5<>k7*&`{)7eBGREO at 41i^b_6>4aK!W9U z4IGImv0~xqf=V>O)U7MUQObA5E0g^Hvy^Y=>c?94U3zj{>h#+-SDnjU73+GM_oty% zQdXF*MS{mt<;W!QxxDxs4mgY+uNffN&?D2ahi{$8a&lwZ<8Lf-)0~viQl95GbHVCL z$}>x8#YPaz_=(BkDT|B48si@@QFZB7%SpAFv65LNM^8@ znF at G4f%cuxQF4unJdO+-6ajV`@Os`rB_qK2{7>XD9E%?mSqB0*H*@Q6E+87B`o=v2 z_eL03s(KEOCTFF#tJlfY`^<-7GcE%J4YAM7mjP67WO^H&3m7ckU|d(Sx~>1aA at Z7R_CeFrDGaA!{pO(UF>9JX`jW+!SKtQ9`k1K2{1$zq)D at A z at bhe>mH}ZvlSE1?f_N2 at M39nUwpMTqYCS~I=MA=p4*_AUw4|kCLPIuS-eri$x~Xl24*~5dB}2*?eRr;G2t<$cey$ovF}-?o^4{Z{%GUl zf`}mceyb8-;JJX&W at I6ouB7_-5s?`}aHN{`#?i`1g69%B*I+T*?t(%x7=s*+Jx0Ak zZdU|w06O}thX_zfa>SG(PnnEy>h0Z|ayWWb$;t4mNy5Ap at t;c%rACnmxueg_k|^N) z<0b!`>|E}25u0v!kQOT at 2n-CvaBh#Xi6w7X1t4ayk&uAtXBfib1|cwV1}B1XXBeQQ zN^_4b!BW;ujVZ5m$T^w^!$*LM4~&?@FuXKa!y#z$j2bL+aj7huNsy7ut|#IgocsVT z3yh at 2;#QVaUQe_fMKr9J|9CPU)7!Pp<-F_hlv(NT}xg(o!K7D7SiAYgkxNg|&;MrTJyUaf%K-k#euC7dcYIZNx z()1j$L9w@=Cy=2g_>GaoHH(g9KE`Ha9^uTE*xZq;<;8l727>py*{n>ZvoP4FC7;%< z1$Egt<0EH-88N$k>e{N2BK-?hSL(sH<(B(PRei0%e9w0@)OS9y*U}pvJ?>vqlkMqM zZMNl-4iw2m$X2Rin?kZd$DdsUN&EUeU%Y1s+FB&N%pfdJG_T^j)Epr67u}RYu8B>ACP_G0wfz!fJCe$ zP;UmujzT*|LS1A at 5Hkyb@u)?^5G4^YIN8nv(D6A<&&kF%Gq7wXpRuGoNth&Cp8ByP zo`n%0P*@!SF at YkH+zt{slukG4K7%(4nhoG at 7?F;d%%ooN`5Bp-whp;4(O;2?yki|D zn|M-XDoS4M*EX?7-EDjnwYPyHCdhn7=x at zz2|*OlMtPTT#gm%~$PJ_y0Mr3$IQzp* zSDVJSuf+Za8-;E0GwXUAcf5BDlH_b7$gU{|Q<5Y^`IrgD0%M1K6=MhJpPbOHj;%Y+ ztamFz(yKGcUVPJro(K+B5IxGlMx$vtr76orA|<5d#dhfquL-d0p(Tq2jH1qQLI_uk zvvYIgCFW#EL)gllHxq+#phv~4a7FJVge$?2%#1%dqj^e z^u&E5wVb~4xpO|*hY6~zo}Z2JBc(!|6-*LAYn~cU+#J>@yX`G1H1 at fCDkKx<9Nc_c zJ&|87a_a*Uvv3oux+|m6OWQemD{+N=pGjQY#D0dxq=j at 6U~nVO0@{?tc at P4$)yE(W#x4qqk^-CJt3wi8N7TB5 at PdcWyoe+>^QDVDTuUu;d40 zVuQ6ga$I%hhC=ZRhBlfo9aKoaX z0jIn?{XRT9IDXkjV`89wu6Fle16R6Que512xBEN5^N5pca zj>JUkh#6m^J|=gV%1ltJu>?W^QNJLfs|mA)rAVrYi7nXx8*vDEhloLr`b_z9mJC=V zVtdCUz(onRc{Uxa2!h9A<^&>-mPgLxuw1%5XRgJJe$8wxGTtq{bdB4>iXpii;uwPC zkX;VEmWj4BeW!-s1t_?eims>BBm;C at e`$q+Ks%2E>$m)1jAISAl at J)$5Gk3x*S?LTXGxy_GL zq0y(kmBO_ at 2lti^hI;qpDt#N;ZCI+iQzUb=H^$+5TP zS8nTba)=N*XA)z;d?Tn+KXG<05Mb at 1@NgW3RH~{glq7Hm%joagG2HTXdmhJfv*xaR zml3-?jh!j(Z|r%F at bz;{HkfIv9UU2hBP7apEg*MzU|n=(5osaSU}{3r`DJ}aIjeM| zl3M9&bgk~lP9kv0){^0gL}4I)fRQl at 2FgAtx)w-su at BU*cNpE16BPs8rZo2%4nlKE zQiUj_J`zHzvP%~2BmoyJz%)V;BneooQmtu;&W^69g0IN-Hsig+vrlS{-C}NZZ6xZi z61yz;(#;h$!Cihv)4QWRz`L>i&llKvJYLO#&Et~Qma*^cf{4KrA+U)Rb|q7aOw*2V zmy-rSk(ii~0d#^z-UlT0qj4Q1p}dLUgZKPsZH}iR0~m9}_1NG>=6VIq5**671T`~==p&enpty#) z1CUt3^ikODa|OpJL>_QKA|!KhI3!q;z(FjH4t^LfM?%5OtCU~NgRybjjUN04;yDSC zZ3q?#E+SZQH6w#^gV-q%@9Fu;?Q_8DZgEgpkrpIFl|8aRYysOCSu?7diZN=r1_We7 zlu2{QxuVisaT0hGV9{XQ(Iw4r5!9NnBog7Bz|VV;BA;6qJreH^#h4JU)} zU5(3^$qXGwEttLO(RWVswhl~$Ncfh}Zf9%hfX9M#jfTj1Ga;OB6Pe5|Xy#-L2pUG% zanA$bR2+o!n;p0r_SQ)eD>9g-+JH7Z4hxq>fU+78#{5w-xb0WuZ)itu at y`ZrvSXuI z&MMOA!>4;~j7>)hpA%Yr at Yxz3v`C3U(jS3_&CSd?qV^=eGHT_{G)bZjvX;Y}K+!2c zjfpee zQA3LR9E=hTCkBDRa}mt)E^yVkVK9Cp$v}-TxMO~h%t`SpzkXg!dDn7j-qVs!-CZ5D zR{KE%rIhYDNUmiNz`8!5*umT|P>T(da*GZU+c#3g6QOf7Xuw?WVal8LxmG0R*Et*Y zN}9zE6m>L6jWNT8&4O~})E^NrLg3T!NI?H(avT%jD%&_NxIN{KPV~4puR^EBa~3! z@;GFJV*_}eE=jBe4Y at fDgF-YjGB6kDUAH73nXq4R$1qXOHxRcUVm!JzXM=g-;gaUX zjT;nqD01gR$k^-Tmf?eQfx47Ha7c6-=L0ci93A-&%eKbmvS$24ZaQ3!WWn}y2P2_D zCXLM8xKyFS*=@&TYiK|+AmaHjVX at 3+Zr0JU=0S{)wPG`k+9xIMQALd!A~M at -L6f?> z+UE4P*3S2}6XIA;-B=+UEIkTZi#-~jQ${Ak6sAa7R(U%$CWdr;zD~_3w+BGzOTUPT zB(b>UvJQ`E9G^tdjhL^AqroP~yiCmIX5hxjqhC`-X zC5LN=BIZM|{ceSDxrV{EM>a2k!v#}V_wyTg3E;V+3}oTcrj5%Ib;#K;co=M#9k~(E ze!{uzE?DGPMVk%SO=UdMOive!H+=4`&yibKd#dZS=2O`2O^B9^WC93A;0&+8#U?7X zBe;mcE)heC2+ at C}@f`5%B9O*clhb~306t~hviYINUJhWjwg-4FTf}mzgV=CU0y+*j zX!aj4Y(cofIhY)|CJRfokskr>e+Rg&hPlm~EufB;-qtAVyhh`06ddj&vV; zqecj17;FicHus)Hma*}3L>M^b1s{pLLKqrr$0#Eq&d7v9XFE)eM#6#lO=V;ah6V^W z5d0SKA)2gvPkCuBJSH4Pm?)w^JS5Z_FbI&x1bYr3(YR!^7as=<&+`Z`atb6uI3&UF zl~CDXWOABMpwFZR)Os^$?9tVZ3|D)r8>+syM-L_S({plbX659O9|3kONe08Abv%!J z8Z3?1PMjJuB5<5zFvFCrcn5Ke#s at h$5`lvudaB;iCG2D^oeXopysp z<%

o#Z#e^Jnou_1BKJ!>O?ZWb?a zg#>~j5RppKrkZ0t`)w*QCkqxrrOH8KXbKK2iX4MZlbp()gXxP2n4A&R$!H9T*xm!N zEaiio#m^2;UL8j^3CUpaAY&2% zRfe(2%mSl9iCF^A5qbMZFO0>fGjb$kaop%qlIM}!!6p%4 at GpXfK4flAib;w$9A+hi zNfp{T9mP`$vCy2Tfdfqzu4CUhxOHU7dzjr*OsmJg^W)X5&(W=N92&3&6JW{8&I0oY zk|YNK+ at d6#gMzC8PE<}}Vq7sYoH?Ahrf$yeItGK2k_nr%W5_KxW+yrZILYEjXPCL> zSQ41X0XeYBZYVm477k$9ik+9OX~XAqklFG4d_3GcoSg3m$bSkP zW&#~xD at Z*B6d#wHTe~)c4q-WgY4XxnS;TR11cg8qxy1zmjznslumQj|W|*+pQcP^v zeQxHR%r<*e{Vr~Fa!Xy!=aoG;j~_Xiw=+0B4Bs!pHD%)(0f#cFi$Vo1XcP*bPEVm+ ziSxM~UG2RNaXh5x=<{_}QNzKeocc{pAEn9NVEH~zm8d(ds#0NJq)Y_!#Z^^Y1}B0~ z1zkz!l$>5ZHnTGkD=r*jBwWXfcLswTi!oSkLly+w=e(W{LxZxgfEC<%7c!3n?kr!a zbCP6&edm3qQt&(sokJ)=%aL&(nKO+;IY)3dD}J#NbkmY79Nb=Rq$p`PZcYI;!{izj znjl964U3XPFeCC7rnB&5+V+#ySlTr1?mL?qG-N^~P#Vd0c{5~ghl}7=fTR(V?tt6@ z9Zid)2a!^EGBys!4R)Cb4c;tIrQgw~apQkZpLJ`Kt3`IGVk`A=lW9(>Zm$ealA}+7 zSgUOJ_r{MV%ycnjp9^AQPOe;irqSYEmaFrudX>B9Vh&~*8e=_y-Hn?*pQnksNx{kc zjrWdx3O$;5WE})`dvbV)lemf!j;7%!1fokHjL4DRiymYcLJ}IrN;WRv5};i9(}~K} zR}N=J?uo;X$;0sbpIbh&>~($LdDWl|02csPW{KA0nCT7zY3EeD&hG$G2Vi&u|F7b4 ze&8fD2Z6{i#I=eX=37_4FDigS6i3`ZfS}z@?(V^{!;(Zu)^CfG0B{x8fn&_)Z5=4< zc77=kYrv{uhs8m?O#UqUq1G)ZufWLd_VvI0Jht2*B;`ke7?>>^S#gNdtQfi_0I?7 zD^=0+4LIw8$7J3ac9Rj13^<(5B;>X0*i26`h~wBXagrzlsI1maIYjhiyG2g%dWu4t z%kSbXgdzwqE+!}(h+c1=1gIc(p(VVqmE}W}1Wlk2TT+hoiA)H)utyUTazjLL?j$iW zCJjU$VTggs9h}0j=7}@`BbU)Z`D5}Rqw<+K;GMH~u-gcsfbiFGBRc)1O|xNAe<^4 z(+srfn{8z#`N~U>;dUhMXjB{?FhS&Wu_H-BHXz&^r$>WfoWaQT4sy{>e8wo=58 at n| z3z<$cG6zH*4TPQ$u8MB0IBD47$-{?}9!_jGW2piLf+R?Spn_r at T${_{7|a`8!SE?& z&z=O%2d(mT=hpeJ%%h3YOrFJ6d at Y@3s-6^^nTI at OXQcON)L=I`WkmxPKB=;D07NDy zDKzP#;xN7D8ZKV_N=M9k=OEA#2_%wGQQ|vfL~R6{{U+E;1G-i|1&wHKjYGF1CUZ>y z2ryZgz;Ww2k%YS!H3?|Yh#<0qz at yDtayxS6Z*$=LUhw#X>k1!Wc6%M)RB^`~4*gTE zko_q1Jp(1J>>cc7{R1*&gMw{o2YX49_^S|6y~(7mJKRJT5s?jJ0D1V5Sk5k7MaA(= zqJ*pnJx8XE9)n2+Ighs at 2^?|6d}w}ThJ<0w5zq6Gz+dTN*4an`zs^L}COk;j&%_^K8~RW!q*xO zLN%6WBoWtQU_BjjAV&aT#hQd0mOB>$4>P#}aV1IMu&99KgfP+~WhF at v#b3m6%r3Br zS(MI7z|j&*S at 6Uu5uo9Lc}^xE at Z_B545}tU$G`$?2Qb{gyz>&U;BrIG%;x*F(^sY131t!m5{zC5txw{IJlh%&QvgGg7t6GWYkV- zGKY*EO%@Ks at L9aZG3nCsNLn1^3rpmsf+Ucj92HBF95APR#vJiS(Ps-gsq; zWR1A_nn_<<(lmZCThAxInqr$sk;WYpXM5W9JNnF5lMWU1$mgzYxTS35$Z5kGp2vqA zp2D%ubxuKvo~M(faBV&|pG@c>~w6cj|Wq^?>DJzl3OBr z{5}6q6spP7(|(i2L**tH$4lJHfS7&aImP88e3dlQ140lxRZ at D~eNuygBPT{k zpO6JY3Cd3-iH!XHDE*6iism?uwV6q3UQnbwi(}5ep?ygwUezfsYxdXVUQ2FI>N50_ zZoKPLvfXf!V+)0U+Im#CRDG_KyW@*(;Lj9q8rYE{%Y>EJ?<_6HeU?&QDpaXSI&_x{ zb#l*TWS2F`Y`9n at _*znB@{+Rdwp6Jl&7T;?VY9zG?~ZmkZ^?2}tW)J#Y^b~GB$Td- zS45SIJ(MbXgs`rqQ`9AO&2XsU-1}YmJCN>f at 4Ix}shnlg!Qb-~V_EAGxum(OGWJ~7 z>W6w%q`Rfsn%ZPD%3G{0mJ(W2x-6}XJ{A&Nr(O$Xx=UNhjinUATdo%e4~3MLSDN8+ zPOx3>l5slylJ!Z|ZDhZILB{yUe#d7HVSyik^_~J8ELf;76YFiOY{Tfo_`la~BZ8gJ zLVgsS+G+7NaQcJea&Xhzs(F2qWyXLXS`^^5acTz$TDMN`AmmG#d{6S^fqv~fu at p0D;0 at xWNRWD z<}-s2HYRo~D>Bjc9uTQ=CRH0}vsFX|Q9^Qr$Jj+%JC0K-bP?E^ZDj-}Fnr#(Jp9Mj zEk!0_?}1LBv@`^>5*(a`IBM2><`(F#c)51+i)P*=@SSe}50nF72C;rgeo(3XvPyqV zh}bA|_5J+$ecxz)o+91D-`_2-^WtYzasvHz{am^FTR%)$qsV>(J_DP20=HlFwl9&F zSYp?~J~Mx%?{6p!$f=_d`E;Agi&4n1Zp_qaEW4+75R&s}PU*qjna6f!-`@tK^_LIe z_^GowcTXtBvcOXs3DMornKk$F zG!_~(5Ey<^7z85&>nl(A9mNPeMq*=-kx>F*HO28+mYD{kTlv5+5MARA zGN8cJN;|+i&!_`v7=A3g9v4pd^9wT{PGFDVbcY~rIf1G;Lj8wWU~NA?F9hK*#0pHq z7!LfYa2GfuA!(+9 at r@@J5SM`8!eDp`35FbF36#Z{@tf`d*kcaPyAKymfF at wxY9=zC z!%)RxFf!-JTIhK{bz?1=o-Artt>)2RyqQiZR9Jc`H-1057wjwfl+#U(HpZH4Xq^h# zu3Q0};60?6Y0G%+g@>E$14uxLdVStMh};P47+|wk0ivVEDA*Akz?h^d5k?}+OdG=P zE(SA33y^veY*E3W^$ZKYS72bqj;0S6Z^YlR>oIdDsOVtF9RVZc)*R?ulENf$K+t0r zv6Yixe$(n#iQMY!@SX38{bA0BiS7~%XY~BVpK?(C5s%xkNat|KBfu^YAE-XC{edH$ z!ui+Tq4mn&zZ~jQb6YZ{>(8ogfx5fU3^C7T&k2!Q%#a-<>*s{B_ zD`8zZNSwK4&+#_!GfCXC(!RU=%)>Ow;K{q)Yn`n at 0q(cEKJG^WefB!^meiCvDiO=0 zhd*QGdsgX&Hw4VVx2^D--F|f;Gdv}l0k8ym{gBNpBA;{(;&*X|Xghja!1r~g-iw~; zb0=^{$`fyS8VZJG3+LAt?h_7BIB6nScAi75I)j|lK+|O4>V7k^%=SMJNy$LJL(%8v z+w$=~kP5Y$uPW!Muh#(goA#Rvk^y*huPt!^S|;T)-VDZ9=pb|)yMaK7!Uiz$dil+C z8oM5{AZYE*+Nl~r!WjEKn!qKO zL<2UPYoJ>=h$`Ks}!Z7O7UOrI^eo;C&*l6IA`z`r)T0$%?jGqJ;&k zMM|8P8?llLs#02NnY7YgI_hSkrh*0%QBfemK{S9i+ik0BAisv{@lw}j#AzVOv)8V% z2^k22Vpw_yfJ#!5a|Pl_m~g`dDxqMs)RIMs{i>Ovz-9#9SPX(lM2=?^5 at Y|abE4ISSlc=O3ks&K(^h&%>Kc)q*_i;F&G9fb1Y( z^D+>2K at U!w=;YOs<9AyWvPlQc2l37jDSd+w{Nx&k>d(6=u`S>8hKVIz#?v7qbg9owa2a}_ at jX=^nN0)N|{7~*uDXiYP zC0-_zQe3V{_a(YluCf(d)}((c4!SaSmU}B#mM~issUJP#d8yuyLXXy^cQ8vu)*^qtQP>6v2^83ni9~iVz+)K^ih5Luib|vYN`%<5 zO)OSF>cE%KkFrB41#ewRD1Ymzog^)%JN$6?nT at MOzIe?Ts zG{P2AqIf32h$r)HV30(DMh8-h1Vj)z_6jB&8XQ?q$vYtMh>}o3l)NP|XR;$PA%vuO zq6L%JhZqyX!k+VsF-sv+jVwxFp at gF?Ml?k!?s2$LKQ5G>!cHfmoes2Oh?*1%7!sHp zGbS~VMgc;UMnpuEmQX}wqEn1u!UoHYDnv~f6 at _yvB1~L*I7^f&6_R4m3Kr3Y5>cWH zB*rO at 5r`$tv4Wa5V at p+|3`oUbs;{6MZ=$HID9;4r7JOraj+AK8lOH-B;$of|8Hy!e z0T4kt&CwFX$!3qkB%r6EDFG2Uq3MPnGiD`NRcdU;sinbUva}(Ro)``>e!t}27>p!C ziw{OFp-J#GF=Y`Hq6uitK`3#;UK}$5n7A-l$ArfmA*`h$AcBlNQ+%`5oVJv_oM(d$a$o zrYwbq#>iz~u4(P at _Vmby<|PW(nmO zk)4VB<<<6&s($J69F5tiiJoAaUrDyV1+^j~B28&l_b$6dw%BnGaJvABoUR2lue8#Cri2j&*F}jR>pC1=1Y8NyVU0!siCf>QHeu_V ztde at Q$Im<^9GVNT<-g}^_@~%<|L)tD!tK}>W&8F+7OqEfO>I0VS^b_3xfkH$E{lBM zFzDUrmkANTh|HEn?^aH|iNx|Yc}p(Mkr#3#X$bN(7&mGFq5^{>vq+hW z>TuBfN=C&iy2mn8xa!e4Cvo(ii8v(Iq8?Cer^r#K+KN#qx%m;HC_;G$TISbQ9XB>z z1hFo$y38UogSd1Z0u8uPQj}DPE>vk3Mc0Rz(z_yh_SM&iDx*Sm3iyJLH79dp$z^qsE#+JQQI%b z){Z!v%Ol);BZ!g4#LT$J7z=H;GTcuZnKG}>hOPHz6;rum1r7$Z(^88*xk>Zu-A?P> zPVNr0;j%d&4g6*9L_|-9>s=yw>gDQgQbZysqK^Gdu~6VdQ3Yv{^R#p*2p~d2^_!74 z;DsIGoMkMW11{htEu%0Y at H37uYV9rU|2y|lm>ATP#2>z}npjWZeil6fNgO&W?oxF_ zQzxRd9^0M^Ckl!v#WA1t)FzQ(423u>c$CB3bmz0wE(@pT3r3M-TFJ0I&1+6^=R1XT zLJ1T at 5+SzH=Qu&bcieG1cw9GZBfvw$4XeULMqrxAciMq7sr~;+qi(3ep4fRk8QVfA zmG2lP1nGCvW4k(4O_ftyk)Yu19;25c+%gA&7)%;svCUeQT3<$w0nFNA_!+fS{0_*U zg(ik3Fzj=9zsl}%dJx%$p9#(DWD?Pi5WvXc7&9y8LJv at FldU^H6C~v{Spp_cdy`E+ z6PXwu*VgP+X&%l?!Oe$e=Bc{Q*M=|P{QP1dkPvpTgtr at Sj}=dk+CMP%PH?>a!;Ox^ zef}TT+}Zrj>8Hfn2{B}`-eS8Z80KSTRH|-0r1CIwt(*4p==^APJ*;+P4(5|&(bVYt zBd|mye(63!B%w~T42AlAIPUJ;aVfgBq^)`E+BiyceGj2DjTIh4#~hw)7}%{VT1_Za zrkY`67FkS~aUZyO4o10hB$=5cLJ))_4p0z!d^NIo?{?P4&!H(c(+Wcs3`vzSH*1ic z%MxOPWAC+^6~TLU#6y$N#DfoEjnbI>$)v<%qN`zR)T-`lt7P;PQ1+ErLYOqsr3F$_ zh=hyKMOI z_p5SMde4lrA$LTJ$F3;<#tF=y?# zo?DrxXI~7P7G+zN8QG)F8#}gs;qoiks{O}%qnAWF-0F4$535AgDKmh$V?;+?nR_-> zVytqM{AL1N64 zF}Wl&ebz-(!nwK{XpCV> z;8BQ4{|0*bGj#l{;qM&hB-oWH7^%V~Qjtkg6;t&U+`tTX98O1J*wZtgKtg?on6F~~ zfD)tVJY)}~3Z5l at Fj)O3d$-;nIq80#+YYZcDeI3soCmHjzF%w4PJf*Nauj_41DIff zvcNKdkW3B)EU%$25-}o6CL##^SfE3@=Kwd~-M6sU&WWZ0hz6AuQUw6*-XMl0 zqG=*vg!oQIA3^=6#vO;sY(4`>!Jskc4Ftt!fQgvQ zVo)F0z|IvOqO(~@X&hP)Fy#%*ceJ0n%|9b!c8afq^)xgqPV)|`)8TGUB`UT&-+_-& z at jl0~>}>9IQTCB2G*EOJ4#5BLKasGBaXCQ|%1#rAbM+}U6g!`+X&+m&)YH_)p9Ab} zcPhKzBD|h1W)lt!Ehh75eP+V2yetvFq>+%3?LQbmV_`^eg6NxJ?=l5tA2Fs-N+2`&C{ndYW!@QB^C+vuD=XCh;S~dK1;Ze|v0oaj(F8!}o%ELWlK* z50of>-XDM%8-))4=znnL;ORazqLz<*pp;|gj~V`Fg!)XUUpGm^c}eQ(Juk_mtm@&9 zCvBy?q~!B?OpsDDraN6Y$hhMGpZreX at EHZT55)g5XC9+-$IKhVo2mJ|GNC zhKg^DN|f}Fkotz~W at ct)W_K4NsH#iCbJP}d2ku;A2!-KN@)hISOMz-xj-F^w;IBFn zy+(n*k at FE>dCe=RrX~R7v=aIZ+tl`}^t~^+?0sB%-%(YzEyACn!STPj$+5}1Nc)_- ze?Kku}b>dp#KN?&Pl>>huASD6;hzH;Z%|_RY?{s z^T78O4`bnPvrm=qIDZ4j8#i+|KKSSGMxQ`gFqJ6{KJ8<9kC^jLWD at inWAGfu1639t zlhd1|Z1S2PfpL^(D**7Ab>{3V&TtftQ^09b!JT%V_TFxfXn9BL2%jPUcu9c`(Attu z>jr_aP?{#nl7FAequTvXDB7veK+VW|=~+?SXRM$g!AXL_82}Za3<3OMvH6Pi{v_no zlE+BeEp%#f)f*X6*)o~9N4r^IPG2hI=R4MUQhL;YabJ}`V??+=(d2Q-})d$RZ^gzpWU zgYpFqprJa4yQ!e$A81h0PT+rNP|zrTt*7?~AlyE$!_(w_72^Ai`pFZRbPSb`fc1SV zuT?+Z%>Cx_xRmi}W{F3fkjNxn1fNMH2>AfNgm4>T<_BkCrzM2pyVKRpw%eT5OCxjn zj~36ED<{w&&J2wu6#Rc at _(#kBZYL6`^adL>l|@LZmdaOoi0)|lO+8PvK5*SOPk=dO z(PJ+0xn$^XU%UB4iBA~Lc?Jj#&Md=+oSjI4I%1kxwnVY^S_AsW+$hLEZ|evEhNO}H zl*ap|s_;kK;5Iss8v2OxvL~3G5D_bZ2^XG;6BJ7-R4Sz#$V#A?l1423SjXrO+8-c4 z%pasZfy8n%b;hWk;6?TW^}e9_!uomthwXnE`;W7;6#rxI9|YTFh?d1WE=*(O|0sBY zBwCNvm)opwxCqcDaBoQg;3Uu>AIf_Ck>PV*nV|?kk2{Av?sje|=Oi%5lMKSn!!khE zeOffOZnm*onm at Y%D636cUKyBJ*X&DlC|M zCyQL%h*e9uW1X$bxp~%JAvKiCxyzI^OT(M!`2H|Yf$+=^{%`wU_9;|veo5QAkY{%D zemd{%*Kly=Nj(q0I)vB?8$yJQ*h%!Cp}Pw^?XDJ2-oCu}S&i?4hVDKyiNRMh4UhPO zy#dIR-*~)L>9T#?<=3?@qRHCd-oVw;tT++V39tIOn4!%QGZ+Kg^h=-Mj+<;f_b0>dM=fYC}ybg6w?2Kj0y;Z{k- at ojsg`bwUp^ zp at 0AmK=HrzOWAbX#ScVxc)WC#@pp7`5PI3khaLVSo(IU2+#?18 at LeV8Nt_^ao9@DDfLc-*^Lk(<u2M4z2-5 at n=|x%li=|U$E`fs3`>eX38u652_TY3$QfLK$$BQkk|Ec at 7G^7V^i1s6 z at ci;<$&@EQ1G&Kj5M=+nXrl7&?tX8rXn!e~QatQJ7LKS$IJe~LI3J{Q@*EgEIqQ#S z?o7;MXoI6Ec~Hn+F|Qw?67tb+IZ*2{6F08_kct9ZXxiD*d-HH=vbc&dE>=x_N0j4w$aoR=WsO}0jQe==@Qo^3L8NSMfCDF|T< zq2~0_Km?STB8Dh~rFh9A{%0h32ESMYN`$sw3;~>A*v!B`d#X)im3$uu!4nLfFe||4 zsEw!YQoir^BbAYc7$9W`V$d&=ko*c8y9nX2){qJ(+bL>77O3*UBog1?>avj98iCDH zdK!~(>%L_-NN4xbqqJb at pI z1DasY4g~>podbTK508_)|2%#_ij&6E8E>Dim~kV at Fg?yp!Ksg17PnoNL;L%FC$|e` zI$JvxGVf}%N{Qu+V_u`s?lo?S|A6Mxb=V3HkF|(uVTNXAZyjNdccP(~qwh{f9W3w| zVnZ4KzgdaXJq#lD9hV8_bR!O at OFIYKnVSiBLyR*nDw+zGKZ6JK{k;CWHA3Ev@%FbVcCY7y_5-kF zYfN6t!LiW0MklpmJ?~4+r15z<&0oX5qGG3=a#yOo^Tc!OvD~K0j>=8>C5aqe%;>uDcW8SglkEyck`R&n!wBcmgtQYyN zB~-MkBb&MX%}2>%)7+^iz1iq{UdDscW+Du5o?M=fr^B1i at H+CI9GXl`M!|v~@0t=5t*mR85$x-kQppCI?e&LOU}%{)RlradZ{`HZU`{{KG~Y~*Hzk12LjnpDeM z!m3Li+a)!$+eOGJsQ$1?1XqVYdNZaB=$%Fd1ec7zS2XsSN<^Bqs6>}|-HdRsC*OsL z^P)bYJ4e3TZTxMaIeykj_trgW)*}d7NAO)dDc+yhe6LT&k{`7{ci*`{YX>n-a)ATM z5q{yO>@q9pJIcmGhc7$_?w2{+>HH>Le#o>1Te{<{Eo`odmL{r^AH_q?7!`dnuj#&Miy89HR; z4F+Vv$$KPm9Zn+FLt+ at 4&N+i6K|#}}1d`Y{9m#X_i{K=iLaY76h~#2h&c~}JSb$%~ zd+O(Y64$u+lpzT2c(~#b`Mvxd%Z5aIC(q}s4&b6cHR^+r413;zk1^;s3}1;)l2)GP zK6^j({I4%9qvd;(#Z^r*M^ZxFyA~ut?*RWn1e(%FLJ^FeL{lU|={2Y222%}N9)n+0 z1k8jm4Td;4Fj1C1MriZ2p^rJ)G(`Lm4gVR;~zPY z_H^9xOe0jC2TkQiZ|>N0P=>d&uvX`3~>i;DIAXuo>~kX-2GJ;r3_&kd)N#F*$l zN+6N=ngJM31(H8_AMs<}{SQg`#?LRp$Zqaw`PH%Wtekk9{*dP({Fd2){6PHyLHnUW z(jSm+6gxhjqaS9qrnTu^zW*Vrj`M#yr?ZR39X-A(;oUj$W+tz;`K14!0 zzCE7POU64ZYnSah0<4amV)rz zl8M^WEa1dDAQ}K7XCX1L)?NY5Jm$~|&hd|6-3nLT9xer#&{ItJ3aPfwA)eVj$1x|XBP~%PCIJWx1;$zb at 6-`idEm|82ZA<*aUaE*sYm+{25fi4WsF zZG8AL&^~gNuV`jT77OD`Skkeq+G3_Oues1H9%m!ue2?2KbsY8|pn}EEX+c5f93(*T z7o5=xOYO`)+^P}MoWm}PXhw=GV_AS_jTjwe1YaBEfuVYZ_KHn at m*3mE4}U;AWko?Gd*@JQu5c z-L-xzsm3#C!p_O;GmVu~%?8iZXGLrdQG!TcCM4?di)i=Q0ucx^A8k%*9PB$lv==7| z86hx+cN+kyzB6x#m(dIG5+5%Zr($%1kOa86@*h)Y^WM;3 at G$qiz)~mu?#N4*$6~{n77lT{RkDs z!eWBb%7j5l+^Ains9M?RCIgl>(zr{U{C1m3jk2Y(@Hf&)Ikqr=Zbm)@Rxx>*T=e3# z7B)9k9B5=fZ5^8gHD?bu3`1EM&@)=fr-AZe*+wZbpxR!ID{#VaB5c&4 at V` z3;LPO&yYFhM5V9{r+Ju(BEZCe!CI#@XSU}(L9?vH#eK-(@^!jK`MuX(9=Mq z)9GVD8yLXq=@jK+c=BUuI^g=pRwQlkf^&E`Zqj%K4ll{FrCn>I8`JlnrHQ(hsYn6| zqKI9i1|NS4aSC04!I)HhxX+4H58Yv1yOu$xx5jFEn6hM;7)SmQ|2Wu;V+kh7EH|p% zhy%e1(I;!m0@=QYg_4j$S*253fpOOs*CSC|Xt7Zv(nEqzA;1``$iA`e$5HqVzs4W$ zKkU)`T at 4n#l2v&9KpW`L!S)HjnIbj=MDc7r4 at TJ$Ir2p0e(i&g?mn4nU4sj(BnbAM z#JfEZeiOwiWYT)PFOz|q(Ow+Hdw;pdyK+C+6{nazIQ~X~U$y)^2h%to?K-^NKdIoi zXTRvXwYd$T1w!~hAOYwA#dxs~^$`8715s2zX0ZQcVwA>T(E6H{2O!W)kKnsc;vQcV zPrx}s6R*4MZ-=de$n~)T)7pHG9I#A?1~M8qFtPK{*ubeTL+2V?zn>i4a~7 at x~3r( zgl>zE at WJewBggD~@KmJsua2#2QMBZ-^l at +X|CDX*j}0BG%lZ0A1Z<)i<=;%?-N&f7 zB_%K2OjBm*Gd|}4;ba4*KQeUFXZTN#!^-~TxAGW$Cg!^NJSUGyao57*$o?MZ#>Glc zYWL>WwHltgq5qHCZf2{Qu8rH5sYlVQol-;>A1Nm1ji4mTaW0VN at G%~fUs zFKTKkiNHjYS;xsoc+~u4#{tl$#SDsp?J&)Oc*GMiGK%Nc`@_Z) z6S1?<7z67tk=DnMGnyg$7xCva at E4n@&IzN;%-6?B zCwmPvax~qh7Eki?{VgT(|0jFwbx!z?H~y at 6)6+fpj^~QJ)#j{sJC%{i50UaJ<%%4m z@*d+dUd99>Mp5it*9>vG>Hm4oZN=h_*i}WCZAhF=b?}tt^;p)bH)miT at H*^4L zBx?w|*RL7{5`eC at 4-0ScIXeMcc=2{*88x_$R$2T`uw;q!ULG_D+5zGKa1UOuNGxBG znW>GEsDdzLq0UJf6-3OSvet7#;k}0^y!o5rVbw%@7+P9X6e z2pnsll6*A=#e at 9X`z(6UY!?%ikuT&vkes~>c+?q2HJVm{F`%qtw8h198wte&RHzEh zIuoxg1PdIeypEx7bVD`o at +{L{E^_#c^lbUl>+dv{{u-ZeW+t`5orHU1SAi#4b0TWWO z4z46Xpr%A~2MIGy&M9j157sb})>1HIJ2dfZ)au47v#PN1 at vZ~+_MXM^RHWUBa9A)x zk|nbaCOkq)aV2LdG2occR%9_QD<&vnB!SFJ43y?nDi7U3#!1>h1b_{ ziQt at 63ZcVDc0)U7W0K->($hIH5t+C=i(2j6xaIaBc?cZ1geLn)E49e^o9E&vq{U}fR((;W5gB$+27HmgozI_CZUWL(yNkP%ACIJ%K5^MNkm#>;apRxB=Hn%@ zig-26kjks=CMd9^vrsE5CNmWr1U5CLMA|S at cA^M!p~Sln9VXm-eP3CFMnD{MMoWFF zC6gBBh;|u{Itnur2&A)U>xj at 2GZqt&6x9Yw`lX1cMDKIF(Hz at Lm5ivIVa;Wv3`$BM zZR|jX(W1v3Kv81H+uvLru5Nvr4hSw>^ZN5Y>EWJode7D!Bj9mh4r7jF2FNcYq?kfn z$}$L&2&p|stZ=M%#02^X^-sR;Ho%Nj?RhbzrUwu^$=sKOoTvgshE=~gBMq}3Y0c}) za~;eT-en1dC)?wX(eQlL)+{F|AH1$rKYB>)0pK6gcwW((=hW(y7;{UPYn+)BqLYci!us zCoO+Dd3Z?vH{bL9A4|{y9=4_cVSI;Uh#}@T`29V-K1bv|$6BAnd5%?R!qiBgwH_b_ z07A-)+X{1MMsviS{mw=aw3JjQel#b{>GYKGf2oou79{@~ZKNaqVXRtyToritzm}u? z-}6EIr!-y)2-IjGbC93(LO6c*XOn-)|8WX`&M6oJAVz;%fCux7rO3dP+4 at -rz5WIg zJr*4;slvhypF!Eku($X zBU^f443strL;IZH-ki1vH>~yaPc!OsH#Tf|Y;ybE;i(h*>LsLp2M1W`_kQPH=l?r2 zkMyCt{i?2b$MnNTw}hLRUF?=>VW$S;7AO5SIF^2wgr-B-?pdNr1__QWjJCmzJrqP_ ze}--mPKQVdJy>@>gNqO%NifNVf|XW_H-#CIPvv3t%+sTo(hNA0i8A)eA8GdT{8N$- zgW~W+oH&!AD*>qBP5?kJ0|$GvJ(B;q`{%cG+1|qvg_3?V#op~2S-uk4^+1qPXP{>= z7)Rt~q?qD9ue=cv9|ZzV7ch;_Py_?8kOAH;n7E%9fE{29xF}HmAi$xZKQTTUe at 1yu z_JXjDqM*zgh5e7 at Kx6w659UPW7;-93&>WFsNi10rYZOCamBd=Zlnz*Fxa6C*$r&SH zq8*F{!yfg-%*YymCIhOAPlUXsELHQu2sP*`tM{WdO at Bz@L+8fJ{yhliC z24R)qn2DVO z0hnn;k;>bW&CcV5=g;anoRd%ZaM+19kX6VE at ER1*^%0Rt=orX8VsiLA*m609LL0uW z?LNru__GUws`q=;e{c+(|0_wnd at oZ3rNgG0bhFpAZjT^-0z zO_G&JL_wUqA*9y04=Lt%MH9S}nhjRpbe;GfUT4bc^&A|*{HIbd$!lRlBf=1Y$Ea3V zt6KwJQbdA5Xm^U+ZF_CD($%)w+CNq=&)dU!;kA4dh<)FnnnzUwRD)D?oo`3V?(W`L z$orNFOvgmP9|5m!uR&^n)MB0gAIq(KnV4pDbIqSwJZA&MXUkw4-!6&F3QCS4OHC6b z8UQnn4&^o})*6jSg8TCPElAb3-tK!m%_khdKEs(Hq`>$60f@$9*_S|(m~6y!OYi0!tOR+YV at nysq3AJ<~$w|J)GyhLY6~*L=C~zXl89Bq`Wg?Qfg$) z2wwAc&oz+AvO!r6LnD>|#zq at LsXM1v6b36iN at Ky`@kc2S zsNxXt>y};cjIN9)fN49;Fe=Gb3vGhi?fO_mjuqmu;IEoU^+fTaW=y^i3t-%ut{_1} z`w+m1r%c4kCo{j*A4DlAX3OZLZX+)ZpFvS^P|CNoABOi75hzL`F=_H=h527f+3PM6 zs%XvKqGCUN=!YIKe*M0P&{hpM9Oym##Fa}v{_c-mb@;b|?TdNw%fafoL{pnvnjq at 2 zaeUBCRs*R+AKUQfxM4FG<1d`E2L4kFxjSLr=|)vaFs3_-VR|JNGO at +8X-chByecfR zohz1GbTb&@*_Er~Bdc at PF;0YdFOzWS>rDat;y1!skrUuGib_X_%55;9dZJphl1hdg zVCyp!kSiFS4$DB~49sdGP)a1m$KgEpLe{AL^XDZKi(jta`zODIpQOc{HFzpz;1AJ*J&HjYzBjja)50hQL*x- at IA0<^;t)3%KPVnW?1VB;^l-n>VeFdFd+=o zBOaS988XEdE^;>UeF;4tA}TOo at F?+Ws^S*WY+DKrNZ}q=Iu<0j<%Av1yV;wtvL+W~ z;t#Im9Zkk|&nx&&=t*#o0YN~>+(-E+aj58E at j8%bwsbhyEI9z-x%d))M&-t>U`z;u z=Vvr(|5qX{v1F>3C<+c at V=?Rx5dbC_q!}s}f(G&;P7G@|4qI+m9F^x!aVPxpjaxF*4XoX%G8 z;65{99W|$zsDUO at P**Kr4juprC#(X{4apbe7?GHm&EX{@dK{N~6l9p?D)W#G!yU&U z<#(|mh!{DN1uDg02bym45wj^mtH<@4gSV}Gajx;OWZ z=k=o{&5ZtU(bR~s2Xez4j;%)qjGr-*kJ@}Aw4NHdip_WAa5oO1tbWP3Fl+NF%w@%=DX^xr^OmfG2tp&J9?3Wp`fA&B|#0-24^bM at _a_q zGc%%0UGOqnu!&!C8==lnqQoG7Q3 at p63V`#yQ$CvwR1$HWn-g^3fsR0na;#{-{P z{QXrG7E<2>i~rqLTf|p%cmgryKWFwDZ`NO`~NTRVgFz2&7X;n%CqzsoUx`iP}A!9K5Vte=R6Z< z*y;UlZJgUWC- at zDnC-ICdV})^i(JCTAgS{kC!2u&pyn7K#mdJj>ge>nGj`#_w)q2-Wx2Lae9P*Bu_P{7UQkm$iXERiZYGmvSf2S1iO z+Mg2~Bf+p at bt~Zbn@{Nd>7`Hb`9glAkSJH`tTQt;z&HH=4}atKedoxvt!q$Rv0}xG z7iy}i$Vu2NiWMhNsU`<5T)A at R)2ow{larH^lbCcwlg#`+wozWEy)ib~n>I{pZUg<{ zK_+sAf9xM1eqYz{rvMh-@`J{R4m#x2c5oa)xQMJA^PXRjaB^TciGm{#5wrvM4goPp zw+q;10ORE2ZI$om+r?ARCRz9nUpA3YY;w(Zc6*#PFK7o+ z%5+HPsKH}v*-xo6JQvxWiOPMb)5OtQ+a$FZR()(Dz zyPi;IInHz5*0rr|w%EoojALtB*0r^3TGqB~eC^C;W=I3(J{QrA%cS!%ZR0urma5xb zr$OxY)5i6$E_`p9mikkN1l>+fO*neT{l^{heq&&^cYf?b6b?mLK7qT4^|}x^hewm( zJk1ZSJuN=vcd4>&{?$Gfl1SvKrY0#^z{PHrDWhk(w;}BZSccwdXFWNdX!HjXKG#w9 zf>t+AH`TWu{NCEroR~3+}=>GUJLe$pDr{1cUs(KlIc7SyA{6 z-XeDEAj|U|IZ)sdKPDOGHabpPA1i;b>hb(hJR)HG#w|b87z(BP1Mkx0GLRA86ssa* z$%C}?CO%wlS90L6|$Auf{xgF^4Lxtp_y(A80o;tLrx11~bJnKR*c2{cLH;yH7u$K8)%%Z4hxlNdO2V{*EcKY85bdoVjFPwFz>Ek`C{ z+Dx-FLx=R8%-AzUkVB3vp-})n6H$5hf!|t9 at lGRs?f0}zYr&}F z{n$#YJGfNtbw7bC>!eN8IC=Ke%EB1B{6Kbz9F}Vvk at lpLNrxu^!Gb8q-SR#& zQ at zaPj!YhfcdNCRdBYxiH&i#SMk~%VYyp@~xdK0m6M=kEK1eU#Pa!CL_Uvu1@ zuba01O^y10$vVjBsNvz`TH=feSX`Q}4 at Jl!kck8O1W{`1|1jZv2XTjMG1#jAW>X|W zV$r99j!zkCv|PwumI06J^^X8b3v(s}&ii8;iDf?`e*S^y>Aa9%mudjEdES>9$iIYclx zn(pv$fWRm)ftfl;SRbh}Tez#42QlXW$}SRN3dO)B`5iwtzsHASb_$7{GRU4d(`E-{ zJ=VpYt~7Fs74*Y!U*&7^_?t+;);4F at Vn~gGIfSbdCt`URfH{J(B@%)^d_u8;ru9FK zTa8~g>?4|&lj9Gc(VB}ANY26rVxfdHBny~i1#X^fk^Wi)PO3nWp|Lab$ z`VTbQq5jYFUTm3|K$luvNrcQYRK3ToEx*Mc+mB~Q*fFPixzEhf<1>xDhk1{%LKH&*kB93H15smU zRaY2dGfa{{7e}GQQJzI&Jsxd)00Usy^aCP`F)aqYn(*WoA zBEYf2DN!(jt6ab`j0`c9CO)()@3mcK$?{d4*jQevd2{- at V#;B=qBx-Md1=Mj{CYah zp at v|5dYyv*6^}V0SdM3j%lxkRM$10GuaYOo5}&YB0xQ9E<_!0%>oPskeZYea6?G5Gd8mTOM`M~{4b z{qAdBEVkFYCc(7;Zo&?hu*G$bA@?BpVSu(;{cX*ehRmI>RpgZ4F^dm7IkO?FCeQ3j zcws-3A%_J!rfV!^!O+|d+W*6i=53K@!9*}KRWa?H`oJJBny7+MICzV~PD}jnG)mkk zCa>SO;gC<7oQI|BxH?RZD_CEkz#GO(#CJJ5ASgk=JT4(Fu?!A*LQy|>(Piq{Is-Ab z#0&jwl0`?tog?DX=oUZ^w5Wg~AH9HBaRU5d`2Pn&I2l#}x52`3?Z5+C3^M>~`5Xek z4}g(WPEAOd{Rba0h-umAEc*#Qxyb1DjC=pCJa6(kq{;7(vV-Rb>jz*upSW26m-a~a zum^^K1Nwipf7k9itC&UHd%gz8((cVJ4 at A;v54bmV_ at TPT$Fm*GeKHzDf_{0(<^Fx7 zYTKfGe_wdoB@?rov8JPB$1d0UZT}|;d3MYa1`yk07%IhOm>~UzM->@Bi-2CZM~Hv5iLL$?pIejE{4N`a**|eCb$eQR z&5ba at Ri!q50QCEw9`|lJ9DCRD$B1k$%f=rI&7nUn^Q_Wxo^7D1N#@3PF{Ns at cUkT_ zhxr2 at zptBjT*Rz;j6$W0{D00G<@SHyzcfZ9qXp!G{FphwI}2b;A{bKCv<%}<7kQ;- zub2ZM1ZR|l^ORB?smpi(7VtdQGV3{t z+Z^qW;&*}U=Lr{Jz;5?1P^=@28gj^sO4h$>%G;Q#JW&rqH0<*oB3SrV=J4^)WFmVjL zq`!9Z`O?YzBhW2iFZLC6DZnfy5fNt&5A14KfG>yx^#G|L`~J)S+xGoGPCzuJU~(R) z_C8xrSlS%y@?=Z&i zkJ*9D0E+-QoB*pY$;t8b(aDf*AqY|7uwoAqF_j*QT_rGcxFi}t=y{OPi2 at uQ4QlfJ zf3w++tj6MW3nmrZ#yqPJ`r~)KvS at h!8y%*_SCgw>sodF4i1|^?_zJ3~pNWdDsPXW8 zb55$}tod{^MSDL($Ta??LwG(p2wVyQkU)N6j@{d}1m%S)QNBeJ1C!0=<>C8?(M^pv zky$ZAhxk_+lca?>fkM|-gkC`xtcasQ3gzdlCMu??Nj+4BG&lFP5};jPxZcG9bJys9 zKkdKvaLqByOy$@LrV+?@T&O)u+)CH*U*Z~mkVQ3t57r;Gi4C1|03Hto-p+Uv0Bry^ zIdEtU9vZm#Sr)S}^liuN6T{OLauhibDz1bw$5bH}$dgl!Zhwn_;#lPif`+w?B>s@* zYg3l~DLtxw0;>vQu~?~C$6p!Gg;yqJR+;z_QYU}eBuqCM%P$+c)X9KgH7;|4ky~*= z at gH#@gtT@;P{umZyqB|$X(9xgE17I8TB+F|daLyr$4sjt*cR5RWQ8KrX3^IvOkCDkZt|yTdavT5vuXTHT!4Q+opMp;H?O4bGku-|FNpeY zUXWvf9t at 6i@g`sb_YaFt^!K03xH&*MkL%0Maq?^vN1HkXwWbN>@jy9FX`rN%P5#Eo z;Kp&kUA8KfM>Mj13h3GG1tYQ{AhpNuGkw(v!cC13qVik!NlbYOJsIHSz2~bHVR#mc z)mF!mdoY!}@p+?6vZv`dK4ALKw2$j--BqJ^i~3xgjvnKg at pgKUohDo3@UMty*kZ$t zz+a%dDmsJZ`*;e+_{@H9DKI)TK_{wVhyx4KSTI$~n(r1lWuCIP;5$gqGdau!!L)FJV0$jgKE$ey&W^N15TGnNMwAf6MoL-&*7e*X?4< zX7zjhUo5(7X5rTL5m1)C6*EZn45k2pIq&4s8?XCM>X7ZGnW-aZFKNR`r8xxy;XfVV z4gimnv?c>o?G|5e=I7o}NsiE;^N15b1C%>MMh7$XYxrLzwRFvY#Ld~~`d2FXvSb?s zvZLu_={Ajdz&&Ru28z}z;g(%-4lx}d9AO0(kBuIO?)dHX7156*`H=2;M zCs%aE1_?x&;AV$qiTtjlL3ddZsvKQXAV3|GDiC^@L_9}EUWpLv)hQJGb7P+WN6CHv zm!jgsx}Up~tmXLCDOy&A+#o+cXD+-C+i+j-`uu*A_k;A3oIj}LUvr|*8U0VZ|3J($ zAfkQV-$c_TH*Osl&EQAsedkVe-w6*SS6e8adS;>(mhpTv6qJ@^W;@EIZ|<7Fu(0_` zKgi*@WDv1oGAdv)AL2iU$9HdnG1sHbQLYWDZ>D-Yx-`DC0>ON3o&yXFZ4Bg5q;L;5 zQe>8^^BOXLn#{~Vf(^)H><=If?w1yUt~+^W?L%PfDB5Sy$NE`amW-A}TA4E?XfjA5 zuV=sjD909Y!*B1f*i at fa;*MC?D-Gsvi zU!a@=^3E2g>oRJrJN=>q)ZfePl6aSDGz5YLN>Y at iDN0h5AUEm%*3^=G*^q!P=PZe9v;GSb6p4dw|1ni0?86M)2UjKPva_i6nLsVN zyBV38fB*z$QiNa(A)>0Pilf3vuaBtrO~KgQ48BocoHR&xX7`0znU3CYG zzsmw}GXAKvR^?^{mS!WXA3^zlme25LC<-Y~ zh9?92zoI?tf0BvdK!TNr#xI!pL*fCDfsKOV?D&Pz;HoY0arnO}y_7W_viD?<)mN4c zOMAY at m*g(ud9I|cyRNORcwIS!CdmtJs!At3U-5*m`yZO)mU~?aLKGN6dmfFU?lPVb^?coc2 at x}#0|&T2Z>cGn#`x~@!@H31 z!VHi<+RymX>y4yV*&*dtKL$^-K31+Ies81+g>(nnf{@L0dGd3zd^@pZI*yv!ODe8c zKhBdk2P58cBrEu|8<_H|<=bcIyK&)b>Bqa at PqaKnYVT_I_4J?JjqtY5cM-SqMIZv{ zSX~_&6c`uG46#0{jF#oI0iNi<$zgLV9xw8iTOO|Mfv-kt%4~E%V_Rax7{WL~I~L8A z;84uQp)^6#WQ|f6(+ at B2cUksv9XInizg#EVu09K%O=HrHi+g#$!@90q-P=%a(571& zNQ?NxduZ*?*0qA8Pi*s5+yZPwJR&Qr?HXe>_CLZ(XAa{h#IK`t;*ly6HT& zXKKyU6faL*dbW}#)K5gHCjq0)7KabeVtP2%&yOGE)Gj!|@V44a3^z*7Tp at 7OI(Dwy(Guz_NI5Gif)I%s3f*NQDZOb3ih!?t+|3Z0 zEr&my;v|Bxu(*Igv>4?y#+4?|7Cr}|$)lp36){t}$?(3$%Nhp{B8OnJNd$zAgX$jQ z-8ZyB!GB9kB=d4Yo(AC12-C$@dg43pgR%NDbE8P`VJB(%9OYm)=5!Kn7QK*X%ZvkF zhz|iqjPOWE;2Mp(;QNGmu at T}r&NoxTCscSY1^o#b+jBjcqfHLpIUag!l&(9Cr1ga|^x*&LM}#spn~yl1E at KJ6PyP7;Ena-hs^ zCobgUG8GD#9`nMlEKF at DoVR`X2oVMPc>cWEmmm$?z-$;)g|&r-()7b{7>;5aU^gZw zGUg<31`Z4YSSpWrQdAcX8v+M0L1I-!be1LbfI2;gufT6K5;{)^atlxfi6m$_Mk41< zl_k&`~EM4#xwBGK1|wxhGLL&^U(=A;SGNZiheV^pG at A=GG*FF_(bw5wR-} zGo42q5x|&(*jj-|&U7{&4-(<`$sxEn<{L237Rx*ccCBhMWwbDqei=*|yADTEx97Jc zcyL~ZgFEm8iFn-*$f70~5=)jKY6%_8kki%rCN=_WhJpVA43uDpEIF2ciijP&$e|aS z59u7-S3`(zfMeX!8canI{bA`5T)?#Ae*k2Q=yj9$hzCjy7CZ9eER8Z`NXRF`rctTQ z^B8$go80Ben4(cA-Drpt^gwA8p#;f!#Sp9*U0-owJ1+rXWs5gr*g&kLuu}|wamOd( zYfgrY2bZap*oX z+G&rmKVeR1=xQ-3^2Lh^FK*h8F;5hB5&hb?2d-y7VdM!Jay$F^rrfNVxazFvm!)(&066 zkug(cL;6Y(%|0CE|k zWGEz{hLR|h2$U#7fhk`J?e`B^>hu~tC#;N!@Bzdya+IGvJf(;D?^O&C5FcQFL$H9P z<6e?~(1?pzP#}Y-i8oo`ol%=R1hSb7r^rxPJs`Xu2g%x4a)Yi$V at 3>({lLp$lFTu%+B%*d77Jl; zIxrayFl)GUpkyZn`HUXHg(Y2k48~I2*=-eq7&9N$U{w-e=Q0OU&SWnr19_m*D|uY8 z*x=yt{}AMM9B^{M<#MfhA-0go>^_1_o(GAb^JA%E4h;}&Nb9Q~ytu^FIlRz7 ztj#yXPZnqF-ni6j7mWQ0)%xtmoOsdknA239{#-l zTKL#JsfP?kC?spvCsk)G6ybq!kkmxrG#Sl?aGY}u6Z%Y@&kq)rj6`w?*ChNweT^?Q0Anb zDvFZ6cFBoV6-r at -I+0Nvp%H at IrN>ke#sCOF_rEgMagHSFy1{*T?W;Mr at Lw6xZFh***xh;@blBS`ghUA#)Xbalou6!84Bn^=5t} za%Nz&j!t&t+q%xSSTQq>2#Or#7n0MhTGleYjOf03=C(ETYh_e+E!S7+&N}a@>bn+X zzcb;X at W&9#$B?Av9Iz`%1x+=%2%N?w$I at qA1YLsxf?|LiBZR349-oZLWl`EVWt`$G z6ir|T!$4 at QH$VZDkfalx#(=X>;?LMtX&mP^9cPT23hk)TO{Bn2FU&mM-`miHj}rhK zAOUjP2Nhbsk+xzw!~6d7~UXBvrqJVEAn6_A8r!1(PzdrtM;rO-s{8MkVlYiiNl6~cM1G6^axc+5xwj5OYxP%&3KO5Jh+a&#tqI=P~&&A4}TE?3?>4X4?`*0UG z5zyp%9f}~pcL4~w+=B+@<%m2)P=^q1dJt?#ZX6rZ}h65dT*}nEggpoR> zftbb1)nLun#{S5qle#2BTTpGMH6eK}Ir3Mj!_{&FxP}lW1R=NNF=(cQoQ*NEqUW6{N!40Jx~1v*rMraTW6 at j#{3zrnK?s_pX%XRiWLNZcm=$*M+X+H{%js zRzsS7{ws&^ctX9Uh7R0 zfrP?g3!OLtfJ`vRz^COtkPoB-!_wgw`H4k8b%Xmqix3XdMPFNXf$yI1gQH?8feZ_K zAy-PQD&7P}QFBPSD7qj}^gyYuJAa}4A@)o`6X!hv?K!}fiKnz_W&?XQL?i}wtafvE at maJSv1Vr^}AuXMkh&r>dxM{#?<{7;?)h} zFVXA$#UADEc$F>gOjPa>^`Ale*V at g}6`cpHOm_)|0qWu^OF+5ROqSVx#xnmf{ z1Pt-k?J-6%j3JE68_urVwX1Cyr0sM*;jHUWh>rEeGSj;sKUe8vs8_lwO<&CT< zbd{gPg(L1?q5q5fET6>7Z^O;luE^YgT%r2#_M~9J7~ZcxP(HIvs*z7+KLmSsxK at 3N zf8_U1IC%q*zhr*Nfu4A(3WzA6s%=|2KMRMzi6nVPy1_5$es7Tw at q{r$I-|~a|FS*d z7h!BB|C}E at gQdrJ0o#$H!~C##yCcN=(0h+wbS^L at xtO-vcB9=%38rU%GqYl1F?XURfWmn~S$fF>MiBhu_HA at sl`D`A5`S?wh3NcS9Q^e%9+qWmT zhi9&K-ou6I`=gEqv#n7OtwU6#E-7rNj1hv77?MI&5e^^=!~>G$FqkA)vFYYFm-PD& z;eF0mv#OyFvUHF?etf%!%>8ei*VFt1>`-v1K0ytq*0zXQOJqqFP!xoPL&4+N5BP_o zzOohD=iE^+Q?NXz>g&u<*7GuV2A$A+grPD!yW%8!B6SJCj%Y)A0aQsMoI?>Il5ps1 z5fs`OkYxK{dfRjF`!V%_@#d>nB)*NaVneD4`>E#D^ zfFi_B(tw}xo at Hlr* z#;EA|V`Q)se00=<$zdp>% zlURXH!AX0D7}HU8hlfss$f~cAJyIL+etb}5u0)aq>6rm9R!ac}02lxnQ|2 at B$3P~k zDyD%oc at _&=tQBO(`2V2z`i2n5VUFWUg*`&8Mc9o%`O%d!5!40?J!`vIAkAU_%gSFZ2HlPMy@?7y#^-0!9)Bx#U>YPaEyR~%I-0z zt;8j72^ENZ2f(~vrPEt1mP6=NEWL87BbXe~y@#i=&IR23F|Wz-uXWQ4J3f}>F|%`~(nM72j~KbOyo z`LB`t2!UtV`2OE80sKxf!^AK#zFtE`Ya|cjOv9LRo`EniM+xT;&cNoY(0pk#ni5QaTMW##rCsrjDO6Y`r$@$isA{v`->fKwD5WCE!UTPIA%u!{{J z^sn!tXsx6EpUtG(VP2g_I-46qx&Nldi!EAN9^K#VeiWM)DHDCO#z_k$Bv0htCZWZX0rC-1)|3HQD(A-eOuXe?$jP(Z91ha6^S{<&;`=Bp2o{J($M z{d^YrLf{Mt2?G^GKocqRQcUFYMeyLjAKe0XqYfiN>H(jRA9$589VGGm?k10 z5D)PhUK4=dSX0;Q(i at d2Jp&ovLHwb&Z?^5_$6`lqb6s@({;I}E?>{HXJyInEF?sz zpm31E^kMgYNT3Ull4EI?pj`h)$=u~V2hN at 7hf;`1xe_W#@V+FDPxUZ^9@>a5wLKuL zPmuUjz#Q>mnKC&- at gHJK5m7`^8d8X>B2;OpbkamL*3yuhsWyt6K6V7_qK~jfl|l(3 zUBFcF7z#KdwIfVcLT|p%rH3gUL%=B`#gE(O9u588j+-u66AQ*N0duAW{^#i+2^rA^ zV`_|ICNm at 8q)e&(7e9I0f1+%9kKKDc+&E2vz(ONx02;mkaGT!RNzF8qRBxT2ID{Dm zV8z6bP>JJ4F64<$xqb;GXMol~>b at 86S-TGq8R$53)cCcMJw!hLi-Tz^Tg7N`3D|ATXK5+WuchB|`? ze+^K z;+~kUh*;!0Lm8OMeiioiEGcCQQj(OVDSQuukB$XAf at X)3nCc32W-O&CLa3CCDPl+!AOZm)x8k(4 at qJn^&h6lo~b!2p_9UD z9dM7e(`#F9+>h6<-ER>&CkL7S55OItG(pS$F0-Fu+(|Acal%J%oz`4*PL1f{v9OWl zAbCiQq at IPK>_g2KYmhnha_=~jNqh(0h<#d@**?yXN)`?*U|2X1OHW^HIS{auX+vcC z(UT-idcGej>^GS)gk;6Pf8#cdIYXye7i5fybgL+z80RU*Hhfcg`OQWXlPB^_S&5Sd zv{tsz?UFS9!}%ENL4C*{HZ)G32`oakGk z0AYw3SqwQ9gRDk`7U53G`orh_I47D1;g1tP zABVxG^}=!(N@}TQ86kzt;0_|MH|592_J_DW$`UsV0zm{LN?`}RwJV2S|8c-*HKj{< zANy}{6S7_w9uSlD%60PqZUCMq)23v0w=vo8v8XN_aF{vcG`_NNfi%t4Izb9PqNY=o zP=1PREmMvsF-6p2IhxixhU~Brf@*$&QARY&}nd$#u7Hb<0ATk2Pxv86N2no zWLid4aa)*X0z?e#f;fsyZ8*g+Wrh%0lN||A_D=6*a>L51a{Lj0xpUnJpa;iq at n zs!1`rj0IANq9rg_5>OEP6CX+I@>G8%TazFeguVml`Q^E8u5&pKNe4pC+G;E~iRh#l z3K_wzQH&!o2&LUP8xjELCJg8XJV^6TLi4Tmr2%2mwfg|}H{3vo$HX1r@&uA3IHLrF zknILP)UB at 7GiZV7k|VM4_WG?=sEHn_5*mt0r?L1RN$^jHY%>=V3|rGQ##6UgMGPSM zAUPoDPq1(ZdF9eig(uuL*du8)G3h$n>Cye1)4F at de2|X^11B}BU$IhRmF@?!LQU5v z65N)2<8VlDU$zhn6p=`P&Qt-ou>4DxoV97sOB#tvQp-^( zL4w&B6qOlOZB(Zs_S$C+0C0TNF3vUiPml&8#Ct+&jf;gqC<*>p2cUqy z)8jBa9yH05Gg#(PERe+n%-1vao&qPZ*x%kfr>Cc{*}%9rNabb7#5cl at 9yfE2FI94y z=Y(eX2ME{$D at 5Yx5ZMG=oDnia$f-p|1*2T472#kqx`$aOi&&GH$trkOCv&=Mv%*I3 z?=g4U0vOl?@x*|Mh9mmuK5m7;xYXh8CvgV;anC95nY3AabJJ(8Yt(r8F7e4<(L+uj zN)B*RtsxS$s%!xwuclN(PtFX6J{tE=OK4`{vQPE}Zmwa>`mrex at z%6_DKNhDQG7+3jJ8zODm zkHk1|>rmtc^7?s$1Q?hZn#Z}2e27X2P7)LlMkyj-ESZFNUIc_OCq~8&!iJZN8!I>= zjNHSCBuZ@@OxZXq5FLkIgct7q*``OOmg0m{_TK-4DMg%MGGibZ z0e?US5Cte}Bx=bfDKro2pxOKTCU3ANkkDXd3k<->efO{T-*fr?2N5SX{h-MdFb=b_ zOoJoZK95AHABd4ZA0Q3~xv7d-196qy%r=rEoP?Sto(9lAv-cdF9sk2PbWZMCLXuvZ zfxs7vs{%n0DTI;$h>;V{jaD96yq}xr{as#Dx8GpYf%jNp%^&i=^q`(H2{pxQ#RIGW zU`#Ss{NEFNo>R}PosFzmU;~5cyXrD6LocwQ^&?UHL0XN?vDQOH2b<+Rzzq+4?_(fD zKG-xz#&|$e`cGM7^unbIE3(7!c9*Ew&;g94_ebsy90jur<+3?Uh!ggYN31*{_m4v- zMm)b&>^YJjVbNhvvL}S!ny*2Skd%Rhj+86bf^cG_y~`11LLdl{@~c3f&aZK$Ltsc$ z{bX!B2fY1b;6G^%z at SNil0rll&vTTFy*By}-_9>$Dg#)|6siULl72?- at _S(KcK*z@ zZNFrE2|koV2|#s6%OiKg|2xz5k%=7O@>9tHM9gA-f&L at Y=?Z7Yi_1p6stJw}FMW!2 zcLeaKpu^!I=FC0^TtTwzHrMi(^d$b7HhFUIM$Q9wan{?GJBE*--9|%5CKOaAB?3YB zbFLv|I7*CL&gs_SgfP7)cFE=DtfYd=Efk1AP_RiWD=0)G1WN#n!ia1pm+}IHrs3a; zIC$zRpZDHE4H2 at IemV4?h}id*B%2*FNo0>=lW#c$dJ;vw86FDj?)$t?QnMIXavqwSpi>IK-l;5{jH;)~S&~hbKU$E#AV@ z9`p%>-b$KPQ!8r8sTkylz?vu1C$K*FdIPB3cT~sS@|qOV5eK-FN=Y=kN-h#95fh;Y z^auL!Lt|kC^}s1{MwI_jYd<+h_AKAh73HQC{(lfKi5(CA8{4mlkMYR8gfu|U;eZnl zCb~@r`F)*763_X;ef~A<-n~qK^zyinbEJJ|0+^&I3yIQSVV~`D?KK<|9e+oSTjA~^ zeYCEHX~E!$!|vQ4XN}Fy4w-X|pb!xUf`lEDGTeLmectWUr9gxf0XfuwL at pd(Lhl#b z=_W#4g=QdRHjJc#A2-a}S*MM8SqI!IelIt@^1F>Erk~iq-`@VeN8g+>6&hps*W&9`@b9{!onn}7Y)pxY6u2ZOVJ>SDvQslMQ>l%}+WVU-KaBBw zCj8+_R1NNJeVZ^SqQ;FhX*QHouZGUc<0$c0I~z7`)4|RBTH(x$In7L#T+BaG5+|j| z;(roOuQ)?aPB5xW6QNv at G_TFwuks=u9e8iCi#H%61Cmo8$tUl5!N=Y|oJ6Ldx$a~Q z+fr&$&wobEs<|=!P7L>~{)Ufp^LhDgZLh8~57HmHLby6`PuM$&>LK|CUnm?FKOB&c zcy9PfrxObjQDl;^aGPwll0QV+MET#o$E8g^G-3)(ciZqCCzS%sG7)f-0GUoLV>&l* z!9ajMlNj4y4fiI`d-)vNHew8OCKH}XtuV0s(ogn#q8}nklhxR0l_y3R23Co1)M37` zpw1qMMKLP`g-J485|~H~S7^^;XrM5Tz9H!`bO`;h*}_5z~d zwO*V|I?55DZU8wJR3iY!oB>3*kMw;()Sj0k9X=91B at 9{XHi|8 at 0a8H-&V-C^$hn1h z%E4wgJ3t;2=gg8SlcF4Z85slm=FuOwxg>cA`2>;jeXrU3lfT at 57D(mo$4d9o+_`SP zwAv6S$k8U^X=d=I$!1MN(6R{I6Aco!Ni2|!q|!7-j75f{3=xWwSr0kj_kVk0Y!lR9 zkIN6KU%311kqgqvCk8%2kh%P)#zKL&G6O0+XPm$mzj at F4`=>N~Z_xN>pWoxW;f|Ru zni_Ft>eZi`Qy2@(5F!X=K at s3??LHm^z0{l at +kY^&v8 at os7!(j3g!%t}UQND^*XT%Q zWnxKP4sX$3ZKdJfjA?z$6qqHERU>z5Zb_q>Ht1pZW zv3tevn*cr|C7(Bvq2M zhV at kO{L|9>3Unv>f4{?_&`)N22C=Q-do5SVd|ofgdps|O%kdGBP)Ec={z2rXaq)Px zIhf^)f594b;mR?Jc;VC7pJ}-L3JQ2c)>7LKNhirFmX#d)G<1h#na4Lax~ok(YBbW3 z7*fw at cy}Kv3V8%XF-r?de305wi9rg{AcG4mu%#&|l%5tDTnW at A_xCCC3RM`Wh)_bt z`W)h3iuEApEuL*TG*cl(wx00R*Az6idkhp+c{rGJUkzvp|F_6 z8YjatCMnEJ1vvMV?y##u!i}rb7EUakI_ at +9|2JjkqkMzqZmcwK zJdlViMu~KAJ`-CW911rXG*mc^li=}I6&Ryo4Yn(hEIW?FxZx1&=A05C5d3LrVQE8Z zA7VrE5idr3CLjl_Ay=6B$EzNCPO8S)MFD(q$8+r8_O{f z{aGVP2WKOr;abzr7_&-MDe6?JY2%Ho;7KF7C+4ssKj7IN54Ac!0^a0v+;pCVBv2Qs zM9GDLVkp=$M$k>bk(DEtM29D5t=Zw}k7h&^wzfso7r_K%np4}|euWXC$gmNr7llZ$Nb~qkV zKgZw~(Xr}fJ+&iypF?#Uat at +R(?r}qc0}SFBR0ga2Om$A|9|}PKUw;6Pl z(#)Z{slyS8Vyq|dIR`qwL4Y#|vq6yW(ekdp&I%1MMI57%AxO}1qZQN}N%9*y-CjQX zO at F(iw-TumMNtSrAeH5C6gI3_0O|P89;#f at m^s{m_pXHBO{JTp*1q$Dhvbraozvd2 z9_)#yoBBh>AU2Xk@4nA$|dM-}Tj^0&qL)C}f1L z5L_53;0YJh_i&-#$w2QEPU(q7h)IV;Y^|DvC!0yk(2_?IPfKRCE0Y96{H}rAQb^&; zx!1+yxg&RVi5icAA8Vt$EQ$9lkol5XGz#=XW=1R at X}I)OpChWD8}mETJpeRLAk+eg z=n_9?Dk{o`YX(sn9i(~Y&{!_Z1K;Em2eCcv%zH`u1S)H)@&@DVQPC#vG at gUHCqfc34%_!Zhv#gM5{N{94* z4|{Tn5&3{T_zI`hz5Vcsltb%e4qN0 at w2oEji!yv?p7M5CBgP>1C(b_m>xR-k6 at aBxk-PaHKe;>K z(0#muhsxnj_#E98sS-RIIXoPbs8pe6uqenrL)g!y`|XuA4*6_2JmO)IC&Zr~?cz at 9 zz>+q|lVJN>YI=)Ru)=*J;+uRU at t!0FW*|BgLnN311`>OND5s1e^PKwoi>cIS;Rvb4 z>mOgV;5qJcJ<}FCdd&gFK%alB3`z7(G6%S@{!b(G@=oV`NY>FL$UaWVIk!jPpK0>; z^L3AuJ*dwAjZm`^Se)7Yy0kBWeM^&sge(z&|FE;dzUZ;|1$|F} zk?*knN(`oDnFv0=MER2)9<$$dS*BgGU(MJ!6j^#;eKJlQK9(r?KanNeyUPbj`40)p z&Zaa6$YyksG#=ksf;USU3bpn#w}88k%= zo2gb$^*0^O=I0D!5x%h|82}jo at USsumN=8R?_<*4G=`C?G6mANTO{5 zyW~q1f}x`T54p$#hRhA*GHnwq7*V^LHjM5go*t4|MB67wB at lp=ph59t8(s+3k_1uiqiUPM$|Imh;b(agrk4V|4ED>Nv3d zNc+&8kI^~Ktz+BI){~RQb~uR^>Q~f+EJMXdM0O&bynGqWHQo6)=&~H0o^p4&lLMis z!ViihmPzry4(`KabExIaQ?yq}ql{*5SfHA7f)A`%2dmj5ZAAYcld4XyqqH4?s!CK+ z!71wRKIiB?!)?VAq2oOT7M~iyCc4sS>p4p*S-AC5d8Y8x3D!d-L0@#{*r`=&s at C>o zM;KvJ9v6|dFx3$QKtwTn5C=RYYdCOxfSL|M8h4on5+Wf`iD|NyOHl at 7$|Gr{Xo#~i zSvDlwG>ui|q%j19N0){74%g`+UtLI=^I^mLf4|-NkGtR2s2I%RO2F^lFx~W>$5$?X z7Byve*dwci|&o`A97Ond;XyZ_I$Gp at WD{<;FS6s zK9u4oN7LVWPdq-p#tZDhZ>P2bM7t)k2Hl+5b?DmGg at jWw zJKe8{>INt;1_)Oi*j7!WG0r4r>1haTFoa7VxLw8^MI|;NgSp;qY7N5!n&8+)Re3sT zr7bE|RfEnr^3-o5XsFbaYK%>bmN9V4gEvKbE0fKMLyJ9Gn)E=KxldXdD(-^(PbUXv zbUylW at pts_)Rc-Nex!M}NZQUhCO9Fm!Ep at 7(Jmy{XCBU8Ah-mykZxc~#R-`NDRZ6< zzIQq96Nq?A=wo#*H?|yA at KE?d`5hu>8*5IRv6a4Yt?I3{{YKMA z?Eg2-=uO?uu2^fU3<%=Rtm=uTA|^i(;UPTE-kmiW=z|(IkcCmoMErDV5}ktQ+c=@X zAsZs8n5l~ueT=2O&EAv2LIc at I<>9=}?8nw-J#3Kr9nOYN4+}C=QSD@~$dS}T`{%8T zS(6DIC3Z+Uk$@nuJ5uZlrw$@I-##=^N8I at xgXGy?cvnx2HHz{NdOi6AAZS4RvTsRU zf+3Dym(+J2X*&FPgR&v$jtmUKf*fwATiYZg+vO#|x}RXe)QTSUX*JTTd8|rvZzN

PQ0s4{;z6>Km(y#b^uS-x*>iLrbg?}x4T(J at FSqSM#!6Jq}Og#(p zrAYWvlfKVYCmNxEOKKoLGGO8>90>g<*f4p8s4VT=x9Iwa8awh{?~a%8e!?WajrAi~ z|9sOygt))T7`uHen8+U(=aZeH`*N1gn{jg|_<*_%kj@&r356m6pq{Nrv*4p4`||nk zjj_iUbhB&)hD6%R(_l+Gz4CBD`@WkW?tF**jgztDZXHB+ at 1v0E!P?7jT8X(VF5&OL zCi#bG*$%|0%8($$VH<%|1jGaZT9`UL|F_jJbbh?PwY?d8nHq)!zkx~;Qb=KvO>$T+ z7I51X(MjHUUDH at +k^Cny6g?yh$nQgt94wDZ+}HK at ozi#Com2X at kWa@7vQjvK at H2*h zEJ_4>@p*tdi?V1XCNV9TPWkdA$WyDh?>}*luVV7*`J|s*B-0}yQcYxqm>CIZ2h;k> z!3Lo3NeZ=5!emnc_HPyf_ZSIbNu3F&`k51 at ko~V26jQ=7Mpadcar3c5WPp4~zC}WY zK!)^~jxvfwm`oeWL~ThLdJ0g9gNY&15UuBUrP$Y5f(iDn|5 at lF9V&8sI=q1~ka?I6 zJu_tYy?^E3WN#HDvJ;`{vJyKIph?JBqO%?nxD&X-qsGH%aA>>Xpiq+xIj$n at Ew0%9 at hOZ+>mMRepZ%Hk8O{q?Fc{TkbV=G at DXc zV4XskFt7s%huG~qPvke=At8?!U<$pXCq%E{1$Ihl5GZ5>3=6$mgbiY5sQphljUBoGV6 ziwQ2_EXE-_k at 9TZ+b1NnTJ}loi6arHr*G_-r0J+$DjZ6bIjda^-wtjM!y@{>v)-2> z(O5u5UJTh{Qri-&e_6|kKka|aJv1G$He=zKYz?ugOON&TkKUcVpU2G~v_DVN6jFki zKBf{)0b}IeOcfWKhCC2libqfZQn3}bbf at HUg8xpnyI;nRU8W~IVG-)xAh7AKVQ$6lwBx8BZLGV!IDOU zD2HDrK7tFVpd2a$>BO070cu$?F3!-+R8K)%{r`EO+yEI7fS2zWOQ>X!k2zDMCHH>O z(%8qW*c_<~5cs4Yqa7#d1rtJ at j(wQj68u%}Ot_1fh)vFnaN`aNxmhHt zY}WEa at 9`rPvcPWV&B?!|t#0VCTc%}H%x3$X)#D8{t-N at h=>T}7y$Y`2+TKneWjkWq4hR+J`_GR z_AG-S`-)(`PO2MNa-o!gCKzO#gM<(QoJ*YaT)h_mKg{~18;_UXF5rq6^f^Iu0?5ZB z9`PnwM3{>VnL;E(dHsO)dnlY at VI6CXAenxDah#TrT at wey766q(!B+%3B!rEf%h&cO z9VWKq!|++-~lj at Y4>{98P6Y^DHM;@=vEZ;c{$F5h~-^|?!4O< z50k)=0L66Z4#l2af0-B?KE95ncp&*QiFC1$W91GhzL{;~G at ZQJ4>-Nv{ILFC at fi5V zB>#{z9}o5FQM}=0`H<1=z7HU&!!jO8<$J?g-WYXKu7af2#`c z0Cj8dGp08t=9=^rXo916l1#9kS8Mwo0f80334*pY65Q4U4{UVm> zs1TZTCk7*1!GCuczLNDj98f$n|8pGWDv at G(Kb5nMW|_tO<$36crS?jJi%beOkOngR z|4;6kjsxHJ7#Mq}2Kgx|ILv_6)lXM{D%caWfOlNn&`QY?Mz-e1JPtk=Db52WiKG1%z){Ry z436;@Bw?MI_BB&B1!4*as4yr;JKU)#?JGPW3h>uoOBT%RnT7_wC05}kYz}7>l4S-2 z!A4Pxq>2*2REYcQrH0aHL~n*9UM232!=5WAXyr4PQ|Ay0>UL#c`(gf_i9wp|CI|4~}49Q2l3sRvaV~gSu`) z at V&&B^-c%)p(gxNNgrKWk3vG0NQXC%UC(lVt;9}BiT~8x`BH+wqr#ywNB%>6;k@$@ z6Z(4UUfNI!Z=IY0k`X;icyPdkqp&0Lb~`5r<1c1_$>++b#w+=+$y;p1=A#^s;Z02` zDNp{a4@;Iw>D at 6XC*(p%?wpe!z}YbX^|w+j4jiAn_VNbdK|w>Cw3f-+PExpi&8ei! zU$e(niUo|pAwo048DxupNO-kc`=tYc5w$MJWadcTUC-U%wnvHzG*|A=5< zKCsYWP~s;ND3%?VdLjQ)X*DC(r1WYTYeJ`ahDI9CI&Gwwb5>JQe%H>^VfoI7`~m9+ zh$upZ4Fd!4g$6+#7)Sh>m<9UyJKw|Padz~|a0ZHBNChN7(7=Rr&HNEz=U?9YEGRms z5KyAx9giDQrY3?%ft1XXvcu+ihC-ZRD at J38{|ta~n|}xZ at 7B1ug;DUmjX50T;!T at L zfu((>q2CK9km5}T5fkJO()uUgIX_oPA;_ at NajM^gA+SDR=9h+YpU`I~6vbrRqVd*M zBZ_jPy#JXYPM`cAso{p-i&^`W^fe at pJ$}s_QA%ry9H1Sb6M#sWXun#_9AUf)*a-nT zIKND`$+(Vk%Uf;SZgzHhKQ{76^d8gWI6R#TUP}2M817fEt-mOGn>UL1p2M&s{a`x) zC;$<_0FmIMDRFU~Bo$8H(cP{y at f$BVx*7DyQoMLj*uV`7wdP?yb7S#_ljcy+RGi9J z)XcJ_OBM~KL|7tJm2Ii+0kEK?P%({0pwYLvIixfeqhn)Kn_$)DZWR`H(d)(ycyug~ zoVMf at 8i|I+HZ2LTOrV+&)J$QbWWypfgGIZySea%*WND<3GYAkQ)Y!}gNHYmW$QG1_ z$plzBoOKnEh^9ti6&RTYO9fdiWkB-)Q~}#~2VF4pbL;WSGMZ21XJ|gyL+xm>xJQ-R zCduO)B*X?9!l*z}Px at e%Y;i>f>jl}H_%qH81}MQT2k1|PTAa#gR<_eDLne#Hup+XJ z$hOKt86X!1pDBZ)(Qoin*#dJAAFbwft5V+DYu3A77A-_Gl~#v#Q^leTBas%h=!Sms zUU+>lw8IdX2}5s_`;4|&cUXqlmpAPJrMh985OPfWl|@seoh4DIb-e9{>22?2%1alV zq4It9+Lm8|)eapU7+) zlwh at o8Tt0vrwa^l#ZwG;Rx?P79airE3e at 1@+LOHUhc<|^wVWpupdMb&7kgEvuQ!om znGAzqPCxiq8(^7v-{!sUB(;_4xyior9v>inP)Ga&1Kb>g?}z>YHV at tk7!*Db<{kma zKY%`9a382X(EP#qgWMa3K%vY(07&?Id;V?zN4M?c==gIfDInWlQY8n)Gk)dx%h-yG zPsX2*@(;bZ`g8RE15&?tU+d-j?Rj{5!*`~BFnnOx5$sTrf$a2azhH9Lw*!Fu;p-2{ z%0r|6O#I_!Eq0o!z%)(<@=#DQDY*tEp&#Eh{4oADF7G&fIYA5 zN4Ig;#VCJ=_ptq)f8F;ZC9(ZKtNK64Y~|Pczvt>1DUn22zcR=BW$Du}qWc)DO&f%U z26kd9&Q2j?ZLCG~@1)UB4A?~a{Xcu@^hNd7Z&%KZ=HE`fT%Bq?Y0)uDTU at g2yEksl z^8cGR*Xj at H4|{TZl2 at cXp`i5z4Fv=82kr;7K5+TN at Q3RTLG%arg$f=)fq~<(9D;-R z-#{no59JTO{!rLQ7m#uU9Y zSPqZa0KU$;JYVPCrbD+y02U4^%(^(W+xc_Qzuehf-V?Ja3V(>Y-~+h$>dSMKKm0Cp z^lpVt;Y0Yb_`ARx&qKR>OngvwYRT}Qm4AEa{=503)%L#5d_Eob+NLXylRi#+j){o> zQ#&(n`<&wV<^Ay=@_oGpmYxUd%@?w)lBZU3tnrPU{c~N-rQ70XlPB_=Hpk at 8TpF%d zu6(ApJTyoAcH4HXjYK79f|}b5?{0k0L?!rgP)%dc3SjhU^1+(SE}jkW_3h-~l3l&= zHSbf`?VisDIeK#zE-zH5&+R*7!>2!P-!6*6u1|kd2IA?OE91qXKLIvt8o03SFHLfq znPWXq*FRE>@OiSAzD`rb2yRCX5rJCI{I7Cp{_$~5-z$B0Sq5K1WmzOoEJjzlIx zhwog9qWB2goNO8#oM6L%SjU|?EAhUWKkH;atNtJM>O87PYLC7$TSfqJt02BbAMB4| zn1SsmJcK<=kG&j6J|X*2s0qTsS{nlr1)=Qp4u5kahjKpP%$Ilp{$Mb8Pu1ZMz#rz7 z`k7!%g9U`;WUR|GQf;=z=Ur`4C34c?UwJ(c#QX#g>OO%3^$@7WIre^Tx006DvsH`y zC&d04(H&-;{}ZUF^F%+TQY3nkx7i?@$WK>;<-?~NOf1{_9nE8T zyJpD6O)$eYXwge)y*7O^K5e*VVD44p!MY1$ayMn)fd0*d=8K15FOi2y0vkV_BCu5W3pUSJj5^_lHk=dgr{4wU>^7;y%XvSXM)0&P zcMedHjp1mriC(S;9>76BfO2%CgcyUO;Ry!9@)BDPmCEdwGZ~m(^PHb7^v?N3mL8;f ziP_u5`BC#8r}MNl*09Dj8Z^_6?k&$z&T6*x%}qMUut(gGO=8Dzdis4IE%@tgvum|` zn|ctkPf@#hCyIEcLm?VFnLZfC=W+j~9|)c;>b&L{=#7#-b$@dhPF>55(9qF&W%r z5>*PpgbD)$`g)0oLIf!+Tc%8yp6v)t_zT1*6(UjqS*+?1MSQvthYE&MCS=%T*2M)B zRVFL09`3U73U)jO>?a+z*OuSe4rYfpD&v+ry)EamEVmuUi9VYxi3`d|(Vd1pXr2SLSt;G5#oTx21=N8|J4#vzNd zTmv#@Plij0z(%)>FNA>0wzGu%gK12-(9=(rQ@(=asYuG6{L}nir_Sh(PN_cW6^@8vC$0ZFlIT93#!CIvad at uwz4E0!USGcyc0&O|>-9{&Pz zgGhR0PErR)RGB39eYVMMvxMl5Y5t#y7>bn?By2}c*Toa59s)+`JGX2j{mt*bvgV|% zG}9Q)nn;p-QvEFR=D<+t#Vzy&S;kz6-X<31K<_ppH8toQPIWmLU2*fw%cs41pL1(maVSw=HyZFc32O-`*=?TcnW`==PTux2+*sNcrz+$ zqLGM7NX9V;&9FxCPKOZz5=e!DF;)?9no@?71V7jFO*rxxCnP5$c_#)PoF3#n5|d}l z`sGJaielpijdZ150*R7c$MtOlO at J&RNj;$BZU97rFhL?9BLooe6Pg>o;p*uWRKXoN zi4Ouk!SkWWcJua{`7fhya<%w2z1z}BB#J8$9(H?Yoc+WvT#D9C(KebK{ zpnDZwN*$?!1t83TQ7q{bcEqa&B2N~TRZ6N>(NP@=Vd!}Yp$(Od^LIuC8q5PbnOY`c zD!gvo(wgZawiKD-)J-{|J$ZZ!b1`c%h4=Gfv9ymS?o42(fYS<8?o4dbl~`f4NYf^k z6ertW{$QVyLK9M8zu%NK90kME-f%i173Jp$#V; zz1u71xpKc3 at QZagO4g?0-EoR=oFV4Np1eFI-7vQ|$r(;Mh~EK$QCV3biG~RXT7yNBvTo}1Nh`8d9w{Ezby*f$MYhVXierAO9`j}$ z$>8yTb&Difgu+ZA&Q0O(ij%l(hW`?+TB at 9$+wZfyXvQNFxsR~fB)tG5K-|CcDM#9# zB5v=UVW%&M@!!W3n#YP&tVTz0#^)j7P(0f%=sSad% zSe%4xM#!2&nYko&bmn^Ip6Am(*MM|)w<5ZW9|E1=tu0zub_KFki7fRJE(c^#6rK+Z zI7{t20O*HWr(jQRB*DlzJ2CB#KZvbA8h at qgG3b-fQb|4mi|H6n|C%yR at OR>>(vM$0 z3L>g1j)(cqvyvV(0Y+Z0LI+`FgP|tlyp~QPX%=prSq2=pN$2VAdG*8YVWqlcIibQq z(UI`SMl2zuk&lxbqsa%S!{VxbXNgC%`0Z0ktezh57AV0{L|1v`+?}pxE>NL}h8+%| zQuYw$3_Tdb;SQY<4npuLutDIS2z91T;CQKZDu zn5v&JX!e3_;i9OGzyeLBK%uJl2j{&emMP at skn|nWPU%W_I}=FgPZzj$I^D>E6?vi1 zRQG!iP(Zp1q$twJjn>&^O-}ZYCs=Z+ikZWFBMDjZo$W_r{x$vfS-u{lk$R-`3 at 7_Fj_C?DklzoBISAo;fN7nBF8fWFvV3< zIC_xws!`SID%C}{`^$)=q^3$e`u2F?7s>Vd at RC4tI2AOj)a<;Ne5U0^FKKnKVPr^% zcw?|4JQOV at FoBg((-tbL6L|^kX*#CE4W!4g_*IN_X{MEk(@avTqZ(nQRElFt(WKI8 zRHag)QbrJH(V{eHk92orN;ix`7-t!ohH;RYnQ}3M-5t(j+sxgL2M(#!%=lYL54*V8 zP24wOjFQ#MR^YU&cS<5 at q?7bDylr_>l_D&nB9uf_?>q*os;a1+{@{piz7g^!6%^H| z$~UYtFys*s#UVG=Ai3A=DTg&}4*(Ok*_nvCZc2`FP7&hk+!i-Z)12L9u(n>0B<0y@ z$trCr;*RD>8z9{(r5aG`eNs#;I;Pg|Se|BQ5w#Z at s-mlNtCt^X^@qVdg3cs#i6gvx zq9~x~ozR`m)pbA16hCX7RA#Z^^NRTQd1Cd0CwKpp7x4R(~(J88LUs3f!Ed(v#3 z$<7$T%*L8wqMV(1uU1xNwPr5mYe&NQo~O*GMEH`G!=Z~YF+yQ5n4wZ4N?@cU(MVMl zJcp#+qqaiZZ7PPN2;w!GFoksu;uCXmh24=D;fxVaI>DMTNQ}c1CcMXrX{MOz-vR>& zVI^g&gWAWiWbB*L?$7e=85;c~qvgzK8~wP75=*T3Pf-M!jcmSHl96O!NHv_}hZw;z zxUQg#!BB1>pWy(Qz)c&NyI49>aC6eUap>ZDzHs69CxnOZKTBg_=pIcc`;>RXfOHLd z4{Ezd at wz=Pjp%#b#`32}H&sUrC#I8{e}j?9v%xz?GJ4ufR!bwXjMEnqn5~A&z|v2^ zrA$UivYBjFnUbf2j8c|Sz at xGX##fO-UzKd(HSWdZldDTeC at T~PLzVCB=^Rv#+qob6 zQ~ZijZ#LE7gCvj&pm*lFqlV{kZ z?KGVwcPnOtgi^0PLA0l?ET_4kj0!}Oh>YAgoNnEmY z`PzCe%V at vg#C)Mc=nEn9*?2J(Th#7U`5f at w-pJuG_cgM|7-LK+{zjqpENu9u at 3NiS zq?U_O{OWLbOW|gq-BS+BCwiKSq)Plb5wR$n45)kLfZ-z+h}?EDi4e&)QRWq*D#;Sn zsnJ3(;ZuD?n5aklY{f)))JLUJ)k^4xVpa%PCF&$1Qv`@P5|a`vu_lyYu>2X at Gxhf8 z*KGJ-GqOF$y^JG?C)4Jhx1W=d?U?U<_d}6HlclGqJ zLyh!(i}G#bViFAih~9gq{#+tXkIv=p*OlMT_4y?)n{l``9ToDkHwN8Md)y5JpBKO5 zw~s#ba!FJDW8FOcYu>&rZ26JU7=g|KuR0n4&}R|EeUgC4n0TFik0%d$xMHbNx}S~S zaHi~V{BN?Njh`zU?!va(webTc=}V<@(;CUcRj5*RY-y)3#+QZRW;Z#?R>xNSYl^%) zjg1E5yUD64iXw=q6k;f%tZ~OMLV!~gi_$>SvH>VSF2Mt_qd_o4kuwPqOiF1N4Gs`u zCV>xyh{8t?*U&`qL)G~a|6GkDNZL^FkUrk9R4GJ<+a6a+I+Kv2__!z<27#nzfWMT* z1Dp69*5ApbaN7aGq)!dpX&WNO6;vJ(a!Qc2fvBhKv-)o_R6E|l+QDlfejJ%~-uAb_ z@^?^$kpSQ(0^M!6YO??(K-$0MH2|?mW}kDrtwC9PySmgBm%EAWtT;$*WxM>oFi+Y; zj0#)zckP}}Z(g+9X|8nFI&HX|NyZKY(`}~P6KgPr+Y=bZn`}&D8f~=OX|~gCgIiCP zyJ#3NXl-_y?KawJrkL2+*xGLF8kSlj6q1xNrn^nHAKBa}qMbRDBq|IBL at 4gKg-Tgh zHfgdt)WenHNn}B$^8!f6^IKUY+_E=HgzQ;{F^VdpSyIJ-Y+;$t0fWFpofZXP>04|} zA+7tP2{OX8+Hm!q5zP^D&H3%Eb2 at 0+8~g?b)6kv)p#Qejp`KJtu3kM-@$E{Q%{GuY`R6qNHaB^4_ZDx=MYN>WT?8d|d* z*^(+kveHdukfUm;a<@`a3=9h at kOXkA(IS0N>``$&1f)UpkFauxa5EwtAo&&h$l;m6 zx+FydtV4oTq(J0IBaq=Cj5GliP#T*SN!d#HU!$^ONy!+R z#*HRMV&hCri6T?=%0sYzh?)bV6B8M at S{X5=ljsF$G|VIj8lS(9W^QA`!+u&iR65fDO> zgYX+ZvIKf0M);Y5_#;Xri8RF_ at fc(7r%vq{nS2s2`Y^IDfQojdAHdfU`k|8y!%4ww1mKTi4IY&1{*rAQL zbmY`VlY<-4vCQTPkYUIS;G)DAAcF)qsJR8g!v<};0YLPMT}!FSPOGX_-(bKuB1f&8 zId*2o)`tX8snIv(AlkOTajHp4tDp`kDX|9X0l4pkVs7T6?b4*?vORdubg~&02Js2~ zeur=#4;B)4v0U1Y%e!6b)8WuDDWQ4&D zOKfEKIuV*A!N(Q|h=@2PRLGzR=5T0sNjKcZO`#V;LBT4XP#I9#n3h6_I>d*WE6A5~ zLOO`_4vdGjSg8)S$G~IM+4|eDq-j&(75Y&)o4&?Lmei^p{x(`Yk6oT^J&N--S?0!8 zS)|!jQ%zyI=9t;L5ZPQTFvX4-(qUMoTE?cC$;pj|+Fn(&A&zpy at kq%JQ4C6(4$*g@ z8dSkwL5LpcsQc)jOHxNQ{3nyx*!8D!IsJ3qC3!HJGu4wup;E_eiGfulu|VS{%__4i zh_Gv$zeCGt^P3+?p5e2uIKxK{1A}50YEm`;a9~3r5TKt_lOcUFVg3+FmnZxM&k&- at l? z&zD_B&~&h+in2Z{G6VM(LCIupxXFm+yq;>)W@%v>#w4ULSs~M?6AwelXduQIpcYxW zLWaeOv{Y<{%d+Zz0Il4Z;lm*;i4~8fNR1Ur?{>@iwwjP}Ajp(rI-}D6V*D!R{7%h8 z=VZ0By at twgrfHTf4ioJuIYJK2{=xOcNnQ7+EKY%@Fwb}(o$B+ zSfVY3k>0R>CxHBo9TCKqcqM!rH6+wR`a?&x-pRb=_DTPq$%ANYR*~YURaHmCpv97@ z`@DP)wS at 0O+v)#5_G92rB|2l`d*q4K-B2gbm`~AS9G0$!<|jZtYoU1 zZpo$7C~^#c!lG@*El84Ud?p0yMGHn`VM(9x&)~mkFNu at SfBW01ZW;-ewX6j<3F(k4KQa4g7GudKI zHclG)Yi80HEfpl3P?OfPl7QtKnvo{yJ5P!dW~hoHI~>BL at cEu!?8E*)F}8_12O=3j zGnh0mFf0S-k^hcB|19zYrWnCL0q`onbc7FsNSPoWVn&pYwM$60BS+5I{I6cgJjpha z9_A{}?{~aU{7V)TY5bMo(obH8#HsiGHV*#o%NX#F(`2h5Laxh;Uj#}mR7y)JNT{Th z3XhThLX*~d)*C8(R!H`xgY)ADWg}{PRivg6!b8d|FmRQKBq5mR88}Qbk|yck^tyO! zRy8ITR+;!!v_wO>j%|u>ID`cmF-+PpElXvG;YeaK zab+Q3A|+!MRKv|$S*9{ZNl{pXC}4y6qEceS^^GAj0w;wqVU((mqHA%n&56D+{;o1S zeZ!PVIXu&HaZ{JvNcYDWOjN}wOjT7-VhED~Bt-g-0(@?e(~c{!NwA`nQk1(fRgok_ z(#$B+3~ApmjcQ3-Hcm;iaa4`XCK#!YiSVb8DnT((RHZ)Ff=o>?i=Z*UG7}YuCZJhy zQc+4RNh~c23{(~s5=@Eif%3 at wVUOx;w|QQqc6Tw}o5?1ce38{(EkbfVMf=I`{-mDs zgOPCc6M-Xyhx>_6rR?#`hk5z}Fm%FmFg}@eve_m8%XmY}@NPXydCwwnagduTVyZ1J zmG=EiYi2iOB2O(ERve5#h9!t%j3Wq09f<27(IDDhh8kBG#HmnJuzJ|bN{Esbh>(I+4um=nM_)^!bsQZVu-a)`IL8abotSMQ z6YQ}G^Sk*ERfxjKUp0BUV@*2z`}EYEvKJ<8!sNB6YLu}#W(=+0Wnr~PHBz at q*&7Gb zT4=5`&Gu$StCp2cg$WZ#n8PR$VrW;Z-N$M!LtL6J#EFFOBBzuI&S9r_oE{z|$5=%J zry21#6btTZLWvAW`aC at tH7Glbj7FOkl9nQv!zG;=8O>%Yw`9|HX|WqL$43oToHa~c z*khKPam|S)&FGF5zLHv{w5K;Ho(zz%;+~jb4xK>bsW_!DluTzU!pv=+Q0U1c#188m zl at b$}l}@o;paf(BNl#D#)^a6IxY&Sl2Eq{{Z>TU1XjEL1CZt4;jL%^}^l+2I!W?eC zP{T2OIP!WSiv0}P!y{)ZyBM1|H)3>}9NKK*PDNHW-JCdZDO9Ca>~eCglai*#i=O87 zcHa`3Q;^uyGg>LuBZTpEPE9!B_pcv587#$vhlHWsv7WIBh(W-6B23Na$(=e7baS zM~d^22!7YY-Sbb5NGEerL{&*W&cIK(dvgMnJS2~S0luZ8SeS^BJzTGg1+hEq$_-cU zRD_{eh>jdLHY&n0A)?9ZcdAqPvtFw9dj67A%&SWQ3<`Oqh<*d!>DDH!c1bZqCd{5G zW}B*Q)yjIN(HqiLkt4#Y z!_(`*>F!|up3VRE@2q1>oLOC*hzBsN(Y*_zjLURESd=N9DFdJE+p zPKo2s|2Xf=jQ^vkacnACMkmJ9SkoW#zW9ged!AoT!6V{XDwF>eh^{~=KNmj#NI at IO zm_v}(O$MaNWTdi|%&KW=D5~vL+U2ClNrlUIHPWDkaFvo(!U_P0BgO%Xl!%&4Vj;o? z%tVg!c!ohyLv;rPeQnbeRV%X?e*JMI;3|^O++oCf%SQg`8?zT zr10W1X6@=^r(41a8ezb=%XG_QX$?QX5^O7n=45e5H7 z&b9sp8S(*xUFa3RP z`Jwt)kV%pW5js>0PugNwP4Kt5jG$&O20f?0FZzo4aqP`K&xu#psGgYaJ0^+bV at gz{ zu1(V#D@;h!YPRK%> z5`dCZ2wei#Gcyx;P79p*_hB^8IhTQtp0_nkX at yg3CPTYvhQBKqR1}(}RMRAtBHc=o z$u0oILPUXtE(C-!#76B2)y6s{h^FU4VWiPqSt3}Ol10kG$XPi0mCxT9Lek72r>10#6DTb#dMD#r?6pEH2l#3}~Y36m}^SrTZd+wUjNw6hD((?`0 z5?GP48nF?wqz;OPmE7=Cb~KeStPV}v$s@$DRW#W)4|*0(yTo|eACBizV-MbAO0*0J ze?QHLa5WK9OLV0*Cx at 8Q?4?Ch{yXUnl3|UN8kor8TdZVBvmn7SH8*La*JOR( z&QJYL9G}AJ&e~R-H5w|$`e{uyRbtx~0YS97)+{o>N+F^fKe|IjHf9o(n_@{TcBF~H zW6nKlm(DUW!Ag~r6B1F21|q~tNh&RL)pA=ZOIY4MyDTz86A-Bk6DcG?<~)bnRi6aL zDfo{yIA5hth4cnVjj?{(Vg4)Mu3FZMi34Z>-+aqM7ey3$kv`z+BB-<`B&CumQ7hvr zXt$L4|HHC+QN*P6aqB%Z-x{%JxuJ`;J?#~bZ^iD+>*yo$Hrm~+jHXPPBCL!^DM*1r zGa*1sjS1MDwpzm(6X0NFEmo at frX?|0n+PaV6yW1g6S`<*lnNY1z`(;K6oy1Yic^7< zFBr%{cEc#yO`2e8vPzU}R=Y(wdAK7sQ*hwY)vinOB`$(qWzr8d2#N?r$wIidAOjEzeQDaTG4osA?Nx9ijs;N}MMx{mdToaNchNRu&UyWG8yikafQIY(4 zCZEpbIS)p}d@(j-n<5Ou~Q zFjOnxjRJ_87AONOC^7?B!aQPB(-sUl at bRThDS1%PN{(7Q^K&)mr&sQ!n3J#5oWO=B8qT4?ngamMVT9WQ=j zaCb_0V;D}VC5)R(n%e4JnA2uY7 at Lvs@MKSdjys)Bo{Ae~IpwUB-c z$qBJKD+AJF$rS%xPi&;&M}O!!niJ9=O2{IXB^D&67$OEiNag48JSo6O+au!qegku6 zfL}4|I1#c$nq?THjySYbWJ!v}IuI$70!U2BD^4#U!N!I{;4`E53@%_)tNF>tb5P<_ zqESCID29_j#RFA}znWLH|`JJNd^^8JPJX%+G@)mAFnuLQLc1 at IDn2zVuY@8&VWC`x8|43h;( z!pPl8`eGk5>uYs7;}tUw7AA`FlH#qdazhMaBOs~d;Gr_iNI)7b1q3Sw3Zn)BiSAL=n66EuRa8?LRzUSDzCy{Rm?xyoucvcbo_wBxf_KcqcR*IFl}A6Z{6l6rG(@-PD4mL4?+OsNO at zV z#`w5CobX8VcHqMu#=)M88Eq`Q_ at p~LTmO=s^f;;2NdIZ*b$SzIk?=%QN~$M?kcW7y z0G*OzD#)q9Dghsaj1Y;`JHTQi2#kow1fV2Rt5TuWQ)cW{jI~8mZRGW6a7Y`G5o7WE(XpY{LM=Kq^o*1z(cc`(fFI=ZW^#WzkW36V-Z zsc-X->L1oPN9<8XA0dHAG7?VEesNuph*zC54`N}61_9fg;LRp(vc*X8S7m)?o16Dmb4e!e9=Oy8h*Ta7 z{Tnyk-c%+rTAIc(_z=2;P7 at uJ4G^R&u_zwlE=yu@#k8e7piWQRLkko4Q}42J69b%q zt}9YTQq1qH0$=SZE?#9O`ErYHP{$N}>js6T9`wduB741-goj?|9!BhH$qtr!e5!idXlqP=8KACK&`#2UrHrd-;=ina z5YREFXnOodz^9-lijQhHvc2mVl`EY1>?KsgQbVu3!*ghZWJpQAjRH$Q0qr0JS1IE_ z;BY23g9(d(|EP(Wl5%&tDOA3v%=NZ>hpabo>9#d>Sw6L&e9{&}gq|c68A}R?j7D`h z(5Kqr(kJbVD1UqVn9v3D-)NrTl9;Bnn5RXPcXM`%vTmxXQwqcyQx?@ys;f$46^c>` zj8EJq``#ozf?bShnI$y*u?g$Ji!Rp*)xc;{mL}>A-R$C60>KXhBe^;d(8d%PNs+%t z48p;ve4uL_9v;USWFU47m;)S69L!=?m7q(0QKA_{1r{nsd4Rb0pGiYN6a&}9rqpGG zV&gQRM6m=($jmUzdE}FIYD{Jzs+HQ#8e*x at St{bVa#qY%w~(w_*-;x7F%>m9W*abV zTei+BV#HNio2f`*$Tu2?HRP?)Qmb}cZqZ7OG>8~fWhG%|NRe2KSqf01C%F5 at Nhaa> zpSF&ilZa4!E0VS=RyH;%T~lp|xlw-nrYRO^&jckmlqreelLiu$F=Z`q&uKtB z3Pd9z!##wc#tG#v3Ka4bQGhXn7)iq~u$S62ry5TNW(;^tdmWDO5HO{+SjV9Hl*5oA;V?U%a(MwfJcq9M7VPmUsWka`dwQLOh>7nkl}#-3 zG|2Vyb2T_pZ)H~kqmy}!=xYaa zCGjpM5S0K7q&JWIJw)YIf1`8g`oFvLc-#PB8=+U{_RskLe3l6#!;_`DVotB`3L<8a z^(GtOk6?)!4{$WuQAUUyahU@&2t>(`qHD_5r=oYB`x*m>E_Kfl!Xj&_u#2chFm&UE z+l=E5(~z*-AL|(B at D3M$XFqZMHJ^j2`~!Y}t|$BdJ(-HW0-Xk~w`q-P09K@(+B$|E zwUG%!UBYB z4o)Siv51Zz^i|9F#ZA>q`M(PPGq~)jqxhBPq_5%)(|aKNGyIL;!1kDLV1FBhB2Tl at 2s=^;h5B5p_o7i*z z@#1ov)Y+o-8&a~mi=9)AD>~__(k78QjrlI_QN(i5Yb0`!$~~`)vK1>Q}e2tWvmLttBR=-Wodjv zUl<#x>|RS|hx z9*1K!WLE8rZnv-O>od1)o&>|d2S6j;PeNh!Hmam4DG#V?Aj!q^i2?>{{Tg;7&wAwe z6XKurO%d6cbQ_JMf`DxX1nBo-C>#b?=SdMLK?iW-`rLomJ=WqTVa%={0#y)Av_NG2?jQ!p^~HW-+u z+eF$>n6gT#35pVFvR2GuN8u!!8>NV7`}1}9(-f#;veJrUE*ml?k{twkQc)fY9h2ep zWIblH&AjuZMfE+mXXNQ4`8d3$=$V<~f%N zB;2U5n+%4afMd{14%!hU5LjD?mfrd{F at lZ=8e^NItE)}bRgO;6obOTc7IIM>+$iO) zMVdR}BTDKJ;NZaK?TY3K8USW;yB)i8(tI{=d#nvWyAI&xVI(6BNZrRgW at cQh&7E7J z%`A+bQ^s^{B?wF=X$LeB0&j@}&gs&GV+1a#`eygVUEV(~I_)OvlB!AQVZe##Y*y`| zj~3)_ve_+?wnb)!5wmpldpk~0CT1DqCC3ICnd%K19b>M}?*~pbNhjup4o3sCXlq^` z_gIX8S!IVhS1op`{aLiSJ*)XzvCM zpAw%DgMxClBTlB!`QMjgztl>quN0Zh$GOmExiKKHY!xZ8^Ob7T>$3*`_se z<-^`pRZ&M>O$x4V870=Kc7b>wv^NWtr2eqY=sNdXNXmiP_~!x!~s9CI?jNRQ}lsBK_*@aU@~89nkl at 33uyBv#j__pBcDNoA zVh$;zhY9G`ReBa}N&aK+E8$v?QC8O;CW^>TH7 z#_hk>ypvm^aA>JJHg;swj#xa?()nB?Nv4yt;&4tRoYSg(3nylsM(BJ>-ux2=C5ZSi zWim2iQpi0F3$ZA~tZ3lwJq8R?z1PWwEq93$naRv6edariGTXWrJD8@ z_+)wYn(x{ZzECn+4FbgwWn_u6)qJwI{+HiZsdH;yt^eoc_T$wXH~3%cdQE-kT9SUF zsiIX0E40>gfy$(SBrubZI!Fk|JEV-QwS`83lqeupNRj&w24bBgz|N5wIHe_e*Ck5T zDq*(&F{5Hu`@KlCcS!D|c|KlFH9lJ)wC3H9p%JAra#QMq{K*Q;3ke1q!4)9f@$Kh6 zFwA2puJ_U21mUf;6W9tJTg(S3uwYQ2p+bg?3gXKRBD8bg3Jss#sz=t{`IYq`FQAnEsPY2 zflVfpf4$ASPm2$Ik~@;u7C9_x&EN82ta8pXswzdmfv_fNO(x=Ip%PI?xR8JCVcEA# z5P*l}0kE2hybLq6KOzmHP}@-C{^QBwsH+YfoZ2N-tXTNo6P~lTUQL}F?KYcEo$OPZ9+#_}tU zQTwa at J4!d6ruv|v-GOrEIX5zQIg}I4Uo{P}1*MyJBt`?o1KF|xTju*GTO$AcJQQ+I=AG at L7& zhZ94Eb2rB7oYguitfw|T%H>W?jX9nToRQq{a91Wfbj{|tDzYp!!M9||111nRArLYNR%a!3)(gH-tGVx)@1ei#{$RWeI;W?U< z6*nqX6;i4dhBmp91ULsGf@Ix75i#Ed&Ss)9(#5ITmxuj=(Ya_wZV!}YIEo0n zxYnjjJ9AV2Dagq;V~@})4(Yb;(aLEK^^?1xjFK8z zA>vWO#{{t%Q) zjOGdg8pIQXU}jPQmcqTF94{u}VxWR3*wj-rk~s_ofe5&Zk~tbvjmnT+x~;t0l-^8<2OMXF at Oj%T-~_djafRyF=U%4Z(W1KX~U-`I_Zo~ zf^%$&j((~fbBkYt2S_yoj#$J&AR7tU-tsD?b3A02O9M0v-CO31(K6r9; zkgRf_Qn2w>?R9vl(w#-Q3)KR{7$^H)963oC7?Fj(wREbAE5K83?oxGI#wT&!+l)Ik z;DSvX3?e(4-#Z3Q3LHH&Y!@_O7$kwh<;VbdGY5nm#TdZ|LNGbahLf2F$tE@&P1{2= zIiU_-#sq4WA}mo5=yVxH5HptCeUQ-EGC2c?ldrPQNx+1fb)@X9Sd~VooaUVwbq3vH zta&P|FsU5cW8FMBj?T>JqpQM0a2=5$qY=La0uwc1&Mxd^LtsIH8oV~_-Qw-Gl5%m7 z;ZG-Ak=3)SN;%;Mp~qqe(SYg-p%QE%*zwW6_N;8xq{r#o08?S<1PrJQXHR6T!)Z*@_K=#G^*Ug^D_i%#0fx92 at Xs zsB9pdnHymSM;> zW)UN at BCLsEIuso8bsKY;bD$>WRbkP=sau_yH78{|#}74iZ0AAN$sVpreA;*w-8OdV zF*egpCiSVN8)ouuk4C}4PKq5JnauKevU%RfAlP=tL>7_Z1WOQ0 at R`R4amO|r0b at jh zlL4`#tc at TP+B7+KA(-vTL!Qg4Vd7#($t|NrnSzH#3$zkJV#J+?XY6*&PZxoS%DT6Y z7RM!ZaPDea*Q#S)^5hrmhq#VRW$j14|9%6 zhsa1#lcd5NdLD-YNzRENAT|{?%;UiI!OWsU!Lei_I+ at sqHfxs++}3tjI~?WllbCdG z-lS~CF{b8>l`Exi%}CaCxaZ5OIsBo=HB~u0*z04n8 at Uc@s;VlxI=p&#b?`xkm1mcow5)n!OJ~W;QZA+)XTz)W*>|ED)P=oOm2R-z{U2;gq)|c9RXV zBYX9RgbVkhFLs#)>0|e#4`Uu4#Y%jQ419&QFmehvSP+^E5KF9w at 5;O=tS!bysfZ3tRvt!(f)h7I|~H?9s9C}Wj1#MxZZO@}ITROsr*71dq{v(w_;(so_d zP_C{TgG!TUL4$ibm%1uRI9n+tZ9JNFZv!m6%A7N8A+wBltB#v#ICe3N19B?%$n9e1 zAr#PDL-A%YxlOTx&^I%Kw>Pca#{sg#6yZrbm8sTPb$7Np`74H_9M$YKBr88<_6H+1NVy;Y at mn8|U4bHS#V at uc0~ zyTwh7aSTb7&!N7Ys=Jlc?DI!g600kbW#=BcO0DNz=SuTBE)&T^6%$*Snr_r->2+bh z4*vGU=52^=0%$LgqFNFTh1j7$VNq)rEJY4 at 4A42u*ufT1cOZcl&cQNha-4zSED&x; z=He%iSat_|70kVWM2^h&n{1|r_w{xe+)?*2z?>o0sm4BMan at lRM4Mo-B5N^oF+3I> zi5Qt`H4rX~2 z+>~2jxc$wxgwanK!J>moo$8HLl_BUVNb-2i&dTzpm8DJOoy(=1U0JfqbD}Abx1!oO zvNAZxOj!p6Ya$j3=;mJ>V%MWQ8N+GGx}8}(yO`%(F-&;cP0nQ!XAQvQ9SAwNY$A(Z z9Kl48V1Nx%bS!cipj}I`bAF^Ta19%)T%ry`kjxu2mM?a*BY?;S#)cz;_B`TK#>BB6-!9OIa0ktM_oY#EqkI at ryQ3$_G6 zY5ANEVkDDCFp7GWfre~Bc30c8PhUO zhIHI%g&g_-_#FS6q^Y^VIxw3&)$Ce}CYz(7O#&jh9NaXm-5BPN9+9_;b}?ARV;FGe zm5h*Sjkp^?57e9C7)}Vt<&DP);K_TFgOK7hVQA<#BZ7{j^f{bIGjit!VFDNMMQF=2@;P+%y>|DffKo){<(iJIn2uZyHZdb)%OjGsIWaon!BnzwJuTtFIk99{BXri2qKtBh zkvb)AUKH6Ix at v8!zPy0^niz2F7K~x#Br(dsG{~((!WPlFvADWZQ*K*gs#&^GH6x| zl?iM%4H9Tg%nwm~x3RL^W1hji(rhcVb8<$=*fArNub}ofY8`qtrkA12Q*A4bp7XtV z07R~ZLtrOyMWdISyQ6fN^QR1d=zsj|_pIdqW+!J}R{Q)m4y!r)TQ)J{jmE at pqDBUo zaxxHWW;FgxGD72dJfV6#&8 zRZH%lx}O(Fpgh=OPTZ%Bo4Y~a at k5ca9r8)kcpQVR4p8Kh4#!CCvK>Z~f=5);Nx3uH=fO?;1%qhcu@|jVUBx(6R%t(I- at M zax0OAM9M2>Fx``V+4`%aDMTKHfM`smr zQ<&k}ga8iULaLHINga3}{8D(tJxLYeB63b>e32fr21QjxtEX)y?XgKd#|AVaEJNYM z5QEl${gM^Xr$I;6$VWUvK>LcH$bJX#m**)w at 8Kb0AE*E38Uy@1$cpNgZj!M;W5H!XWi;bL{joNQNrMpcx6{kigC07m;mbT=(CI(9c zf+0}SDB$EV3mX^?4uV(tn>C9uZAr4EKW%FEIXfxQvvRpARTPxz)lMCikf!thlY(MY zb68L0Dbrk*w3CwcJ0_KlCMo!sb~~JkZgNRK_J3Q&vtXl2S2xB-%CZuBOnlIT;8+L9 zq>@blKkg-^a99ioLERJ9BtHNL%OYVYe7X14!fhdrzccN3wEniR*08=t$n!(Vu!(~A zogog41EmngY^|StSlXubRy3iJjwc4qn8RlicDQWeDT--gkg5(Fv7loDS=>~Lc`E^6 zECqx{Pez#2300*AsHqy6X6%wGLdh@>fKl`z<0-|&Jn|k4=-WI735zkTJUGHK9>PHo zNTPs}sxgczQiX{zQ707DTNXxU)Tt_zl~+|Qu7y%n+|!d7k~fHp2^?u+D#&Hsp?gq-bRNs|sxi5h8jCqof0Wr8#M-Cl-gR;!N8e-Yk--Vr3BIx zLqNz999mhF_#($q{B-nNx`u3MGpd?Ta}(U=Q076ahCyR6YUH;gqYqCfeQ<2RRmLkG z9R|&&%E;*9hMisxO@^4;gSp`1H~iJ+tsE7Nv{hXuSUjGjPFe*q}5L5Bb%cM(W5JYiP1^QM2+4p z&73N+yT=oSP=#40jFZUo2`&b}{$L&H23v%#F) z!#?8hIT$FEM$AJt5ytyA?T`R>94HDpIY5hOa3D- at Ja`zfW0|7oQOP;CV~%eP6Hg{- z_PaFPE3?*VG at L1hF^V>gnDnd3v#X7ct37EYce?TlsiiU7T5zM2b~u-3MmJHW?lVb- zsPwdLD`q(rrRoxN(aQa)%!0iDHvW9YkQtp*aQ(4mTuQ z{-ZmWB)H9OM-N{0b(XLKV`I3`VFsB_2ag?^t=eXlT(FtRI#kl`#%8;0aU2OdTgPI*X>lp?EYFu;CX=ap;!7jxP8{9XlZgNqciRAcGe%vBqq0CK(WEK(thsJVrZ6P-DW16Jl3_`q-oVi+y%=ruM>q>_>lVj`(vt6+U@#)GCS65C*uBrEyIJSMAAo>(ckSfN4Sj_W4lQ zRbLt}xOa%~6G{h-V{&u*Q^Hio${idI@~Uvj)Y&kPO3v`+OcBU%pQG|lh%(=<<#3#W zoyI4O()mb8fHW_8=~vQlbPm#AeN0s02b6P!>W??mRqP+Fj|yze9a?xeZBp_K5^k;b zT-{--52!uFce_V&$z?R2OoB}*B%BGlRD^s?IvR3rG2W>Ps#4i$FG{-KTJgrVEkszN zdv|SZ#=Vcis&z*@e-fKC>l2Y53Qw{_#CS!0%aSU{$HZi5W7_{C4st~G5j-Gzhy>Xb zGMNHSAqnWfc;WE9K!}-LZVZ@&QYLU>#F4`*^ zts|gTuBG8VsVVo at sdmV}--b`@fBZfaTuSV({sq4s7&~2wDwx17Eas-yZja1vSn&g% z at afc0c_(UjhCe|snkvXuBOf@ z0}gX>--c7}BYBI~JJnkR0vn{?-25k;@Mq-Rl@}Z>KMq6MA*hV~-h1b+n%6rSS7FtP zC!p~WmQkKMUL%pNNV#00FZ|{47Lj~^YsR$!R+ltk=h760HahTUlwt z3fTksOdvXjqhf8uo;%YaR*v*XqatVNg9UuU-%trfJ;MVsJMrN2X3fE2NcI%Jr6THi z*=OX^h;?^go00-a>HRQYF;e4U>PzQOMw^oh5D-T=4Vcp}3z0ql=GM!%h<>-`UmbPY z%C}OQz7bFIorefTT5U?hvlT8@cKzba(!uQ}*AlH`Y8 zYL|zcQB=ErN%?SY4y%EM{d;L4iYRw{IcR)y>yt55D()8niul)J-yB z4SXy>i)R9XH#`}t&K+VJG2+jCU0-7HNPZ?33X+XweARQk4%3ql=kalvGH==1v*KJF zoZaYHTMeE3h$j`oruKA93IEK2wjm2D-L~_qdv1PBG^G$`5luq5n{avBRLhWZf0WXs zzk#&iYqTp at ka46=@-*t0oRovuv+9q*8>%e{Zhfs6J#~)UcF=!Hn5Ejh$cpG`G6_9A zr=C-HXTu`m^PPk!Phmevc20;;FE*}1D_bJv8O$uZ7#H^%%AFQ4m{2ro2-_)Lt!?p) z68I(%-D5YOyjs>0KNSDOk2GK`-_4HF-jFyaaWIivLK$hIdV6cX)?n2&@)lytpgi<8 zF$8_Iu{o?QHlF`X&c4TTNv>9#_-R|5)P_zKS_OXtYNEywc!*+k(XK>=#>QmytH znaLrel9Un at 5L4R}uk;|cVsHrbN~BL70~J_+>jb(C&tAd}n~e{%3mk%nQFrM>>F|$3 z&nc0);up*=1h^Cw$JH)sGq(gXt9vpofJk=op`-AG6h23m8&^qA;;Od$Q=NIvKJQEi z-6<$jiYrT=6S=jL)n$O?JkEb^z*=#$daq5IH#yi}-my at HEd^4sQUTb~Wo1v)Nv~)} zJy6JeU8Nqndl4OzRh%-fN>BPjp>C=DG-%v4srU}>?Rpf?ZNJ1xYg-Ao2XSKyc-mUh z1tGH%+Hho!4)2OoviO91iEg)=zW&^M5A8?yh4S^W#kxCu4SkD?W5IQs3EbQ5#iAvM z?qYL!O=RZ&7yS}s71`>7J!d2shcwLvOkTDMZHp850+N4iejS=3St51pZCi0$pL^iH z60tyHCs(8?-xfR10|wml_0Hh(wY}ybQ!BoeijwLg_!%CvfVu?5U=bsek`wG2VL;O|dzRqB-=1 zR_Ng-mtvMmvOz#6>SO4txiv?7s1UW4FT;TRYr&4C!=Y>fLzOKIBi|gzY`nl3# zS(v7v1aUzJbyE^iiH;vl)2m6?OwBTvej`kaG7J^^{3+$rJW3jy5qEei7MYIuy1!4Z zi;3<^UgOBvtt at l@J8&RXRtVj?Fs+u@&+W#vEvQ%Bw=4cPrP6R3_4T{eSVgu%a06-X zmx|TwuhutfVd4AhRX2jZw;O4idNQk?^OTCMV(HS^bq=a#Xt3y6%rqcPg$=3${V=09 zvvX*a39}Lp^#bYs&yT4s)pivafAE}u66v$)!Vux(`CQY>RRI4fOU{8m@)$%imQ#Tu z(<>E{Ey(p6C4k|ejbGPKtxg(OXU(yPv{(`_iFd4b>c0GZ^w|9E(T$)-s?!GB6!QQB zxV6?m7wBd=(*E;+kI1$4^SE2*pdFc7136bi-)XSO at 54hZ&9)@jvAvg<*4~G7fC^jO zV*z|jv?vXVcko?ZZ`{O?&hV*e%Yn&ZFjsM~9yvfqOHD&_&qp0d&neHVGyMRiyRGJ$9-*)ExM0^(z>gsiIaaBM+irMw`VVj(sd<*IcQS6rQ{kxa(GjX0y&4R9@|1PB12Z-=S-h5`yUt8oFu;ooHpg*ruV<8-FeyPxP-cef~RGV zROcRDpoowNF?em`|IpbQbMu-I=sex<@=-&RFzQeC=)Mfhp}I9+$DD_km7AE}L>X)N zS){3nh)<_ARJLY_4W%|T)F)|;YkD;#9+32Spv-JQdoRm0Q%y;S8XjKATQkK<++U=2 z7-69UMySrCH%S5LF=?g<6I0<>;YvNzPv>)b)7#TADnq%7 at EakE6{o}AcW9)h$BYBZ zE;Z-4d2lt(Oh-IcWHwAq z6?hwP+7^MBp$s-y!r&;Fc2nZW`o8|70KfBH9qNX1GR!`sYZaD_ga zGif52S`uI4%k(F9H3}oH80XYP1opZSGG`RsQW_^6AA-Yd%^iPW)2wb=LyB`vC)c)l zAI at C@yt6;%l+JgB7$3QU)fKGFqo<}(`y&7gZ;|w26|p7_U=yugUHa$%74n_vRI#S^ zRcT0<#;%i|QXrxP1}n$wg7vDZ>0Em{5SbByZO73Z7 at +w{+{Zhcd+tX0MC9*rc}Xz1 z at rPo~t%0k&DT}Y960PK%F9fHZ*oq5b^A^)*zW4>$74Bw-m;}bH7Yb*2*yFR-g#-aE zR2hCd&4#-~_31)#E9{P~RntyZMZHiOlzgh=e_t`*Gz0D;+7DS}a9AzA%w0Ra3(@4& zYiN{xzu(-OND{VV*L*{xvo5xj9`eD z<$vq8d+wNfX}(1LBgMZkI&t}T7w!G8#UCCX|CRq0sLP zm!e7ZUwgv(d9ZmL*&Nh|$>Q at JPO;E^>UHpNYYJaK*_>0~osePcW at a!>xH@!2LtRl> zR*s3r*OY$=Akr&kI`DjQFGBLX2vdjLf=vKINVNeAFhL1{Y;|V$6su7v^KX0W&nAZp zlq(14RT%)s1cw at z8Vh{o<4{=3eBc|wT2+-~^|ubRnKWHQYmxYvX_Zi1JLMr><`h0G zTWEYQa*Fiwyk*fX_w-P=`R1~l5zM%VOcv;L$cj?Q0MjblxgAzskOC2f&3CjJ?Tar) z{;@HZ$~BHURIbuQ^o(ke_NoeNQH|U)2#3EnpiD;RMl-8i7^e{fgc4tvN5~M2 at C4^_ z0ts-TRsU)7n|wz>Xthe>lk*t5x0a9utsn}nNJ~U=ObYoo@&ho?zU)@kebCB#@FLWB zwsrYc)`;@w5srwxq)@)YrxCj!+Zt1pLT}NB7tP8mg1Cr}Ksc}0LMG*2?5^xet-Pr* z%nqf~so2YQ(Q?5_r~h4K8O?70ADNk&Ede{Syo6GD``6+0w0N%z8SvDh{u*x)FIw1^ z*usNkUe?(@CvA^ThD4J?t3&q%vNUC^6<9~{3;RgwkSIHA3Cw~JwY;?U09x_W9pYSR z145Xl{D7VdLgh|h<2>GQC?-U|k(L$;O9i`BnwN7huk$k0q4OJq$j#>DDiiM4~0X zQYxK0f6waCpz;4^edv^s_YL}G`7Dz&?AxA~fQ4)s#3(?=_}<>>`;=_E$KQ6SbH|GX zpf0f`nhJ13mos&$kD*ta+LATqhla$a$`!0(Kz_frRnkr3qSUb-#(#J=n0~+3;jsD~ zg5f;Yf5Xi&>_1s0sMPljwuv%eeL~A&+)efs;6J_FnP9E4Z-Qd#X2uDzVot|zTF8F` zqj70*jI>bzLT$t0F(+33oP25Y-FKr82I-K$*}d=UwKU%GyQhhBL5|=L^Npze149$* zVxX@^A{S~xqs$~6k_FP3^1zmaU#SA?wnJ9$VkLCc7$85<@>ZYmQWYZCTaa4Z_+kpS zb_+bJR#B7S24Ir;e#7eAU0EoHd1AbqotpiVM8EVuwXa!c1-X$u at _w`IKtAD2!9Utz zvw7*s|0r1?op-qopi!46qfFdEise3BY`H4exKb6WQ`LuklGxKrZvS at JSp4JAFOZ_7 z*Bz$o9{!M at b6-=5LR6VSXS;$WT~fnK&iHw<;E5HJO{f$tc zyRr@xW5+7Zg>HjCV7&ODCFDc>*|Zd$K{L429kr8wvqvK? zEAw6e_!%7A;;s{EX9tDq(t}Fhfsfyhsaf!HA=b3D_}(&mu*V^0_?VAJMgwnu*f2;= zP25nTG&p=p>ERWsM;DzJ4FK?SLie;{$dXZU>5rN{DxyN&mg*)`@is~$QwoPUxcQLRoNG^|i z%`Dk=iglVHxJV||o`%CcB447bf0_v&jVGH|{+HWnl+yn}!Md)2)inZ*^Ma)TW!6rw+z_6}OSwW)|JwLnk4txo z2zH<0xkf&@$V%_-I6Y>oQlq*l{(Kj;B#RRpl`fe{S;>zHGokqUVgmIQ8mf~Zx{urNJ<(hFZr<`MtkMw0?GNawachDkRz29&iMH<3e zxTLt0sBj(}--l5DDBa}h(}>&9q0uP$R+%<_yi9xW1)%hfNC*D0_Ci1Dh3pE=#o;Ni^xsL`GSVHFfkd6#t_U at kYS zV1*Bs=-~#k!ZkJxPMRbNqz|UCw#ow_4KBn~O1*NW&exz<$+pa)tuvg^mj{PYuFPd# z7$eS6yQ()COIa4-euIT at 8a=vuc(m#U&F0bde>a1lC7%b>ew4nz&mNILd(}Q<=cD16 z+%b|mK;I5!Hy at Q}zRZZgaeBp398+!0=N_Ir`4jh}4;WJN@^?V(+sLa$jLs8LLSWsJ zWX-MOFZ!+(dRh63HLn8dhT&1TAg#pus~EB+<8KF_lJm&#aVxIN*BC*nojz9+hnHdL z!rSlNAP+R2vafe1BRZEQ&Br<|)c5PnCWgu5U;YiA-C2qnNPItt33;BxcHDtr9PcSv?N&dVKqMe+; za*6C>J6tt}7cKZ9m#D5>iRVM#p+azi2J;qfts>sCm-5VZKWO#e95T*Ka|$E69yHoy zX!&Qtu>Rv;e?BXb<}wh000&)#tPo$@WMdX(0TjF2`$|RcC>E$<%qBp;CWgEB5OCU@}2m1zn0iJ;Tio{A+FOs=ZqC!nh zU?qUmLKABOsKW0HdAnp&G|OW?GQJa!-v at UZc(9g~r)|sLoK at U-^y_wi^XBu?N`q6r z_la-%Ero4mz8%{<5-K0g?DDo#VtQoBW&krc?}JHcl?^MjfzBA8&G4L%8 at h(p8Qua- zB*H8&a<+AEqM8yIemy at Z-|r-!cdkHO1uI#!D|runot at Z8tdBFNFdF?O>qz;eCb1H= z85N*QfiG=pmfHj+&u+ypR$$uv z>QbJhhu=t}v={t;S7!!E2M212Td4kD@&&~S3O|P1)XRdU*IXhB%=?Q-NpT%E`8A~U z89)_LA*T`!BRjxMDzPCY!7v}qQYCv^aA-Lmr&Pc-EIgEhbhj`d^>#X=56a%(wX0{MYMEfwP09u=Alp;P|c#+R8ub+00+r>l#L&7+KAKvc-A( z?#_jI0Ox<_d at 5a&<&teb at Ap#1KmQ%+5a|!SyoUwRqmEBjO|5?L|jdpP0B4%}1 z64B~-)U8Z&hVnuJ7q%AGR%l!PHU6o=3yYN!-ekRM%J~YCKcs at 7;8OC>4mEv`f8$#Y zECZ4O;pX6yFeRCgEHy;>y_C~rgEAhikH)-azIPT7iRUhF&hGVxC+wEoyf=P$OLt`G zGSeX8Tj$D>D{(9{XeKaUlrmlLshN%mF7{BhRt?*zp%?A!b?!{#rrfZtoijF%VXZ`q z_OQ1|#kX4P^^jJV$6S&DJI%G$5%VkX=58B$!KL69;v>#}d4M1bm%3PDKi-vYt=gXE z?S@`fXyy#@F-cAqPW6H?gHS{DTFtrFD%|ixij?tA#^c$-riOy%;&701GCZF&1F7}q zx(}gM_T&{481ka;Ae2wdPkr*1b~?fX_IilZP8bsuJOe>VL-Vky)dDd}4HR0Hh!DtZ z4BZ->|9DEpqOIEjpGP1Rq;~tBN3xn(^Kd5$MaAA|9G9Eqcnk6~hJ1PLkJ%&JYH}3-9nCyT zIKSr!+WMK#aA zZC{Jx=7E?w_luWI7|+y5g_Nu{W}=r!ETC^PchV+z{P>cFQ*a|0$eAH}S zK1{JXu)0{m_Z6T%u!dX=c3BF7XDlnyT^5z9T>WwQiB20_v9b-O;FSWFbFp5*F at T84 z%dNy$qNbi7QRl`5D@^#y+Kyk>nDDpG9s&$(JlGUos4>c*E<%BF7<>Y$jATh#RcCXK#)xYy8QUeC*NW<{>)^ z`s;vl7ZPwAA0VRC04S)Nx#m(<^e=Ust;c1SI0Fbt=?`lI8RZO??P(+rOk_tydNvpz z-uY>;tdjcU-&KEzpDTNV9?Dl{uQDXbLKVLLFtMNa;3dgaNV zm4PlJbEaA2>#oKRQ54lX6?ll at rDy9e8Fl~iE_)wqUH|=`){0WJH}$8j@!dTpogZhn zb&u|<@rm)(p-X-Vtvh(L#p)u~jlA>Ht!zU$23=sXAQtPX7yj@`$nc<&(-%0qUNmQ9d&AV&WC0o*$bO zWcsgjGWKc{G5d{6Jw+IWFpmQKD~Np~xIqL at exxCpy5N$l4Ory_XJ)usn^$sX$Mas; zO_LNx|GNPa60vuH_Al0raFv7QKnm%=6zrke;5o;iK)FGL94pQQj+JAzHd9dPQtICq zyES at Lq?^8;S(Zf_xm#qjwbwJ2av>C;9p>#*yfcwLKEOtq)4HlGIscJL!z<7f;=vbK z5_AS`BE7_EkF}oLs{=8xTiBk0?~bbZnV;0WYV(N}gglr7=0aoMSWxhd#~0e^4(+H` zq&zM^2wPJy2hiw3xbX at PBdj3}BHrFYkOpV=R~`)F46i>`;xY5QxN?JX^>oVx7y*Vu zG=DdGD{r?(Y6TbwPLQ$mOHig($=L%mwK|mL#pye-Cxsc=EPrclt-)b-Y-^JXhk^Un zq?dM~2^gL>H-DoC9(e)&1r9Ls!z>OoQjiI5PZ8vjF+wPMt at bzED=2$#>B;JyOP_rH zTgu`uKkg#B5vHnaJ}zBSjm))%Mm!vO{4ECkD{~7 at ZXX+lxHc6<8aqy|Ll)M1)QX7U zsll9&xhZ_lWr(mm9v|WFpqI%BKL#dNKjOXKm4s8;hM8)vsIAaq^f3Y07hyud9(lv{ zt)+QirDXrKbv9TRwc~pZl(inc?+T{b zjt<CXe3&Z>rr?S=@;9sk9!)|-A&X5=|ZI<)b57HeUj{~rjZNaCf z$>pKr_ei(8Xsy1L9O^F1s2Pxixt9hSq?SX!e|fTqY&A$%w-tM~q4t6=D`;9P-Q})A zxJx$oGhNN}kL;fzVXo!+t$(aX0wd{RGu$@SLrF4fj-q)%{FKms`$`!>10kp^m{w^< zxX&3zh^Im-(y;1e17L|`;+}0xN_O1($Mube!i?+R`Ny?eKjc84X$NKdZDs)+43ezF zw0OroilwTgO}WBLu$Us+1^UQr#oFsncf_~>KYp+;*j=ivNLC{gm&Gsz<~^^-)Gkp> zBg<`-o8s`ap{vleE%Je@$i>vWRWmZoys$OaiwAu at _*RK6MEmIj%;;CdyPZ7`LmY`^ z{^-%yXvqn!lh)=&``Z=Y%2*$sTvBo*;Rr`{0o7AS>d;#<4+AKHl!IbwNg*H{>0ecf zAi5@JA15rX3u~@IvviMn4k$ram3QIX8I+fuM0~eM2$>yWtm8{;+!4z+>!_6{t-m4 zgHaU%_knqp!2XPLCR<)|pz)Ag7ZR5l^WBz%&oJ=k*!!Hq-{AIQirX887;=}-1V5La zKrczb*|L+aqU)=!8j+G9{NhTK%+6R!bOi-dnmp6ul~>VVOOZA?;)%`TfX=Ms3d{W5 zklLC18t$`5$(VR3vc5K;Hg6ba7fo(u-o55mn_l^xqwD7T%qgZs{#&ms7ajez3R at wB z%d1~LhTVBG%YfRP=nw;A#)DoA7B_~kUU!uoZfR^j)OaRYOuPN~a8e3V1yMQ8fe7Ab4&|{I!o-VEWGIxC$2%V2`7F!Zk7tk%122gffa69zmE^P`p{&9> z?%_;K-k#yUT^Z-e0V$R|54zbFGwz2)YN^I8jj$I}+ny?@OQ(lzChIJ`PvKn0{;&8>8Osbl1_rNWW_JF4NqymY^nR(PVmufZo32ZrmTd#aXsJ4=o>%0+w!gPad7vbc?z zfbz)gEQG=9A%;l;t?<~uMohmzH7G?lRRO5m=P9RKn$eXI+uc-BG`&U3dI(FkQgHr_yz)t7q}JMOP;ry*W^_Y3P2?S|-j(k^5v>v!u`nU}h6O_p?P zIT0JWil{!N(buc{9X$!fef5w>7B*U)=L?+{X2j7u3qPu}Jx^+_UO%j)O5_-w-g_+n zV^eWJW+)w7W`Q>JqC-7jOe2(2W5@`(F7*rhyGICF_1r}ZNiBy!RTVS?I%HQaA#`(J z*Z)WTC&IsXk)21~x@>2i6Z==WK6`&fp0ZY{Qt?%Id`&V#<-V1ciog)O{>D_{rR&0C z(1uR5IdRf|fEY~7|MNW>whxKCh$UusvT3zlzwNnvT2hx6L at rbhwd~r_J z at 6jx0UToAAgP*Sig+R)Y9Aj7xrLSgE=etm!-etQg0)^mI^`=>fa1#U52;@}-YeFU{ z^H}^e#-_nqIWxa at S-6Of#{}dx;A+_FwQMLhI>70x9f#rCJPc z`(KvpRIRS24=xMyK$cvVCGq~9xFH*V)yzs5h%+bNd60?5gRTMbwiEJ-;)Cdzma(ah z`b%21)s3#zMat7bB%A7dc)mhM-m*`TwL*)apBMLjOdn;V{DIy|ow{UWK^Tpm@>E)+(!#P{F)6xn`rT;FSeb$tjB9~>7 zsoVR5*IDNGJU8+WR76vUS-#Fd_ at KZADBmVav)!RH~*RH8>Ei8 zG2e4dbUH{=Q~jE2XHiY{7`@0<(+7s^ZxZ&3rQy&_a5ZjIh369y^HlZfTx)+y`}&;}9sup*fC>m^p3#bVUC z5?j~eDr^f3uw*FY`TwPE^8K at 8HyV;ABv(VOlfU6_f2Q!4mYgJgD)}>8@*X!BQWtZ> z|B~8fgSwXE20pM2Tl|a6XZ-?rqrL$Bvmqq?ONM!X#Vby_M9 z1azX+WFr-#`XXTmdMoPqPk+U6s(^%uSS~t=pTzMkhgQO>WaMT4`=#8kH&>YwcKZ;J zw99X>!QVARw-Mw*w)6>=g?`&#`EI+;?UX9L{tzc#%Jy9u`u3va&2eH at n3bL$Z5dNX zn{Gthk^Nrw at n2M9LX+0-Q@;1cCvJ&t4|*&(g%p7yXj|@*SP9U7#XEPuzGeBieZ=iX zjIT-`_EkiS-;XEr34qXQ)`=n<&Tpp4!Soy65<{J(c3a$T3-{AbSUy}21FsU<}-BE|0A-4~Nb z`f4i;BZ{pVQCfkXJOY)%JHj|D$WjUJES!vl!BBp3@%TzC0%e|ptWhCHS&RP_lD+h0 zr13lXS5)+`v5s_It2G!TY=mX3!=RNVhgnc at AU8BB;Z`GnF!%frF*P|mcUSY(yf8vI z-3)F>6Zz%e&zWSnH!2|FbxI*p{sL z&^qVT+iDc~0?7?SX_jNV?=!WWxkIK0Nqfp#%I3_~NiZizvHpCA`eaZBscy9C#?ZTp zs-8lko+85GW_nWKpqXC{uC-#XMvjLW;4CeHPzy1M-W0A3t{=1T7wK(iB>Q`);eT}+ z29U15hJBVYA6VHXf$9{rh~D3(vi=gT_V8IknqzMK^?&naOQM9gC4p5 zDCo8OH_=cElCIlWO!b at fztV~7Q={5S%%T}}EfjzA0e>M8K#b#k>#H4V_#|kaq0Ba( zk6YTD$6xE!Pu;``^^uegtYt(xlDEas9(@i;Iq at b8yml}O13&q9(_>*v5Z z8{}wzk$)!Q%Qfb%pc3!piKiN$!x&N&*Q_Qg-a}_y8{beL6dZQ9*=p at K?e=>0W-531 zTqEE1U$v)a#&5 at n>4+Nz%(1-#05x?#wsU`8&re?f3&%=fhAAXr9L*x#MxI<`Q-#;k zeR(RJo3OWJIe at hb@DyW=i3`$6Bz1vBP at FN%^X=}P4{{p?Y5v#$yrw9-1pWiFe1Iu4 z@ySxzp!w{uVE^91V{Wm-|9EK!*So~} zJ9No3!rlCkcf=u*^?dVdUH7%(N`eKfdeK-va$;h`A+aQhoK_iJ)+)OL?S0D`D#gH~ z29urJXeY7WwMl;xpR at LUmad8$l}@|cAp7*N#h-j4>P2W<*_jx)^8>6F-v`>I-!x|( zK9=PSMbkpu{j*YstsZM+l8D}_>s1I}%Q-D;oo6gJ!UKGlpL*jQl80f|MA6EIv9;mT z*7lfJ$%P_$<+p$9XJ2+e{%p6f0`Z(O6FjZjUwCrJaklp zt;sQeMOqm6BInIfq4Vo;=EE>fe1nyv$Ez?Weub#|+VLV+aR}AdS#=rIap7|pS?ssu zXsOilBhGcGKSa6~+-voq^a%&_OY-n2lZir&oqXq5q${CNHE6ZH(snyaDXNJxX%bTf z5M?HrDd6O8b484CM+)qtZ;nQR<(pUlic3=AIsTfkM?&X^u4X)i;>1W>Q)%k1$XY6z zs`X9tMp;dNnR9|dO~b4=&~^3)6r;tRLau at LvZo#Jy+L`zev2Mlgb(m>)bAYVHdv{H z{vu_7;rjpLZ`BrOl~$+NFyv*GpI;G>8Z&avUHw~|vV(6RLVb6PlTO~{x6zB7gJ0Xs zepjOKa#n?ostLPOY+kVfwAW&WcUTwd9z<$T0*_0i8Me1>SmEW^fSFEX;L>krRPcIcAfiK!5XE4}Fw31*6b; zA~I$i#~ANQw4Q%m{GVFM91IgAZ@~MkYDFTi{2yD2ru-27X64PneN^V^upzH*Ll3KN z=O58kAf)@cV)pZ&Jw`J?)16F=?(F9lDi(E|+1`_IoWws^ha+GymJgrGP zL_+rc~Ue812S7-JAU}>9cz5U!Qx$J=B1vLuTs1q8uQ2wJ{PEu<$%93j}n(rIS&ZKQ> z`@DK{CcxW9T~R|w@E86d#gaN9GJ zM`u{nPoRbeG|X3%(_*QR=Tr!~|5X%1Wv9(J%1vh|6S4KG6rb0)Fm6=fv!+QW7h>l8 zc^|cP9KMq_OwPT}z-1e at B@BWw5Sa?;uZHo_3MEi8d`nMvy$ZyIs73An2S6m=?x(;9KEd9O51`e*O z at r`_*4Q(4={so!K_;r6}{Iu{Zn*_1*?^Zs%9cf9$~?dS=s{Ii)5 at zJ2c=4T_jNxZ z07Ok~f at Tei#D{tH(fSItvlZriCrV|0X2O)IHwc4fH>Dx?L|Nwp(ldT}cKHqN^A%tH zWM$d8W!<#M|8ok*L}z;Sw(`Ba*x&0Q97OSzxa>Qg-}j!sH*ww+-XAU#Yk27C`1U}0 zm+x>;;0*sJc1XuntO}m~n7yN+u0~3Q)2&sob{GS2ke&aNxy2X%uxffo!uHW=lpv%3 z4v&0>r^V}g(F*dnq^@3L+Lk#145AZ2W6%&~@~XDxf6X z*xY7UPbmvsDO@?-CWq}!G|MUNEsNPPqWT!k0L(u#i+n~Hq%)*uzM*c|N98+-cs8=R z=pqM|YYQw~8i4?Uk@*+)5P|oPpK=^n$$yr*dJ2hcKdqUiqEZX>r`AgQR$mAWOu96R zq^5dtqf;L;UqRPn9n@$7N-|Ba#5RJ|TZguKQ!1cUY;viJu+7t#t)B{#VCGAu_py=( z-#&*(MF~A(9WUe**veJ&Y(?>r<>EMTLyr~5R!Z`ULZw1$Cl*#M{FC8%y`JbaLn)c( zZ~jwq97u#FZW!{gXz57>p2$csd(omaSnrVff3hk_6+=mZf$&hRA{%sdja|3lkQq`jj5Mb-Ael_6 at Rgxj;m*>@w_T+PnsFNh6 at 7tC8}Mrk6jGgUfg zCwrTZf^|rqzZyk#TfaoLFRpqs at 9!=qkCX4P3#)|=FHh1=Zov*3VnJ z8djVvMY4izrF4SBy=6Pxf3!NP64hy2Gq&Rm;Y3}XqQ^5=^;IPPr}ZvNMDgQ0HwU+C zHt at yhXF4G_LWUoR5`7Wa4&OgT%f3P#6LviLOXhl2RFd5<+qiD;3F%UgwM>WP-yMdB zc)gNhz4wlh#@VL>XL0}PLeFBZDmXR*aL&YHNHY8E?ewz3`j>W=UAdzhCNOg5GO7Ef zrbecV&Vy)jspj{I;+o0<@v%&TUH>C70 z{7I1N;>Lr5vRF6(u0uIO)p`yb12Q+&)}Hq6r*w%-^bWo=Yk=X27{c at m)?GOYI`5J9 zq(`=w4&~iX=>!$;ss|Si{;pM+|C9R{>!;*-!%t7IK at Yohb6=upE<$P`$f4%yPxiil z|8?#i=dZRs>zk_w!!zX^K>V#q4;6%czKdDb$jdUn7DMp&a+M2Fq-bCCybd7|H=k#2 zrpyoYjb<%e6s&glCGcc;(FUJhLb8B>EOmBu=hDYQzQWGmyLTdeb}VIdczq=2TQtFW zI at Fy+V>vHa6B{eTkUPbTnkG}j0{j-m$cwo$dy0EVn!h5&Z!|~Xkh)dD3$mXp8y<>8m#e at Y~b zpVKe&WL!3j;ICu*Hx?z_wz-%=h_E$Z=ntZk7nX$_uc-$HrPVE4QUayqw@|;DY>7N% zC-c2h1I)mc0*5P}_*f{gPFziy!eJ(Q{Knz8B=c|)fw*{76A|*L90A#LGj8uYKxIzp zYYyzV8vSR^n;(EIKv+X^_ZCyaf7QRP7V(ePtFCzTPEI(Ta;2AY^W|I1YToC->10I#zoM1qjr8Ue}Ow2+1 zWt=aXpmdF1+~%|Bp4Zcmw_Ke&0MH2(6H6M79f#yod at K}yj0S38eU}4lVCndE&%CSn z*d)JMN3v~t!q^h9JG!I5T zskzV)70wI!GB=SPb77&QV7~vqjkfN4ojn6ZQD92iY2WV`f~lmo`*I;gfHRGLLivmq z+nm#hhPQA5p#jUuY6>w_TN{ebn{uc{_;`ssI!hlMxLBpNO|Cck^dRamVQ|(lBA10y?p>k7mc;qk&afYEjIdQuBUG zB-{EmZ{Pj-X^mYjm9I74KCScoN)q&pqjlnA at E68IR9+OgCvWn%FL3O1Mgz4W1kuSk z6Pa}0#ACV-PnVxbt;l}w+k|c0+jMC at G5Sw<_PgcxW!As{F4p#V^u}Kpb4Q)e&9y zmrPCfO}mAClF5UMiqd6+JDC+7AL$~fF%LU^Xwl|dLpLex5pwA|c at e9b0H-76TrJr& z68Z8?^Y%{uPbN>b10RL5Is9{^+;NqT&6JK3(^%=3&IX_7&9c% zMQ_9>OD?M*E~)!EYazS{rNwh?(XVXV>E8F1N=IJTVaZLpZa$no1Ws}F+(twOiOSDi z2qD|3Dh80SN_hX3v03xNpu2)16xEZun%5v$&Cy``R+fKb>iO`)!})wZm?RoQb;8ML zmo`m~j7oR_gND;EV{_H8~lh>PR at uENZ%~x*4(JZ|`YsZ8O{# z(V*t4`Q?S$3NTn-?K-=BcS_C~i9F6=;z at 5ma;R$Z`ZI|kf8Jr1N;fxC58#$uDw$&m z(r(`v;pBOV`z)0A>9kqt#@pcnw)={73f%tEs-vjhg#UfFbeQvLxgs7aS?VsLc$KH- ziDWsy(x?zePl+6G~NwiiVYT zFDhv>vyHzKQie#pLtxhspVVsZOmW1ZKK>)>x16 at yR^D+6*6FVNb`uvcWkwsKxewA? zR+J4vY8NHP01z*2R8-vok2aLj$%8u&7 at yY3JN283PR?PnHo#9M*sjqdUh9^Z&Phk2 zulMJKu51!_Dxj&|^Mfit7_~B?a76vB zZS^*+{?Ikw_&7P$%J|^cG)_0H9t)%jOipK&jbsWv%#r{p&1OppmQp+HBMkMK{m5J? z2Z1^7`5Vy1eA6oxyZ*i7KUraQs0>}j49Xmdy5e6>mqRj2&UA_EqzInHzG~E$C5*o# z>~`EJkKBIJd(df;RQZdwn_Be?IMt?85R3iT;LCJrFAaLHYvAikqkA*6AGxm&M>sZ* zcZxe>Cq^c4-d$>y13%bO&75bqj4f80Cg<+RpGnI%SN(m(a-TJW%KMRj2+eAbE-7lv z)+%b$yTP?PoinDK9=JplA>G)Tp$>u%i|Uc04XwG0)M9Gz(|xISxdUax*1pM5`!W5x z%nJhWR{1F=UQ^?R6Khr>^t-n9*=uI34KO#^rglePIt?0-HytMEq9zOXew(xw{-9DR zow*4c;U)eY;Ni(Y-rE0$sWc+K;G-;1GYS>PR^|4A*Z~@i+S=;n at z>%!F-&B>mzl|q z-%#ozmMW%ha}`dpmOi=X`33s;Kj?Dy;0*?UsurL&h7cviocY-H87M9)&%>su zq0$Hd(jD!8OZ)-)*$b3u?Yd3lfM>n5I$SCc|1Qn{(SpV&p4<5PZ1?ZS at lQ8?in(=8 zl$jv2uENv6(OT7{zXvD9|<@kj`CPxvBvL(*0ynmTj8I`hSg|A$vEs9}eD z0ENkx?&`9k#Kj7Yx4vOat(M(Q_KeV*GPKIV#f8@{yvOEZyiy)tpL%c9z81Fw`aG at J zn~O7zijG9g8IL)*4vZDcw7yb}de?GqJ;rFv#I)_CG-7-(%&UJbBlM2nP>8c;G?%)V zGV|Gy#*M4#8*7JeVh{49t>&M;f2^Z47_RL<#xEeN(D1ibDAeCZz135KVod*=k=IDh z!va^7hUeMhEJ)Fv*`FMdg(=hB&I!JM`X5=ltFEciQ8eGE;!`}boI)|Q8As<;2g;ix^Xo=Ge(oMk3oNR4 zP|@uk+lM%7NMbchyQFO6l+N(NRAQ;t*C9GBSPyAMEAmpZVF3x`sSnE*jc1uu+8P2# z%Bn*5j~)7mm<^+ z-&FNn0tiX9w1Ve0h*;@dHVC6o$C(t827HZF;h_*EfeHQE2Hmq_-ni_GX%NLoHMA#j z#*65b+a!@K*%g=`Y^aVTxiyTbf`OvL(j22?FOYhPcw_N#Mmm6_KErm;KOV8*E8qu8 zfeyc6%vCs|d~_QoLjnR3O{4Lb{<~NxSgWQVK&l*NPK0~sdvV!!4f5==e=}1+)h?$a zi+wGnUbzPWEoNK~%8aIqc+Al#7Pb1US~x5;#8wbfe}W>4M2WZe}XDdfvHa6MSf zca+=OCHzugms9nO%M|6FephB2K;{+qQAyWKPMkT*K@@!ysnC7!QG~-#LXt}q9PC}- zS(997O at SA>)hS8TNM^vVqMaU491#yxyfjdRH0?|b9uA*l2Q)Yps9nd?N-BKE-iYGd zTg>rwCh94vog6}nam}3$E at djHHf)_^4Myzm9 at et^*;ARST66zR;i7pzhf%bV%FSko ziAO-_9&^|!q}g;;vKZ}5R39Cwq(b>03M at Ang`F+q?^;uaWmxV0-vDy%GtnATu)1}t zPGfs!i?D|JbL)TyVwXhjOPG_bH%&DDn{aSzXJZ3^f0=IX-vtJ-*!d* zd2zadQ6QJ7Y_fq#7i?mZP~V5UgjIFtm|o{k-2a?&uqvs8jlwBvyG23_nDFyjZEHjK zcMDmTHfULI8YilhaeL`slwf+-kAvO6Sz752z}TK#4W2K`9+uFP at x^i9jj(^fTh>d4 zhVs-encZa9mn3XMy8Qg~xAZ~l)bQ?+IDhAvD840Q1CO{hTv`&@O$S&ED6U59f0XW?a8*!(e8 za=Ohq3==lSCtE51>wLibQ`^wZgyA;mPt^^c1B!{7 at QC8v08{`18k`qpnwESEDrV;HD}%G`_q+ at +oTZH-<{+z~2z=3m=b at F+`6#hSyVa>n z?=ZRBmmzm}BEySj6*qYH)mCf(M45haw;NSWTYOjpb#L92Yb>n*mKx6Gqd{bzltYfe z_FrOrdx-grgwGvD1;1pd92;U_N^~KDud%326fNq;$ewwSjA2p5 at Ck@wDFsMvL2esE zO427nyhh0$LGytoyr~GK^c(> zr-IisroKj^*ta5T4PC$K2nAn5HuE&vG|h=V2v`5els-2d7cp?U3 za~l|igJz5`@I`P%S?d_f)N^E6lfP)%wCpY5=SR0QOOS~7qTXo~P{T>PUHt;6xRtl5 z4)bb-rJ7Yl6$};tXW7ycy#YMV6WViKw+bMge zrQp`38-I=E!=wBL$);{0u6Uw$lzUllDh$aXi<&cm&Ub;Ulz zqabMbQHJ3fNNyTEFpYgL7o+UiW<&>nZOeEYdyuFRYvSsd_R~-lo8tLC at HyDUevJPy(3M+P`^MxA z#MWErcQZ*Pc}c2%K9W{TsyoxgsS%e_+>;=$KW3r=GH}ZpeATUO1;?6O_L^tM~Bad^Iw&7WK*|khXBj*nuz4a2qf|^m at cV%=SqeQ}xiF$kX zC6moClu_+^tEt+PORwEl>DkI>o at m&8@()0)t>$0Vq&~U-D28A*!!Y!LnhDVs+y~Z` z(nM37m28v%!UW<-h-a%e(@_4Q&rWmns%$TJqHh$L;y}j7abAZ-^~YA2qnSQ$iT=l- zmGgnyY2Z18!xf2>zi?)_u9?zf9&;snqt2(?uMV++ZJP4=W8?g*({Jzd2p47A@8n;Cfam=gO*1eSNs`@N&Z3^t?yjl($l)vDD`mZIFHLG at g>7 zV14*z(9IfPtrC_~3Mz}6bZ@;v%bMqF$C`eU?1PDXa9H{Wz4GDU>&a4Y%@mC`6Yny` z{-{Kej5(|n0dl+x6UZ}BN=YF?pt at Y{juohP59PXiUM6q7{A_l#{(kkZ(ofibAGTtm zdfTm;>Jr>F{_L+z?VZnN7<%rWSzC at 3i%J6__5ceDD&c?sBIdZiqwG-1Rn%aH6bF`m zIJo5QT73#U_n)V~GC=iKq<+EqnxA6 at e_K|HuxMT{Ff~#nn_GB}utSCx_LBBec8nl) zd4DyJ&JN8r%;e}&m_(6O;i0aIhNS7~Ir#1M6WdK*6elM?IEs`9TepzwPS)UvO}^%& z(j99FCh?|4qk?!fWP&xMy_-DSF?8?o@$`j at 7aOX>^=ES15qHQ1zh$$LjR3_0sf^SB zMP6 at fvC?H=%LWBO1`^ZCOpFthOvHXoJZ5fj30=K*Jgcs6 at qza{$zzRJ=qoMGQvfdp zklaDmYA6xP%irNb6OLe2Y0?dQjt;=7dlwB)xfxg#iQ56yKQYc9flnX~Uo;{$>reyM ze53>z;M-82nTU>pNFyK&z|hsNepY{#VHuR3pg{IQ7Nf&lZ at Q9Ns&{;3`mCyfYVUu? zU;Ccu_c-l!)pOazjj^h8tl*(38<>v%9MfFlIgh%Z$kVZC|%6|>_6ynD2l z<|J at mUj1yDer}L~m+3C0Ii39`wuoqHic?4{M&fH_i%6~7j zKq#DIEl0 at Hf=|zMnb44k00w%uZ at 1`y6RFiLMl`Xw>%iM}&4+i?L`&1!d)zd*FjWT> ziqNN~`X*XaLF&!D$T3~D9Hti8Ji~8f!PsV`u-)!6%PRsnduj-|y|S~EnblE%Mrvjw zd}r~CEw9>3dOT?*<@Ap0^%t1ergc4waKQ#B)b$2Bs$A>PbFuU14EE027__xuWM}LX zcm{Pab(eY+R8$=5kjr zu(fU>>e>pf>nQQ!A at jZ((Qg{{d{L-k=O*wT&Rs^`5V44XfDEwObVO{n&~+X4EX{Xr z*cZEr=%ESqy{^v=`>CUiXwT}1Yffd-wh3gch~kl2sghr)ry}i0xiv z>x9z8DBzvy`;ZN(wV5a%5&eeU5}Py7M at N~hh5i)Mcaej{wAb1=NnPD#=MA9}AsXmr zOPAdIWNf4>hmn^@PMThcD2AL)(d1~T=Q|0%r at ihH&MQgMuFWZQO8#3aG3u5o!87}I z)ivA%Y*Qaa3ASU4j~IxoV*hA_ss8fv$Kb_w6Z8c?m&}AdQ$ypX{_gq4 at Oe4$ z5z0s3o?KJ93QndNs^a|0*dw74jo$hH!4bdi*)*A>8Z|Bk_cC$PaOr_B%^l9mK*q-A zBw{b9{}%pR_*xo$HAe`+S$Am;yttUvymD)@Bf);jH#MU?Bu-D--bW~F z72J7!X>pYQP_5z%KGFl*v at q)8gbTp-O6a&?do?McO)u?9aghE=G58?Ypx;e-TV0HL^3yzggg7ZYJ$&6#2x#n(b)6qD<%%k+Zc$b;GGx?`Q zVudyxbO}5kZ~swyhL>>UpzL_ZwBjK`>@uxgNdRmxvwGAF#0uGHvEYORnqcXiM5U8$ zT`_AZK7Kk;_3=+DOcciveB|7M3OT%}&En&l<>k{Z`HzKfHy8$Ux80E*(sFCHUo0($ zr!9mIu4Zo&i<4F=yGQqiot=oqG0oXQw7AyCsC|X~^7Fy#M at cFed|t=wi-mlsH#t4` zZK}`L$A21}#J}(ks9JE}vk687DLPiq*PrEeb5cnJl4UuJBu10EDyK7y%)`%t!k at T_ zXQd;{m1dAibylamR;P^$H%Z;v{|pA)>7PYDCT&ICejjqf&}pq?EL%O+xkSbgTke%n z9MBci<=LVGwTUHXt;-kQ1kZ|Ex(UKi4d3-d7jOdn*yXdOf@<#3nWTnI({kln;fsg@ zSC65v?VY{j)TCDMKWUq~iQKo0)RSQenaZk*wAM(S)KY8+q~KfUy)2pN9>=qt%$EL# zm;UpCUh+BniB^K1>ic9jA6PfU$eAn)d~vT6qNznw^RU$RNH^yg8Of2UlYKG#zk5!c$W_ce6w?6Us{IqIe zXLx~7S51p}`eVGMB7da(E>cKyb7P)OdoKC+EI3Ib>Bp5 at P^F>9g at T<~*cOwFqTJUBdb-%EZp+;T6k#3H2W4 z4HI1nqJC%UqcE^b))xWH?3pAaUC at NVu1z{Sfc>f1O8_V7xMXB3EYVp)NIq8FE;WZ` z`q5&7Zl}!<*8&|;MY_}MRenFp$b&u($;@+h#D2OUPDta7ElJ94UB_PDNPakVV3a43 zH+_S{8nCaNGGUkgU#x6BYQ=~{Qn34$HS2w8*^uYmLMF9*Th)X$XF=QAdH>ZH%~vNC zUI9OopF!bs27hjHhsctxW>!PVS69M6D$LEI?C54I`Y at H;E4a1%WpwS3ZF zPc5A)_J`m9IUTDD^3+?Rc2g5WzqiawT at WjM>cX7QJE4LLAsi`VaMONVLu`WJ!l&L5 z_|rNV4-zEK9&=rIY;e)Ofd>lBCe!0kz;y4%^Y<}2`hg7vz2dMF-OJuUG4`5 at wW(?G zhEP-~UdJ*}Bv|rMoYT}Ght-wN+xLIvuQ9xDDZJ-BV!Y~#b9yy?(|fVJ&Ho4w1A|5~ z%!lylU)1e1^+{nmI8BpOmnQZl)!lEH>Zc}=`KY+f*2wcKr*a2#l-p$v zK8q9is!-0-nA<`k^d|LNPSubK`P9BJ;rEv|t`=h69}NCt%aep}{3iUVOS;g}KxM>Y zqY*!<&O2$~u3{Om7|3M!{mtNtV!@@~Oycy4P7e2HDD?B$&3kO=Eo0jvA|f2N+Mv2Z z_3)FVv)s}zcc*d#4#kePUw6>F_}Iv;MZzyjSws)y@^R%I`pm!MUo0<_yH+W`2;A{p_#S-17ce%v4P7oC3{#{K6dAifoF|~jF)dOk- z-pKQ|x<`~C)pqIgFA2jN4GH|*H#R924y3zV9(!M9)0SKc1S($n<3(C~jKTQ4H<5?R5U<4P)lyg8;$qZ94a8M^!o7{^_8_;t+`ClZch z8|(@hnHjKR)q49KuZFgd_&k8)@@$eadH?S;YIY_2Mg_a?kdlc?os8FFLs4Mx*@f`F$Vu-}&WG=G7q#>D2S-hKjV{ z9iANcEJG=7juSJ z^L`?eer5geQuvwB_k`oq# z6woV>o@!4D{DFg{Kl7WIYpqvY&B^fmze8uGRmg at pJ^?A at b?ccYZ{>$e&noJC9iimS zYrBp3sOZD3#49IXC~Vt)c+*x5QKp7g8WDcI96B<-8{p~Pl}9oG&u%mDRdb0EKQ4U= z_H+-KE%K+ij;V;TaepYS`NXD~X<}14COPR#fzh;=X9X8g&VSxUMI2rDA)$l+WLMph zmw%>Bbcw1KWEq#%b$j-oiJ23=df`OIwc`9>T_0G(&0%5uOZ`hF)+@}P2^oK{j4K at U z%D;RU7O}56k+j;Jocie?y3PHY==qC#i82K1Y at SY3yR!kvmG0i%dNH^<*u(YJ$g1H$ zx4Pi>5c_QT>b>KWljxsPftdBVa;KsxoKC1(kP_Da8QjKSW9CU4MxG4Na&9W7XHs?bo(F=kXU$K5u##Fj9&@GPYOElP2Z zk!f{#GzC&AbQMUsIN>Ws^Z_^{>$Ox>Ng$roWAUzrhO3g|jhZT2ARQ4Az=vU_bdh9@ zQPtFf${ZA}rsu&Uue3zLj>g79+R$jDZ(UoAm_$}sV3E7Ixek at X9-*z2T9V;#^P5(u z!nIrC5Bzi?Uh4jJ59ggFv{c~??o?Ur%0NzF@=%Vkwzn;p2Yyt96$Bx`CD4HyeCS3S z%`EJQVQ>t at Mze(GTx`BtQbBNUu-#0lBf1EJ2j}WoMB3h<$ZO8h5UfSTsl%GQ8JzWMl3%7<3BW~i5b zG1{|D#itdahG=amDpoN>mF;}$rl1~(mx)-k)5*cX*Uz<&Y=Dgf!JHDo?sjusja?|T zpr+3CMitdaJg`y23E1kn>c1M}Noy=v&7tD at y!tg5Np)B&ENc6ce>Z2$fAsHSSeWgu7gLvDEXg;TXI9i@`j>J%k*zi~w1>O$#lQdJ zJ{d1}UJ%?Oy!;WlOD#l(4$x9nEINh-NO#||JN4apzCH|srpl?Y$->HO%DEyl14Gv1 zctP7s_jU4?+aIUW0$yYsST7--1$rP#0)@1? z8fnIlFOHWQzr7)E#enbRItwZnk at X8Oq8NBN5*WZy#BOBNx7?e;=y;D^tlLf4Fz z^GIC;5>;gIPcD1hukU8PNpD&H%*op+oLSWhXuJuZl7itDw8 zw~)-!*fp);jp~CLXwoQ{YVzdV`_Bg=i+R!;9^zC7ziK$HWj{-E at OFSW&2&1#&$8~!b^2uExID1YBvM5U^9v}0NKP% zQ$iiyeQg^1WQ8dO3n=M|wsO?-sUk(4=rY0+QEv)vHl1U?h%HN#(4W|Df1{Hjj>&jE zXpH5Me_N5<+Rw}uGMz0H36!iks8@~&s{8kbY84(bG4`K8f;nBgl4opPscdQ=LC3>T1G`FMHKCI z3Ep&ruh2D1<|F at sOhB^;(RdnQ?#oXGsVq9I-kU`=#yoE^duS0ocx16QB??Je_yCODogq#X#*zt4l+<$|&2$LnR(OMD| zW_m+c;YjYCFCA-}u6^Z+`H3Y?H-!x;riwmU+_HY*#)DaEbNDgikGuldWK7Wy-gl<5 z;l5sK95yWk91u08iKe&A3LvT9;yQ#6I at 6cLkEus04A{|N2|~0qJ9&<|X1qY3x=2;O zOVs$C?2-~&;5I&>yil5W19%s8GYeh)0jFZyYQbfQpD=Luk{cCnOao_vDQUk7!OgGS z4GtjR}nhlZkMguW!REbR)6+b#SoZqXbFq at WZ2_>Nj!w at 5hbUF|_MG4u>-Uo~#uD!39}q)K)( zXJsqZo}>)pUbP%~9{GrsVL6eBm;bv_M%3wk&CPe*bo}}3?t{1HXCz8ij1yg-w2#IW zEw-69<#M at ukIljU87190)SnN=+ug1Rl)&Fym5eVyX&&ygJQv3L at J%CKU)ZFRZJNfv zmfzr%QLPYF+WQn#pAQsmMl-{epjvl~a_7X96U@)Q$ zK+`Q^s1)TOT9DP_N>l2nqBVv0;Z+$IW+&q{>V^~YE@}n%-lONN^kax6qi93CE at ZkK zT at H8h5izk5uyxyj7%mC3Lgt`C%Be{sNelW7R!Zp=96=e;@Gpj;(0tR!;~rx7Rc^}w z$$oPtG=$9r!e(gE2Mrj$*DpYLva3BZTsuQf%VBR=;&++PNvVmTT&cVzW~UeCdUg4; zK9eq`mLb~B;ttZS2aEuZP42ulOc+ih-z7q3=_`ZkO$>Ro^3X)JasVD2J~LrHQi9$R z0P!R%l2!a`slqV>Y(T4GKo?G&DDA7N+1RGm=ezT2tkn?PZYRpc&jZbc2VHsI)j0 at g z?la!A>ag4;a at RZ=Ne|{Brz?_(1^T>o;zHfUVk+Kbv9+{6N90q?-C`+U z5zuvGC~5aM&m8t{3imrt;fjwnKzL6=n$_+-nrc0>AOWR7(1lT+H4>?oZ$7u71QmhG`1Q#PUQQ!`@C-J0+ScJ`_S8^gdL#RXN#L#T}4ps`Y;4oG6{}vw4Dbzp^GqJq>{(f`BbGp=CUT at HQQd) z?!xn2(Qo$poKLTXT at U@c7^0T{{;dPw-_b}*P7|Yf`{|`yXJ2k>baU%oa$-A!_(-Lz zxUbWLDT4wXXJti_=UoNJvJx#vFP}1;OI#&xe7}FcUfO3{^*+(v1~_77pvxvS&JmUJ zP?51b7AF(8RgjwSFZ}J*a|1amW8`h`9hjsUi%jr;(VUx-D}-1-v{1}HB2rPjG{`L$qUM8 z*co|!=3j1_#Q16{=kX|}j=l)=r{mZugJSnRduM*6uHMPm{V2zJrt+1!mqq6o#R;W~ zSlkcVu0sy{1yNoO4yPfYR$)w?=KLh1MD`JLnE&D7Ov0JG2Sa0-K-odkwd{nz$TDXg zu$Je^&A)9HuV$aSF27K^_w19>By^f-*E+3^K8%_OSz$@5lkqc8=9ZO>yNlqum|2-t zG-v3lRIraR+Uv+exMwAJhgisly}4xFEcCtNnP`9MK`d_d?RE48pKU at 8w<$lftZRb( zqFnqn?OU%JH16*{e-`FA8`2AwHnS*Pb*(v|`~xO`Dq6J?W{bIZeZ8u!d)RxxHZoGO zPkrpYUOB542Ca|S&sP`=%wF{}TU$%gknux5y~9B<_+_2^=Yr=+w)mBy0m z*|&Rb!d at stRPxGHQ~V-{h0~ebeEQz?QL!dw`Wu(U;;7DMD@${6YO`a&cKq^$#YiaJ z;w|L=BXe368RD3Z(B4Syb$Iabo!6$Ho7*swl at cgb5;Ws>KA_VZH=kQNwZ(jE80E9p z=%VKo+-fSd!vf;K4_HBL#f<$XwG~a~Lrt2mdTEPSE?tG*oX<1}gVLWjr7iu(85NCl z4!tDPNsY6Az}GXe>71vy1GoDgxjz_Qq6py%UzylW{a_|o!lY!0!1OMUfj`J{>tUx6 zOmigo*kpTFxq`UoN%|^ZvmW_nB3y_?(=pG2T~ItcPBT2*`w^STAybdbS&Opt at WIDU ze>x;5N6s;CaK;^1tW&}#>OU!;X>VkBA+hbre3VOjXv9Qmq#9iD`)Qam;*PeJvGMn0 z*--X at l$up;E+W5yOo6r=2_}~=WIZwSi#m`CdKi95VQ)wJR$7Q}c;u$uN?{r}FF=pX zW0lP?=iy&kNxsJOF;1vq-1f>~Zsn6{fQ+LyL5VjV>Ly>&+=lR&QWzescdL3kc7cd{Xf_X6yZC8^NTu+I3rB9 ztXDBTfbv)l0_~e#1#XU-NMC2zUr3Bqu?r0y^lT44+`}U3@$1Z{$kptgugR=4i{2HPz~~~ip(|XTPefv7WHO`PHOb5 zMw_T%RB0a6aNjPTt-c5mF{v$zG!?b-kmYRwAO<=73zUW`ByycCCO7WmSM@*aZVX-m zj;3E4R}7*T at S2Hwr()hjR5t(sA6KL%H9 at 1Y{3 at TZ_EDvoA_<_Hc at ZOv#T^=951DyAJA`fx$2y62Pb?gZ!5<-qH0Vw z{NWj$0=5ZjN$oHJ3g05E5I*)NCUrP%h$+k8%9;Z3TY8(KW5?wI#z)H7rLBWLR#pvQ zGKMo~g;tkzGC0i`nAK$^kGIvsU89 at hf|x$i=>z)IEaHw)X372LB(v@~%c!<>!e8UT z;Zani=O}EzLD9O9b!t1_ZxB0J)pAXr62X5j`UNk+-=XEn51+ at WV=>cJ`obi;WJ-kPr=R>V8IA at Wz$e;lN0bjR_$^JZh*9{wks+4^K;Oxb3XYm_dm>Z zFW5de9sRQX#$Du1b4=V1i at Wh(|Jm+GFrr8BtxkW}g=*quWYfp53q_suJ5{X%y6-^) z)AnuF_w!6Dnp at 3ICr&~?y=|b%mK5g}FEx#D+yNoa;!`7%YwUSdAML%;Bkalre+;hFqAC6BhsTYAKdkHNWzvWFGG zH5DQzPj5^Kq#7%=KoZcc at l%jI at V$=CzQD#JGUd-z=vwW-+s5u_RPju~>(z;5&qI=4 zm>=b`m-4E8P at fLAcH8<0JS*V$Pt`k_l1KT&2gv0UP>_VdWB`9b!s1q$7X>^hP#+&B zjF?TNV{ZYEOUzO?b9#z73p?qioQy28xJS>W*@04ORwC0M(aedFX1xPN(;JxsD(7=IUj4M%>|O+$-bu3m$P^J@?S)h$Y_S?X821h#$d~iuJbX zq*U9BSh!;NAedD6T-e$M!lTH1%5u(pd2TM}N#~Y-?c2*&Cv9T3#2M@}FL at 0o1}&nB zMx7(V)XJKMw{N1J>_r+tvj7=D=D&AdtG63HCkq#?cF0{0RYp~wZ&RjtQ0l+6$pQ&= z*96$#6Glz^pFRIh*m#V#$oE(~31XNNzInRdp0eEF%N6A3sCnr3Z~)qJ-X(FJAj89( zk~_uj=l*n<@V(*NB|c!`aa>7#_EH%Q4Fr4DZn+gJ)5> z{jq_Z6vW0Z7V*WL{vg+^@?$=iwFm{bPsIp)2sXQ~zg}!OC;d*(@#g6C8#y}%uf3J^ zeK6 at OG@$3AC2{IQ$pvNdEO|OFm)p+9 at zP`-rTIOFK)TW0Ke(5y-=n#WFWPgZe!BkC zZ{;ECXY2ZV^U-?}_SoTGU*CG5hWDefV#$8o#!f!^BJ-}^%(TnaM6`KFCwJWNm%ndM z{)L_^9r$Z@;qcsL^NyC|FP~?eD96LsO-|H0>i4Y_{{DSp*>2d z*~6kpLxe03_@}G@>2slL`yU?XhVwJDd~Dd6871gA?)p{Qb(G7y z8`F5LTt%BBI-o5f#RAVeX{(qaz>Bs43%h-i7~(0M at UZEZ*8=Y>qw52An=jQ$JvZ^W zqAVJwUfZiKs00p;44>ynV;>NGJzcr7Vn1?GGQzi`Ebn3&F1|fJ-bcaINf9D86Z^dq zBS3Zy3x&Kor}4A5r=r<5JZR(M*ZxCXFyA;I4<~3Cv`?w?-QhW>xIR*#o!uh+BIWb^ z(t1}WQB$C(Q5 at -3$DzF~tg23`n()uNnd2<^JI_V^n(={$@T0rhGp-2zz)lISmB2C3E$ zl-fTCEp?D*L7nLXj?FanuR`Ta?mv|u!U at Bi;!*5kV}9{J_o9!_@` z6;8cuCPuTj$iSBWa>%-p&9l?fjqd*>puZ%z6b4>0UT&^66}GRB?d0Sjm}}7^%+0ss zG7k;)AAQ&VvOs)ijGXHUPb at dQz^_c)%N|QW%a+I-v_iyBsZtuq)?xjHDrv6KJS}w> zN&U`Prwd|toMrm@%w#kQ$&jzzpBz1)kSc?RM(^+hir34 at 7j;Qw=k8AFps&SQukV5q zsw3X?4~43IrldDGicWTWamd#yhAWpo)s<)voR6fNKUf;fX zep=AbeZ3W({ul$yvj4*+`Lt)K!kG^)09$f+F6S~vbq&XS&vo*Pa>8AlZd}p7F1W3O zcedDHKY8MQn7NWmrH)eUuqysEr(Es at 2F@u!bjp>1FHyB}D##%A83 at uZY0wqw4vQ!070 z*- at lhFb)KJdb9W|zNK|{z%L!ej*<#Jnjhi|;6_0I(T)3aBdGhoHJ zfwGEdQ+>xmdcXbdin{q)2D+ME>T1`iy2TqKgnC+B*Sv%;IwDt!=!2b{mJJrc0H*D? zA}#*@$xQmP{Hc%aQ$<>;m8GaBNmP-96?s+UlwQ9w7L#mV$!Sxpp5w%njDFg9 at 2O)? zt`&Q+=cv<#C*2iLd&2}|Qp$aaTmuK-UZ3aN3QhI#?Wj4!-%bjgXd%bXtzX(JX>xh2 z^?$40`^?^_VrfkYz50Y(JC2gJ>S0Plw0>_{6|o5ovivzZ#lHE3B5+fcax?Ry;(chx6a=P_3hED+~gmvA}5M5Mb{XKbFq)yf? zh3b86mmuBBHDx)mmk2-2%NQStp}F%%q at 0`>tj0b#M+By`n~T!hdG>l`tauF z(aNxjt7ciC(?p}|VFD3U6ucdNaHEOYR34mV7)l%4^Wqu>(L&(r=hqGrq12$H$y<)# zXispX>+omh_GirI=esM^xv#t1w?w>bTICUWcACH2bxxiJk^}EPq`!!}ZoV+mWMaZL zQP?+v-kTQcF^V-31MvuLl^o5-?8F*pDY$+70L<8*;tRG({uR0IK-ASUq;6dA8RMJH zQA2n!jp)I}eN^#zV8!~Huh*}$GMX+ZL_SbUuoRF!sf{Ys7*s6d at a@pa>f{f{w#)+; zm}G}(vA3)&0`}-yjh{sY8Mv1~aMiybh zWFR6&2_Zz1jSwXeG%CB0j%aXS+H%9;7CZ>xa6BCIoQS%)eHe{y*l+tc7^PH?YA4iB zQKriYDz@@yy0_5sN68gZJcD9|k{l{XexCPu|NCujE93FRJEHwcPjMy_#8nc6XrxbT zn@;1C0yZ#H4bD!V&*o2LB}9hi%6LfpgGM{B`07u(q at OHDkMKS?Z01#9Ffr!A-CQeW zi-vW-9Zz~IU_)3QM+uJ&1dgJbCJ*S!`sI=2;Wg~Y|O;H4tA-oR$Y!K8u5bHIe`M{c|=cVLgc=t%? z6+UGPPYM}^GKm8Tgrq_)8GuBpSYjYcB#JDEt1$(J1&JeOlMT{$UAv{JRU}O&qOEu& zV9hPY8Dv>#rZO0oB}$uQYfiv&ww#G~Hlz}r2?(X%#U9Gm> zcnfM8h zIy`oef$Z>=Jsyrcfo=3jJT4%S*#og?nkk8gN0Mp+NU>|MAh|&||1lQ9XefY^3pnJ6 zl3+tM4^48Ma|cgHlg<9y00DS{}9qGGmLZq3cNV;`A~n=C#>-$iWG zw=o20}#%xcg+v!wfQg(#)Ee!y-gmF7pINwIdQFT=IJ4NRyGH z=$r?{P|ZkWBhbZ{NZ>sx=GfR-J_jfs=@XIevfEB&7^(UQlTiriDHUZ^QmS*aWe<`w z5t=ZZ2QAHs6ydsLl~T$`rVz=l=^9BsQpq*Q#AJ>{R6Q%$#xY+ at Iy6;AC0|80`57{s zEcV at 2;lS#Gl}I>nFlvK_;AkvK{6T^AjIL^f+8Y{u^2s421G4THK!?2psR!qHV0;}R z_axIHjwvD0CP-pRO2QeRffi41=h3?)*T6(SNKd{d7BL;DGUf&hRk8Y$)$x=rLa|p<9SbdOHrmaV~OY_ zVr3gJ(6TX0h}=z~+=r at 1tw<$V4wfpY;29E_1xlMM7uW(1LDpdq^+&~EJC~wLrAnwC zZ_L6ps;lHI3BpGs3>;6ihsb%yj{dm0;6 at pGI>cf=&!y9zme z^JC7M`=!fWB;@4_Hk&rMwlkg`4kylh=5ry>5yxhs)?J~`6QKGvPDqjUBap4-LHXGJ zbAX|wv95u&1kjGI=g~dM8NKW1mpk6^#XTaB4+y#=?763MvUlWi{4IS!e*3yp-pTT$ zIuoEzLb4#GIVBh|j1q~7h{PI6leLo4V;VOkX{W;@cw)q#H!CyPGG$ozDQRMtZjfm> zSDKj9O*%GXN|^ZCZzSQ-QSxy)H0R50PjU1y!MFV|OtK}m$qpD|s|;crRG9at&Yx$S z56FL at r{pW_J>H*loB4+JTFN#QAcN{I+t84xKPwq7d*wITq6M|Lol($6i_wEaUAD1s z!G`~XBa35k+6)({CX-7bj$QRk-_mS2LJj!{2O+sucOdC5ZV(3usmrNNa0K85_D(|P zNcI1mO6sm)$Xs+TB8KE(!O+Oe%|wz>MDwsUfyyjO9$Ioas5&|E5i4e#O46g0NZq@| z+~M>%F{l)5-4)xa?-27LT)Z_ zXgq|g$5t-qQbc2CC2Bb%hMSuH=e?%4Q=2g8NsaV`nVp-(hUh@(!6pj}v5OKtFhH3c zXF<?Q6RW7~HJu#=NU|!)+8m2*`JMp9s$`brnn?l+ z4mC8usHIfw?VA?nDo)PC;v|+?DH1(8@^3S2?N0lyZmuD)?rh|{D|DLk3OL)njw(Z* zXC~!sB+n%X=GoCFG*T%PmI_%mvK<_!BJ~Y1FzQZ`&u)o3*kd6qFuMupPnN at m zlc1#=xx4^8Yp?;cPK=ieKoW=$f?rB79N?-Rg7P`TmmtAmDG@;gjjv?|I?)l$f#9ax$>#_i2EIE*k|5`;@J{C>^KE^In) z2RD$^3q|39&c*LywCK^u4L5i5h^2yTv9?JQ9Gq|`Ce51M at 0G%|>eNMPvs-CmbxWw} zjns;3acdiJsV5^&9aY6_YHrclR=T)hxyhy zr(%nY&C#~z2UIb!s5qE6Nai_1<+>DX8Rp%4I<(VCvYX0p4b3ryvCQg(CS#!`%KtlYq5JIAf$-RR1EQ^#-?FO24Zp2)%aMpt722ITsSt!`hjf8Ds%{79?4o*<%oWSIl zA%k*E2o?oni<@%B12iWkn{q}O35|k>jM%%uR##_3B-6sTT`bh?Bnmcmy!1^@%w`Wr!9##mx#xy~xD$m>Je!$eii8Fi zBJ5Dh7NHU%$mVn~P=br#a|R?rK67?a$+MF~O*}i`(@Z(BxO90sJES*n4o+t>u`!yE z)5Wo-&Gf0x86?oBLgBRhc^(=h8b`6oq_|T7 z#s?9KGDsUu8yYMSGb~I?Q-78)=SJIVKjI9a*U-`w~du z%WRd^i<=NRB!Xk1!1)W|CfPVVF}ee*sSeuaqRLvTYFWx-E0cUDHvO|UI8Ic#Sm at ES zT;^r4Ia$?B+h8}*lR^9i7Dxs{lSo==W+K=+ScwF#IOGOd%@;XjSGjSRcEW;96e8t| zHcsr+ygS^OHOX5ln+HWS-LrVn+Es7MN~YR3DB-kXxLqB$ZPkuy3>OU+GG5?ay1{S} zn}|-vnLu at D$@-B7s~Er8XG(ajPwHVur7&2mQ(u>}@_&dfm3EYww-vw&*0 zcQK<*5>4AVY;7xk=SN1)x+abGaK?hGqoW*f#8&Ny!L*WG435MQIg$rCm(7N58%CWJ zK|&;Vxy2V|4RV(-HQEr{09y=mLcSW*u2N=gCXSm#R&KGnr)Gyo3~gkleWA||q406x ziU^N*eMLaMo(RYhF$!VS#*1p6(pt?05hdiI0VWGWa&BhJ&wdtY#W#sm#5mG_usdQz zb7Nyuqjx7SMmm*FjDl;I1g^WD=uLSxmn3q-Hzyg(#0YXn2!L^P9(oZI4}wmPQwyEsPT=B8FQRXg_$7!L$m at tpHrT77H2>;Q)mxN^Bx(N<+I at Ssu#a1R}GUyvGDlA-h$zktc^1w8RlVfnTZeSlcv4Yk&8*E$k4glcajdSCC z$0F`3B6mD77APL-#VKpI09~1x+<>s-jLZ(06bL|0YG4WkOpIwXROHFDaFq*w+*9rEpK>O(6yQq57X^-Pt}d-Uc(Lo`Zg$Sy zf^q>hU2c_ykX4vr(r`8! zO(iTUP6cR_z{KRxDCnJJ8 at Ej)LWeZqh~}KwNhZNn4Wud3Y#oh=-4;nXCQX*DT?I at q zt_>8>#yB-EElOES4flwc>KK|Nnog$kWSX1|Fm*8_o~#?plabNQIyiF?=PI9G6?5Ih zS7<|3tQGK|hp6lWv&EWSH1K052AbaN%z|vSW28q4fOTDfg0TS!!H5=kXX}K at E!75I zYy~)lM4w!~I!uxoDLIE}>tw*Et>rx?FewZ+k|d5ENidN=@CuJ&UaFXaN+Ce-{yx0( zAkq}E-Wm at Xd{ZYFmP9IhSrGQIk|df%kSDO& zU{aIYQKr<8h7vEdjs3$(P{aC+(kPEG19GAui4s+jq+%eLWs?9;z-(d!N`UwxN|WGV zj6{5=$2lEtn8zzIRYVg%2{qX5ifImKNIZuxOATpl3Bw>!kkgJkB_y1FDqIYdwTh{; zF{LtT2Qi{siTGEtOR?8_wr34vKNnRPx8k9}|#eqZ0RFd(0@`88(mIBgJ#92b|N>PDA z&G%t83X26vu{lVdQbtE2HzIXqChjo!n8i7~ns~Twa$2Rt4%S77Vku(-IOxMr;xJr- z&IFK_u~T)cmnzs;9I!H$v7yRA at db+*0t^g6i;NQVIZ2eX!3E%R1lUc2M>MjP#5i9g zG=z{#3IsU?grW>f;h79n*t;-`gp>@hIN<>5O)<^3#ao3O*$lfFvxg?ln=xlYlMY2E zBg2;U+E*h^806EJ7`cOfEYoHrm9v^}s#j~f4Rp6$D2lEnRCgKC&4{wd#Z?$|eQC6o z+s>|9)U1i5)q-d(P^ys?BE;+cGjo&W_dg{4AI-n6k at g-m0P=y_0o?&COAJu5q>$99 zN=p1iE+n36qrvE}w*_pH6pYABEJrkQn;Q6By}Ik2TE*&v0RxarNMW8pP(eTxTrEWw zh%V+VJ+?8rsauB)P}Q3 at taI7Vb!_8ui(KBzXhvwTaEj#c`mA`fsC;dtTBCm;ScOWo zNjVVkNv5?Uv$2lOm4$Abrb@%YJ|7tKt^O7XrIz$jirtOa)TK at 6)oD$Yk*qUV&6%}c z6TwoCY*`U4l6kC>zf(;bRB1 at Xg;{>H23UNSNRNQ2{G}TzMk*Y?HX)LqnhnULX%BqA zEE{#_*?g zF=aZe#UiA`XqH z#8Q)ESjO5kBFeCd5~C0Z6m(TzF$ge05UNzs3b6`V0;&^|ERd5d3f3)cjcuZ%Z55)e zAQZ6&y<`}%D#27FRRDj=cIIn0zucUS zlQ>CJ6cQ{bYG7enC1WKfFvV4jRw^j6s!J?*nrM-!qUfy>s at C9gBmmHVkcft5iAaiC z2N72By!s`(1{k$s=1ewf#FNQRN+S{}5CHn693&QkmymK*A1I0HRoQfmd!#`I>Ivh( z=tqoCRrc1YJr3iOWRE!_j*=f7J4KQ39zxiHMkN*#zXEZCkS<93Nr1LQjlrWJnP{6lBv?m4erI&=^2o&TVLB?Q^C6@`0%r)J`X5)g zLQ`g7up=(N89eV~|CC+d+~ z4;c9f9_KBqIGL=ik>0I6#(LP);Wm}VoQ at PMp18xJz6tE{#Xlm3F at 0ZS0(Y+hOb#Iq zB1 at 7{PEDAGM51pb7h=X1Y}GcWHEctKQ5?}7I6BU;%Ht$Zq(u=16Bi6He8yCAcZtEH z5)2q9kVFJTlZsGEvMU*kY#JDp(xoj$Shb?fQtNcCwbGGEiAX1j=MskvNL82;QGqao zpus1SS!W?cK at b!~L_|zegrXQhD20HD!c&B&2x20br>S>XP*6lff(VEr6hNl{i9n+~ zu$VDKq z5_1srQnHQp!$PW83c%2F(=<%5*u<7%;|i*Zlq`*y!KsUeDVV`kBr^uhH`5AM%wn!u z>twlIDJ-(CYU at 1BxCBFvhbAGopotL>$fZ$s)@i2#Qw9u(3I`{xQ;tU)y at zOTBYD-l z=)A5J#W5;XDlJs1rDDvF_kX$Yd3blp3{I!~9&X4|hU4J*e7nwSwZi!{X{5$5tW9K^ zX&A;CC0C0RLe)zRq)q-n!=#b?9LoXvrynG*g;;mSCf^d3Q~I=gnYgaYEQt`QvcCr- zf48>c_ZX at 0Co{mWgf>C(L`n;L_7-5Fj0r*&M9NSh5U at k+Ai$JxmLUkaqy~Zn(ITWu z5hg%^@5qY#w at kqJsYq#$7qi9o_Y znP{LITp=BcCWUy4A5co+2T=RDq3jTOib)_IW2~eQZV(blPHHDY2$-nI8znT5%JE5#O4_ zCYwGvJ!Z!?;84h1aRd*i6X7KuqBa&@WN{lfVT3M>GzUdg!($awL8x3+OvGtB=dH=K zZ%wFaT6)(_n>DOEm7(19O>qJk at Fy0%4Cf at oWF(Uffff-&k|9{51BHu-LSn^V7 zT1%%e-HqIvIkzy#-%M~~$)idbBbuh}O`9DUt!%h% zYK at Z3YOvsTX|rZ;y0yahEh;xHlY7^|%-dB`(u?%Yt|oZL-ggB4P)s)GcONkX$TG?E1kEkIpZ zc at tI!9>K>1aQundc1_uv#Ku5mOEo2wmbYXG#B8o)ST?hEE-bPsv*?02n>o%$VOB;d zD%T|KB4qS#j~$XR>0_AEEa#T? zg_R>xAr7)Qcf=;`(G{|xzy;w%O&$hcCsC~h6BrWZkmkEXh)LNEgLuI(gq&9hA|@;_ z=_Gm`4<%!(T71DYKb>bj!KKAj957|mBO+3zhMPKkeeaAD!aedRC&&RO_7>`q9_eu!LkUVGKY}z# zH(XMmBG`Qs!y!dcN~PSFDk*0S0MnzU4`~|&s8hr^6oce{(*}iFAL@}$T5$p#*D4Pq zFRZg6H6kTYRDpU$l!PUah+;z=K<@=TUmk$y(eME)ijSa#6g)$)p|Y!oa9|xDIGA_~ zPiuqV>^n~|DN3-ArbpN6qtbdET498$sO35P(RZ at zU(vLm_*isRs2kefFMgHgn zq~HbR+!HE9w337|MMQxvfzYA2zuy#~VYqgQ;gb2U)o6dB%y!rKkqRA zVFuwG2N*x=2ow6^AN&a?qvQJP|L67p!`p4>55h;s^IG4hpZ=>q`&iri?lDEqfdT*U zOxMCr2>Gm+Fp=B-ul^3s at NB8wyq#ew=u=Iimli8_Vyey0dK!KLJ{n9%rDQZllO~o= zf+bCt?Vp83e(CoF=5RZoM!leGXjn8+1yxlkN`+Qfj5=;vxv<#a#@PFt{Yf|+#f8Vq zCqVnW&Vl_qju*(I at sa@&3lgA24*EPI{^`QK3Q0j$i85Tu<{F%TNA~hDW-j=!&SQuwtbusZ{^z_W#S#w|hKDcE3;l z9|OhR|J3-pNyt9P_7+M-EkvaO3KbYHB7rK0axkC{f(Nm&CxL(R$Gu0PByItpo_xc7 zqDY!y`ThXGpPyO{qX!o;GM|`_smwZBm_O0e<;OEd4BW|p!9&q`P*sO1%wBIH#QW9H zHmm5a7jq2<_{U?uK$k4ZWYa-5QbzgS-OJ{8n*8n`o}oyU{Brh^53U>lfqqZuKPqTE z1~(`BKe>Z-*7#zdvZ4QeG7VINg3PE*=8HDb#T11qNwHnQDC(hu-X9wMqq&bM=MGx> zG=GDU>Y9p at s^60>bzMAIW3sR*V5)_vS$;e)IINJxnKKwts>@=@MuNafm9RZy2x3YJ z!eC5Mh|+=~3Q}YOo*YofixG-=GYAO4o~b|>PJ$M#LP`>4F-2ejVO_{H2EuGd1(5_M z6Bb0JoheTaEe2tuBV=qrh~1c4D$m=CW+jr;VS*F0xC7ll*&0zfk3v=R5sJcToB{(J zLl+3bNW&PIrYRF4a13OO2r)2BC2=Tnm|AjFiK-#g+3j=%Js-1>*|yAwYyCeKoVGbb z_Q+Tc$sH+Z?(AJTl|zzDic>Qxv56H~EX!8n)f?GNaU~rRtvF&GJaj$DO<#2|EQ&`3Tf$H#SK#N!e4g ziwN2GsVb|wLUT^W$0+>$AAFry=IZ}jtJbiMM0h`aOwAu1*3nxi!$u}CN|?vQk*lcY zb+Gz(_!N)*l{Qbw`k&?e&V&96eJZOxt0z{P(Nd~Yy~9bUwN|NHX^jn*E|kqlwn-ML zB+{j}KswP9n4BHlfhdG9fvI3=AB*Pv$K(KA(HD6}Lx2M?2ufRu69oKdg$kly2lhX4 z{D;{+2;M<>|Hy$+kSjA)KM-5~fA-J*)$Lqg-IM$%Q98{s45cIP?e}`kYeu6Y5Nc$E z!VofErOJ&GY4I3|ngjZq&Sb;%^NR>U&`5;kbrgNG|_dlKLgnBwT#K~%^qYzAlL=e2e_mw^P zM8zK&DHGX0?HCluRZ1ukd_($Re)-;R{!)J~9GfbuZLQX#9SSDut0ySuPDwQ@;(dQ8 z_8Y5lE1TM^4VxP~H1tE%@I*N(MNlfRMD-#6clbFf910Wz8z7Sj5+w304#rGHRxyfH zwJS~G3sUG5*WM#0nz74fg>=(l$^d-Hp!{-F*h-0 zvRuO(8R>5{>-n8r_i`+fIB*&?Rc1D)_O*#OZL4`w<_NTQ{=Yp}p zRk|%Zi=7opY`B<{PHuR*W?4O&bmfU&0g1NzL#m9qX3Dgi;>^Jif#l=kQ6O>a2fxtV zPjQAl6NC>SGgJ>rRx#7LH9xtEQZsH`uGYY;duyDL355Xmyduf{YGA zGG8M!7aT!ECWZ@=FhML;GAwj)$y(%?mb_O^X3XU09m^wujWnz#DsQ at z8r{2nTT1G~ zMU$0wZsSPiaHg}3+qVpuPR-hEsY-DUmM{>1Xm!8vN%5=s?@8;i#6RN7LR`q#zY|_c(N^@xF)bZyr zRkH_4T`rbwBx6i=6r9I$?Aw4j9UAzOUD>RH8ky-4PM>tWz$*(NSSWTe4-P)ucOBt1 z04_9jdwF~fo_x$1lL?Kiog|TqBOz`O5&I3HUSsoOK_Al(Qi at 34afyin*#AIax=4V5 zkbi!0R$ve*eT*W90UVNf8K477c+?K!$V?*{`=r)SVgQrRR-UfR0*zMqKh}WHxMDd9 zY(facl4u5#6viYYdl9KpLVwj{0+IOj{0RjM1eg1N>Bp;N!0>JPH{biNTw#CR!v_Eo z;4%+tV7jtD%(KrsN&gf&MpiW9l!s|qcF7p3&qkw4l?WXyo{+4grU at n{CxHSmgoub5 z6p?~OAs85f9AF$I5`<(W6qr6T1c?XnKpG_>(=d{dQpFOfLb at u)4GEF~e>3e5)EFLt ziGRzurk{X6g>;?AGUYs{{s(9|A at qmT2=1NE{Htxek2mnouz0lO6Zjc>Pw^MAdm72U?`PBs?!Zrv$J2W`xOl$pIxL9v=g%a3?s^xa4y{g2UzwA&c{ZQx_OC~L z{KL7w^)Ez7_bN9q|3eIZ2PTseQ78C8yEZsU1Afq(2J+sax}(U at T&AHpqGa?t4Llvu z9u7tk8#~>TX-*a8_5YPnK7p7RNN7`-Ljo*(z2}3d_ at 2i=YI14%UV@(2#N3hnrHLoH zcd6j8tQ8_oS{hGcn3CV`PZ;vQ~t4IYWrrc}Nul7ars1COZQGic1O4`-V1vsti!>KfRAz zqrk at DUIjjc*ztX6X!si+_6EVo?z=Knj<5sk zy}cgp*VEK``<~@KfzM?YV8S0G-*>zpLtV+-yui_;(ItHbx*tQT(VHJZ(vEz>VfEi-?caI2`=&bEy*j*iefQr0zVCha zq38}lZKFd)>i}1LW6*j7mA99606ceX?cm>h`|q>c(@hC_4uQA5;qQBgu7G^|1Ji8w z&syt_)opLNw%0?T4<>_8diTBQxrTcmN&tO6c1B_C_fRy`=mcGQ>HqPNOy=mXhzeY>@(Wxci4-tH=-povO~eQ&nw z4uR8(M4(e`E%Ui4NUpS06$3V;lwUy6S_9p^wN;>GXjK_e5~w!SwkU#?A|!z$ZRj+5 z8BnB(N{UGCdi$!+zCrYFW|R~F0ry}IQav&?qCk>{n5hH+A_@;Jb$hS2zW6?d``(r3 z-tMW>>+iAjP$;DU6asaDY#OSdZI62U3fl&NXk0pW!_-4_-*--1+{b_b000UDJDRrM zEnIEAxJ_dFjms&%xPw7E?>Cm&ZI(T{>?xghOS;!~-N3c%_h*it at b$FVVK>iPxb>030=;J0-tEEY55DoGt3E1Tz2$t} zUCLiiF?VZ&+HQt7dF^`luX at U!^)nut_U`fK&ARmMxplqpD({;g9`n2JuJ>%YS8?s_ z*S*$t(j(csRrkBR7h=2a&w9JP?cwF`Z?8{BySwPy9c4Y<_r31risw$VO=@b)_36m0 zJ+FJaxqH3tyKd3fg7o&q>rLMFzAduexP2WnRrRj(yV>tP>vw&WS&y#j+k1O+yl!iI zy(_M_ln1tbUT4?Ov3hOY?|DT+WmsyYHKv`nKxparL at FyK|p?z2-IdVnNjVYWngt_qFR!x5MjCH`&}6 z*^N842JAkV^UJMYY^CaLR;+5+?{f69JfC|QeEaWsj$9=5)1L#b)u(aF+h}5gneOzs zci%l*b=&S=Rqf8FHmxr1?_J#6y}PbwuejCr&6eBj*KI_W!x68e&-yt}gqlJJRee at 4K7p=yF@#&0zc5`S*_I=R at o7o=(TReaF2~k3;D;$~P;VJuhR+ zUh4vftuM1ijT8&%kDxO8(uH&eifw?qw{$+b&>io6;B#y5bs_Gn!l->U+jo11VE0|`~*`;UDL`|YTm;MBKs?cMhC;`a7??`?h3l^~|xHSf8b?C*Se zKq(L#dm4 at IYp7W&=Une@^`xI)UXM1pjoO7=r${@k zoW at q}E4Mwp(J7C at Y>$~1OTGW;4-P0X+P&5WM$=$Br%pDIGUGCU>wa-v{ zEr{#arq$h(&7deyweM at IQh~KuS}$x|(;Yir`;pKMG4<~A=ezHC_awjoiu%ASy87p` z`L(X;yRP-+?ECJ$Uf(Wxd0lgv)a|L!YgeS*%%+}v-QY7oX!W|oq>?%J-scxr+vfK< z^{n at C*g1CXarWHuQz6@?_HQ?x+_iFd6+*3gE*+HY}-L}dJcE>@{$=cGkyO+FMp8Id0plJF6+C|>p at EgkAbv>8HJdhO4ZzcV({bzRla+y4=j((OrAJ*zAjkICf(lh_UyN}D)+TJPQ1SR zd*hdVwN38zi)&)G#ip%|P&8DtLA5o#I^ewxdt<>>NxRoqdDzuEw^-2oX&lo-rMH)f`r!JI7MXJdz$xEC%x)jo%g=)IyG&wme<_Fwr54%TN%{aCqs69 z?{@bNG|zbIyVrYa=QQnCEG_QsGrf5G%)R&A%d)w=^!nRT_Q)fyXpa28_CLov-O*AH_qd=#rHqaf04AZa}WHB<1=ihfl;$eO3>Gt|vZN0mR+ z(w?X4X+1JVjRucUHlsiQDkOvm1OR{p0$>mcf}V+jVN>+eQ)!`3X-`S&Y3Zl|wHg2c z|9 at uB^q~VG|405$`YHObrHAj*Xm9^l_Cjj^ssFG1U)ldpb5Z{{iTo9RieA5Fz{|xD zu_1ISF5yBCB4=O`#4nUSZ=>per|M|4MYUe>8f51g$`G8+HfH$FhyQ=6?*a%SRXhk( zC{>_Bl~B;aAt#328Ydy90sxV#2*#LI>%>Bk?6Ga7u}p0yj+}QokGnU-9FFgd?=JKJ z6+r60T|&ss>go}3Rc7&6r?u*6nidKB2Efc2(`sz{sIGAb6NCPz{sN+JK at 3hq%9+ at OJkqHYm2DgemHl#W6O z|K}hO)QA!g0!)GwqOk!eNDiiz0|bD at Ryt|Qh#rtqWu{T0#Bp^91|cGm&IySrN`}<` zs{(WeAPKotcp(wgpp>ZG0%9r{LWboPasdq2e$A%U4WVR5Dn#1J{u+lwg~NdVR*+s{ z5ZTinAqP^1r8GJYO$cE>&`Nbx1UW at h(f}Gqx&imV#GsGc-HP%;Eg)p30Z{+=v5{I< zW~K?6P?R6}ClDAQ`HV3CpZ%S*yOKnff?^4#nm at PTaJgU|c=Y&HN>NChcH0u+SEGa(RxOu$tXpNF))g?656+$f~ zG^P@$WHuNmZa6D4?c at AOxUBl|p1fh=B;UW(rVJ zB_T@?(iEh{Eh#||i%OXdA%v<)m)&KWQzQkYq9F%BvSSJ)LJ?2}M2JwcDilN_!6ed9 z5QNAkW+;_F6p0cN5TFzd0}$Z~xluzfRV at qcgC$}jC_*X}G$bxa5g`O9$79IQXrhv; zD5!!7rBnWpO+wf at s6qk=iiik^kP=XTvm$b44q*AE9_CQokz{1jM1=yxQnZ6W7)D7# zVJ=9SE>sN>D$t+A7#a}3g^7b>5W*r6qL6|Dk_lP at h&s)+pnGpGW$*d_)@PqT=i7nG zf+QHKfTdC@`zVhm4s8H4sot4OIv{SpZZ@#H2 at dv}mHD3!V#-(vb|XRa8Vo zi8pd$dR;kEL at -q_2qYpz2vo0m*Bp%bEvPz at 5@2A6B!H*XMo4&(l*~LF94HA&RFWi# zYM_!Pnqnd$ha6KFk|mN at -#w2}qG>31Okah%#Chl9~XSB0?ltnxzsVDp83ki3nO)sF;!{fT{?XN at _wP zrbZ$vNv5I*h6o`TfFcG)rmCokSeB7&F;^j>0ul-u0;VZ?5E#f%%#x at u6p0d5R78<7 zL<6ESnJTO?98{%PvXWF-6^3F9C8aVDGD$+xF(nd`Cs at fQ7P)ZB6u<-cdGd;E>b1^PU+>NA#0B{o#55gKhX5*_pfRUyc0(}9!RWcu_jNaRHu}r_Z0Z&bM&cmfdT1zqO&WdK z$c;zStu&Z4;eVO0?8OG2Tv4hUnq8e(aZ57=MLMcxtl27=YI>}XC;nbenCUvU4jI&bb#-APYbugHruCrKcYFC_RQ)x;^?Pg7GJ}% zJI at kyVQcLF=^QP-@&EU0Hdk|?hI(^VIN`1_%RDG))rENZRMVE ztH(sQPPkV&M>|gcp~pA#hbKKu)0+%^;q3hg)V1Hez36mHG$5fSuuUOx%zXNly{M&z2q>EVyJ4O(N#K{P?ceCM+u!IYpo%d7g zgZ#-^;UxNoeIKy%ewRJTq(;qmHGe-G{NosIdqsxNSJBjCL3O^rEG5D at Q*Z9OUP1TI z3>)x8B`PVY=Ch%ROmm*Ds5 at zyx?@BkeO7FC{-V|u$m`}W8IRli| z>su6|VK(Ufs*yxaYQH%BjR8t1=^y?Hof0is!S!SbGE;Zh?!sfn%ACZh$191 at yss#4 zrO{*Z$~29~lAffQoa`xIez6C?-xtSJ^qAHjvqh0C7&r1_v~B&BA+e0Lz<*dz()}K5 ziw1iXi>|F1DfTgpW4rx;pgznEfe1zx01rU$zaud}i?aeiA7_nN`rPpE0lUq9adNK% z)Em#FW|n;|2B31=-|CieHgtcZ+Aa~NhutGbQJxp`G`cXaCwHBpqpSnbB-ql99z;(# z?;ErIe^m8$mT%CV-SE1qOXBO1`xfFCbvVghK}9!LG4mY0HQT0IEww{o;g$-GuB$_e z+E_mp3}3?zXxDs1l4bsqIxy$x=y8xog%Dci^*HJ~G_e%EDkd~Oo=6Zz-Z|eC>_#pc zH6d=ci8*j+6d5)Na;%Q&hfjBZNrh71lCKss+9;UnBfJos`*jhek+3w!ZVXN?owM1O zw9!JMZ92(9J8_&hUr81-LKV_#O8Hgh^}N|et=M2oG~yE(VPz7H^+Fw(9Dast37fy1 zA#IpgmmSTZcV$Q0<)50q7H<8vc+j9vST09NL++T2IIX?St9^N-HaPyXoA7btb8O3y zcpBqk1B$5*GaG}}Qgi5TEbOaAQy9j_f_KjjUe3|}76+%?MGiQ;j}vVxKsTc^CNwK| z1z^9mFAtlpOcTW9wZfYw3#U-lF5*MXAlE-A=|;@cR at 5mVeWxbq&j}D{u7;08i9WmJ zhhrw?igC@#`W7(_u#FnJEh!xh9V}bsa(%ebN0*n8uy at JHoMvqyJHO=C+wSD$&7MHE92-qs!mKI)c1LGdu(qf(Fv6&7WVd>yxRh<92~%fwyY(tOl$zAHMLg9NXGhvY>Qa~s&ao6VwIzd;&4LlT!QZ6=*GbFZ$-)>-C at ER zU7H9tk4ZZ%8!~Ez^~@R6n>ob)VKWVs6`5Nhpi=d**Womx9xCg at MsPN< zYaq2u$wE>!CDcQwau;WLnuZ!=bDMiqHaiIt8otN$2HqDpgvR|!mL%Wgbt5LaZl zZm?I@#5#!PO82UBesSa%GB0B%$Mzw?FLUqJnm9)b?2ec at u=Bo$zh?~g_MYjU{NFhB zZi6%MiqeOvxejq$-Z!5vEcD-Zy^hb4mYjdu`VLEOT&%B;b~K+)^=R*Yy*}Fi;XAT3^>;QY;uc z7y%PX at Vm{?`sq0<(x?t5kyzWopBIN)Cm4|j!aWrH-jY##zG_rr&knsEoU at yJXXQ5@ zAZzULdp-D@>h*t{#(2fej#xPCtodQjZY}A at VUhDb;w)?}mLz)<1WrelkE@?7V9$E< z^L+e0wY~kgdm75{Ick;$>fMQusj13+H#yf|hi;Y|Kqh=!lD7zIy5Egv;~n*65NGf$&HJm`er&SeZ?DYXv^awOS{(Bmd#v21p{7$1v9 z1w at gxmiw>A;~F at yu${!Vp-LGy+2Plnd^hSh)7{yA(%aTZnLwrSGXe6WH`_8zUyj}} z_$GqG?Y~y{JB^f(LShhmEtHCI7+7-T! zgwmHE(;3>V&&!Rh5SG^>Q+8kWoG8bdHQkq5`6xjQzX_$uWu{)Om#b-UGG at b$vJb)4_(NQtH+N zMVK5C9W#k}{9xH6Pa?EJ`_%WYl4gGR!Sr15qW1hZVeh&8h9hzOzUqx z3+CypQ^Rvfw-+>SW;%@Wq9qX0aCttC`|!e4T3z1KYkNmY%8ST%1cB03vBb%9HjI%Z zWgQqucl{bM$}a=h5G2kvV%Ze)KEfIdHoY*LUjn%|&hUUsuj5QHs&FA8DJ4e93*txo zrJP=>!uzJty1>eF-B9Q7 zLAQR;&6Fkm2#U at 9qD%Nya6CnjyvZYcZ+Z+MI#|cU-t6cpCS$GI!ydJ36W6TCh4s^O z)z;rVmBxIh5 at ZzYN@hef)gfCvxYmf<2OSUV@%c|{oTtkk at _BA+QhS*Ay-&x{1wz^rbtv%l{-vJEI&K8$t4+Afzn%3!Ro+% z+(-PF at BP!$cPM|- at gLcLQ9X#3%1F2MPu(H=RZF1{!2Ik-LpRq1B2m8o2NnBeH#w0e z*G3!E6n)vIaE^Yx(u-24DUOKt;s2(-s_td7d-IyG%uy})m?@nGb79Q$zVNcHD$GdU zM#8FSVkIH8f-vNC3w+!igTgf-sYCW(e*^!rLwlp2lHkk^Iws|Xkq zKb2*-*it(;RIK7YC1u8Erm9_>@#oW7_?uT%r0G9#na_u7UV1Y84xUoG3oRa6&3Jse z&BcZmWl3pKLWb+B#mfz*gK-j|siOmVfe41voaHzbKJ{TmIkaXZ8IO^Z7!?_2EGZ7d zpBoNBqON&sDmri~;MG)9nf4VLBfusi=7j|&8ORnF5H)Tn4-rNKQM8pfcIe<9VrJ8f z&^TaBsL*&me#iFKboeRDp`M-`P|nZCd-z@|oZnb-pViT at j7^FGb0Sv{@K8ghnEUZR z`LP8S4`X!lbp(UXftGNtUC2CvtUvS&v8c)q;p9CnpW at vRLowaZ;7zRlk%b;N at HJ9r zJ0S3WO;$BTyzg%plQw+^2iD*G=>>UrhrZwLx2l>-X{MERm0<|3{>kQLivu#HG03hC zbcuGfHaO9JJ-QkSI^p4}Y`wEQPweW|FB7xdSLc{`e9HcN*;4yUlnyb2Kk;=oE_)FS zLP#F=N|r2**F at xe|H>rZA8!*4|F(~;)9}aag8{`2{}|1Wl?LDRj1+Kl+}}hu at XciO zYNCef4EDD-FF{AAE1e*$89y(+a(<+14}&Q*2_0ELA}kQ$i8DK1`4x_J5(>^?@@%-- z8j_TnmJ0@^?^260`60eY=fZc_WCkDdRlCWv8V>=x9aw;n-^ek;5F$do_YW>J+smP< z&zG*ORh=n!MW5Pb5wl at PNB>gu59{}D*KF!8u`n|z1&r|ZXK<4QMxp-jkG7YdkHMh! zNT~!xnPe^uAFAR^W2bz4 at k!VuY3hPENXeG;XQyEhKq_84- at J_qFL)1;4roC7eqeU9 zHOEh3JbV6rjeQ8n7Kr^RlrGghO&E%^^PhsBZCqm`lPdiOSoCw5^6GW^Bf2;BHzhzz z^9qqgAF0r4Od+Vzs4;g2ZGU9}b5g<9k8ig2FJ)6kG!dxC(+stiCctj?xoX`383)yF zIM0;iiE1&OcHYi6>V70zwY19#6qOM(E1`EL;*z{ptfOt9mu&-xj4S at vHz|+xvp0nD zoD_`3qmfjU1toH0pvO4Wvc|VlDo;ZdpLuD9x%cgIGKvTtGzsS?Kh$KobYN$0MeM>4 z?F1rEff~VhJ1;mNkuc|Dc7o!;vzr$ug5(99bAE9g3+iw9Nw&pg_ZcLKC?H4mn2t!{ zHEm{tXiRVZVC6N19SusLGZ&f5Ns?NmdrS)`I2W4?3db_%FfAK#L0RVt;LV$rvptc# z)637&e|zoJZjuKH7X$gs3}Bh0D3}lHha?DRbqQXFpVOm1cDXp<#%4!UOGv~ zmJv2xi6MqBIX7pltnM{Hq&f;t?dGG&9E%l%iozxLYTlU~SY>Ikuu^@6fx6;b at h|GI zQXqHHfrrrM0O~K(a#;v(jmROc!r)9 at G5Axl_%*I25Otn+_R;Inw3^S(4g)3ON}wc%DF3*y!N?;542FV> z7&03LhcsZTFF=2!-_FEkurI_KGED>&-svFsp!Rt4DSBkJGaz1FkzxFcNf0ABwHQs{ zzmVwuD>0&p4=5 at 3yDd%*>51Co|-ForrI!a+6^YT>TzLuhqn4h#$+IY38Cyh5I|d zj~9o2u>FzTqI?u^Y&k{IxdsGtZE87vupbcv>*l?^_hJhM_4pi!Lnh=A29~Xj+xJB(P at 3!W`F at X8|wmw4JHe2t(bI{COM4pNY!I<(PVRnFk z^%Rs#`Z_Q)1_TL=T-Zme78;4^i{G_ZTm0_q)N==z=EO)*`+uTrthg||%R{_bSkp_A zI`ik{tn2jOq_m+I>Bi?%nRcwE&|@`5Z9IGtfLVDmoe?S-Y%3&Ams^Sp_lhZ#CP53? zKH0-EGcOTo4BjI}$0)|V8ZWn>>(7sLR at z)|^VNq4(zvBwI%(pDQOjj{v|vGd6o)cxMoS zAFZ at ck#Dn^=(J?P=*$;gNqITK%CF!?dd_0W5-8U`$693Cb5_a>#8(=T at P?CfxZD#kw{+wT6a;RZHq_!J*mC2GM8<5oPzodc(*bHR!(Lei_Fg at -05dEIZ*ucdT%I z#@`bEb`HN+CfE*n;SnkNqYcsQAtae5o4h8%ZX!`g>oLwJ1&0FV3(Qi^CfE^s+CTCf zefKhGvEhwLv&FfD=*B#MKQ_un%6YM;lbflcgWgFzbwvn7Xn_|&Zv5*TbDWNiSo=z; zpUdGyO6_75!RRe(i;vn|dB!)C>?<=un|+Ij^AN`-W(-qSRS>9SQ9xu*mC9%`HSjpg z!RB0XUvw^K;80LZ z;)}=}mbX8FnRP<8=`sV8-({xKKS?q*O!>6vTI)Q&;jS=cHZVD8Doal-8Ebp{nqKxFibQh43(&Py|jk8tp z$k2X8MP7pUDlAMnhz>`;tI8zk<;$G~^ARB|Y+QpB-vY&F&Z{6DtF)22^3#pW2o#ib zlPUYQl-7GsF*7unEI#wdUVPr5%o8|@hbqibkrIpC&SF$(HJ`+&ilEFfF^fv>+o#7> z9`L7!d4$<7sG{@2NYzQ_paDbaG5JG*!|;T6X=!Rmr2>$|!ZIJZmb0Zl5l`aOx5t~w z1L at 4(NPgGOl9+r#u>SdKiq3u3rT(w=XI1L9Nho(-h)8(bnc+>bQC8WNRlC=t(Z?vxSKg9kP|3*ucT&EHDltCFJe3vFJNdMTskcuomM>0r at kX^_yIV2V?WZ%qZ z)8nkPV-E5c;59I{HBf{tK~TevFUEeI3uc}bO!f3}pT#ac#IxO26v}qt at fIno^xCd{ z9K1Kv4B2iJuMF7u!$Y}%q>BUmFE-Nhp5VM3*h at 0ZzI50c96KER~`nLjyf`uyOHB4up#Z7Bd z=i_S6-CSw&AVS!ZMPx{h4MlDyAUS1(X3Y~*v7_c=N>fj%`-Ynzb0#+4+jUxf80WtR zi;iw4_lUkv0qZbXwbJK90C*^$?DnsWxP)Xwjrk6AFhK;3Ug86zb>*eGzE%3*gzJ zHiN}+nA|wUBn6g&ybDl;#2nxv0$CL_8V^=upC5_Sg>{!6o5c!^$(O2&Vus^%gcihS za}NkUVTjmOnAdK-2ApFvkj_C at n-R7p5P?-(u4ji4s$&cnj$T0D(h@#QG4BKMou}G# z;YGQ4qCL`-E2lvbw>9p>DB8Eqi8M-8Qu72&;cQ&gInh=yd&k3xrPWNfHxaDhLBovs zXSg>NQP5U*9A-3(OyG1Gg90zkW`k=59$A(UHIqwqJPBJ#eNCP;g0-}mD2De83yKI734?gW101YYNQr@{>q#Pxo|=O}amYDgn6CIS09y?Z zP#xblPW#alD~dSqBc3E41PBgLvViAOhv^~-GmDoC1UR_x87hs_n+*bHdx9a^v`AGH zdXHj%BO55b-An88ZteY;C at K0nE4l4|Gv`%qVydxTr=ydTqoeh?KS7aqS10$=VOYRx z7*&iaMG^-{_=u6(I6yf8jw3MYV$4=^0aLjl$ZTDTyeTGXVg-ca9S|F&&*L zBsU%eg6zuDqO#G;4FYC>geox8=X}2 zZel*qZyh?OGoO_QjnK(4V}T9O;6;I>XSs8=b(P3^f-q>{(M?by4*jFkec!5DV<6SzE*{gxLa=uq#QWN- z8 at +~GC+N)jzq2)R`!$L3mM7`%JC&p2?5|TF=VMI@@MB7vSH^qi`=+eDXU_KUY=3c7 zs#2=Cx+_VgN>x;0-p5J*Z0kAB8>?jfDk~OHR%c@)m)aN}0N-`@xCDh+QB|f)QaT4M zGpMr3R|S&aLdkQiH}QlBNBpculLgz0MG(S;j-7*@W=}4fyjiCna88|ag;xohW zV>bCu?sT}B86?D}_)~*+NB4arlT0B=h6f!lY(6b*T=Wg{mh3>o!;R1Oc(s@>B?V1E_Tp`-mn0(&da^Ci z1LwvN(W0o$EN4Wv%NwnZ#BOu=`$5gr40;tdLEZ2vZD4{Y+DubLG$~VQHfM7`ALEwo_u< zlI9&mn+NPSnH0q+7-;JbLb5reo)%-1!n+4FktjBiXh5i8K(@Ik^cM^Yv4iQL?&zem zg;Cw!+rMM8GczdF1q3GiZId_t6%{7-qJL^w#Byleu at Gc&kwiZ)l>DHXO57Y-ijI?UgS<;rvkjEM*Jjm!Aq?t*jSaY|XH}2i5FVlTlkxzYL7wwFQ zd+)bdB1QJdVYJ8jf0qFJ&?5W5USJnlU)yCt1VvR9)(>yXpTI*5LtJyCSE|g%1xft@ zX<%BxT!pIv(Z>ZQV+YDLa`nv_|HuBYum3u$HGC&HHLd>3!=h*Z zCQz-v>GahmZqNGRYK?0tGYrKCJ%L}`jp)9xkLy-X3iQ2-zqY&0MT;Ddov3nnH)=HfrqU;{cV6+&1XF}U zk&)p)luD~csgPse at cEz@^pC&VIyn1X9gAhZJVZ+b3Hn$G40{=L?|735;ZXqS13()H zB7 at jg6~!5(L?gn)9R1{E$I^3 at a+}VFWc|q|5_-PQ2X at mBx8IY{5$RndLYhqlNXnDt zBn~ndy^H at YprsB|ws$x6DbhIbi`fP?Fm2WRyCQ;RjwOdMP(bR7Gc)syVVN3h8wes| zKd+i~440t?YZx$m~`(J<2YyMS%dpzepab!vEZkW7#Va!8F_-ug%pai zEI{!iKDR~3q@>B2mKSzc%=ou9X+*%Rb*NVhX(Hi-*^(uwh+nszZpHa#w?-|0f#PfaRaAsMqf1iJX z*)CQ{Q-U2Cu)w#oYe%A%S{z$@E&OiPt7j!rr2k}g`{Q=u%KhMvOj{ja!|eE*xP!Y0 zQfJP38M-r+2 at XK6s;ZL&i4WQ-lVVsE_-oJ4VfyxCbnZB}K9UT#>icg42&G$UU{+>H30N;t0>idR0IrGX zENHf(Kd41Vlz~ZH?J_YUy49QuGn1v+rZT`|#|LPR;{-wk5XA(0h3$u+(tnrSd#M7X zny_d$C^4pAnaEaZ96QQ23~XT8aN>4BowZ6uMv)2Z55-vxDReVNgIq`Sj6uq4Wha|J zcp0)pN2G)vtWqoSE7`i_nHEXF8Igu@(2?~rB8r7>XSyT$(a{XiFNdKpRMkwFOp*lQ z>0{B#X473x{xOZiDFZBIIBn%~e7B+u>~_#L*j1R3GE?Z!Ywfaw%w%9r5wV&tNZxqx zYgRJEmdmaR{|M}Z!VPKs*WQi!vmDHl$n!r>7A&Z=0yt|qOnq0vf|E^ZDtN=3`lXt z2N+|+Ng8Kr@}iT6m&%4JT!ROfHw~xZ84!30J|UA8EEA8k*}4oP6cLqGz}5}@T!l8hF-b at N;|gZ^OEBC6e4^D&{=B+G3B+&vY(3S6`RK+%RMAR zaK#v-8CS(3B}FAB<$1_ofGwV#S1hg2sd)HP3h=2tEu?slG!4oISUe zPVk_hVB#?!Y==q0e)ZF=YM)Lo)*6S&)Vi4`CQ39*)CF5Q*MHU3^T;vS0#j2!*;MRqoB?A}O3 z)GJOghBzZ{Hwpt0f*v;xs&%HnnVR9;dGgR#IZAAC*i>yhMKIBu1WpfNmbI}`^GVb3|6yEOhf-u6)Pbaqo_fd zaB#vTB$5GV&NB#fBwV5VJ<;)`rb&S@xprk1Z zNRgN!lz^a!3Suc3g at mi!ieSt{u%!S*kP at Lt1XCpd6A@7OkYu#fGzdy6c9LWP`*n&3 zs@%#nr6WwqL_teU6oitkDx at +*h!j;)4Ga*_GenG#K_fFtv>`C1K`9Fo6jFScA`XQxZiJB@;ALEg=ysNhw5-RFX6_!BUZp zfkKqUGDHAHQ3#Y&NCXluIdLS47NJT?DM*kaDDRNMQih;{gPC$-s0gGe2#SUmOa at e~ zA$GvRftp33i76sVDQN;K5~64&iUN`)f)I%Kd9gJxz_Iq&5LHNr<5>(4mLiUVTno4A zaQXy~abtN_%BsI5#Yd6Ce=ooLb~!m5w7A7o$wkaErLT2&N{tk?wPkcY+IK;D at bLNk zKJEN~?_lAiKMd$JDN0a3%?3&_5eYz3Q4rF} z6%uY?Br+oUqt0}a1yImai3~Lck_TS7WGlImS?D)J+70pvisAd$Jt at qZy9_-`4}Pn7w-F*CZj5 zFXkg4r6{5G`q%07;NS?@;gSb at CYkP?#SNhLYwY^mnL4YsgO{8 zl*5+?_uTrw>;RP#~BQ$t_b#x7F_!pcW2J-Y%>6~uKHF(M z3^0ORER={iEH#RKl;Z<0-A{IBRrHrYn*TG?Nr{aND-X-%rQXmPE`cBKKFz-9+B$>e zC2*0{VwR<+p-j<+S(L_Pv|9=$2$CsURHQOJ$1F(2_49snV``p|nmO|3hES*`4WkPDgV^x_Z7I!UI(DkzxY!Vrjay at x4dr08A z#Kt5f0+5(eaKOQ*GYFW0=%9YM-%}M#Wfa at MM3tZ;ADYyqPhJ;5T z7I_Ed^6T4sCk9Z-0+jO>hKEolE_|J|ut-TsCf$XC%>CjRVWdJAGg%8Sx zrw|f4Sx~^CDOwbvl#2)=$NRa#)3=j*4#~lkK`%3tPbM*egh)_84-jOfG^vn0k{W`r zByfo}+P}nhF5xL(^s^OiBk}pWHTH0v9Q}&u1T}FQIbz`&+hZ&;75*iaFTnMNbq*x`0ppp_88^FL) zWu&r7g=t1sGigbh!S~}4_hu@}*^pu;RADL>QzjWCfstaFA(E((icFPVsicEcNW9>y zOQei)ang`3YC}kdkWEU48H$*Yl2XYKz)eJuq$EVdctDAu&@ezWC=|4=b%zoMMr9+8 z0AVW85o8z>6AclDDP88Wg;*hABnZ(M1VLHp(9#=DY$he(!OScmh=y38ppp#5aF|*! zmmI*rAj-rr2Bn~pnusYRr774NgTH3C5t2~s>7zuQ(}fBYabOY?frzG6>x3O>F+%23 z>d}O3mk5FdVIso=Dnb}~_~0AfO!r};(8)}y7{tk8RWdReA~0DBTxAytOiafF$S??D zhzc?sg9`x2Y!et&?FK?I_-0#>$GS`ef>n(eQJEN+BrL&$hA0^a8a+m~SuQ#bn9n_m^FG);fP#)GnS?)CL at Sa)rpNx5${aj`99XX(hnu>ib z at Zj#zqhAz!Prjq$J+#oq2AD1|Mk1r+N}u?Wzd|KcDB5-YNkwKq=7GO+8xE at GR|bJ! zzc6bOn=1{&hpb^WmdD0au at 7?m+1AlRn61q at 1nXgLNPPE`>MCp-=^P6W`` zu%xfM_GT_5&3gt9Ic6we*s&f>`kR9gN>?OS2$)tD9h|nSm=!^2B$7phYX%Yt%s&9` zFc^()Z0KmGb<>bNu!1bYI2_>7B?bf~rfoZn z?kBo5Hbf3F{6}gd+&QzVG)ovAY<>uHEIXF_YEC4v+%nKcVKNR!G(?c}q at k`r zl!+aSfM^|27!M$sfb<3&r(5=OQBdymHI>jl4KzkF=6j}VRXjT+)n+tlA<0QF?7)>X zNqpuQ*HFbGsA7Xr5W?gci44U&9uYaVgiJLRx}6IX=kHgChf-z0A*^wGx0v*$BlALM zV$5ru&O=F}Poa~7gd#wrr)N5oxacq>@w)-Pk0Hr0*K-za?9)*;j;n`suUh*(H;)zJ zPD;_kOy`x`O1vVB8(?n`6G^31!JHUFq;h at PEDH867**LmOq{egV#STe4I4<38R=j_gQyz}bT8XS_U zTP6vXOw4ekXfRj;F&Kjh$O;&T0$e7WF~PyaAel@;5SEP{;?{zKGg{dLF-SdPA)M&v znZT$trP4AhDh+&Tll7O5YqD390oHTONh)~xckvXNs1xxNooN85vH^Vo_V;|p(BJN$ zDWEx23ZTrIArpAgIk%YBB5Ara|IS_d(|Mj at JFhVt0s{$+R|J#xJR~N#F~XAq$WeM` zTAzR{tO6Mf;@V{XF!VZpUYqgb=kMdt_OlR at K*cdnciE%2-n`oTVAf*XKH;854QSd= z4;NKq(}R(>VYokEb9&UK*C9+4lF+K8>(x4^aSR(j!PLR}HX{H(M3?I at 9*Nx>td&+L z-C?4{-i)X}=BYT8hL>?vmUf}ZMkwOQnqB0gerlK at N)BK$R69WAr!aoL*e-Q3<)U{d z@;E#CZ$EVyvOC at EC(^OP_cJ_J2cPjV@*`57a#P?#&>-)Ih4sLH(0|kZzJWy0zwIfQ zf3}?}Os5$}t%A!oN8N$?$(~V-0|Exy)u4uijx_(J5=TI~(X=;SmjDO>5kWHwPXLXJ z_J__9 at P{fs=p=$*xq!ewAc!YK3D6&DvDX`iwb_}>%KFo>HcgYfB=kF{2W_N&XDF%i zoK2)`YZDCCjm01BfvULmR3GpoV0Yy>lEiH_;WSlT8i~XW`N{iIPQ9UxnE$Yy(-6h2 zcXY3YCog~OblbZL$ZHP1)$mV4`_AM!O`t&ZAc76CL{Mi}xP6~b{Kf+Cy;Xb#&4pLc z6G$a3Cp%qM0q3)VqfMVoqilfI4ul%~g&<3aT_9hefzANIj)LIKhbRf at h3Ro62{w`4 zngodc8#ZeipU6wzmU^x1gdqelmQ_kn>;g+3!Ma@@r!2wBZVT8af& z%wqJ!Z{vY{#vvI{*$q at xQ#gws3qo+L}Cshlzqi$fDwv z;C$uxgo0>77?A+H1wS2gbaEdhBA#zP2Z17g$aX$*4%;lui_}MzbUY4=u;30LXh|?d z7ue=;l!eMANU;ruWBbFRlxq^Hz{v6Vb6P1{Ea0{1<}_(^(CD4&DS=t65SU_Qla at wc zJ+^4{m94+}*e}UdNDM4=mx}R)7 at Ws-5(I^v8K&d0m}oM+jP{p`MW`yMbweasHgpNW zXLwjww!gUE`yRD!#Z`MshCC@>;-CQzXkh=yg)r6l5q33Hb*14k$W zN_r_KX&~+=sZzPSc5)1C=6H|mb2=(}8@{r^CnxL^fPn!55nV|;!*J~WUjN<4c_xKW zK*{%#BEi- at l}uxBX+7T5qiV$p`+36D9-8}1$MIH`C1cV5423*tpT85pu9{WFcf-5&04@}Ct`xHo+yl2^Ft(hRYfOg#Q|#$ zCD&b$NH{Thij%&N4i_vHFT&@&lu4Dj3o at Y*nj&9l42;>!*1BZFR9RG6TTz8ncE@`~r~7M4;}Z1K32{PzYv8F at eH471%P! zG at c@BO_j&@5D#grECvBE!;B~rD?CDhobj9v(-MXgb&Wt9*46`-*Wd-i78K^2C$ZT* zt=_}_c9qG(r+Q%?3jY#pOh0i_{#u3^wRri;+{CN-kY9PrBi0;)xoCPck6?cX2RjSg zYGw_f2tb;F0w(YTY5fA<>vQX{-WNfU_!bUc5H1VJM1bKz^A|uOnqlY<#GT5Nt`m3S zcX6rzL-~0PQuFAHSFa at DdkTX?KK(?8^h6Qi%@Ig{K~xGLd!EOs$+nhiJ$>JvDYSm= zVwGvs at xbR<-7n#2I28_p!JslWZ&>1_IKzxT at rFK(^F at JhIITAoB?x!Q_=W5jGi=Td zK^AwjBZ-C?8I3ae#xsm>uW%4b at NFZzHfO1`df1XAqZ$Y0Bm=Od4T~`JNlwm<=E-6S{hoyTu068-_%Fz^fEJ2vV=rk7?zq6bm)$w$6Vem9UY{EO9;6c>G8VIu%XK;e(P?>m}Fyzl*Oo$Xt) z=Qvf5;^&{vl13umS)8p at O9jd7uRXiQYE5uZa+^I0+6Dm= zFn$G;^jboiZX( zsE9G#L~Y$)V$Qw-Twg~=55$Z$wOedv15qfQ7lfA+*eYf=iWpKe;vi8~-Lf=J@!#%>rnJDJm%-q^h_lA{xsi zA>4m$pe9RYh+Kelm=0Cv2SM1vOZE&ZSVh z$^5FIs*}VnGJWbcDf}lsrL~DUYTNu3b7IPPU%r2Z`wEM!M{Vw>x at ab&c}eYP{^>-QY8H-tBq`Clq+>6~)jR<$-U+RNS4<$Ug#4+KT@ z8UPSL at 4rtB%^;VDPHN5G at zN)fE6~Ic%!bTpDm=|0F7;X*6-=%r=bzDtW3K8d!a%$( zMER{pK)}hX=@Fu^^g^r%u*ybjB1CjH4J~S at QcftxzV2Eo>UFgeRkDJ7Hh}|;m+?G~ zSZ_$gW`e9yRlEl-d(wnr at yx;R8$NGIUt#(M*aZiR6tdC9+wBG%({J&QEky_J7`)=H zWqGf1l97i&fyJiKeAEz$QO`ya&ff@??&#N~!xLVkn`TDf_7D+CVbR^(v`6^4G_z^k zTv&Ow6dc*~W%w_T={%O8kyGXKx#ciiYt26=|e zD)apg4n9jg**&>6 at e8hUWLoIp>MW!Es>(Z~1LPtDO?rp?V1DvOQIm7%`mvLQv4O13<^*uYZf;7{F0`TyYM5oPAp(ua;)XOhoPP zXzq8nBa=I`gUHtcX@*YcdUQT!Z>HMJs^(sl!HA~ISfn=B1(fG)K)`S&Q@#X%!4DKIgy)_k)XJM;OI$XGo#K94q1`Cy(Ip^9ADlD zYDge}y at Ypbx4hqul-~yOdHN0b`)h`Nf9PY5^SfSh?T$^I$FJwBZ5XhXQ{J+r56;?T zqa*7YEU_B1DH#~R7)(MGQiSFSgD%Tvq8VtYpuzd+PAPS)2$x6TMtf%wnvD=*FfvdP4Jqu!bBGROTas`#=tCAr zgEJB!@3K6G0292o$a*J)xK7N(#$%?von~r*gW`jvxn411>`$GJ_8&_u9?z6x&lvTe zs6EFcn6Xs7BN%S>kQHL&Dj97fWU8uXS>gEOGbOcb3zbBqtQ~2H#f6RsH%UUgAamsn zM4?A}xgmk at ZGoF-X9O z=P}Y25*!Y5n$4?0_A5+KNJGlFg{Kh^z#A??rMYP?86Kx`W=3=|)L17O$OEQB1XmzV z`1?pngN)><)uaoI!lHzlrV7(QIb9iraK!-1g3xeqpzk7ts9Ym5Y9Qt(O^)?_tN4NzrxTwyVf*;*;E at u51{bHY at AL!1GpYEi>|w{jb}-;*|w^eAW^_G1S5a zm(#iXWP6)x;_#cX(jeF116 at 9ZAw%AL1?)I8cBs7rD=+RGLsltqkxDq^?+2DXR~R4^ zNQw{vS>Ovdhu$KSLkyKej+SMB#;taOz at QP*Ydg|x4K z;{(_WDKEN(X~a}d>!ezInGxpiJC8rZ_-Cg3<6~R36IK2jfa)}S3G`lmNxKiKk}O2w z6LfE at o8WN}1hrI9YWLtBhj_f**+7-WIc{XVN04zHV~k!wb^{AL0uLUK1iw}P-K`j( z5U`}+IkY`CXx+dhbrm&*MsdUE3b^Y170J&i$Dsb=p>mO~zEpgx{oHi{6Ga4&P%z%u3<-)N3vH1U@}k$ZDVnv0747z8^1W}pt41qIl!apedD$ehGWYu1 z=lYJ12iN6 at G@nI9g8=?>$o>S~gR*Y5GnmY%UY+O#P)!-ZgL%2fDhKR|7!iX5GaYae z6Od+Y%fwb4clB{%RlL>NKR$3~qNRAj7Dfg#<=dMNN+`spTG-GYa+*;=Ka?27_$NAE z{)=I$*T3RL4p~so>S9>YV#5G`OzIOyKJQuH9OfjVuAHo>T04p>Dma5OMn at VQ3tOiYO<}Xo=+IlXj-!q%Fz5VW zKh{-%b?#>h`i-U|eu(5J!Uh;i8i4uf<p*M>qW8{U*NZxlw*^;ex({&{*t3{l+eUP&&;@KP``;fG=3z%!mC-RVk`8oIa|YFWX0(>V!|t@>C^y&@-dk;T5h*@uGTvlYHl)!2 zkBGlFf}4uMC7?uLu`rYnCWL-mnBivfV{54Tqzsd#$cQ^~1XLQTxsk?+r} zjC=%6aMBSMi$<>}0!OX=svN5iP37_ppeDRi{J zbTbL6lfmggC2h54yW+ME5NH!Jhk@<#_i}gHSMsIO7GsPYSWUI(hzK4>Hf#*RM at F&g zNdb#2bX_8$gN%caEZMFH+{HHkSZ=<#vI4R49ORniG=$H~=PN0!V^ODTXnOF2?b8Uw zD5yBhL14LowdBSvd2+mB!*Tivng0tm36*0c+1ytQmLSPaj!z`P!ntje(=^<}^gV}^ zH;T4v4uTHuhy)JUa0Vq$=;9qz3~V;&2k5%?TP|eLIb$*dNJSuCeZH#)ev1iSGFcXo zc42ffL4R=uF34i+B0P(MZn!4yE1bm=R4hK2Eb&s(o%+j448pnKLbDv`d^kJl38 z`H+Bxtqt`SB;>tM{7FXc-{@Z0bCeY{~1 z&Rc%Gw#|I|vzwBm(^u`;LXyQM`AB1FoqCedliyRH1k@}GoQjgk+A}H%dVHpU}jL6uC2+oEC+ at 48- ze_BfVI6GW^yO%Wjc%KM*3>#$QjBXQv%xC-pa22u-=`Fx!BNP3!KMi{1o31J8x z^rJs~)X1fM;lZ-d;OJvyC_S$R^g3;i+5Z*7Zf%xyzCF at 9FP?u!zt>9S{$)yy&R^(I z;SF)aN5K8$0ud)+&WF!Kz#z*a;J7bwF2Jid>)a1P!%XXB!JB2;AUSY^0zaTcF#?gy zAJ;BW)NoCT`tZ#vu132(T3;4!l3O5I2liAjHWCyRJXK8___vS%rk{e#?j0y7dVW7y zaS>T%gRW*@Zg*BZN(iCcAP_-RWPLscCaC-v9FcgkN+F)aL@<4^#$`;%V|zPfXx2oD zwtMtVMC|>I9F-&1 at 5e&9PKnJ%ihIyJKdJ9FZ5QGJWEVP}4H+Q6AI7RVWs7|!7iI at 9 z9s$cnyrUMKj&lM=(M;Oi2gd+ z>e6UqZX{pB7zO at c>AUCZNpT86w(Jg(_OKabSV*7N<}`in&2H at 6|7O&_W=DZ>jts&_ zg^+h=)1ddre*bsP)ZFyTFre9UHj^s&W6)qG%*31G!JQf!)T%nP-UH?+q*a4qqm)0m zrw%F?y9;d2DF at 3F!7_s(yU|&hK>i2l^lt3W=vaoKGV-x~4vgl3(A$cDg9`%!l at O9- zs}rR|5M<5Hac}e7URqiMF+a$(bpjyYr5(pE*`q0tJmkl)i2KLPP(+}wj3;3$inN#z zF}}dCu(8iDY47GXnSzDpF2IA7i^5)ga;8dWN+3EiM5>=o^Y86Q;&DX8RLwAB8f%C2tQIV76kWsI$fyzHFNJX$#L*AMvafOZY?e9g?Uo}*k(=8 z7Zi~?UHZ*5D-g)kCRCKDkUp62AbO;vL-5LU>==R1^77HtGM-b$Q-}3)G{%z at DGF<= z()YOUt z7L at TwprJtCaxCNn(_#zy-K)Pz&d%WYcc;k at -G&t~NGv8p*|ZIi!qJ$i6vXL2sl)JI z2?-UZ5X=kIGIkd`0a2XV(9hXGHe-NzZ4Z18peyk9KAVpSgJJgt-x5ocxwKcvj=e(n zp5(0Bwxcgjy9f83ObDXYq7Q zU$vN!k(&Z2)2+0c$g7b^PZ4-R`}868=G+l zW at tA>ggN~y1R(z8fM7!aM#6(fi=4rr*3>e#qnDz-E!nrB{+8m(%}myiyO*u3pouW= z4tMlKmZQ&sgyDA1MVfO#kYx}SXyu*V)5t#amTbd>E>b`dilo( zF3p at 70|60qvZAPFVVO#pB2sjXT)0D+sA=(h=HpmM)K at q+X&|WHP;DZ5$qr{AUT8Oj z>CR9Svyt$sPjGe&S~_*cck3S?%`@%*@8$g~=BfC6Qr} zYUeD~ObQ5|TsOkr9k+zf(oCphz&K3s4*Oz-AL6kl2_W;9fce8eccBHX70^HS)oG^JTIo z(f%f!Wf7Awm0C;8tTKLF`iJI#(X7*}#a8Y^m!Dv)JE-x zH=CVwP|ahSh!-3P7XphU9BkavfR$98g-ZojJn5=-9Ea%as*8bFdKt$cfyM+me2zAD z$=^f1cYY4;-E7oWCIIB6anCUPP at stw&~g(t6m(>`*uL(Lo63sM1`F8kViO2M!5ldZ zgP4a=L!8wWhp~pe5M2c|tvkJ4CVIJ@$YIs5dB8}sASo}=#n}Pn(V2~DC>b)yq;*3i zPN|TNw~%X3;JpY;!Y*Kho%U;Ra at _6NY2YxcW}l;vOl)w^d4`(WlPQU9xe-yFro-Nj}b(^x7A}N-5n|J4&!j4)<4di_0^Q)83%gd%Fi#=L& z?}%QFdBA2>C8TLqGch;<^OjmdJW|O>`pi0Eub*Lqxv``k56-54SW$pqvG#8gOE9E^=LNI0Z{2)=rh zxUF at zHkZa#5lIw^MT8?88%$#r8kL4p7)Th!YJ(W);wQ&4NS(-G5dlRMnPv%&JYj?d zBoZ+sOQF_r2GZUc*l at h^xK4BuBS3-tMbINL(o=0jNV1e%EnOhkyDmJ&hw{)X5NqP( zK1A-pvMnJ6lU!>0*o0Y0lD&nQld>Spz-(wPpP#Vm&T(x+zkz10i`>Hu2sXrUnh>JE zxpwR31YPyx7{{JHa2(Ckcm=`Y^*VXPd`2pnQ!GCFn#+u3Ns7sElp-S4s53 at ML!=C7 zGLuoOS!6jlFanqo&IV)%l1YMm_Ius~ zLWf|i7O`CvQY}rsk8;G+saqfvB%fQ<$h00oZ9df5I=5SeNF^O`Uz4Obh{zlaMn at is zo5sZl#Z&r!x5wnrI`NP5BNHIE!PJm-04?GPfQ#2pme)FDB^t+l}S4HqsRT8ql zxg?j|v+sJDMWEF*+xN5^Nt_Hu(!9UCs6JkGnO=OrA|Nnf3olr z4A>)$1vMWK_L&;B6*SFPFvUbeSpqG&yPga2Dkl8S-}V+8yukhV-*wc9X02-^7(j zI=8ZLZhHs|=QMGXbL-nQQ7)1^O(Q#a71hxZxkcS&Z{4O at E+J_@HXLT#f4GL} zZQ6>ippjig_tRl&^oa*Q1(=I<`(pzsxY5p5@#0H6{s+VaU-QF*5fdrLZy)Kh8kY7J&;t%{EAspa^rmU?IJle`&c_D zi-QEXi4Dr$fzhDuC@`6zrBR^eg5a5HIT~v@@pG1Dz?)tKI2#iJ4?zSFY>q4FS%Jby z*;mY63Wf;SPASKUe?BD4v>6zX%=DR7W1hVgw&&;=9LgMg!w1`+^G18I*aRXRLOHeM zG<2O+8M*&Bb$IeRmTG-52u8=}v51_Bqj5-p z=a9PN6Hhaau|oYEmTZ&|Y2fBgLS1YWB5xv!A9&2RFx7dwlXjeG=GG0Jp{b;X#>Fuz znuf*Xh_n&Jx+6%&Gs8{WDblr}7njz=8VH*G=1W#K6?%V{#}n^xLs5jN*#k|JLvG+JT?Y=Nz#*K%(! zf(JA{=pSi0!EH64y2NyjoShqK;N=@?_reGjN z2e9_xo=Ez`9hwYsA@<$)6A_!kF-(ZjYwH8Uy~+Xy6`Jp3X}#-Zbg#s9%Y`|2i#DKX zEbsk$9HH2z|EeOI+J`K%=^qGbtH^Z)CTeIHD{Rbv0s~4+GaEe6oYZH&h at Y&Hxq(2GX&~;xWm>CG$@A<&0m8p36?j>0yD38o(mLV2!;M z(xY3PG&L?%x7iVBF(OrJ>PJ8 z%(t$5yuHW6eTVkz({jSloKDn1zni#|2JkG{^^9t=l-&7<77HYmD0QN*oPmvoVyJ~_ zn5B^D*KrssChRuRkF;#r>LzbRL=u_416r6vyMY}}TONVwycYMC>m^YTi()C8msefIto1uI&qX>h; zl)rG)8q^WGp+gdz4=mP9Kt0UUnzVXVNoE5>-AJk9269_q!E*~GVT at i@#&8!fB2&Gb z*!80$OeNPMHs?9H%`gd6^nAH^5jrn^nrdR{QuGwfb4V;* zvN9PYSJ3$p^i$+7;d6%85KS(xy4NZ7W&MG6z at a^abXf6Iev(RqY?7Gt>V1& z)hQ|sziqdQD?SH={V`i>-E?ibM=h@=spug-LJ-%o2YKauHM_anFekPJ(t<1auBN(_ z+0fza8@;eud|1X?Po)vQPoV(CR5)b8UCuVPq1T5*X%1{*(^6Yo%N-C{myeO7Np^8e zN~y7OmTdG-J-M2&>Z(NIb+i|hG#AZg)V4925n7I5V3Qwn93N9PH*LA&5UVfJDIDU? z{9-U7D77e&Wcc)^D8(r>T(iIQkV>-ZQD9Iz6T7q at 3w2zQ9Q>V&#u2of?Doz3KLpu7 z3HCeWyqwR_c|8PN|F8_6`8%tC=q3aN{4-eNl; z<%bh^PK?-y?#|!KAmqPDK8otv<_vzlU8Y^x+QL6%U*QmZD08Uc$h_pRI}Ghx_=-ol zJH`GQh%a}nr8=RVhs^@M=Oi$hg6DLi?C7sZEnC5J`}xJ2k&L#xgKs|Sh~6P_?F-v>M_Gt#tfnsB&cfC}^ujDo|>N zh^)SId at aM3E@7#zss>8pI=C6|hDtr!={=5|gYmmwXhHVepAd35#p~Xt`&qoh zI;L(DWb=PP at b__2DycS*9Bs^Fi_f3G0LNn-r7QS6EGr6icvAsV_SLd$L{rJrTFH&Q zMlqW3^HV@;8An^&MqtW9H;L40*4E7w!TB29e-SyV8*hQbW{OMhYM4GcX3ZK)j4fNH z8JaVf&apby_q&FUWNoZlHDOG_?~@=SSpoaDx#8aDyodAukb&k_A}f&zW$cF}x%85-5IFn~D2(iEHVeI9~M`n?EB>l+K7LwG&D_w{Xi%V^P;W4VEE(loR4 z>2++yh5K&|_l+Tq9roJo$1O91nxb&t`2IRQa$qP at ouLVMen~x;xhNcfjW8cM>^5)S zFIhT|LUH|(gq~v`l1?NyLz?-o=Bc%P*@x1w%i~9XBzNl!P*#J{fz_;FG{TIo=6^WxKA~B2}YcH255 at w8K zkGy1RhPy(e12*ywbVG&=h6Wy-A!}}edvrddxE$6oUC#WaqRhjHxqvecIznYJ)`jXM zK at p|57GnghVT~iM4YkSdN|rMSS(ykwBBuldP{M(Uw5rLS{A9INCuIU2JIk1sB{8j# zGY7~_181*Eu>(EIu+3~7XcWX|A+SFze{R0s>Pn1G8N_#l1K&Ls>D3KSxt at CZG|N$n z*Ia3a!s=V5l4l!LGBg))O7owX?+*qKnD+33;(NB5PrnMvYAB8zaL81^D?l+R4HyZS zN*^_Xl;MgsEs#F-}L#zsT8SXQz*kr8gaCtU`W;-NNunk12BY(acC46_3Fb| zygb#Lmip^isZISz)c=DjBiRT1?#9 zXk|XLN%lsW+pXhftK+z~IY?$1hGq#GmW&aWULUl;D3igU4SyrS=FH#n9{SExZif|q zXXqwr+~ql$j-oa_9Qd6F;kCg5Cf0-YBW=Qb7^o@=%|)j9RhsKhzmBS6v4Gmff6-Y6 zV%jkCRehewh4zb}*CAL=L6ut zI+w-WE<1;!Rd(~l#%CJBMX@*QVt~sMSJJX8tygNxuBU-y`s2&KAv(w<)@mNLjl~cZ z<1vcLBt;^zqJGyqoPa|iiZ7)O!?6UzvCrp$u#}BezOFi^R at KZ-m`k8=aWc*t#uJ`^ z(SgON>11f*vpow7&0~!v)6>H>GE{GineW}2BPB#|%t)q(30AUv6}8m?$d2gV9uMjc zKq at FW5j-PXHz^=aGB9mqn%ZY6 zUavOJ4cS6{bS1EyDYjQ13DLT%v3!iXelI&tc!A%$nWFy6w7TZS+CuuVlj781>TM1D zE_ol=XyC^TI=23I8C+}fcnb(4I4tNip{0Yx2ZLkKhBFfjuxL1NmIqiam_j2*sbuOz zbZK_zJ;A(%+KC|ykbZ>e1A-tQys-$lTi!bZA!`;l5-%Rz&FtjUqdcHyIS3#Wl1Q-t zodB^y7HoXPOU}|la?@cTVX1R87NLmd&6_n$YB4NRA0Wjiz*94dtIl4F+8-v!#B8C3H$oU|HJXvvJ!dB!4vL?z0oeys59%(ID=Sk at k&~N2K_pXg at 4A|L8OL=18*9*3ARh&PzS}5yqG7 z1_yBFd9XBY^qUI>Ul{9v&yNmBhEkEXI(3=VkfKp3P?-w=#8H?Zd6^lj2%>0yBFc7F zq!ihdYicaI+X=H;#~%u1JITgyA&%Pg+ES}ei at OxDpqvBc8%h^ZL!6gAeKE=>w%Q;Z zl|_p%#k1#I6ep8XXZ8iq)iP{h0>3Dq at pPO|obdB=s;Tabs;ag_=NQFD4Hqb(MURx`^iW^OOz?j8YozN6-Aa`vRc+w~I_CO+ at 6 zxU_*8T>K$}Z_Q=JcLzaUFxX%@M%lVVD;ihdRhF0Ggx``6kpkG~l8&d1(@Ngew>==6 z8kX~;dNV}h;m0YOwMP&~D^bek>xd3mOu>dICL4^R7s5PuzbDt4amM{7nu*^}aZyrW zrRqFJT1gbtQfU~{#L32a)rcljs|H|X$cIxl%%~}m+%`m)wV*)yD2qfh932>BSYlUG zDS$gy&2h?2fW2o5b zZ at gYAAEmGG`SU?tC0Of`D!9>Nvc0R(T}8+$T#5FL{`XyOL>0}*O?^&G;7O`CebCEe{9lr^!a+ex)W1awxrBUx!^Ixiq6z^ciNDyO)ein6v2D3F0h zX6*aN5q$PThI#z(6e#ZPqS20A?x1LicvM{@g$DrgoZV8LWiLR%R6T@=+-My^N`w%A z#Gp2JdhVhzEalgc+u^6MqO1lf%-4CZT_+jM3=~Jy$&S&&Ot5z|o+ at Azy)OfS)RH<_ zK;r;rq0)&dIQ_s-?8h}BP~2VRouvrbIcmJLqg-h!)@%WIDR5#px`J_!Ga5}5H*Rg$ zB7DUV%~??7%IT;#hv^35#Ha?x`wz5Zaf-aCXI`(>Xbu>Vp^5hy&wh9V;rNK~Ws6S7YN58C;k4G4ZioOrZD`41xl zeHX!b+K6G$?&Nmi9YF6<#Wt$aqmiR*b|<~-cU{xb-W zd2Tb$Dzq&KW=6 at KjvO1#E;A>9H`t&%bAe~6>WOT7cJ620r{w3l{8*(`O11G at -mOg< z9DqQygOb^4YLZmQ;+JD>E{nW20G6TiXc)HUDX`n&0uacxp6~iV{+7q-I#`3z!35Ixh*Kl4*TBLnB3?UwrL0M<<=Wj at d1IN7Los`zLby zxiP4%wZ5!diNKn~HovhbT?>rm4z(GE1{s099`Jv-$n^Kl4L>ZjmdQ>@vSOvQCqpn8 zBhZHc)1h%u?e6EhgVUF(4{yUDrj8b7ErFWRh_+#hcARE8%WEyFob|K2p_9Ff)4oH| zWL^2jK1YpLLv}t*-cNH+3G|u5bqy+v!D!`%MIj)Ruqq5unkyC)n(qUUCMbyouosdLGkaDA(m65?_Y4g9Ow*1RRL2M(AjM=xL9{g at 4 zoSy$t<5E2YQ>H0nm%DmU`*_w0?ivP5O+{jbQ)9H&VFD}+lL8Q_sspfzP&;SGUsJh* z*5_UQxY7e+PF!>qk|Y-mQvrv}Zy<e zjLGV`P)?)B(j2YBMtiBoQOht)VI(zgTNB9UO6v+r4HSedb`@pKfgTKuGDm=z^8FrASnF;gLWm8`%Iqey9ZFS~vk|Zk51iQf7ftqxd zij`8R)Wx^yewu6~Pdwl;Z$*W|8>{V(!i9-JpZRBiuT!-!JKqY4{Zv1T+Uc9vgD`hL z(Ilna at Y!ycB>I!v+t8DJ5otG=@G4D%XKNpz_kLKGMsV0VX5a4<4v8OQeKG2m;8YbA zrzSLtMq|1J_h4RmicO22jMZ4gZSlp*doVM%b7TYjHBo;|e88R0h575T)SaUd)M+<7 zf4Pnx*&_$eT47eLG!!{QkY{~6xf={#P&spQ$@zl2E^N+Nd@$qT_ho5d))@8>w>+S1 z<>iC&^7PwxO`AP_`5|xSdO&-8k4Rz*j`vz=<~Zvyu?nu9KCd@!+!BtAO^9-_ooH*C zgc2*xbV&WQ=P8f6tWBpOd<}z;cpeJkLbKFI4aH%m!#V+k8>&Ty?9yP19fK=MKCH;4 zE*a{ZCm3O!%e1J9C9Y{;_`zas&+jy1&1f9xqQ(8&lc{aFB(Lfz at C|ms)##(1MeU|@ z^RPJtYJ0WFM~Vg-65BD!l)(7t>@W3;KGOqmFu=jGomXj)-Hu3(lNf>ah&T;qZ+Lr; zq+UZ_2+~O4r5Xgs29}Jd(o!DK%JnuK#~$Lm6 at lOmLe#Q3OOL7szHOM~I|w~FZmCd2 zyW``&?bgX;Ksa^II;r8+OLqHpKf?C6aju14VgZzd>-}k1l**xOJ7|TU}X<(#+ay)Tz6<5V#(P+-I20uV( z7!mOen;F)+Lx~Mmb(wKpAx1%=%&2)jFi|-^EM_JvjP!U|0x~VIvsof!Qy%lSnQX|S zF;r5|Y-UCEd^1(AcP6VqobrR{DY0771XAd at c-~1#h;xlqJTrY2mQ~i=wcIO<%*jv7 zPO>TJ%q>IYY=R;dZO$8BDtYmg51zVlu>-QsN)( zwAEcksB{cgg%FcFGFn4kLu0HLlr at NQR64DbCP4gyiYSP~4|v^4Rj9ixU%snuuIY|~ zEQ}5;468Y3XO|nSdvic*olylML>?_NvQd>2gNz^9NVf at 0B;Ox>K}@81??8b1MoA<> z**8QfDKOn2A=xYTJA$o6;7BpgUi{6h@ zfh@#KY&$qxPS@=OA876gwk{&Wk}u(Ywzb)0j|SoVOFI-fTLaz=iDy(gO^ZqWWV!tq zE?Cc1ST6uDeVUbpwu>;qc*3Bu)x`shn6Y4Zk8WHlt2-4ktpWIuLyQq&hj8a<>=vi< z7(0DAP-#Cjw}e*H3#mqo at 7NEJ8PwL2lzl+|bNq+4`9G zz!d3Yrt->(w%-z{M?KU!&m*a$VK;=B395_AV&eoB;pA#N5$GpPS at Bfe{5CF)xO;w2jbCop z>wLFuWs7)CzVI8SaQ0*~hsYVm43SzM2wSuheFv2XGPtfkCo862g%|p^MAXVg2 zhVI#r%bgepFg9Bq>!F#lce=MB%6|j$YR>kTcfxL7%eaHo%^x0}v-0QkK}P*Z;Pd4C zdD!AToC-Gfh(*`5Uv8ogk%Mvx4;Pp{XYg?z63#Izcc}x67iKGlTh;`a#w!J8JVwch zW1ps~uU0!vl`THZl7lfVqrS)Aw%8KRUk>yHf#XINa2 at 6N{|MZZu7?Z1Qo->o_WHy) zM_-O8php1 at 4|Vf^cFv6ZZ{{FFUB+}ElzA!GOr^W+15M06gjOaI8 at Xr+(V7X22u at AL z()Q5^D|MWf3{7&dP`!`SRtieuMyNzO$KAUzK}wcIF|_$mLLvR8oK%M;k8ijyslvaH zuOzWObVkw`55W9SQe}^)!PSP2TH`?Py+*0Uo{b516jwnj at D0Vaq^w45dl^y|*^T6P zTypXLvYdt}sKpNji0p>N5RPYkV at e>$dVx!M-$Ks}Ud7L|+~)%9F0r3XKke32bp!9> z9u`*Kr;H*CV;=0eWJjKtP^VK4%imDxK%djQPTnndn$1-hW4h_dj7U$2eGQOBQ3p at 9 zm=Izxcq>O4+U{4?{@Oc9b?9KG&UxML6PS+w;>Xlfw(H~{h0I;I4IT-{cO9s#s`*=> zuj3%}Ed5A-sJRG10xup7tIOw7d=XSU!x#ix!6aebb0?%i*=eqIkRwb-DAG?%2R{7W z01ZI$zt>B7KjL1jmfXeUaM?ylAI{F8X1k7g=$2n`w8(j68w$pZMDk~%2Y!)*{T^sHg+*h+%>M<@No!rs>nLlu$8Q*C1in97C*E)&N zo_HB4lPyyo!Q_TZ##}R&KEyj9xG^TmCmR;g+k^UzXJ57lG;ND!2Glv(bb-zY?QbZm zR}`|Ubec+h`Rx*>`p#y5NAN{O^!C at KL1qJ6crWXpIxdBpWazcfs-~pf%2HE{hZOT@ zoGVq0X{eEGNbb at 2R4VJ0eFoaq%LnzYfjs)i&vJ3)I{$XWc{glH==3#9JYXWf at ARWl zLHUw+9Z)5JZiK#NkCR6vlzl9?l1vl*cnkF4FK)OiuQLkRhWGq?HI^W~#ylw6KW znLNt}MlUQF{Khy8K)P36YA-X4cxric(oE9XLs_G}sc!U z);*hd_ccrdh-{&-?ah0sxVh&t1PT!a#25h z6E^JsS%*B+I9bEUY5U~!&V_WyTaC56sMs>Xey16guguzm&c%)0?k)8cF}Em^)AbPR zQF`HPV)v8ursydh9FRI at L0%kq97b&Wi79qD+q^JG#$@LL_=h_5rrJ~+zLLEhS4p;( z^mU^ZGkLL^My`CKLFJz{epD$y;a29w9R>>s+?D|ME+PiWD3K)C4E#gFu$r*4XYilL zt at pjRfFRcGqHI%|O=1XfG9{cvnwjq-JGI`q$(60(sGw_ahP)jTmUyUa&Prt at K5$2U zn-jUMqV09xm&HDq+?tXN=R6y4am|hHJQ5KNV7tRCBjS&da>k2SkuAqUEk|Fs264gW zY))x)Q5;g at s8mdH(=kU)TAZTds0stEF2y03wZ851!=YV4klINRV=yx$%ILu`v+j_k zC*voEqDLG&9=I%6q00!mIuaj}W`d3z1os`~k>%=kiA>`=O7}@*yApOrXslWR$qrZ5 zopXgH1gSO;xT?7j;)T)}iN;WNqYBPU4~dyHnaB4abKn>mbhNTXgjntMy~iz9kVE6t z!GX?3>mKm}InNk|#f}llvtZHWSNax|wOunDLrP|1P{6u$FVL|>lg~rgMW^j~RA1~W zEE!)q9BXJVJP|%_=OHK4VW9FcP;v}0D4$3Xo=uK9h`9z-C%A{azrX4*P#B28CJCyA zq88 at AMeXfn~uOi at b@$co~+KL;j*d#9EcBLRnyI$6|V8M_8x>PA5NQ6-?xk;YnVO(Cp@ z+iY~kn7mlC^lIl>PL>l82prishGWIgy5;@qB at uo)T=7l=sKAEO#5!*EmFK>qWL$gE zl7R}#*%%Q61{4^OQtpVdbxMhm)VhjNrHa~c)1)qzCekpx(M40hqI5a9>Y^7&hZL}^ z;)N!vOswMRGw+{TrX$PB^Jl)j3QF!BhYvD!amn(m&zj1U4F=Nz&pWc>%l32405_2VlwX3kNVUCtTE(XxpfA_tgT`QfGLx z!G~DtF~#RLX7*sYjIr5ODz<8g$7O~m6)FORo&`WLh0h_20Kj2RwFoO&IlnrEnyoDF zIcKGc>vUq&G7;yfbdzJ14 at 69PH(*B-B9N?d!UkBPq_ZfdWX?or={FnfC>oId7Hah2 zI2o(JqiSajkJGPNI>s=kbW5mUw`SvIlF#n7?WOG|Fh(6v<=QzJUg{Z#bc)^BDmLjD z2tzT_M6k}S)lCNe9J`ckpMs;-TJAj`#m?~BXHbKN at g;~oIOAP8BN$vAq`>uhYrMiQ z`v)*4ZpX+h#5J1ti?YY7=?p6dKC+siDcW&Uor!8tHPl4GkhP3(fE1d-rg}9Q(Bf@} z1%{fxcIdEiuHY at 5hcF;!sO%!o1?)>g6<>qRzrg&<>80z>ZKT at 6U{!EbFvh(+)acM= z9Jd*}Sn(l22CTDs1n*0o&~-$%^%v=}g4k_|Qf9wNx|C_wAH(maODbBqjR zRdxFEIPl!%|Rd0X0mIEsydKa3unMqq7yUXsF)+%S|ys8$?rVZ z_)!8cW^*re(CgOR>Xj9%u)tZR-(w3rd*n#dan5a$F? zn52p- at H8V#Jq>IY5iV^|VB{T$3pUqi4gfu`jm+8dY~HUvuPucmWy}c)QSoQsH2>`} zes;1LgFDU5$gW3t_IcGLNjq{7afkz-GRDnMaQsn;dm0%iB6+{LC5^P+b5#aor!YP{ zuC^LrZK at EpvN1jJJ6dWyPl~` z$?kgCZFwRscVs~X0p at z&uR;2)XHP_VmA`p2yXAub4?yt0BaZV9OAO`>Yg%~uDjQ=H zA=w+bG?8M~1mqal5JF%PBiY%r1kjYf;`l40;tVWU+bvQd=%z%A=)qRJ!J z>)IOEH0_K!LSv~V$|@^#5eClLDa#G}FTh&nUg;2)Z200W2GCmlMB?05GTz|YaJ3d9 zB#=%s3 at xc{^6ND9`zrNkA{qvUj(#!+JkB4NgqhUDmzFZk{j65i(L`a{Az|9Na>|jC z*>RNL8=1EnkWHgU6AyPgyuM4#Ty7Q1{4Z}QvTdc^; z1kMMn>NyL?7-N~rAV8%+iuPn_IZlD=WuldOnnVq8SoFp z?nkM#wrs+q=Xz2j;L2uf<8Z5XX at K8cNK|+3putLKBfnK)ngmtLCv?F9g)7`wEY%aD z>%}=H{7H41u^?30#O-HXT19`oh%#9D;co|bC{z-*!qbY>RW{TN`cRw zs<*RjT>6eAJh1|+ at zXFg$w!`M*^wnI&l&SFfDEL}wU{Rq4wj%?!}!fFP|++7YSrf< z$Pp9+l3 at zB@l~QDVVdybVhuXkp|PV$aX3BCMR3z>A5Am26KSvqHkcR{Hs;90S7a0X z&`f(2dl28&-x-Dt&~1?snMa^|dOCb at wn+CY;m- at Wn z<9+`hK>Eapc&Nb;Wpg7oZ292;lVn{Lt;sJ*0&$B$?GmpiV?j5Gq{$Yad`GeNKjvHY zBRxm*hQ~kS=pU^0M5zWvY;Dt)GR$Y7B at yr=+eI^-4r!#O*YwV&6%Y6L8gjKfek5=o zd%Fgi#wbBupO`-Z4O4h%*m959hXW{MJlx`!T{_?F;I&ph{&V-tWHr?SA;(b(J-8u7yo&4+P(^WIIVgh1>pZi4v?- z*gmqv8;H)`c-Yclz=E|9=Xu}V5+`NX^kOK~{6m?^yR)MF9n(}|-dl>SOA(&Rc~<-WUG!LfMsxi2S!i!tW~+vI7!ub&Oo z+giDth0lWscC7F>T_mv1+glqR%R$}f(qLp3I4<=KxpUL((eg4K`j^471b5hzofd)T z+6N}IXt{(8Vq*q3Vo`y@=!h`V-FZOIrc|!ibWQ7H1}Wzp(W60E0L&&LEeJvxkzvGl zD;?AEqb5y3<#z9}0-YmQE2ky=KceDTosx&CrbdV_W5n&#@;o=Tx$@(&=*sWXHQ)FaRzvE)a* zfZ$qY(C)%zLyY1Y{d8`sA}nfRG$0JzT)d>=8b(|gt%}vdo^AW?(sZqnpip5m>l7BD zaf*m7V>DBjD#fzXIS&TWDuIn}N|o+gfxMA$YGD-Z-+$!930_So5XBb~5~R$u7MZFG zFgd8rK&-XSa-!YF8Lo5{XQ^r;BWr5iAcM9hklG5f2!kxj;k(Q_tE4Uln9Az3cW<=9 zIT0=U|6k5Vqo0ZJb0Xy=w~qj%LR^Uq;1XHyAeUA{lL;_Y3KBwryzZ(oknC$|ss%04 zg-_B+;zZ;le0*UtsZ}%?&ZaRe?dEN4pQY%F@}a)*P@1WWx!B52iPVL+0`i-xmbI*Vt4;zvD-|fAIuNLpQSz-n}_kLW>fVYNFl!VSJ>^JLIicAHy2S)pm5oK7Oe`pV!Zl7 zg6UCAc4CMXv_#=aSuSI7Db*+qvXX!+OMNwzS9NtOHX1t2EDrizP+LI~w(U&bg!yBL zoCmr`U3%@Ywm}-sJ?l1;D7;1N+dT$EFR<^6(7R%d8!sCfGVj_78S9Rv1n(XUp^#GH z1{sL+1%pH>UsdNGOPE`0Ou2&G&6T{5tGg+HoVW{RJb{yY^DdS&3fmQ~5KM;E9R%W2}Q^fsUe0FOEKy+ at KwtKPSV0B at uw3P^ zZWGT)=|zU#=O2{!$|p4m4+`-hG-F|gzGaSLdYC80{1$TCrZEA^VO>l at r<6&%(FY!d z7+Lcui$u#y4M}{zJELkXY~@&T#~m<)ze}An$T}kvU8N_MPTel#bhzvhBX?zpQ z&ySuEBnvK+^u<8uyk7rkJ$$pp;ZSHpMn;R9$3gAF{HS}DhNs*q<0;8Cq`9w<=L-UVJEa70QMg461rMA2uVQ zVK^#hE68lH)3_@#V_|-HILwQafP-^`C#m-I^29gmtp2WlUps7voREI$$|GxrgBE0n z*y(+QA0`qVkBbfrXkzl=hbV6p2do=y)R0A*=R-wS(`y(6aIjm%4d%({u<*W>kt*~^dSRA<<297267IRU7No=X)Qw7OjEW^Qtu;0%~iaB~Y? z7?eDXGlUe0nh4$&pkxEzK!;}?iar6?TH at Zb3F=mHPJ$?@!WqUtlQ~n4>td#xymf;@3r35gfkPRAxh(fQg$F9Gp{M zyFO5MGF4{H0vQrvhC@|I<7B}#1~U*>NKZp6%V@@Q8e2PAfKj1foq|f at vm11pH6N^c zVgwm#V+;fKOVRyYqQ{IbVdF2NGg}lQSdb+JEt%DR|K at 1ne9JOBP+D-e#&le z&h&l-$vpGE(Vrve+h^{6*3kn~HcA{qI88Vn%lo?-Rqr`>g#l(wYHdlD9)G6-CBtMe zsHC*AJ;aL!=o0MW4V*YW!-Hbz=QxnqMV at Sy%vcuw+KuQN0f(0Y*F<(afb}VJWP|D`hO&ry=T9IXuwPND1tW$(! zCpiZsUGiiMLy&C|3y_>M3tL?-sWn+)Y|LRXSw%!+h#VQtj4ia1u_1(s5WtdBFFJA2 zE~^YmH9T9kbuu6{dKkR3Suk*J)AAf$3B1wh)|0S+##1wNF)9h>=_wMGn|c_oGJ_0f%-@)lOhZz7H5#jPrg}2P+)Y~?v^<3?!{{*$4n;j z<;}@-s8dJlp`c7k2ENYTcc?}kw>q;~Bkpol)0k at sy$2JQImz1BDXq1<5?UF{@hDOb^R?ZU z2mDKqd9op`57&&JOP#xoqIz=*M0bqp%vfh7RX9%N^LfDzx5B( zro7dp^f~f(E$QO)XvmBhKeuUNSFR1DF!&R??1$EE3mB{7+pCBF+I;YIh;p( z#pTrwl4c`#zDmZQ2KpnpO>H>w+6}FA at x|Ryy>H(2dY96LWp!zZnVnc^%JNQ at Q>!~V z7#AcIa7b^bFW`siwS?ShSV(#mrl-EV398vA83e7r0|y at qn7YkfY+G$w%t-cBUjsia zJJjk|92gr%9iwcH-0#zur_Olg=@FYWhbQ7A*(dyBI2_f at O&P91*-vJ$O+pCk$eop)uATJAv$1xV>_PN_=y0~_}!6yps$f*;a zZenyR9aI`KLsx5bzHT~n`E!n}`wI__cj)zQ4y9lj9__f;mmo%Z<{q?jafK*7MKZpH zY}OKZr8cZrJJ}#b(VG;+5rodR&JOyW3HIMvI$9BAs_)FzxDDCMzi^TxYxB#zJ8-Q# zyQ1n$Q-?&&j5)VwF|~08_S&v&aBO3S6*kSW&__~BHJMvxsnt5;67Wgp{Od}^&bghs z?QJXY`%#q0P*}`MB^J!>V$LYT9Zc?DHiX?enGLf{V^2HmV4OwUJJT{#CCp86i!M%( z;hWZF$Jl;FOkEibn~b#^$o42#ka?#a?G!p9FRQUlwL!q!4TAd?HVcE9Z-LY~nXvBW z*~aCXx~ty1*#43V$<%{UZPJMC(y_hp{a{Sb(4Nc%jf`}^UL`e|Xn1W-jF8QuiA~AP z;MiUjvUK>1b$>4r>*IU3iPW!;Tl}w4yB!Y4pJ14B{--12P7#v4_93-YV{bc1S7e~* z`>M at 0ZD_6Nv-#~%r&og{3Jh>?V}6z(v9aoIU3!`0ZXa340wh?Wc87mdkP{qFc$*)gaWUM_utnfj8{sC4o|b80#Iz_hQhgCG2$CVtpreJxgsIOqB=!>vdrYipL>@KXfqk> z5fEL3^o<3bTD6IpIb5t_a$P{HJ=32b1E;YkUgBmhq3)f&L_9p+3nHh)cNejVeHx6R zVgYXp4%DMt3qJk%gWDrl7C8|$6ktRxO)tL+T4dOu)lL;iGh$Xl+~s3&Z at Q`^J%tE) zEC|)8 at bau+VAZtB=iXj3mtACm7XGt`p9MI#aj+pDt>qr%YR^8uLCL?%nZL+?2IH-h zjFF$2>{<#Q6F81;Ue4}iTg01fC4^?xHpg{uaT?SJ+}(MPTST#|bhE>FCIj*~fPmJeKY;#h4pGNDjG at kANlC=jm0HJj~}*?9^e#nxM(q6}-v=`|0V zfmxrvx^!X;Y+~H8+}y;TG}#(8hfZ4o70F*B^jhPpAj7eC^P>42$IO=OyBE}I@=Ubj zIyx1N+P$*`)}Kh1ZBSyFwrKQgSwTla0}YMKv~)TKbFMnx_8mKqzNRX?Baxw|3Ck2* zL)>8HkujS+MdBEsaZ{E#Bs!MUTs?7!a^ygPwV{--oQPzZB_y-AK{%Jj1qL at psM&z6 ztmZ50+x^6y_}8|M)yW(5UGaG47ai67tXFaNQ8yc z+9k>{KJxce*Y+UrQx~oaM~`b^XMFR(9X4r*z+$ItO7PC#nqZdEr8bGU-gBHl$`KC%X;;ba_654EYQe<9jfiFyir(V*)wH? z&*@Y@`JK}NSM!r%*MjspQgyi#c19igodq}&`c2O-%=GieMfD>F#hCc?pLM*7I$Fn% zPP^#QZat%OYy)>S>U>MdV>b>6kq|dzZwue=i2ROpMb7S at C>_sUb7tvpKOlw@ukjU#3m3t!x3~)@4-C9!`#^_S#Y1IttK+vZq7ArnI(X} zF?#x$uWnqqfpw3jN0zVGjGJ|G)y{w;nbrW;nD^St69b-vMzkNPQXdu=gD3t*Iwz=iXuCaI6uj+^CEM(z+{5|5`X> z_&1}s;feuyvM?g2f(c}}Bp~IW?^Y^$_Fc_uz<}^$2cZ)O6l;12y9gn-*T){JvoQ!~ zg*oD1NpCV6L>H9WaR*7OH(JTLE7(CO!Y%jyJ^*$MBEaVB(cE=8w$fo0mV(he% zxtp${2c at Vu_|3gislE(VeoYE8en9Gl32}^TL>Dpd*Mc^LIPW0}&Bydr(z836zK>Mp zQ-Jo6B|bZ?y1Gw3ut>WDBqVq}MS z9Kk2Ts8+q~eB*w&b4+II1;mIT{iK#EQXxmBq-kVuCn1csV-SjtM+!ZKAyV*}8YH2| z5s#~&vvaQF+jDNJ;#+WCtgb+<1s_S2#z)}1qhxiq8tT=OwQEGYRjp5ju~spZJ`Qe3 zwq#e3vRpS$w6rjU(Cev#B~sFJck{&RIjP69>pFtXse%a+Yh<1cahW(rjU#e&y&`rnsM(t~>#1v)zq{2&|nT~36F^w>6ESF4k zQp9x>sx(a6WJc_uy?ClL2N{JaY*RgeDe}GGXX3qYkpVBPrSvSm`qa3o0$L zyNhqnTj9E^DixH>^xSE$V=aS1W%;#zSw~D4d^9D^8)S729=&;P0Ccw|r6jP#XJPzB)q at dcPSFr==^S1+D zvE`i&J0j1GnYN*OLCI=k>3ZHIxV#9yM#g)s_I%J-Pc8qG$E*Ks>06$x~hCs2197o=~e_V(cYVdUC29DErw5gzvj z`lO!!Co7}7LTqx6xTSG3swx^tXACN~Afvt-8bsMt>fMRUUWE8Or&&X2;P>O=PS`=% zb0PJYwk6J`j2D}`c~Q^TsKX*|Q9cEwN{;T#A6hu=;}?RY;#w?+Xd|mLE-SfXE$zXw zu}2;!Z|Q!=&RD>o*RmL3;RKl$VHi#>ClJhCv)p_O#GY}ycJ9E!OVm5YQn1lX5M at vz z0RkAGmw|iU{&e>Hgun>uF9HC&gvAR%rlB~>azYR at hk~4v=X99aEA z`q3*ru+7QIX95%ltG1~*(GO8=(yW{a#le*I=FB at QiJ5@)PCCgR1AvZWNgeP*qb5|D(n)rRi!x;5p*GQ8lNe0$>^ghjJ>giqzs(C=pLIOU6CG>@{;?wp2 zVh48hQhw5;ETo6(a&v3=^@VW9)92yb;o{Tf?+|#I3u{TIYxJ+3Vx*^-k>0l{s+CLB z47^gCpz`p?;<;W|ZCQ8Qs_ZW at AB2uXbsdHuagoXpgvWl>so8G0Nil`Ozwdffw^Vg> zo!=Z=Zzg>cV|m(%5-hk$U4IpYxbL#cOU+7^DJM>n;cl*3?5vXLxmzw434RuonS7+I zye*X~Npxq%F_>)c();73j(6p9rC6uRve{90(n$!B9Yjcuq7aJ~dni=(31MAJ)YWf? z=KMV#BtJ}^QWitJo!6&zr3C#wvVUl!8qa8z(IwGQm$K-$R6Ek8CEYIc*3%|ul(!Lq z8wkLJBTAQ5m9dAy!b^1P$!xbtc^h#?ca&L#xLmGIUkfQLuSLq}onX7&BqR>rqsN=_ zn4}voG~E#2mK}{MP=lb1UEb+?u5$3V1O{sk?f7}5$mbngG5eu8a?cJw*cwws_uccv zlzU^sUjjdvli$-G)A~Ip4$-S`VjtHF;^<042B2JZA!rVsDBsq zw*)ug*IRY at JJP?OAZFsW`6@;Vn^x%U*}8W#Fs|70Z7eFs#xWsBZRqCa+E^{ZTL=}M zG90n9x60l^v+T}y){f8$&jtp$8a%v at n13LD9EX?n!b!Y7M_qAzk_FFa1=Z)ib at BVI1rHqsk*x!T9D at K+T=f18^x+=OS*LX z+;X>SJyBy1>btC4=0k7ua!ih7WWZ3tb8N%KeaRLR28+H0~9AU*{R(N6?D29cJA79__iLZtrD;O=`z9Hex2nvU|xX=!X;skX=) ziE%KlHx3vbKqA`0IoUfUtY!VdUg#-Ma~wn>MVYiAhcYlV6VfhVpr_|TrIJZ7eE`^i zvEKn(&-hVQs;Af-+yZOgHw${Phx4sW?z1B!6AH4#c>MTOH5N^+AV}0{ zsPq)brofDJ9>_!StF?~ygijs&>l)H=?CeivW7E>|Y-z+_2eb~n_IBeA)GOq3ICo$7 zpqpCHOV>-QiYv7-=nFH~O*+X+R%_RwjZCtC21hU7ZmMCz-BTy1aG`q1^KSOsncGaVm{XcPHO60(ZL-RU-OBh3R>-4CM2uH9%IM*) zZMAfSp6wMK`V~VOxXwA>lH-Lq{U1h*F|EoGTjWFMh<|Bm9p;1iPcgW5EG~=}K3a-0 z_sVxp`#!ug_-QF2#&|9ZFY7XL%BMfjPsL$qQCCNa`DdlXiad2z^C9)q3hymZYwG5s ziz1v&8_o at _s@KhnVMQD-{DLC#xxlpw+*{oKT at A|iF6JQE!O)x%eqt&;up%}^B@!T6 zGxr at u-33dAT(x;L+Z;ZQQ?4 at EUWgi*ErZJ$5#Is0+9UECb8a`l`tutOV-f at mVfq;! zs5micm}_}L*$uwlH(T4j z+TTX^hNCz$l$*O`9l9oYO(66aDepZoDbsO9Knn9XXO}ilgE{qX%_yYH=35S`#rh;YyP!9w=w1A;vCLg20Tt; z|5I3v1JAdYUwmrYi37ku?IIi&IoyE!{5oL;x|a}kgC*-2C*I!G$c6-bga4;|67(CIKy$$e?MnU0WpXd9Aq5pxQsKU(q!O`MdvhQ%HCqLFvgA3 zCVAYq(H@#gS-dp~bQ`=)>g(;=XU6<+x98qb6C=S$P;iM&Z9zDrps6@$Db7{K0 zHIIR}i$8$sDC|3Ro$&zVzttPVpbHglj5wDW7bL~3jSAw^nBXxo3b3p?on_){O$72s zs^aTm0l2qf`e>Z_SiH93H%!2O;c#*Ss&m zJv00gh(4-vlcJFf=pC48J4!FmYA`e at 9)j&8T;Rs#9zlr(q|8kyuSscG zIfAClwzw{DpXY>p;sE3gd-LB*7dWKcmEJAyQ!Hr$hQB1DRLzqG&V84g(f5FVux{Vb&`&hiIv zcmu51K!FiVEe{}66o#TZC@{;IS;HLcyOBqo!ru13Ie(2m(aHB)KT*fw(Iv<5AUhM) zra6i?ho{}+ at EcYm0kf%wbOpUmjRTypQ;8M(*O8M&I+{BiVUOKDvP3b8PucsXw3=}Qpi#;ghrN`l4?D!e<)LZ at i9WUV%{ zF$3o1cbmF^;{=)VY{<)L+;H|5xt( zzxcQSp~JN-iV^D~&`SwIAA$M6ivTp7 at GxYm7m)<bXVoW7alS?d+z)p#EqX?>Gq7nfk01GoSGcvOzsyVPU zvKY%nBt=sa-f@^&1&~s-F*q*5v5ZN%D`2^S$$^ML5QKzCQfUz<0tjJ=1N#VdprbPo z#$qx|$rPk+MMhLF`q>~8B%#oS41)}ah{0k~A($wXrsD%05s3uELRBPCNCc}gk~(0? zC73265{E_!iv+^JLTWXv%))__8w8k2B6TS;!~v3-A%wzI5>b2%=CjVN$R;1Z=NLQM|wafVnJs-m$7HLOY@(`3>F!n6sa2MCB_A{dB-j>Z at S?VMC7 z0Hm2H1R_C&WJW at 2h&TE1&>UFPBgPq#Vq)P2JWkCTElULvtTHLGM8YChW*L*89%KU& zYB7pHz>EnAW}$)$48%zlk!6KeE*r8;#wIa?4OOhSF_Sq7{NyA+K^i7aLxLXIQzn8A zF~E(O7y$&dq|F at +GJ>LJV3uayWTLX9LyTcjPGV#t)l~55`DfGjYCE4x`C3D;OTuy%U&nf;$|6! zVH>u$uhsd^X67m7t}KVDzEz?fdMh({`#e2DA{cH9nivNCa1G3o`b-C+KWPB9r5EjX ze>d&NGvWOa&**xg?96_rKX at 8US|Wl9i6J3MQozhZ^uS at i`Tu9I(d+Q?@O-;8*egWI zRObdsbH(k8@`CJ44wsMLKoDeski#JV#hnak8I=}M`TmzS at w{cD;2Z)l8w{rOD0t!X z&}TepCFlDs`cqPgL44?4d`IpyP#;YbC^1>6K0_DZs)NMmjOIK%P{n$?^vW>VK?Us1 zyqEHfh{@a<9J*o4hXvKvjOSnFN45T6lebN^`WL?6mAnG_ zHd#f)Y`^(SZ%a^78eb9w6n$^1D+ZMrn-2O#9 at G4z*V9JE`<@@q98uqSXgfBK9kpxj zC%R|rIh`hh<%l=88Fj^3LFSy|MDTRo^vo~yo<+J%yhmJIvFm|$Kgfx>lwjvYaerIl zZAJGh5G?eVih1dsG(4>wX?r7BM5{?N`$-g;!_hC+Vb2Q8Ex?&!%3;6tK)G&*>wrQI zo#=@C;HR$M(3Pl0gYjno=CY1(?G!10pO8R4iBg$y!+&r|F|d57qU2Kh=F-_Zru= zoPc&ep>{t*Uk_wI at YWyQA&qEo%F8QfxT)Mt><#`$4d8 zJk05t5LjU1lCU2)$)$8ar_U(6r(e;IKoUUA5E8nEz{UOt`AxG&%F=IBt#@QK}Kjg}Q+MA?ibV+O6ErIXk|>KX;1F8c{)l?vuE3;iDEs6Bx+|5yml8(!5;eX&S;q=HpbuNvJkA zgi&UPnl(*O#;H~qF@=c4!Lnmkp{B5QnV2^QQ!tE$A&zw<#%M^ULZs1#iibsH<3^I9 zty=b2S4v}cQh(NsEoMm#EHbu*UPwaVRbpf%m>68P3}!))rbTNmRd8cuSYdLqHIpG` zOzhy$o8Vw*TOor_ZA~>@8#0<^9BM2Gu+vyXRGwu`jJ6L0X_~BgqFh>OVO&ClgL-X< z(<2>{1{7metQ{UQXc(_S;Y|`7kYg-L7(`k!j^;!{1Onr9re;>rjDtnU+&7ZO>>?}_ zc0i_}=@G1$Hrqhpu(ZPlMMf}@kRZYg(?T$K%_eAJTwqKLWUQ{0V<_KRLs*R#F|1mZ zqz;%;ni*n%#%4IkOVcQuH6vOLCy at +<9Eg99z{ZLzkiDAA#t zxL7rFw9!LWLqITugM&zzHQI)TVGJu7TP;K`q=;h%ftc+~!O0HMRLwGFgigF^1~|X! zn$l`%q=;7G6^8JLAqX+0zsOB&)vY!g!YIK*AjmpW;cCG)S%nD=u+>T!$}p^lC0N8V zo25NpHbM-# z9Koy!A%v91W~qUtB9Lr)CX0qs8q6Caf?&eJ5|T1XQj>;U%~IL_xsy>Q+3c2#aRekD z++&R=2yHQ9rdl{tDBcZlU`%R-FuIWDEfhp(V_G!=gAFkejSp@`x4NqcV6jpR+?X;V zntP^~t&?#^G{!XHt(U@^%5AnsHEgL|FvhbiwG;eq6Bb-qMj?rORdQ*W zW?l*`{hP^_c1IRJnXLW~(EjJ=b8O8-K?g98--3F{5(z|sfFL9wg*_lV%=))kRaB~W z2lM&PjLf at 1)*9Ik=2wVFJQ5e%1EhdI5s>=youa#(m7(w(ca}UB3nhxA2T+0e5*Mqn z^AOS_{&L+U4 at yp}HN%ZklfF6(N&kUH;>Xt<{DjbW0mwM1(3WZ!)eFpqz>})}DxwRT zgsuToaee8BWWdS*P6


}Wn*hp6GOit*goopk5b2&$Z*(?8Q zZmv>nqPitFm9BW#o==Af!LNI%)!#P_+BZ8gO^RR3l4HE&(EaL0#X951!y~aN;&-%8 ziMq}EJz0(OvkQQ{=0qXW0>=*Wiw|^Bgkd at OBSKJw^bZBi-S~Fk+Ub at h)Jv1pyXeN< z{Z}x9dUINJZ_I-kI-apm at CY~66iP)BAiJp2E{n$vdK_>KM1DQ>d~vXgE?f zW0NJu9~H6-*%u3eKNe*4=_Hb&-z^{??hxA$A?-v#kV5 at N*@q1?!q;QaIRi_6P+crM zHOcKJZ_kR6t#zbcp`SrvzZ42Cse~FISZSsGVrCj48RM%vEHm?c_g~~RQFUMo8ZsuxH3i!y zWHTmYGcYnk2NHFUMg7g at F~t;7eP#)SQEvkynJ|h7iH(l_yJ;9;F^!BHaIAXEeGLIh zF|qvza_0OM-)&G##p;zH>O`Uto09CzDDo+W6xHxu?*HFD_?A*umF*S$q^Y7*{&UE~ z%mRtL)KuV4*^1@wjQIu;Y z!1!xgbDw$QqoPp=q8Nk??mg!yIB@(NPYzcO8wm0xEK&8ZH}@)I at C&8r at nrcc)h_OSzPsB2r|B zx6?IQcWQEZ85CL*BScE>oFJ zsq$?*qCbj8(qdx|Gn3Z-4)-Uy4VY>6xt(PRbjga8mL at 9&ER*7jd;@HqY1#T&Cn>36 zB$0>ApNZJ2(m3A8!`4wXJD&~LM^-eDub(^{{hZ^KH(BF%XwM&;4KV~w zcFKK_hpyZ@%mx9w1EJJuOfOnuc~0AeF>ZYHXqZ8*Yel!$-L}tG*YF={$x}(Tkonv} z^@q56WrvZvJ{Eb>Q z#z^MyF{OHSvzKkI;}pS|TAlexn+)XPdaP(`uOZGYjT9JGkF9i}QsN8 at dRiZx$itzm zNQUc&Ca_oNSqzxV7P&l4n)m#COGQ2w$^emaG|)$r^m=L90mox zLGdCS3^$D$MD+ByJj}STQE4?wCNV>?A)eT;elan&AC)wXHicFtkxS)_dl)eH zrJ7cnOpQDJxw~{&%B&H>pcU>5(9$z16+}9;R=4c}?QBND;xsr+7|szyl9u1B#Wc|U zbYY()_A?jcf!pLoJ=$I)tkblJB4T79hxMO~$6n8Wu-4mq*|mvXoen%%)29K`JBVUm z*8+U|YVJ*r3i>^;7bEj{^e>N3E(RZ%OcqiDLI^PoNtH1-YX`N`Oe#;m-OGhOw-Jw( zgX&wR}o>WvpXxo2vr$`*i4;1UaY=6Q&aFz!Ru4vm&I~Yu6e(+kWk3 zqPbXH`0+S2?3BkO(qb{*s at QeThL*k#k&6E&%{_d2lVywNx;&LlQw0xEWp>dFG>x0t zHi>0lB+oo)%V~;oP*e;Mi-#B(aON3{gS;`Kg4;8OlXWy%J*vf`PGd4GMX5 at qzQD;Q zJ6L43Q;{X~_4zxKhWqeRGt9#ZQRO0#xF}{}fEX~unDQKVGcE^^znq4U&69iJ(8gvp z1~Q|Ire_&|0~iT|LC6SR-FpwlXn@(s!XYX#^h|IuI(fPsh=aofH`*u(QtvQw zAevN;3X>{Fl8jsHRuS=7ditgf$0pIkqS^N&yks%wTn0%@K5Dqy1`-e0c$kM#hhsVI z)}8$)ZS9g*zA%xrY!PWfpQW8xi?;XtgzzsTKBhL`jt_C6=ctN_&b&_ZI(50G;J{>P zj6bZ5b?^--sN|2F2roQck at F1X6rXL!iqiur`HIHh54&&9+dm6AFDkaBFl-;+0Y%a& z(T7PG$tNyH5fgU|nUd8=SUCwunjMix3ri)ViP;sZqQ_;o1b8Zdcle at X5-@nu1Kz|P zJwbpN*KKQLOvDi`d5g0;N|)^H6z|DYAmnOC0k;t86;jt_t;$jok`SzF*=-*Qbohg~ z#C%{eXb+ at gQ8bM-nuiS?>k>4r6wI{ci-nTG`2z>%wrDmQ<{gCORzhd7lgV3r6zI}0 zv9>XmLkH2Uk3n=vcgW`s!HzM8J_UsZOqo>zv=}IDk|xZ<5_c@*cNiE1AEJq>QfC2h z#)ys1S||$H5g0^39LJ0cp=Q1-m7$wgFmz!_>j*SPiI2(}k4A>m!yMXaETw0459RtUn>`x%)Xu at NV5PIjJMRaO5&1 z259VP=sa3f8nqvJxEKuo%2J3)bFZAZp#URGvXBlAzvjGXG-&%u6ld8s6hGuByp}9K zw$1#>M6t3iL5>}%bZEnt6NQ-$rxHbo{3m1of&W+cU-f)lIM?w`8=T5Fv-CA(sk-_| zUzB^;cGWjd(Xwy8zJmcl{5-Iq=gN8k}YcHYkc z+;P}sTxh2w+C1C8-Zb_$bA8Sa)uGX^puW#`+I?;$^C{xpy>hMQg*H|xQc4P_DJqO% z{An6E-DFivEbQ(#UzP> za!rX+k&2umQk05HkfRIc505p>MGP><^lb z6jd?f&b+grcNm$jVF2umJRvKWLEbt`Xmnv=7c2Tp4{VaLia4l(QDvAUI05tpZpz4y z&BEdw98mZj0(ryvWzxrekQz6HN@=PoC at E-&FN5n} z=9vP!bj{n4>8_-B^7t-I{uE*fWDve@;|iT zT at BQKn?%9OgOH^9qlxJzZ!I+w5AQS^Lc+qsn5Z`z;4P)>?|}8G!8)Aa<2g}xM7RVL5z$_l`9IK_tN=~^*&C=k=-Nd z{Egm?-{gO{T2}I)I<5hUEEU1dTtk;un?^+aD3F3^l*4d5Q>!S at b0ovis!(qaQ?g&-k7r^_%+i za`c<=BU$e1J#WX-aOv3?crGrqprU?Y`Z9`{sU|!QoMc{6{-0EeI}0FQwEQ zjYEi0ghtM54U7&i7 at R&9uiebPYCMQu70^&@K9aREjb>pw$j;p16^PJF#p*w}V)Lg- zs{;dw<^%nt10ztt!wh|9UdacS^YcK{r86J$F=L4I2Ggtd_f at NJQ(Mqr3({Cw>kMpM zDq9Z|51K6|8ZqBq4cy zDI|W$lB!K&r<}O`rcpZIqx*j+*?wqq62KQa$$*jBXN0Sfv>b0$?G;{F8(hrR{#0t$ zS(HfVM7#Z79%nCppRXTP>fJ8z_q@;af%vZ6)*a!5tPgW#d|=l}sBU0gig|h$wBKE| z_Xo_srT@$qCUzR{pHDr%hStNw=-IUz%f`Q at sew9~zl}dpE}UGmZr^#~@M!B&*tZ<@ z+0 at zrSn^yC#C3P>C|vuuYmF7=1~S}9|4|EMPq67U`M2x#visylf`0Sdgy#axdfTeA z0~pPsS`0ByijVD=BT1$Xe8s2VzSX&ztao?ad=2uE7~4eX at ybE@#?wG`pSbva;mAzR zP{a3g4t;Xu)l8!Fh+%P|h>Rps0VFJ?b?fUAvLF3@!Ny!%7QfK7P@>T|aKO)SDTLX;mMYKxY%L3Q{jVA*_lCN!Z*>HDpwd zB6O_-A2#eNlkzZP$WkS*$$^XGBF|Gre+SHHHd5IYU25gsH at sy0R^8mR2t5qAc2Str z#bqHZ)PR=q-o)sHBew433|rG z5a#l3=he4@?M`Wa6}kR1(YZ-d()awmpV%Ky_w&|HtRL_gn4A-ggeehA4D4cV(e$3T zX{T7=!lY8QAKPV$7*@E*H8;dL1_tjOpNq&#(nr)wn3l?I)kz7DOg_R6S`Shny)EMi zAyEMTixe*#i%LRV2HY?S=_E8lF$nx6T_k}J9x#F!LOVbl(MW|CK#74Ti;hT at sR_Y) zivO4E8h<6J!#K;)&KndDJq!qT07yF^{+JWl^l9BFpI#W0yMZ8n#sKs5 z;6L=IKbtcwYO99iNZ@;TZyDafjS++*+nbz>{W(I3uR635+{q-iFe39jdu&Xwn>{Ri zhNS#UB-BxACsQNI5X?2x4Ald$H=E at x-sbXnw*~;ervGR(b35n(t8_np1q4%jN+DSg za0C2*+~Op}RcyI)B#<)-DIyfx1U0gf=SFMG7z#YKt(}VJ2^sRZ0si-5uX^cM;vZzT z$F{uevPmSgQMibbDAopkEY at aiEqLtVh-!+YsE&D^ob!?~?%E>Ihm0V7aP)NC3_YBc zj5MF0PxY_We0Z8*R|baLyMAcx4>iX&B~MjMFwCg at 4%u3ejfdvgdK&$l2 at J4wI5Hq= zbdyH_*Q>x2rVqtMFjs>blE`#WQ8(JrS`IWiRMrwi62+1^*&vu8XxlDl8D~A}E z!`JnHN+GA?#yjzCnE#mmU5%}_*&Ue&vTPj>%E|rlaWa%Z0^+EiZ zv+OJ-y^|`z at +ucRJ2F}7Pew4B*;FFV+Vz63uC39?`w~2$5lGc|U53 zD3vGn#21Nz at EHQc;lcoab5;KP9tDtOmb5yA`4Vuk^v^JlJTU*3rRg70$6o)Zx`|0j zQ8ZHWv;_ at JMWT$|!h#nXkT|-!-5eQKgYUu+|*8ZY#~nFNS&{ zdax)Bz&aEa4I7Z<2P2b_jv4x;Db91B95s at e#4w9NFrdYKj2o at 1+C0LPgf4TOARKCu zke+7Y*Aa_YIn;l~x%?T~lMJ&J_{b2anUvKZ=VjI2( z(f&VrdGrmDYC!cBZs&N&&9lQ{EX)KZVO&z7f*YjGs$LR?_0`0 z^eDnVpep at M54NiO&anI0(H>l!&edbDWk(O|RyLllXO54Zwy>&Rv3+!;eTjrvjSxZ&z-TW^)_^^jnPknUm&yQ9bn!595hw_dV=ae32(z+{r z{C_o%S;-Nf(GH3q+cRXpYnvnIJ$kP`@7rEi*TuRwTh$hXOqq(FFB4VaV%< zYw(;b%DdD-NsrqFx(Ly=&2?yiNN7UY=8WYApT4j9VGyI8%Q|D_H*Ef81g7}H1OfKA z6^xBCb7cCUHitf5kmUdtvmXd%5AtME#TW zz#OVa>z|HY%_ at 5&K8XY8kAIh!$H#{&+j(=FBd)in>1xv$6?em`lR-K3*9Gdc9{nM0 zT^#0b#x5ere{30|?z|%*9N52!k9al2kW?Ve><9%!CjO?K*oj&W;lz61DTfSII6YFYr`1KZP_}f$T|=2!NB7f z0$%6lfyAJek&fTG=Mi$*=ssM!xZnX-YRT8G at 07lkT|j z9LGV-bsU62j?>+nheppg`ucZM?se=vUIRwsNxa8d%zHfjevjW?xDnhm3yyqK9nyTSvd5A0SoP0kv{9>wob#vfztOWkphm z$uL6A@^NYT?`9_^AIO8i$zsS$wQWbk)2L)?OzaJq!I^4{H_zEJY!?M;Z!e|+HIKaE z?20#&LK&hllR7h^{K>vDqtO0U%G3sJ$h^DgNZ9je{g(< zuiPRXzlri-zTn3a6U(#S*}~G21vHsqLE8!wIG5KohBl4mHDxx z5zNR~u&=4j0}M1ga8yK_BTpa}??=lYcQ>bw99~19!%NGZq&RvrV)%ixwc|p@?87&dv-9;Bk2c$KQ!VNAj3c3Smqsg)pWR$IQ at XObo-~miPtn zkp+^I6SpI9nBD8$5+ekrK*VMn6vd}k zic?1k`8pjJw~-h3?TYR*_#ty0Of9+ET%FgH2U9Z?dKyJx~svD0Tb11 zLpm)`&Y~?hy31C+%VcmSoXm3ghy}(P3x4?;VRg3#qjkk1x%uyxvW at q6F|Z$I;9~(W z!$AwYn=@cVN;<5s&*YvokM`eumN@$3!dvA!+yn7Z$6coOx7aC|2 zAF)_yCLF8>L$VLL^HsV!zWXb8`4$|c4-V at L&*vT6#{%z3%|Gwk5|V!t zi|RyIa752hek}UBbNnr=wWyEeJIU)<(fJ4W2TQph>OIVi6YE${#XrEullVN();52G z_puwX{OZtW7z_~S0$gx}QC2_ at p$of}aW at R#5?OT1#qSpVo)#>kB*WFmDl5ux-D6G% zlU2E*mCeHN{)Jr)*L<%bRz&LV_uPx%cV~}#wpq0Ng zhVi$uuCB5tD{eT9b0NgB`Z;|#nUOaf>SN|RsX8N at gJ{e0ZL#>=IT75_F`2)C1EcDy ze>?5J!qRMQX!UClZYmHja14>?$~GD%EUVz?n%60=$r zI56jrmgp*M(>Uh*`;RYc2V9!U0C>Hb+lYv7#~QNV7g{>PK9=HMF8XYW7a=jCX4{Vy zH(!LTm*XL7)UmCdsYz8F3HDk at Ufs^HO1&-4+8vqZDg zO}d}a$*HH7)nUaBwyCa;5T at u?>$Zxt=C&Ga$15-0-#eo=&R1--vcFc%Q3J)R8Q`v+ zy6CF_P>XGMjZ at V6nymEQg%s?uoc7052k0yXQ0F?~Zky_2VUT)=o at 12slOl2uHlJw% zSafp1{_BPFqbGz^2akgHnkf)|YJUOY;<49}-Skr``a?+RrL8(*oy zrf>WTJncErN15B6|2qqxSx2dmeamumm&K9`*fpkJrbMYL7emJndBzrcPU3=~DMJVL zeJ+vElKpJB45&XJ;!K$oB0%TnMWW2KS;$2;N0x8ZAMD+PWTPP>FZqtZUQ0t>qUExA z4t at r!B4&2w4#K)-5*gS+fsB}ve(7`suAmAZ!mN^+;L`3103tL7j}*gwzdQT&QK@j4Z`s9wI8g{|rxldi5*JSMKRdKq1LeB47I@$5>|CIFo2k1Yo|63#v^g at j~euSI) z3?3YVX4#t`Z+ef)Cq`8aQF*QIt- at l*@f?UW{Gd7Tp?XFRI>V6DvwK?Ovj;PD zG!siBRh)3sT54uc{#f&O@!JmX4P6o)nCxZx?;JVg=SaeHSEd}%d|+y)IXurn2)9+S zDx=T4J{~u>eBS1}RP7q?42_(|o=ND)8;;@D^F5rNeDgK;^L!b24VTRW8r+29TN9yS z_}i|p_EDtFj#cb-Vc0&R=lkO0Ft~~jdVv0Sh9P~*vJlL{Pecpy6Y0AB z81#@{2yF2TsT`$q9At@-hnZsqNpp9Niw}}b zjf)0IAj$uT!LfnQp#4(Z$ygxVjUMwH2%pk~^TI2_htmgE;|*F*>ZU!j2)18{pgqMn>YJxYk}?MbGF?7bpVv6A6$FrjnuvH->Y_ z_BTBO=SoXO8x}Ymf(;*_WPAzdp?HWRAh2+(iCE-FgB1lM8EZ^?ooUt9cEwFGqrt at 3 z*~yjjZ~Nrio0Y-x;E$fd#XgMFGZBDulRD at f0drRT!;h(tc*VZSF1l0T<1*{D*y5cc zo$Pls%#M%!Ju&(BPn)XQ>u1T|67SZlLF_sn+k?2r!R$+oldhkVS<45z$=90pW0ob{ZYX2)(>8fn!HY4T)`A1j3Q*w2>?`SE58tM1?Jw1+M~@af7dlkR}& zfHge!!kA`cE at muXB^!b0 at s2w?X4Urd9DH*oGMqQo<2B&yCIcM{u4Am~)hoP-(xu9U*+b>(aF_ha!ndZ1(M(F(a zyK2LA5*fMTA(?V>jW8$L?;uOLhl}6?SdpJ5M1|k|w1hI%I#zuf7!TCWIYkZBg+rac z&9bFjYpllgs{g_IVqnsxlN7-b6om-{(L*5;n~Yd~EGfo at 7!84rK?haC3-&S8r071Q z0ybWfRM8-Ql at d=rV27;7DfE|^c!hlb$(=Z`PNscZ*!LuMnom;i2MFeJOEEPe)^dCI z$?P4Xcfe=iD(M>pe}lcD)#0x;egmRtT6yS2W at uy;z=SOzq0c$bw?x) zPM^3jf1r^Q76 at jK(m{bq1mXvY?j4dyjdDp3)*d{o4Dk?C)+03 at Q{oH}q9E@;ennkW zc_`#ig^myM1|JSm;bfM-0O2M%bq(S%R(^r9Xc0r<@)(FLSRd3*?w*GsSTQsWUPJF` zs5u6LWPFy>+8md}_XqC?P`|(J>-zls%=}nl&&S;ThvyTlgS>s^v>K)0X_x$d9qtj0k?BK+DT5R-#3b at EPiEPk%P&cCm-(;HFBd`IwUb5E}%Y^Om% zu=f>rGA%D19(tS?dbFsokm{Tz*{9l$Kti*3=5d_5c_ at dZk_&FG9n#NT1o~m(Jx3V4 zH>y>%7V*r~`kVuakPgW5GqQ}|*P&>#bSH~lwE*l_hf zK+GOL)chC$cQH#OvKbT_JI at C;e--RXeru~ocC|vbCgVZZWDLBCW*FrswwTwz!`ZT7iVK`pg=%r7iCD+n(k$I%!w+uX-! zFjf8GQ?46xyuHh>w at hIAg)A>wm9@{%+#u}PYSx+!6IEuNFF$^9XnII**?qMa<%5Gkihxo4Xn zpVI at Jt5B-~y5?%z_Rl_Nya*?{KRO#?8N7WDoL-u}NB1_{UlxPZQZRF3{nxGZc5&%c#0^z-UOd}V;{z}aiJDcxL7 at s6 z+oif&p)*r|c)(XisJ!)%n>^c3oY)2p?l ziq?<6kM(s58qB?w^m(LciQebLaz6}ovE*T<{Hhm>I?l3zddM zxf=O~cSrB|KAHz6*5Eh~^N|p!dI{_{5kS%k0NMHeLx=D2FhAfjr4mH32;pKp2T`?E z>~cSZo6~%&@;&jQve0=y^ zEF#7%jiV8{=Eq6rG+`j8kR3m3>_rIS5NJYHD|E at sF z89#pT4MOPqOb|oJYwz>*b9kPO9{wbHPs^oQ;ylGFqwH}p{r)c~)SgaHqnUYIjQ1PG zUw at TjbRiGd${);vE9)!&!=usod5+%iRJH!L at xc8>Q)!hIqfFc+BA?H|N at F2(qJ11yN z7~Lv6!{RXKC>)^v`9poeas>`Dyx`~|?vWkP?Fbt{CXXI^M^J=aQw$v=31BcJTyvoP ze?$aH#q3Fhe02+(p03`$j;;MG)Xh#vIGJlg3M1 at cJ#K^1Jj}5eDZIeN774l}LesdX zlsTOAGB$^W`5U#o11c+P!mx=@M=hH;Pl-t_mco0e=}=`S!O7Yw$v*GT?KiW)<$o}H zDd;{X&qV!Nsw#-SZ(SE#E8W!S$<*mYB{U9{{K#qY+K1 at q;+!TphZi=Ub|Fi`aFSdh zr*qKEa0nlVMxI9B?2h-pzKc4?iPoY)nwY(eo!?+X*=h3z`8I at UHF(a at Vj@l?$ooF} zo1|xl at j2!7e|58+C8N(-c6<&nPVe(;xZx{SHrvc?1`MFyw!D<_pZYJX((&2YkFmp%76Xtj z^OxP6p)Erf)JhX;TyU_UvICA97GW|5RWZ8F?Z4o1N&T4m1vcR4(TeZu)_-%eXqV1% zvoJR27bO`IgP%7fVWR=@0+t!54I#y_!!wh&yZD5BTUGiQ6SO#=A9-)snl%ITM42G> zQ0RAZ3}Fl}@O=iOo=UgVGUK2 at TH&LpDM2naYp5JZ!4)Y{jLi_9cv|_bAKfhB>+vVFVGN$_Zu{iS;54 z)%fezMFyUCbk^ae;LOd-b<7f9K5Ytuaq=odWZPFfdsc?<>#^B)k=;kc7x zeBSR9%TJZJyy_>q6|%Mj!(}ueNud3fo_Zcf3-Fq&aIFg6?a=x|G at 7OGZ{|G;8fb6R zzQ@`$)FH1KaeiNI#G{tUg(5n at e;wW^fG*xFK{9r$43} z3Im`SBy}c3dH-3dWu1y!s=~~(_9gTCJerQrfnI+X&l}V&R>s2mgV}HpJ%KCqIrF at a z!FZoj4 zwl_#O!uYOqx_&=KlV#t-B1sJ_kDkNyKg*NWSBR1wm`ohNZZO|Dx_3!tAD{UmgI52UX;Xtj<+ovD!d>IlX*EAy=sMiN( z;-4jf&0Ikc=13=v+_HCfbm6B}_*~(oOskGZ)ANk3LYk7R40Aj}QpvpY{L`^lO zO?=IM(NABf^~gyHAqfV_ubI5VBZ>^qqYL0i1aZd(_mQ+B68DaB29e5vszItQuA^;w zCoDI*#UPwef~C at 8vwJ zEv-lnS at P8I%zPg!Wi?l)H=N$X9K9ZL at iyx`y!N?kLx)$N9AP}e;lo+Y4$5A8g<>c& z6N#fyE2>9}U-Fh3lO|2qs5?Sk%=F_$>}lUE*yp>*6o9F^xWU?7nl9dD10<1G8=>9F zoUN^srb4@*b1Fkv&|(gl)@>sOB5HNJGuPD3K(7Wz4dVKkOlLyfOKsrrgS)+%qb?ZC zp_!;KJk2Tbh;mVycjArgXcl60z*}a>q)bgEHfESE5jpItl2 at Gly5CG2+ON^;34?9Y zM%8)d*LLH68ILZGJICPWfv?>V&2%Uw{*Q3{G#w9(?IgJ7Mw8ovgTiMU8jRIY>EW6= zjE-cX1Pd8v+aNAt#KhWTLHPp`C-)F2D4{C?Y&RD~t93F5_M1k^ zg|F++ySKPTR#$G_NNRT at x7!h1a_VOA at YwwU@Hf7lbmnKWIt3#>!ZCg? zH-()XccHxksRFMad2Y at ovzh3@6qaFxa*T_HG5|TeA1|9<6CFXJxO6mhW5+~D+Y40F zezh8<8g$ahO{MQ%#mG$omuwuMFP#xZt%#4q5BKY?ccV0VM|4s~*4Op) z)$e at B|9q{j^SZ_zoVMTD(R0({ua&dkQE`~caA^!+?x5EHQ<&&uAB7J^x_WMXm0 at mc zEitlq+^2Q?tltZkf`peyx}tkNr?C$leVlI_*NyR|yjNKAj&5>%-SYnU-u;~2aASS7 znX_O1cQn6R_pd4P%jN6+;_jLXNKDEO+ZDwk+j35FTZHsFBTDER~PqmADb)d94e$ek-nVv3Hn<#jfZ<9J}%VKkM at YX z&Pa&=QHT-<8%8XKgTgsY>^Tc0Viag7N~U}`;t$|?9%MAo?d*)P41x8cNMnS3M4K9I zR2E%Iu!$a+ at H+B@n&bAAkTPQyF1k%kE=b_#Yaa6L=uvVtk^9GcpF=nyE)HY#9)*hc zA~`zW-gSG5qp)YA*FxM}lHR0;Voil>U-G?nI`4G2Jf|h!-smnl!S|-o>|<=+JUku! zU^Y8rq377rl@^#0AS{P+vQFW3{gBj4a6q7KBuuV}#(m+OkupTzMdaYiKBkF5n?(iX zKpICNOwd^HWcm;OVFa9*z@VKIF&141jmmVrc{q29+m1d*=3 at W0@v=4q z#yfg_c`Fa at P%rbo1yv<0V`7uPbg;{wiVDVgN%!mZ>o^xJh0CP56c-NMX}g}7H5Fe+ zWY!n!rYV_6kUUm8pkwq|$w+7znKO-=QLyER=r3_ilE9t*H7X8>(mk$hSd!zD7%kZ( zsLVQXk;>0|IKk0Qa>)>%NN2oK$~fyDlawU7qXL3~k)a>$R3WI`!Q^)!(QN2(uvl^d z!*lhLf77{kYQCYz80E-5iOUNq>cD}0%PQ(-a*(8>n4r*n;p8A9fr`+lsj9v|Ho(0^PVi&DdV`|<76$$PhMq$8R9QMm!L+u at R04&%kM z!Jhx#XYyMIch#@_q&@ex@F|3irVJR0K z2<MPwm=MmxCn?Dza%}Ik*bsdJM$2~4K5-VtYr|60) zIUgu%ynaTn(>j6k*Re_+w=0 zjnbhxQg|Gye5Y{bvPomqk|QgbhsyQip)@)>davKMha*7DrDy^ZsMrIcx-K zRF6_Y_;jpL4~*ISzJs1y^hl1Bb`T5d{E^6L(?WNBClW)O-;Ce5ml~ zS_rRqBW%p at Ux%OQ#f?6x9`+s+6QhyS3o#I|xmi~_?Dm)^g at WF`*sbQ-l zRW6tecDdY}y0hVz%=$qJ;MLkNhTMQtX2gZ4Z#d{KCRL^%ig_A@ORoXNL*k#1cXlKIKNOkm#qdNP)S^%#Rm|pl5tw7Z at XlrDNc{Mw+DZV zq?ulyMm{IT7gK2DU@$Wy3`oWyxzLn%5#baGSQZqq28b+Znj>wEkNs-e>BYSZpn*Ilxt*d6Kgi(%7V;0 z7s)Mi3fF3wmSCJVb30;r=Gd$42BQzWfw^>XHscyk=p`Zs;0_&nGWO#7&(wNN8qPDB z?E|TeBP>xs$%YtWl6~Y-EOA;RlkoHvt%Lu8T-{E2l=3o{>`RYKp z$YvR|j1DmTcMDfQY+H&z*fB^H8#Er%QNUwK#~`3graVe at X_2nlwS!2kE3AZq7 at ew- zGcsL4m~T5027(zj0O4AvW^9kA8}W*B@=u8sj*zE!_X;LS@~HxpgKv&-baOrl>S_6v z*-evo*0JqrB#ug&Vq%qy3|4NKl*u-FHsuz&Knb!~DFY>dzJx(;G*}y3X6jNb>Mt~m zFG~EO`cARyx`F!#5bT9jV>;DKZPyU0HFuDKF z12jaCkNe=_ at qY}#htqxKxMQBcKw!Wl{X4`nZVv#=$$|48T{_E=TU@$qq}jxSuv at Vjt9^OjWRdLdYHN zb7SK>O~ep$Av<4CEPjtb at jL_FdkJz%Uubd7_(oEg=t9CjVXXTM`9 at bjP7o6>hOoVm z(D$-`9k7`AW8)sFG7r8CkKTHJPO{?9>!7po)aL>2XA>jsJ_IN=gO?th!>^tPD|P*n zl4349g at rCq08CL7M;2U0sP)Hvx84a7Pg$i(ixQ|BO)&L zv|wu9kWeHXercE8Nr*C!^zZ*5^S?U%4Z;7c9|RX6FgfB*EcdA>T&rR%-b^C^<`CjR z)Np93M*S}34MPN9h$g3NP}IfblIWN0{hjd6QBgA^lb|a1p9FhlxUaLj=pDNrJE8MC zG+ at zb%yx^$P at BY@$+^3AcQD7jk?v-$Kbs^nr6yT0lApg386gw7PTss557tC! z(~x2;N-YfZ3>+bV`2lADA`t!dNcP at hU{Q(e2 at VMVv-9*E(hrD<4p at C3zkIyp(Zqf9 zkVDY36~*yr`yBc1PH)iRSN|nPvQA9igt#B at s8b@GCRg(=G8df8XeB_H(M-Is)juLS z5YLD9jtob at TOSkpzHjE52+XAaA<3NM2N}?D3{q^L z2M$j at jTM`mis0PFFZUeP>F?O(!xdjSj2trbzcqg?zhWu&VP0S}J zks36Uvp%*+qo8uO2P>{e>L)QZNOG%fL`}4lLG+uJbM>Tzk=Hedek#RL=>RxL4nQ~` zdB`tm{evCCf^WEi5r!iXg?Y}oO=d9>%5XntF)Z^l4cQ)$IsCgVW at KAWWi^Hdz)8nB zX&x=aNebS=$__wdNHH)pmGp%|SS!>MVK|FHLOM#EM;>q at +R7)PYqYs(V`T|dpJLE> ze>E{JNiw|%%$XLN`(Gz?8$W|j+1x#*4j;*5g>c!XG33~rF3%=O%AfG0;G<5AN~Y8;4mf;^9c&D1M5Y|01!a$zd`csJst8$1>vE2 z=P9uiDY=ZDxQpZQ6WfAjq7UCV!i>T%piKO%7C5fk|LuMQOI+Y_#w{7Sf!y4`_#~1N zW+D#UUWiSjNA$oFNACK<9ZiXw74pt(_Z_Hkuqp1QG4i1q#+KK!IT>0Q{7pz-{C!tG zdoDpoWz}=x)A4goIy%Fg)m1SGxsb?+imIyv%AQ@**nV8qk9Q6`8+uOjpx%3SbGt*0 z;Sb)?aO}SIiF2_dgf0aOrkoi%yz!)%B(8*LmC+Fxn(Nre4ud>sMu&9*IxIF{uIm5n zonLB1#GO->#xzv=FZq?dcXN#6Xh`pF at au^mcitIQ_?TPg#rx9__=7R=qx-S`+-a)_ z)eiH1{9itCw_?nXC9S1rbiwcwR(WE9hUKZUuH4qmVe<{JDon;d#4oQlAWN$ce^{4o zL5z|3li8NfWI5hlaW_xfryaX%(VpkG!-q7RFcf`XT}+ZlCc?~lf{an165Ki*F*I%V z;%aNB6IlUU#YCe`5(MglI_shM2?u^lRhih}>fme^+hF8_8*CG3K-SpX96rD6S~;@^ zAR~5$7&cHfAS7lYf8u}V{7+o41A97pSN>Q=9=*ZV}yv;TaT}=Dh(I=Dgb30 z at F5RZ07>QSNoDP`qGoXGrzhX|4Bl^&@cc4y*QZ!c5{CRv8suSn3bY6t5^Ugyrn9#@ z(xdi}6C<+r!v3=#f$%2Mz1|b8$f0MW~j|7P9q zwZ9(*V)^Dx&7k0PjA>Gv4F~ajoMYtS3|J^!e0w*=Z(|fZpeLct&@c=vRftj`5*T8>G6YzcX zi03!$ZG-;7&HFc-#4$9JRWWo at P^TNxS}e!^{ z2>52ceac}VvPelHz{%_hA-145k^f^@$f%>`3RpBX=s?9xVN}FG8JgDwQ3rn&->6*^ zSpSrHz2PKks+uEVn2E=A5=jQFbjF1)-Rx_cLPw`Rny-QKN5{6ZqcXa<1+cX{SL#`+ z4+?(8LVq*E97c%NoDq|uC1{2m_~H|h;u=h~ssKVgH*#cDNb1~$D`mq6R;617QSakA z(X6C$gvs6mpdiLfvaRI2{^mnz`Q+k92Mao-cN1T{!5o3t^UyC?Fdma)+wi2#ReIw7 z9P>ULA!c&%hS2ci!-eo at KzK3kj9y~qrNIll7%Lf7P~|S+(~-`UI_SkYCBt}&`j90Y z2kd_*8|lOh`2jG`1H1s-&^eR%-I%rfe=y#EnqEeTXv1tvpQyL<9M zB z91tjqLU%l9H;)xJmS+Ba{pJbrRFBs6zv7s}1el+TkR2^u-G{g>;>NP;3#DD2$k9&j zhbph}>kP`;t-4l`zwDdx%3QP^I-AS?hfhRMX!my2h5S2oQB5uF7^}fiQqYII>g^zNos($CetO&I6EN!&=je!F z#~s9`bCc(O= zm^1OC4U^IF)@}v^K(V^{5g3reuu3v9t6uF#9-zFpMN##45+|JYjEmCD-Uup-+|Qe_ z(^1UoLjv$d^lL+$b2OU{I5!^-q?az}w5mA!KF6ei*v}ZoFe6Vd at cD41R8sB3&1jJ+ z_S+a10e{Hgtz{4_YQpLP*~0Z zci(;0vH#fN=OQPYl4OGMgk4N-InHAcxo)Cu7c+8kbovAbg!;0Qk zumVGI--Bs<=eV9hvE{&i!b*gqdOB?Y5J2z0=J*1GowhFDUt#CK at Kma$82G)Swnf{s z<}-|_sf{0luLB4V154STIZTdkFmWQLNy4u#YJFCQ)9 at kwhtb9B+vT}>2B(t_0}ak6 zb at MG8pxcU3sPpUeyCv${PMdrNd;AWBlNop}eEPld%hZ+3GHJE+I)5XlVg>#Hg>di@ z3<=H!Kw5|h>f>9t_js25+Q62$MFgX_E^H!>!-l3P70xc!G4hn63to}(cJ%5afBytr zXN#JLg3fwLAVFRRJq9j$&fPy}?&q*zLJ(;EV~9mxdo!8Uf<1nXMjsRx=P*KtSe!&C zE;43{$&r|@&RVesJ}$jlG4v4;&l*3$ul+-lT9Q3*rJ#`)i=mSdKq-QVWLfzI-$ag3 zpoe?oCG*a&FJ-+Nk3|UKS^VQ4X~5)RpfMcumj1|m6J2leYXbP$I6GBS`!04EI*c_7 zSDztfzn0xq#{0#bYiP66TC|aK`1 at hB&rq!IAI9peX<#z{H%gSkJ5FQ8e71UYX&D+x zU&MYtv=gWgv-4n at KD-yKkMS`6Ls$v-cu>PLRRGBsYm!PlH^NcOU8wu&7zTRa2|EB9 zf*?>`MQTwHq(@KB`8k|^tEYW*snnHV&<}T8mt5sE(OVG`tRo*>y;D-MCs3Eg-RsfZ zRcuogip5ICJKkQ9=jNvlyL0B5S#9lsT(if{iAF;g4ul%efrI9ew=O*{ZIBOkrCB^f5N5W>w7TiMTdt8bhR8*^3m$ov>J%K;VMXnY3=-498+D_H~IxAD*6qRsk~VhVy2 z9XSU?eSBnI-dcXOY03H#dE&kg9m(60tHZOT at UkAA3)7S#=j5gbQ$Pl71gTGX~HYju&1_0|0{y)<9bb5y`nDX3e zl0F^75MY%1TCa80&&qdX>ww31-r`!uQehV*50Pw3SSY354l at ZQOd0wR8kjR3;w%U+ z4_1KiY*ft(h77|1R2Ik%+Tt&kJK`yeAXV at q=vRXRcYQdnvR?BN+)!*iGoNXiU|I8P;ZS$H(LM zfdWD-TOZ|bo*xC$W*Elv^1QnaIb~59TLa}mI4H~YgJtin6KJUvN0Eo5aoyE6aio4V zYid=(nFk6-F&oV-y(6=;=j6T0sRlxOeUJs=V|MO!qAZ%Lyn=v8aMUIWz8lzWN?HnCgDn{p7#=7Y$Q>my#AHGNpoU*hoxH at 48G8ad+bH>i ztTpTGgD)Ar%@bzS^g8OQzWq`}b|;VMhvupO5*))6F`1O9nUtp`$(X9VKcC+F72KR; zjRXU7A(uZGijcX=(N!@ir>Ic!M|b}p&M`1)N{HvfX)<5H4R38EhJ)d8!-5X3yOP8R zV`~gq&BG*~uepEQ;hH$%z at Byvh>^$N@%(=ndn2+OkPLyN2%)76m7!K_S%LE+Ow5%4 zn at 8|FzU0g`5~stDr at _QdBhm5uA>p^)ZuPOu%pLb=MNYBsH*JB55@;!Cz;`AU z>+n|5#hg3O%70Mj3B2g9`ZWoqDk4_oQT{7XD`{r2I$J6!NA{L1cNml8<(dZQnR+>c z{@5?1Z+n>rDN>6QCS!1q*Lqm_IiMe~eB^$>B<|`BpMo zhws}@G-7c7liBA_a!Il2IrVc6px82CklmHvX&8!WZ8Oo2=8TX|XGCAO at A;hiURUjT zwW_LppJ#t?ccs5Sl>Q0cZ?}(@chyx_o at G^4MFbK-QB4(8(9u;*Ofw9|MlO;S7Lg&q zX at X+wH6jXP(U}mKWV?~+M3Ef`qOzkD#+wl94Qdn(K*Jb2HbbN zoJD at E+R|=?Gr^iezn7y~L4f!qgs6!1IK7e}5LOrg2HW-4a&F01&iwYCaqi9Z?UI!t z$vhJw>9l_9>+%XbO<<+5m`GXJjG0g9`xVnxE0O^qtT-x_Q{tVGfr#!w(+BD-*irt` zW<7hCg_WJnRV1W*RjuVHHO$F*SQ1iT0of#TGJ_%vjw!y6Z}+!X zS0=nVYJ?b!?frMqJAWt?(ltnG{iojtA|G{+p2Gi#uL;V=NJ8{}4Yl;Xk?1(eHZ*dsO9%y)MtGe2$#*(cAzRR8L`+tQ0 zQ#(aBs&0riEz`#+E$mIl33yr22F`w-fp*uUs*mktNrn&ib3oJw?3yqAjy!1x(!U6v z(x+cn(5M&j7y1-|xiRk+b1tiYE^RFaAYu{Z%=dWWBXfEL}ThaK7JaJK|eDahD4E5 zi(KF1!<71^`($WAR?6+OENu-Tnb{OY`0vvS*rEs%33qn4(k~Q!6Cfjx at i}j^`{_sg z8F&`3?mq*4t4$5}*~x$1`1?)gQ3u}sPu=rJ)Z{oP`#~U<9aNb=>HACqEFhEiF%G%_ zieW?5)9LANchB_D69maBm5e_Xr?$lu-EQX~$nLO!5*utkvg4$X+-&$k$`uWsH$YTE za0g&mG7jI~rrUpZ8JJ8T=I`iy-l&t{>^gt=4POsmI1D^#2OvM#Aa+b`2&rp~HIK zRZrA>{F4 at LBFaRXBa+Y2WM{o-F{sreO0;88WO`d<*>(Jt+HJ01J}}S!)+IUzpEnO!t)889sd#IE_-4&sO0k3FwD%{rW(9H z+9jMSWIH2EJ5uCe`rQW-=6B<%)?L#(P~3KT1higuUkH}&D5IWy*r^Ua_>Qi0DAyy)U>Pruub zi9S%E_UnoX+z5w#~}`g`zCIkrB=rbsG<4Q?Dw&ICtMU__ZA3aHh6 zu)u7cJ7c3h5$Q6|f`-0}?g4OuHeqz@ zp`PyJyJ;ZWxN>xdzMXeBsQuAfc!}PqaW}L)wfghwvTk7D at JD^N(lgne0u-N=Ot*zf zQzO?-LPBNdP?RAaZj+><9_z*P7i1R at m|%wVaF#K~eEEqMGp_7XU50QEhn$9C_LXTh zme#CZRhtSDUjRcO9gN+htCMX6>zQquB!&}35<@3e3tUPS4uUcIL&j3MT0N`V0~`aMGf_C$f=EzSo=~9c zn==K-#Bvg5vIlGML at Nji#9)4|*!L)phPj&TWAE4AVlm}FMp3SB~%;O;V zoD^7| z$=s1TTXFeJ=??jbFqkD|Mtlc&3b^dM%xxk;?uP2OSV6*LkulS297ql=CKPCaqm<%Z z(`ehd55Cv)ri4J7^&eq)HmAZh)#5?V+Jfwc9Rw!w5o50rD>YCxkekfB7ZWByJsfTh zyrWQKRk)IYK&l3CLEcXG9|QJ at RxYJPVNrQk$e at 0j^$;d-L~t&|ss; zPBac7!=U6Nj~!=nx8Hs2eagZalsUMtBa9M3%Dq1PJjm=x<~b!N_BGxHlPu$cJ2GMq ziH}2fgMQG~YgUIFB&RRul8}k5&6sM5 zp!}1Y!NQ`B+(1A)3H68#+ck~$`?)~$r0HofTEW=GuO(g=*N|d1H04+Ziw{qCNIT;&PDQVQd zki%STOh7%O8Xzespa&tKIh_(kGZB=BZ*s=26#bU~y8^hLsIPVt$#f5YzSDQVU4je%r{J}l9c zr$1vWYFAF_d|>CIuUrhth_a!*MlT#83R!-^&+#Fgn<3yhuwGSW!9pPvheOfKL&*TY zMTzG9Z$!HnEbyPO?pebof549iQ>RnmgLY`?;K9@!^CM$Oit=nq(EoQwU%JEzzun2L zb}V5J>R3O at Bq4Tk;m1+PDo-&X!(xXy^XqjzhV|ROsmyE1_A3eg#@C?*Jr5r`Nh zXl9a_f+C_}?)YLTe%OkT-x>8*i~v*mOqT;N{0vi5^)O;>tdf61$-8Q-oaJhf&_NIe z;4mQHlUgxX5_|^Ld|Q&4Z5go9qM_qUuHs^+aY=AxQGYz at T6_ zDqb9Zi0qrq6w at Wd7BW2JATxt15h0%n;k1zZX$q0Vk1Pszx)|S$oenWj`39EEfaQPF z<1tI6oYBCtvnMpk at 4?*e!#c`qicq5VoFB)5$O|Am2;A%k%+bJ6^BmcSCXFw0qQej` zG;wJ^#Z6|r_;1mCnoDClA?SBOO4Z%bNWm at VBU;-I&9&K#ox#4Zqk&|=7&gHApy-Ds z`v}=9GqZRa&Ab;6&=m%SGO~c7Ruc;CNg723U^ZKh94#rcNf|lzIJI(g48JrM_|qg< zlT6UgAoKDah$d1{g2Y%0_zV!}Acoy(vtiW2M0Hg1}NnwitNjf;raxWu-|dGY7zBkLkjSTiReQj1YPrrh at lS=+|=vj)ah0k=K5YB0nRk;P(!F zhp7hVnSlC$*zkB9qUUlB85nE`xPk<3 at M9Z1+F{X(RDEn>R+>?}jw*qXoTiDXl4C^T zRBIfTmq5;QvZt3mU+ivrLu8QQpkks%>_v1Ge`#*AKvGGI%uOcZB(!77Yc?E4z^rnZ z0{aT{jzwT)oPfiQ0 at P5KS|mP=gMj#sEFW^f2XexX&_qRKjANW&Banw-xOfhD3RbmW z&sNBVVgznREK zFFY8R$sFS@(gTT2A&vBaY=>ES=ZVS8eg`SgX9~-KbMzdr4Gd}lF}TEx0-=cqSaXv~ z1VeZN9mYJ{0e^4SP~N at P#+Ppd&ixfrn at rl@wd)UIcY^vo4UHBYHb1`j&WmfxBVVDr z^*HOXwof>S5;3WnH{&}#*uBZ^nUfI{8_nv#=L8#s%)w_IobAWUb)9UmVrLu?6gkNS z$qP=kYgo$r^P>6bn%LLht(8&Sw{2gqNbSC-vhHu&4G)gD$2B+4F>_H}ne|d>Q4$N- zLS{(n7z9LQArWH01yd_6sk0(PmtR>52$X(C6319nQvapHI$*H02MJMp+MtQm_3AY# zgtMATPGZqE^H at __X=WrT20oJG)vzttIH%rPY8>k~o)gkfy%t?rs;h~hpe_$Mdi-(r zf)L(omxGr5f6$Q+Ci3uibS;Ax3zvZkQy~e?1%Ww(NUk>8rc*OAAz`DSnb=w?b~x-t z3M}LR?YweiNsnaHI23&oPq{l;QNMB;>3IH#1<79M1WPtJbt>YGgo!=#^r}^2i8bRVnF_~&CG|T2H?M3O*G(w4N>X+R0rPQMop6$beM1B zXm at xC2nQJ%FheQvkJxB{wlr+o`pomtP3ZAuS??VNNs3PSV#cVcqUs?Nl!WvM7NNEjjW3Z37r%b32 z_R;IVucexMoei4yUUTVkb*HH;=I*Qyw!OZ1O0hEuvhpLI>*6gV>kjh=g4x#vd<~Jt z{(I;^%p?-8Yy`|1G;3PTqpg8;feIN?ag4gwwJP4K)ipg5lq{(gxS}lzRx@ zYuZ?pScBjk7Htke92Ovpl1Z at UjF~8+!6GhTMtCUEk;@3;B+48^(Bl{oI56eJK-wJs ztLI~O-xo3=7B0=b9{D&SC!^=HR~C&~aL=M5gM+AD2*4QdF z?SaI{gxX at G{?YMFVxHr;3&th!u^Jm){R+bPDw=YraGo{46crC*90XA84w7#a;rDPG zokVhbcsB5Id6*#FH~|RJat;{fXhA9%N*J-mVPuFIC-Zyyj(M`))I!~yFs>kT1ReBH zaw9E92sh+^RLjqNJH5s*AP|8TF4SbBlbOL`@~{^W*b#tpM$Qr-%O2M+i7RP3QDQRY z?XGyLF4kt+u5wt2+fx1XK3v;LVEw}y8>xv>FifN!AsbGkvNLhcfVKHK=Dpaaciv6sAvw8V{Ea4_eU|6BOiO-`3 zl at ze6CZsCKCuerI_p3J*{+i^cYGOjw1u4t;5TNu32Bk3-3wNrLBb$+wg`m=r414Q~ zKfl%X|EuL+eM8Y-Z?_}9^Rj;}Puilq7}}CUqMs?*;N5OjM2N;D%2gzgRkpJFrc4+@ zK*82`z=*U55mA9+rZ1P!4emX0E=-okNLMe`5ns3bnIGN$4xQtHbfFWK3tlAjp$D)Q zej!m=Q!#av+bAB#V*v zq6q3o$~i_yBh?>#X&Xg^=88p&6!MR$- at 0OG@8gGiX%n)i&uv;cr|5~-lbtCC`XBky z3K4K(&9oYJGwj&+L`{T5{{%!}pTkqOA|@yDeczB@ zd7)QID3pm%Der_rf6HwNps(g7K4bk)?@52=Fj~2BLy6gc9+S)5e{dF_>(37pizIaY z`GWA5k%Ws&UOK?r|Q=g^Ra=MqDxc#&uH@&BxR zf{H9B|9EGL`?@FJVP$|<(Qz65dd=jv$I&h9=Z&PL21#0^l0z&qJc%J#Hc1A~U1g3R z59pQThGefKGa~;=GHEUyPw|;sui>5A;?z+o5uwP*@-}KWk&O{P_J=4uXjZnu4K`{K znr1RdCAiL-GQ5 at Bc^w%jihk~f`MEOVO=kp&q{56=vZMR3L>wV60Lrh%C zRYj_?RIs|LFq5oV_u_jU2!~&2!hgj~oT_XGvgrr0-Y-lvk9TKpRF8LzeNB=x zVYW)M$#CYx=^;7EA3uy88_DWxCnuYcF8EjA*Vs+X#u!-`o}y}Xj&fL#`jk%Sk*N5% zDP}LW=~ZH^kmmCp73Rm$q||wSjoTj`+I*v!W8v^sSrR>|Bj`uSWFdkeP^g+w-jNbS zuDw4PAe$snNs!gC*k5R(28G%l4mhCgF%jyczVpczW0xFr36UvEQIMGunK4dsn4~Q^ z0K#BGDdCAK5gLVBQFbFxpClX at LNm5xs{|jT(rQZaddc%Xk0d56lfuK!!DwY4)ghlQ zBKyfmwJFHH!{b*%M%j(V-D6FZOf at Kfou9tYEe0h&&+dR>&VYyG^3qEXV09T9z7O at l zpx2a%u+w9ehP at 9ge|o+|+_6ZUwVskeWTglG-VxJ~Ce(zErPR)Fcp`xz)g)ddJMc*l1H9fP4}4fW!1~yGeSXCe5EZ(dRQK2dPK075xN?f&FlQf&ITb z(0TcwJ&&u{y=PxRy$?I)=NN<98qq%UaiStkyF|L#WanhqPOIFp^2^rNs5n?qMP0EeN z^@3nUg;7dj4)ja>53tetjt+*fROb$oBi4QzeRi%~vRNulO=hsZeKMnwm^M+~FmTs) z?VViX4me at CIR3iRp4Q0Kw^N6PheY~!N!*~y$jMKGS#Tq(hk^^vWr6CjupRg(kBgo(paM8u7ltc2s!mgs{F6Ox^X9?0$a zKK&`&K(5T8^+5cP>IAPu$J?VUB;-g<5|Ih%3KtfG8N6lzmV>0yhASZvgwfO}sJtnF z=t;tjlw}R>jE}iadlbh9Id~9*S{Vzyr5Pc$y(h!T?#VE;vPsg1+B%Q9p`T(~h=6$1 zGf2f4;R0x&|4bthz?dIe;|jV4Kk`JP8vxo3pbI+!)}$=7>Zn>Pa3cA`Q;pi`*=scaC5c9f!Z0a_9n)2eYk z79&=FnP+kz?x}IvJ2{kcY-|eopnSW)$xkRul}}(V!17e<=x)6s!;%s4di!}A)Wd|1zA4rx zubz`hB%F^ahQ#Q1DH~M5Z4odh$a0Etjh_hKedePga%B4EEXie_;KrohJC}edBeWP~ z!sd{TNCF^J&vHmjAq>lnh|fDUUe+ at n#3KA+91K(U?Bf|+>hYllW!3T9 at kT>a(mPFZ zhq;iRwv+2vhNoEI;yTA52wai!b~a7NQguLgK(EhG@}Hv!+N0lp#(%8+x{>WpZOusz zwGz(8CMJOagB|1}|3qknGxWcdA at 7H+l>T6@<(e=YO8Xz{~!IG8-`;oSHWg9bks%Mh=Gs8=rOtx#izn>m|vUT`BQuH>0Ia|ABt8!8AM#%>dbFy+E5<$#TAMp(X7kE=&o(S~3oK+eb`q*5%H z?{#Ws41;d$SQ^;xA%j<9396QjLeD1l+O->V-PmC*6q_-03_G2tg{IOQ5Pxv}k<>|UBdeVZ4ot040C07N*{)!51%z^(OKb<|;dQhi z;6{NQha%p!n%QtiH>wgTvCNS$Vx;trk?4BelU>>D&!sv10Q at h?XxSft{UMwRiNS-t zB8iraKa`Ezhc>9cFR at iS&OM3VkDM##p%2P|d|?1J^d7~Q1cPsjEIqXPs^O+Hb7j4> zyVeumyA^6u6r4aGUJ!9G at e!@r at 9fBzQVx`XH!KMP;Mj8_EEKI^$RnvR8nr8vR*0h`= zZ%I{+3 at L(0s#Gn6-L^;nFoVIFW2OK9JZPBQ*viQtX zPmx)eRfbIv>*Jom&yl#a8%6^`0~j!&K&5NuR<6+4H4PURN+d)`q;4t&(&+Tjass|puM~I?QhYDeI$Pnwv8el&sS})X+H=D$x-r=@hpf` zk3WH$l=3~IC!S8M!CFEf3ROcdnh^ub7uQfjhH{FKHi7_Q at x`s^DWR>d0B|m#KcB5Q znibHMrv$Vu9KX|RrQ2JAzc^tg!-U~b*=sQ&m4i|4g_6iH{ z-i+;pN8x&3yp4pBe51g_9s}4fib}Fd5eW0nF|9a?c#hZtLM_<=0SrL zOrs%5P&6=s0*PX{ASo=8G~_9NK}p{rC8S~qg$QDB zxW*bZW at r3#(@bL<61_bvvDi;b&~W*x8^>+j{cedK?un2rEQk#xouSpFHv7*H%l^u^ zial>gPall+s9vQ(xcQ!}j#7!1L-0|SKKf-R^vEIlNe599BkSPnq}+Y4fu8+n&3NWl z)M at R}pz>1 at wP?5DAB#=NTS-CE@=>UC_+P=WhYKd_(Pl-Mzp(EBk!B?DoV`kh*y2Dm z`%@Em?vIGt3G%Obh#{u|<$Mx4KJ*|-?78)r9 at khXo&+bEB?(0h1gY;?7Nqtog?{Cp z?ql)34io-P4MSPcA`O$VE_%3WV1_{7$2>51GFs{@%UWqB32O3@$aOLNs zTN&E)jtE_i%-|%Z(Yd;EiSX=^J1hsP5I&NbAP_MZ z at 7|wTMX-8*QkcqMX$Wt}Oy7h~;Y at oAGQ$|M1SFFHtr$4Z2~wsh zfAHNRMMfmYRx3PXfu$GfhXaS_k?NrN7`~7%VfuX|sGBk}SR?Ywl6;}T^JdMTW|B+Y#)FAq-S{W*O`1#kcbFxv zEU^Wj^O|_q$iwe7K619_9+=2ksd~GX28$u%#ii{rGFViuzNIUipLFx&@-`!h_DLo6 zVH7-C4_NdcxGO=Vij2ZDc9s6Pa(IG5r}aV59ODwHDP3r?65I{|C7O}AxYFoLy(Dml zyizP548~zLnwXL$`yoC12fZZty?yiPyPr(QCdwGmIg{NXu5v3sz*C|3j7HgT{XG%W z&ROEDB=7AYk_*X1B;%kLlwQ}7nB z^acUotRc81Pd5ILUydY$$GhrD8eRwM(L1b-PS&uUbg**+x8bO5!O{@;f_eEWa$Q~~ zAo-mjdx}S}=hFIfoR0(Pc3x+UJ=9MZH*$SaBe>{Bn#0yBOyU(n#MrDxPD0W{1Ez8A zWcEXh8z&(mIRY5bhXaQ98%$;^0)_#DRDsDgVrlUft3pQVL;=9^mGUV0Gs9Uzg)2gb zws3%@7vf7xC-e=uDE=WGBikFF+(Gmdk^5>kv$YvV*ylbFN3@?>e%U`((oR8~kE7O~ z$?Y{7x~Z`?DovPbOZ3T=a%Xd7&7qsfiSm!1va$ZizQ zsWXwIA43{3!Q_Z|^3k!(nG*!+mC%+9cQDr^!+Nc8rnQ+-`mE$`X9QcZ0W8Oh at C^-D z!eNW>=0tKOfxzTPb&cD(@?D+kVY`tUj!~${v2!riBqWOs$n2o*z5Q|rToOg*LP(_& zEGtfOCWP-RQg5 at h>$E03vE{456r?ItHtcTh-+0o_6jrcXxl>J>a}^3-sQvLm6qpOt zp9VCdG!~^(21nj=Y}%`v9o!l1S^A?#soj>^{OM&+=|5_uoV1%wBl?qsPs;pjkd#=H zbJUNNM4sTB(&cG^6=XsKMevqs2 at _AatJ2KOxeioMa*bOd&D5 zWe4aSQO>u`YM}o8E+%hXAmaIl5zo_AR42+p8Gd)vSboBSpRn8;IT~=u205b{ zMO9JqeQ}x_JBWjmCaR-CV4ca2Ml_pViaBkwgu at jf?w!ZU(edjT`UsffM^a+QBt0KL zW3k^HYU)i9^<*ahH504ngI3UX#w2Gry8oOL5-?9(kcP~L4}nej?%;;m=Q9Q z3^e^_z;<8(n|w~wzouCxLH0h<;FOtxZs6$9{@?>NW z*)}I~4;1->)$5+Kx5mCyE2Wje_{VA{55$-QhX%BW{0;4OB-FD!iKM`tkHa*Zct?OG zO{OLpqHPexl4vqXq-f!#xgf^FVn}0wyflP)C++?Qw7-m%C|A%gfvf~Q at D!gWe^D9@ z!Gx5w?)gYlQh`8$LrCC9_!)ush(kT8_EDuWnL})#%A;Rh zPLvfH!AhQ?m|0bynNZ6P%qGn~I%zF`ZtrYYKU}Y;>^u5J?1;kx;=s%oqP;;9`!LfV z*?fr%%&bW(u=_bK?JStz?b=AOU7)g+}@lC%;pU{R10N7>!5ujC`Pj9Nb+dhjwPL!Dr}%qu3N zuTsUGoCuOh`@2$@@;;EsD79cVchyagnjXX490YghDj%BVeWN% zzb5 at 8ntV(lr=gIhdPqocd*YB~sz%wpZO*7>=yP}#M9v~f#FH2ocMyLY-hsG>?_p~B3?Gc#6$P+5}1j(z9J!BmN3aq-e|!Emz4%BseGC#g&( zxVw~9QjKo}F0DV6$YlNkSW-wyGc||fbAWBq#2Ygzf;MO{bj5*&GZ0fjONV#a+dMy2 z^lyC!9K&t#+um1Qt2VpN9HvdSwuQtZbL>x#Rt=R;Y8m8e6qFoHXpLc*;#9KP7;41X zf;~?m+rW^nWF-*OK)W5PCUX){R5%Lz)i<17$B!NQL3k8a2enHOP(+|appy!;)RfJ^ob70oTo-`O1umd#ZR~nPs}D8rS=P4t$>|;xr%~B32WFKy at BTAe)Q$#QB6$fk3Uu2+l z3o5Frs;aAMaDaM``p$|tN1thTXqz2X8}F=*qe1Bi;v1P8Vo-JYsrYwb{7SEuN$OLI zZ(CRw3~6)rd_q0VdH$qdoHIszH(6rKH}aY$fyp=DO!7f&J&LoTYkob at 1FO9q)LC0(k_B)!+xep97+(d><(u7)KHV>zzs(NJCbF1laE* zg>ceDlf|UyXoVO-uM34wa|O=BLT}T%*knT;8;Yy;_Ka zFbZS@>aKQ6WsaTMxBRc;wpTO?isBed$6KwdzVu)u(|fR|K|%#xw9f;0{qO_%rI zO6fQLA1CI=~utlv3_ z>kbh|<#6S~)X9?*bm)SP5xHd6uoV? zz;`}#N(>Lx)TCVbv*td4*3H6xvK|N+g**0pOcc-1V13EKQ?lghnWf<1h;Lh(Lzo30 z6$+zA;3L1)f?uPr2UIaO2(67MWa at y=B(4}1LJ`zYY2ENpB!g1}C_$1UN5%XH-G&C@ zJ-hwK0T~0bQ?sI;hpklT z zdclXmclCCcOQ?R}VVRSqh2l9%NP#hfyEx!Bj|<^Wcu_#<7K-(H-%?9Z#hmaQke2=%>j@^s(f6ib|yvpYSz4{nr_pVvo?4 zPnk!mT0v-9UH~UR*uT=-tFigH9jo)4HV}P>A8ELIH}gX^Dk{W14Ck^|nEgX8WsswyU_o@|Z5ag!f{Yrs#@AbtJF@{pd%JY>_6 z>A;f1h?_|j9eq6xM?P*>dwIASB01)5kI~Cn&;_Db=B-z2qJ}}aYGZ;06CfFofnZ+W zVe0QLz;u_X-1S8_c3rv0+@&lMHAyuhH;-M|({Z at wyXW=%`1e z6i&|XgAwz!)3 at +=L!9j$TsV4{vcEE%NO-?Za*njJ%LK$w@;yW#uW{}dTQ;wvr at OCE z;0K<@g~^dOn`k98!hb1S9ZsjG7 zZ4h at vd1uMQfx|?EN_yw6lscLm2~hZZx5*ta`LWQ2>Me&s3D%(44C19OGBlJ at LzLS?%d!(hu8 at 4 zhD^?%hzE^9tSCkpVlj?ePt(ICua$`7jl3H|lvMx4t$LhgL2!)d%qjs^Q-fA;aN}!4 z6e|-lRoQG1bB|oeo~m-A!Z3G~^&T<5K4rzY$4L^+=H#7gIBaKegM0-8ZF(?I#qvDC zORQLltTeFeBK*9*f2X0-Ex{NWyYNUuI|xOw{-Og6nildyTxr3XgfDQcttJMCZF%Ft z0##R=k4-i$Dpge?iqCJorw?LK9i-!Fy1An=Nl~WOa?)d^jOootnWwRr$Yxv}xK{AV zjHdQM11>@iLIDKAQ7;Mr*eIdiIE_frM_DC}+N)NsF_xVAdv at ABo(x4TsOLQO8tpSB z4#_r_Fl$jU5jAEn@$fq|2X#lsF)P!Sx#qn)=YbevmKJ0kwS~0{RmA1mV|M69ge zXyy)Y(;ufZM0LGWa!6+!PX}9&Ilxe--H;+A)~oaKl at bZ65>TNSq5{-N5~_p;4J7FE zKMhYNog!9(nF%n&nJe7q8*E0%sFfU=-(os(({}5+X<~fVKFyx~_BhDg(K$zB8ojl) zbWTNw$eS7MR!qM%^6zpzX!k0sz&gpjN0ypB-lcYMZ$$=pDue`{_oMHRr?)Z2hk~po zVYt4;=R8%vKQ7aF-cP-zF0uKDXmgrDe#RtDmCh^%`I0A{6I|*1N$xw(Svpklh>s$5 zks~_QzH0(wyns28(Gdj65=>N&YJROdHpR?FeXv|czbg(Rm&G^rM+fn*4$%KPyNLB} zQ`<%?kIeNWV^NUIn1o9}vYZ;jNidy4^gzH4qr?YT4qlLyfPEsS_!CO6Z<}HN75op; zhvX&qvlBy}7?n>j>+&rffORj`xHs^ya;l+zF=e`fd;sA53+ur*?)2lYLDT0BB8P-6 z3)bPuQ%XFZ&R8|~i9y`2nQYua!&qBgv6rh{)I|3&s*PD{r;@XfgyGEi~v( zP6Ee~tq!1->AcB?i>@`B=}`!q1>+9Mps!vI#9x;qRh^J zNj5q5FL7Py|JQrW+H-c3__7XwBoj`@9oWAwbG$k6Nhn|n2=it`Pa~RX69!1MGZ2Op zMxUq`8iK%;a`(xG4hkf96E&Q}qTMwY)RqOn at UVe>R77P2IEPDY)!WF2DI;^`A0M>f z(T1t>#XJYzA$4-+hyeM3!Nq)3CF%SGxSR|hobWcUW<+|VUj;eout!QvtY+V3 at 47pp znupxm8!tCbV?Nf#q?0{tTloB3o({>sN1R^Z at 8sS|qD1biWE7|ev4ISG2c)KX23O#_ z?3^NWIuDsCH7a$n(tBi#hOu6*`0=B;JFz?6?#{5UhE$#S_qDZ47=|RB%e1}dcZa at 4 z`X5Wc_0qb at 9edH-7v8}1oiHD~@J0p?G{p>viD4W_2i at _l=jYG6%!@5cN^DL62p=h1rHWk|v_-11+gdMeH*2i$u${3`lke6u^fsm#r>!iZfL z27vdBLe|SIQHAhIjazX;8L9 zkFO+?C_1bdntm-#eD|EVHySz-_MC_L{iCu`<$eMOltU9hFAV0F<^|bE6GVjsjH$Az zoU)8XF8R42EBs-Xlz#9zqD#ad=8(;!XZ(>9b zu~TOh9%}G at oIH1)!;d{kpBWhpgfiyE0J>DtTl+3+8FDsc`m+ZlX)cMS_;mTL0Cx<% zS788qWcUUH3RE)5G at C!CxA`h3tnP97VG>PZvIFVQ+ljjC*E`jhmhuZPEnD4!!KG4IwN<-9IjUsdk9FW+M;DYqYn z5j77M6I(Wbd}bR!f2tCNViovLyW814YX>iH`X8ft`=s~5^AC8Wl#?(#y4tJ^m6Gh%Q<^81lkJs?QyY(|wthjNFDv%KJ6rw{$3-c0- z^JbAmy8DE_zq~$~4%s(2kn6*QkUf%ox1A>vCaGQ_#Y2ksKp)!$BKDu$e<1naNC()z znC}E4l2Tx$a2IEb-w(I_gB}5iKkWu%r}g(J*l8Ji6h4lw`&qFs>h&Va7-R>nkQul} z2oJ5pFvbIW(%90K#gzDhHGjLaS|UHc3hn^suX{R~WP7JTm!+~{h*}NN;qZ7Dqeqoc zR=0fPs~~9OBhf^Ii9fV+`VKiy^I(&>e#Ry1L#$VrX2C%!ZYWvf{uY1X`dSc;C(g-z zT~+BQd~#I#=gw~OzqS}p0L()p0XYb^fT0+eH}f9;X^*Z$z;InVf1i0=u{@24m$~1*5W(- zz4;x_ at hYKgZ+m!UPsd+{b%hNe9oM&t30)#c(%X1)K=XIGR~QVIgPVQO6nu_jv%HPn zhM4%I|7z8jQ(zkegdhbEEA1VDqYY6cg at 9I7L~QvCGDj9Czu*4w_u#ou#g)2bgbTyV zM?JiuFC(@7x~5|6$ZLE)fMQ4xO^$xaHE?|DTu znWFP#eM3knS#l2I#4|Q#bo5Zb$+ZlT^nP^%Vt`~KFZ~iAzqBGDN>ScpNRb1UrA;s+ z@&3d6e*J;L5YoIXOh0+}!}Ma1}>1kn-#Sz17!Kg#zp2*?fWN6f_amP=|7QQ@ z)Z;)ico%U4+P|+!z9?+%Pn6>|NFEm`roKYdcFOE0;zI&S^GajP9SESh* z=>Lqr4S}06?#hDFzn*LpPC z)HK`v^a4;AeujEfbN!eqlI4S;$HkJ}5je3^7u0?To`;e*?1iYa=YK9aa=rM(yr}{JTE++a4m?1pSev>m z$&n&v;mPIhrr8V_ou^vsPS+Q@%4N!Ze4VYYbFZ3Z0` zmNY!#em0vA%5^$PQ~og+2a7VUlHwHqv($O~jM{@$0--VwdB{DZ3g(e|8k0k2(8KXq z#@6dkd9plFN03ki1Qa18Y!7oKv6s(B16r>jiRyw6Z^ghxqF at T_YTznCq*BFlHJQ25 z^apJFDJwrf&QK|qljIj=rE)vyD=~f6lMkf{|LTPSJ!1cY7wU zgon&97&InhA&mwOTsY>6*lixhi8L|9Ybj#^%8oRh$cJCDtD64E^LtH|IYrFRZmZk{L`*iO(cDn_dI at 2DxO@|~6p@=NR>D9$51Awnn?^8fNEC$BZo z2w4y696sj%B})TO`|OP*sC_2>rGF@;_%ZQ!fH&dMY#`sEkM)DIGdnMw`T)51QU|xl z_r!!Dn{QtgK?+}mDRE zx>ECqU%Y;+2U_HgfshQzDSIREg#wU+;=sasngLS`hN$c*=^m*QOp%Y7!WE=4QcA*7 z5g7leBwBvB&CmK%kyrcserBBmD%bV{j^2A4?;*hslOZ3FiWDU$QSl~6GLR>VY6p-v z=b()&PEG~>(3rs#w1Ij>H0Ni%nYf*s+S&TaeOnw-3%=o(ZA+jCTh0a2qdeFzaSwO*Z=*tyG-an(Rd-$B8^ad)x!pHG#!HQ{-p&J6uuy{*9_{ju zIM_Tfg|MkM-cCV=FNllf4Ww$T4i#(lY)9!S{++*lyt28>tOgSu$s{;934s1}k^EBr zyf6>+>>9KrD+lpW)MKkeI3Waru%r687`gXD6Q&%NSN`GDVBOF274g*7z zfi5XMI%1I@%6?J*rV#t%{%`s*@q~LqcZ`<-kPbD2{}g+-p3)=jNo)oJ;5wJ)6G8cZ4@~H# zwXD@*|AhKq=99IZ9!6<(pWaXO9pV1=MJ8mkC7u4n&zR7^8-w=)|BzS%2$3{kM*y%e zUs^c5!WuesKvRZZRek;^&w<;FP#Vwi9o2 at Gj4(tPsY(?`aKWOM(|TPGA$R`>+6viUQ9Q^F?oSB*N%$IUuvgC_WPI6{A z@;qFHcnonWPgk!)otnbIg!4o{GDu9ou at fh^QUgJtB|4qm-NqTb2EQAhwg&!lB;+d1 zcF6(d#~jOHNIlu1lYVJBIFT_{3$u)H6RSgWCI*rE&U+W{F&^m!9qBf(wsUiy?v2wX zCPyP2+o;c zszo*{mFfwTEXSyrK$1;?b|1zV|E#H*4)#gk*!p_=JmW6ouU(a9gDr7H%bt9(r_qUyzaZM}hjPHQ$;sGb5k`Vvs&yOepM7N(jjg!E#~Au$WrzCPpZ% zE)FzowGh9?fG|;772;)Au+rb>Ky#vJo-3Are$`?ZYG_39;*E=o!!c*IFd(mUs)SRTg^|DRnCtX zprVST!5ut<&MD9u+#bD+N;bO|rJf3RS03IHn)dvm;Ati7=}vok^j*6|?hSSKr&hTG z)(_^IKyJ#Pi>x^sI at K4Nb|+#%hhB)PJM%Pf{)F|-Nbz~F?y|IJeLG4!|HIaL<8$PU zI?59pZtq9NV)ie2!uu0>1Be_(68DYbSjePIGE8edeN~lJ1WhUs6~wPSlU>?>2$vRA z-Ibx;l1cjC!Q9epvX%=t$@^Ya{ma*C^G;0?J%v7xTBVeUj2!|?>>V`+^@t#ONeqzg z=6@g4`KUQyNnO`=fbH>+4nzE8ZT#+ zV#tt7cB)LaTT|lez-$Z;QM>{563iqzq0!TlMsu5PikObp9AT5kL~sef^Sm^h at m5_N14n# z2v1|iYA5 at ZJvaY$XICBYpF8IBQBkx8;EskK^W1DZ!)Lf;RD&2q%ppc>(Yj;rW3J$9)RvBv$719$w+%uJ4Z?z8OK!TwhhM@?`AyB-Y_DsVw5(DvvoRKfc`ki{t z6`-e9%)@xmX3WO35V at wCHpVB>rGWgcqrn}*$NOGqQx#Z7g;q)6#!;&xk+G}`s7xtJ z6v?&FT(h-p8yEhj||F(OK<^mr0xYul{%Yz6i!E9 za&&Zh6^>&N9b%n_L`qFSRWTly4gHHA@}0Z<7sQ0 at R4l?|MiA#GADZl}tC=(59=#;t z4dm?c7F2!bSdg at Ptx@AVNjf`DgHB%qZk(FB&2S!~bq_iA(LXkfX>uJ!WN_)1ffV)C zRDfDy%9mF1pNe463dfg^*)ygd9w1wtDan zSIf at rwCpcx@Occoy}D_m`fk$3yi!iWc(FRdI1QweK==v3D)Ut|G_=tY`%F87!@Mt6 zBqX8o3iPzM9Ur}WSHIptr at 3(!md!Xkz%PgDx z_AKg9(XsJI(LQ5m_~h&8omsFs3Cx~C)sn#4 at I4Tc1q&wDoSQFuf#)5>CY_+0dSERL2r?qrrn9 zu*J+VF0ogd8UTJUk|Uo9;&nrC;baJPC40f(eP}(JBsV-&&B}GBHp+!r9vFHq&M3Oh zXnh8aVFMJiLn_CZAT%*gKxI_AGe7c~29qE6L?7l2oMm-vNp+kY$3P zL8D<8v)}CL-*{+S^!E*26A-*Xn1Jbvqs$_RO0Mv* zC?W`gVhZjJwUrcnzPvRN5C;=}r68Gw8COf-9UL3dJ)%%#XCt%S1kh~ts&z`#o4#Y* ztYe!~O)C+mn5CMs(-j(NhLut&jVnfzNu^SiN{LArL8h8knp~2jTGLLoQ`imy36Kgk z7_2C!6s0Jp6qKbZGO|EW;RWUVH6%|!PRWKodd%#Ev4jog}Z_d@?ISB6eYTvKAI?2i+ zabkrnh_sYaic2B(4j|b!s;`3gg?gg}Vj=*%f%fn`iQgPO>ExoS+N!Xsid9rb28 z01uUpe5!&O^B3E{NV-N5d0L9p8-lRjIchgMyiW7H?HQF3ha)wkLXm+r>ZhUoI6geX%amorfkU-S%*WiX4O at azYJo<2sQMzT=C1LaNiopu4y= z$IUz*4}ArraPn=h+&VM$Cb at L*=MFJ{EOI)3XnURoAL_ey6OsCkCW3>Nlil)QZi+ZB zyrFWkAXRl`M)qsVt?o}x)4lDwtSHNIfrS<{)uCp^;@ ziB!v!PLOwdk0~M}7n1`dp#;ExaLHVTIU_AL=M84JEl0TTsT$6MQ8nu-FvPLWRyTau zZ0~H{NzI7&)O)@?$*=ahofdVT`8;Ac89q%WMP#uyDT at WvM4Fb6CIBdEWL+VtRxA|1 z3Yu at T4H_G(zhs}iOD7SPzF^|8X=4_}Y`JAsTD5D|xMD&H_(Oy&5fVK~D-w*1M>+A) zy38I4M4hA_2X_OAuuH}*Mt320ypE;?#xXvrVFXlvZV4IzDA1V3A-lm4a5G5;P=ZJIY{Ej at TC-5HRUvCA=v)-pVx~fNiylud95YFA znwX~G&DO&V*AyM&0-j1D$n+y-<=Q-f1YLBPLU2j{8CDWZbGJ+yT^O`sfq{f%W*Ea# z8G(r4Xq;fvhF0SVJPdL}aaoBKVwa5bnvd*=Q*|5r<&UACcjwQNUTA+T=YhQ-R*O~_ zd1G#)y at 1f6!G#_Sg8hoPYOd7{R4{lA<6%7C+X0+%W%b zY7a*JyD=s!w?rp~pKP8Od&uYPoDa1668g at swxM0JwAhGH7>{SMq3aKA__VbKVv6=j z*_)FKAoxO6bC)fpDd(sM>I*=T>oZkoeUA&|#LFs==OcvVr1Y^I&KW?X5JcL_*Mi6Wf39Dyb~ z{Az-Pfnlu4Bq at p_h^Z);s%9c8qc%q5f-^DLY2Av3m5>QS5q1)82*XUBoOhtca~x78 zn2?E~h#XKb*^CxwFW;>gXobS{d*lc1lk$V~AbzRNhsrObyfgG z*rKb~nUXMVshOFQFm0)sg5tzr#9I`X^5H>#b{J4>lg567OO5WUZB|!RmDO#;V3rgx z8CABcZ56RmrAkt(Z51g>t+iWfw$QRetd`UiC{$Z5R$8sKRaI29w6wKc$`%nwF at zF? z3RPvQ+eh|X8z`f0<wxLuo!V#&lGB!EF`^A-c9M>k1*X%7&T>aIMZ-7ewzbXEO{!n& zVi3mk!E9Rtb`<3coNF|M6Oj?OrUvtS-p$HV(qL?591e!eI5)@sOdV9|k&%Jy)eE_) z3?4PtR-vxP=_*NV6TyI}KqyaK!RiuY9U`nT$V at VOq%|Rf10dja84(*qMTm>x!Kk%0 z3Z8g*e`6eqVfqRer4e8ji_rjRk0sAeh+fA{=1IDnl{M z)_ET>^q=JUaLGa>q at cc|Gvzm%6k&(lCJH}%|4a+rAgv+^wGSRZIaFChNwLIMKK}9W zdA$7ZRwG6(7 at iWiGQ z0%IX2U{jI>T$c%PhLsYMkxYHE>^6znysDR2!wK-%*MH%1(K00QY=wbrf4Jp zhzSX3SRg~Z?KFNi-lK;txPZ2A;Q!1S(+hX84hE`)OrlXQ<6a#zPRD7 z)W(7gV`W>yfU8BgCq$sb5TO%lYKvhq<&lLpkkUxg(I42aZ}#Ocr_MhQJU)X)j3B|# zenUf{{=90k+Nk*rjvwZDBq!96_fkmo)QRgxdvKz%iDJoV^NgRI*y0))pr|IHDq26Z zkjMfc(+Xz{d^8GVB0&g*J0%m>m~jluKF^G4^la{a&tv19i49RA*(8%yGs^?c60oP( z8o=#2NCGY56JVcBHdmaSJV{}_5aqn%ozWp9gKP;jqAemr<{--&)Oxh)jTy!x>cg)K zq0z(!)GC1$r^ziVMHDD3B7;M>%M3VVMeZXQ3o#+IiqvAPBZEZiV&|!KzJ_`S2U*jN zh~|`3mJV!8>Y9-5 zR^u?Nu?_29WSdyvfFr4n6)DVO?-mjXC6ZoJQA31*)TW9}Vw)-1WQo0i$dwx at 5yDAH zK6l*v`R2fS(-j1iDM3gPI`3Yc&W^`lOujBVc`O44?UuS@8(xTGFo(nDkY&wX;Knd%XtOm;G8qV}ZA7>l3W1Ly zj0iR{@tKkLa&6Y*t7y<_KXimV9W?Fq$+l!BBB zCKLOKij*)WfTdy;+<=s#plTQ1p(do2A_|UOeRrv=hMtYP;(sHrKj|Rb`l5lDh=7VF zRQmD2lbf>!;4^eDa6WDezv_UPN(n@|5Btyj!_v}*pEr#WF(Ni=R6+Bw){+r`#6g&t z;Jj^jqc%c03+E0a0YkuAmCuW5$V`^v1CX!+8R3*Q1j)H^3L{~lz%>2_9BjlGSrTr?v{y_A`Xh2pU8Ziy(JT$ zAbp!op6}#2M_Ed=l_Jsm)ad5+-h9$~P9fRXIO`4`Bi at t~&G?aqs;;DLP3;0b2I3=J z1W^sfZbN-=b}NJ<6_bGMi42m;tCTBmT at dVj6RDke4m1*v3iTe&y2INj|HE0e#azZ) zQhaF{Q}^w{W72R_We9mkWBm1l)2eu9h(!(qkRF#b}Gf?85Pz))4e at JzHo*3<^9CE7(;-4KZMg7Fe49p zxuz-lQ$*NmS9CmH!trk|PR%QaI%4NeYn#;x%SM>EX`& zLpH=R7>}qm`L(dFWkWNuOjba!eV zPdTd0gu^JP$PCp68iV`TiXkwfT(E$N+lB?0hjpS_N=r~;VSN#;8H1nLG4o-t4GYbc zdyF|k_9>=dSo~M}A$i@|Q#1oX at L4Y at 2%h(eJuKu>W6DpIB& zi#!jI at A2M;fGLi}Vqp9D(Jp*V8#RnVJ~!~k>4J8HV9v)tkEwteAS5MNqL`8g2ll~4 z*rR(<{$Ih)8a6JydoP!&nI5zLS!&6;w*)OSKomg4AL7hXQHXD({?vglSUxcm?3{#8 zu0CzZVgu7D`Q*_n&SI2+dZ6ZAKrW*pcOWK1kReK)r$Z=jcmfAR at ANxi=c?40r?py= zqjqNO8`c!cD$I}=!YKzLV;Y2mh;5RDMIbRVEn|VNj~8>I1~MVTGYm;u3`kr`Vkol* zKE`#%%>l1W42m?!;Z{o%;_SI`A#Dq at tTDypcJN~wfW zNcoH- at tP)#GD)#fu%eVwl)Euih(R=_6lsPuq-IGgPTVHV##=s~8NzqY|Pjl_F)7CQ&qDtZ*~=qXh-dVjmwJmu&|FQdYWsr6AYVIRrah~7_mUx2b%^M8pa%y8P}vUBn1a%S0G!D{-wNy?K*YoW5k?XlEFpBV1`yyEnGO;SP at U2< zISxW8nnUF4r?I1xh5H2XY2u?%ScQh+hEG}8fua%Ou%8cX`aCfEHL07C+segMwhE$8 zWTD~_CF{{|*jVyNZhQz_U|)%#$7|bE2FpwFSon!s^);-P;*~ zLa9lN)Z=OK5(zeE+nQsjbm1Bjsy*^aS3Df+1~e6dQ8A_lGD5O!!|g8zG6y;?*(Wfg z3=k*I!L|{hk=57^4WMWPvN=Zw257XnbUBE*gtkZQOcSC>9}1A_qq4+8Jm>vg5`R_n zn~3!YCm2LR1eQu6eph3)8cyEWQF=&{+z&l-x4M;5<`i1lwrFF;vb1kflkx5h*w>p> zp{k}P;hDe57Q;rZ33hBPM>-LPlKz8>Jd$iv3wD at W14R2C9{kt5Ld7UVY3W-9fOawwId*1I@ ze4q$8KS_7sr|gCFf`Qlb;)Y5g#G}Lx zFhC;Q1R9r+L%oNw`TV=Kli2gAMrA%V-|qXJ?phb#WPH=68EvrAVI2M*504y&60ClE z(uwlSk7Z0!(e%p@=wRe|EaFDYb*+-)8Wy34{M(yYK{}R$- at -elE*D`h(rn~0oSb6r z0NK1;A&DP`|2Oj=QRoTw_WMM`_u`nAME}-Uh^l2zx_ge=G^9EuUbpUJqYwDU{N#SU z%Jo%G)OH at l2?=G9Q>~mVEWa zTHeUj9{<|p`xDW&>02EM(Z0O{722lLCu9O9N`{R1p9k#Fc$#{Qdy(cq2c2)}`3_n~ z!d4QQhzbBA1W6@{2$2MqN(w}YDJe-IQ at f8x^-56?9<0Z{QD_9oQihcd=@l at KSHBM4 ze0O+pyz9%KKSoUm_gG6{=R|v at iD*8rZR#Dh5kYMRUlMRUgQ6$UNbDWPOyB(rAm=wh zv0*rs9y;vI_wt-NB6R5xF*E%q-`BDl3SgBApkksbk*gy#Nprd$D0PFO#}?UZzTT4T zZZO^8 zBjtC-%8YXa7sfG3K~(*QdV}h)(vavNi%AE_%xn6#F5mnlM7_qb;R;9$Gb4B_=ud$9 zcPOHG4-Nzb91et1#dgGej=#K>pn_A)up$YOokJ{dp1p?p+CLD(q#woSHbr}(UqqJq)An8x!;{LI3)5$!V* z{92834lEz^kL#4%MzzjFA{W$tE?ia3XkZMK)6-_Pz7iH at W&%hXNZKQ*Q7Xh^6$&x! zix6&9|5$tT`~ib<^Aj8ckh>>=nTHs@=ETQ2$(kL|P=-U2>`Guj zx**iQ4EqfWk`3xd38omSQp(5N#-1N(!KgTkBZVfGO0d1yIjlEPik$4!!THQZIf9hd zMiCvp3xFF5SEQIYa8PPGGGZCj33%>dGSQ2{sM2enV(#M3Wq;Pg*6gJy?}ECqv7cVqz~U zb~p%tT}Fl5nX>`6KBnUe>b$hAwCK~e8r~`-rr4^6EQZ!e9M(Ht*NHjjS6I$n6k`@R zS&!Su?C#ysMoJD8bLZK&J2^ir at 0zRGd5}v+g8V*m(%UfMb6|4L&KEkTG$zR{O%#k# zFqus{Z(@TbIe`D()|m7E%3y7g&}38 at G~Hri13C|=MEAC{Ss#oEl?)UfA*(35+pU>-IqaU6I>J;>j?KZCHn% zgRu#cKBrBl?9);4jl$|1`pK?Ddbic-)SkCfb9Wt(U|oOsV0G>ApK3P#vxWi_0ejrN z9x>|ygUu)Ntzt*iK at ku-DG2Nd#DB;~WF$83?1TsTjg#1t3;Sv47~PKzjO$SD;RnYa z~Fi@!MYz3f^MMFsU- at ep-Qqjb7wu>`}(cq z;Zj#47l|(<JbU`0?E&d;zZKR zNe@$7 at W!4 z;wOpvzR7WP?rhDpAJM*tn$xwi4Pa6jYc#O`i*quXr49e@|FV>P8fvsaVfeC at BNL2c zY!zbQfq4T1Si>;6RVpy57}KJ{1yMzg6~!AwW!=L`m at sJ!VZ5RY5`hdEvcbnnF^e0w z6&xu7NkZ0PjB?aaBqWH9w%QCLtY(>-8w at jNSZs;o4x4nZ$MJf&pRoU^I2_F{E>#j^ zRM}BV$TVvTHA==YG2r3{2Ia|MmRUeBhR-PtFixh^d=t!O2Pdv at Fxb}FrVW&lKk%@J(fu>t z-!{?lJmctuH^6(LLzf)OaL4J+(LsB at C3%nQWr-I835wtl>WG4?i`V}bSvNVoS)Qc( z`MzkSik}lmV(nyOQX*0zFtntS1qt&W5>RDJ zy&;hrliJA*HmzpqQIccE(4 at 5tXjv?9f^Y$`GL9zJq)OxMusau=8+8z)1fpp zG$$~afJ4{6_OGNSCzJ-5`gZ8Jej7VwdWFRmF;=vOQZ}SfC*JI(QpKv|ip7G^yYBsb zPkSb_KGL&hyCmL!W|! zSLiO19#9?w8wui4KN%Pr0!VxlGZUOmz5t8B^32s#RZ$fzurM`I6-8B5RYX-)RYX-( zMO9T*RaI40RaI3)RaI40RaI49ua^ghLDFG at As47H?zBk2y}wv>@yzYm6L(GascBII zk1mZNQiviLa?j;G(RD<_0}`&Cg4;*0P$49cVk$qL7$zDFl3g7He5Yk7l0IM>TtRV@ z0th4sd7*D8XCh^&bWwOp)l8X#vM02DZ4ZGC`WOu8#2h6hp8Ug2m58jxx@%Z!T9I17 z(pHIDj|tt2nKt>qXcFV z5fK_i8;HmZ8X6#oiidM0wgR|_j7B3W>V)E)-7_Va!cm4%#3oIl(FU^=$?L1welLNBqk)}(z?c{v1Hq6vb2JXLDH0$$ zRu$j!5N_}UoyJ1$GXW4}bnr-A>avlyWM(WFk4p|t*knk>CbI$!Sx`ZSDekrjqQv{I z2J##h5<yWVWI}*%JdA*O*#Z!{@fxZ%xVlsV8QM7e!n7B!~h9E_P at xGe)&u%w3p#J7h=d$2nxhx>0eeTx`IkZk(ff?(V#H#1MP>?Fd&OC z9H at pqqw^f3oS70+n5S1b zX2mM9*h0U%LD<03NGTNANDO6cFry_0N*#AZXiAAC|Ia4^Ki9jvSqg5DJHx~9Ho2oG z3w=On_8SntIhU}|yC{8R5JQHz!9HzkTi}frM9{1X``AHrbwff>_WXiwdFXllDdV-Q z>%}m=7IzoV@#DGv at 5`RDrvZmyLScqN!4%&a=Jv3hoF^uQHjC|(i&K~pg-@?E{g{Un zei&nZahsuhY-O~{!nTey{3B5#tHrnR*PV&Vr)7lyIqP<4=@?Yr6QTJ!!AOc!F%pji=kJNcFfG+Tu~qox>%L`X#YgZtc=r4=V!mTwIpr7`r35p+%FSBx_nsO-dS7RFzOo(F{qD)qrCP42h_On^q#y62h?! zpnoJB%%Ha!Bvz8kH;JHhCRk+EGR8 at aZo)9ku_V?88nh5s>(3tZXx at g(lJaB>%E&>h zBC0486b(AoL#428DexvA> ztcIf5a>oY8BItzA*mptg@=t{Kz|{8`jlJ4&UdfXy>W6OA>Z4XlHD`X>(`!6&@1yqf zf_EYOo!iZXPt#-;BC!bola&WF&mKqvg9#-7$)!I5&XjbhvMbIiSEzjN=2^P at 949x6 zxk{n-9QN}hhC2tx#y%d50(9&4LT8c_EjPvzh*LU6F9JI?h6`(pSqI3&k)@3cX$C%9 zV8W0N0r_HsO|yrqA4UXR5;U0qc!^z1GLvK5&|t+IVA_O;=<0JfH90tS9-CD8<%O^M zY|3i?mszAu;x~>!M_LeRm5 at Qp>_VB{jRn^#6}WEE%Vu#l+`5+R)Qd4xZ)yeE$&TSf zAcvsgg1Ee3>L^<#v$EEf*_oBG!8mvj$-_pAF)ftRf=gX2ayB-t1jyT*&Lwm-VzMeY zaGX?|%34)UBIibTmL0e1)Jxn*;TYkMLLH+TAi=^;lxv_cF<6{X!HNtT88kep;d*tQ zqTVXB7nLGSjACh{tis2K1WcUFakP{q$%rt>-j=Y=$Xa*3en487YMs zWlXGD2qUvFSr(c}eA^I+7&X|OD;ncLQE3!4BDyrjHZg(?nlRBMXpD!Ify}mL77baO z5uFF-i+QqcO{Ovo2=_}0LAo?P24(`*lv#swzG-1GwI+fRq(L{ka5|Ja=Xb$?$s8)( z8gUImW?}1)FuFrFpgNn{H?f9{8ApT+U=|Eb1!^UXWSop;V!4vaY+`V&;{U?Ty;RE} zpgQXcRE8cUGfy}MCb2HK#DOY?vg)Xj8o*)G0EY}TW!4&Iqk_6)u&o>zaW3GY9Fca* z5)#`o!64AuRYY1<3FSm!($XFj0Y#|BLNudEr!p47V8XRgOKO!srK^>c(?ip=MyQQ3 zqfBizQEIGs!wxW~b_ooP8l4f)+YUSgLT?Bq$>$j3dd?yt;tdq$4iRZuCfJIBsLT!} z8UQSVOJocRj2ml{{Z^a|-D7ZM21qfpAd{wOC*8dSkdW}3Ls6xT1~^vs<|@vUeJNHo zyCB4BjF~JL!j!_G>NFy)Jp(CKWJsk=T1J$F7{*u>q)nS}an-QihVXfr{AufcfCy*eKA;A|;00G1pTV%G+c6q;0j{Ew4f-aNxthas)WY`YjJWQx_I#S0!ytDmJZNOG_4| zWX9UZkub5;-c at SjJ-Rp&(aKP3kz3B7Xv`%CQL03VpiE5z&?ONS>2Y1IR<1D7`Kn510`iRA4BR) zBZ?%DV11TysO#zIMYJs1q8E=b_bVF}ob3426ndO}c3nIwHX=FV at 5 zAlV2^I-)xSGkld^#5JIBb+Rr3Y&M*_1AMqL;oB%G4k^ig4}7YwlFH0uZT*H$e#cqZ z;#t+XHIK=r>&iBxB>!v-<1pv+473Z97f0^+_Fjg*8*FiMsX%69s*UtagAqYo_l zJ0q$MhcAhexn^oLDb5YVMAkMCT*PBHyb^Tb9l`DgX3u5klJq*Z#`s~E3+-?+GEY&| zgCKsRPAG>aF4h`7l59^?ml)6$HB#Ir!Qc`D5)^DzRuzkC%T%f=$yE?6LN8cD%yrNz zDq^QdCkiP9kAk?1urwpkf;z>DCdFVnCln7uot#RB0V?;f287zNw2;t{>L>u9!@bnH zknti05oI$t-MI32B~oXAH#5{;iiwU3V(=8vTf6IiGwRxGbK4(Mx!>U1hQ7NWAMP;P z$fc6enhXe%EYglNzq(GV=)NBF1B5hOH~tK6#S&qTdsMggjc4Cci5XX**s zdUrc%@zIL+>=<{ckc+$8#FORe-uYVjSLcZ#uvVnpSX{s@?f$gtIs`N0z zI+%4bBqoY@<^z0YnAW1Jd4>_o+u!E*FK8;J;Xl#b3Hga)4F+QsF#$6~Oi)DtWI~lE zzQ at 0>NMP4WARa`?=Pk~{P;-55{|A;ZlNfT0V3;Y1LK{q)gpeuZ|2QlLq(;K(pB zA_Wy_Y$1%sZp_V^ zRXHichM>|r6(VfZT2 at ZR56WzY50B4Iw7#F0xc20fgpy4Hnh7d3h-fjSm*7g9*vk&W z6g4`OsI>}&N&};BW4;PqesKigIY$LMVq zW at GhR&Cj-bxUY@`kAC$if}*jslnO*4eSb;>k{y`X&NB=KWL#vhJUNja1EPab+|cq z#Zxn}OhCby6*U|^AXG5IxEcv~MwrFHaNPyM*>dM8FR1 at l$w@=W9DN6p1G~h6JW=J= zj4*4+^Ksk_K_OxeA*s*LS*<=3&7F~{{bR%+?8;0HINXMeGwcJwX0Ty{AcP{Aw|?F4 zQA)GU4-Sd6Lr7g3$DFg0pdunxbCcM@ zUGx=_;y1ybM~$6_qUb^*HG%vCqwlcM$^ zcTd+hz5Kfy%%{P}xx|W<*Q5=0LoyhWfzEU78VoiW>+z%@r$(K%m27U+&Wcu{NGdkZ zWI_A3qZ$Ml9f6RX-A5N3_iUMAza=A=C9+yd>&XaQ0t?@wluKyZIs<7weE7T*&^m%( zfdT_25qE#31Jc!BcUUNEg}^`{K!MuzmG)eYTP^l=Dem at d?ivo|Kw`7*v3@}1+Arzr zi$x@#)iVv~a&?9dHv>6R`O4I72Jk at ymo7m<5Qht$J1*M=2FxHweuJ>Y5e8Z^x8rk# zXV~)>NC(+Q`-`J>vz|+K&=gh})PluEWM&B&k!|XXhA0ULf-6Qy#FN;PZ8V*eximfO zchyzK`RUh!!N^$Dj$AV|2F(Pc~?7o zkEhlztNReyqD7gRnn)NxZ}3u?1Bnb1js^}kB)JA8nNA^zC+bX3fG139RDvDJn%s}; z79G(>SdcU-WFeUSz9B!m2Erg;JXg?q<@TKD`LP&IFQvrhE+-oH_c4$r6cl5s%H at g5 z&5lk<<*cgyb(>OuVf at PZE7X#DkyF(b{pjo8?>fJd^e_0aZQ@*AFj~wp(erq2&AK-0 ze=o5260o!K5fUCD)8{Gv_rH_L{O!%F{zh_>S9DGcDo+;9teSDl2anAkjk42NScIK_4m6Io%wrzen)eTj;m at wrtFsz1DTO70Fv8ruNM#*`ESSX at a zfNi87B4S4a7jp=Jqf1O#CK%vjtK!Fcp3;|k#Q%kumXkPyhf^#>&)LA`$2dd}yM{n& z8s;Ot+HK0~Gh{IFJYe at J(0pfjP?QjVLjRMz|2OxU9~F%eLSZNxCI5VVTd`)nq;rN5 z?agaFdw~j3)Gwt=NG73)h$(2;tWZ@<3`H=;nE}+l(fU8dry<%5fldtk5A$J(IUa#6 z(rE8I=sJ9_7+29D*$Lw)r;1HX+J1-PeAQMdVyM>3pFGfd{j5a(`1empGv2UXm}oRa z5@~5O69>F>ro#=zK_Re|AhT}ihEro2S3x^imtlq!Bz#4ULkw-|YPJNvpsP?f#P5=qk=|#}gHfx15BzEzc|BWK*oXR-P>C2wAN+4qpqykI5iR_< z_w~jDk4z0KX+hqAv4a7St>AsAlKtz$;2>CGPSF at mNqnu zVFs5nDti^_Sdc0<|pr*fu zJDCePfy6)RaxU~sYBJ0gUu2U8jGLspOAF)Rc?0%A^ODEl8Ytc%Qcp6US ze8onrZzr03O0NF>G2GewK9|;!ufiby)_q(grUvXk$of2=Rn`Oo4FKZnK}VnNPRsb% zxZVK9+cy+uHp0jOLE}Ukqk~HL6h&ibF(dIhQ?p!MXiPZ7#{wcUl7=I+cl5uz{if6X zet8Vn;K+|Iymk^%iy|5f?o4%rY+f}1(J}8JpvL8cV%332!m_)PF|}EFn|op>u5ip^ zV;c?{LBOG)JXu<`RSH4&2-NRxDY&+^steSybEQ?)!%h~iVKlbU*;aUbj|y2` zbhaf?c)Uax3YH$!$HFMc0wM``<7R<6$%^(dg)9{1VZqXRtCtCkoiyR$w6a*#3gvco zTOMt4yYFG$xX78Ln55!(VBc&G>39e^k=q~~oM9xmGB~5H=w4EuYpKi6P|akHYapF( z3<eq^zJpkQgx!i3<>MN)(Nb4;m@~qEwtgM at 0k?DrFF6oM$&MT#=Qfm@W>;tkM}aNEZ?{#W zZ-v#5K~_C^6N@y^)dJ{ztO2wft_OyW`zFpAQ^f}Fw zdpi-7F}ouMq`5h*;>a+ at dOQv+1L{n!+d&5WLDo&ik~lct#inj^OWOHCaE7FfZsn{f~;xkT*j z&Vucp&Q6qqF31gti8U+`bDdjoIUSoi+8Fel?Kddtr)1Dt-W~OJ0_3A6<}&j*ct-u9 zrcShIjAy4c6gX>nJr5`z1{)rjyNov7bsUCk(_D at b&W6+gH74hcMI(MgxF{D#4+(`s z>#dH&V1bj2^}*GQG0>o*%Eu0QcNq{n#SbPf=8mqsBJ?-f?|qA<+F20-Q6M4!*k}zQ zG+%*9Lb#C%j3kUNbd#_XfSYecK{}I^8f3k529Y=&gv`fA$o)qVJT!uHFbDa`(Q#w<@!48KO`En1stFmmaWyd^Ou-HiekSn>I zbHMCYD^tjvdS=jx3Bcu`o#1V?Vnxp-;A at D(k`W{lA`=$R6FXRjV at 0?vOP8~k)$IiZ z#1(J{F?`c;VlkOAEwP8i=K at p5Ac!!w~Fm;1Qzi&3#I7`V*Z=ovE5zf|Dk19T{lN1BhN)F`b2|edR9!$~#-i zF$bxf26v*3jcn5Aip*MPVZ(@W_(NX=8kT!8THC#tvMh{4iINjRNxPu1%el8B!Mb2> z$9qA at Ng3mnDKl9!Qf*nNfQ3=h0|n0X18^wLdt_y?Apz%*8G#Kn^LDhk9forh^q)3b2`rm9jN=u9 zu*{lSM9b)twqJL4ge20<5xbkL4OqM0S`nDvYTprwA8x&SJ>}bxRGD-!a+x~Q#aR*E z&C-WMT~#NV=xD(6=P7uL7o6;Hclz-*-Ehbb;$F#d#s?B*kd?#{tZ>M%X?1;j%#bw9 z at e;YF+Mu*Fj=09i$%G9Z at RvcNa##%Hagf|zMTg;=CSlRo&e^psf*u<}shefWW0|OJ zk(f6PGVIORob=ib4Ol0p)-oe)ffyk%d%&Mg*sc?@ZIf|#VjO7M561I;NH{?jl-X65 zCPrLUuJ?l-ckbh4?p-u=PO(&kAu<^WA&`N~modKOxVpIP+GBP%m6Yn5#j-^wC>@SY za>Sw2134sbu5VHyCN$e2*g|hZ8o^5&ISo%OhiGo-p3DNd4{k6 at t~iFP8oDu~I+B|j zd6Ki#R4jPhowvx+B2x)nHf<^cg~BdQ<)xC2y|LKXOdE=c=~&#$KV9DzH2P at Mn~84W z+ at 58pw6N`D!H68TVj)xSr{|usMLZ$}T{WEDI0ExKUrM4>W+~DTDHtvun25oKcI>s_ z4e5^i5qxvAWHEJT3>j2u*qAVcc5B9!t$Wclk=wKCoJeeJ;(o9 at 7Ir#jh~Q0j3>^3w z#-5PD4lc$Eu?y01O|2XwlyJ8M4p};(3KLw>Os4!*HrqwIZ)3AJt5sQR2hWI#m~t>S zvJbW-xx_ac+?-aEU`>6CA#GX~r_CGYQK}xI1^Fw^Oxf z;TuG@#DXUb=*np}95rV-Z897>PX`_xF(b7Dz4OkrLFVmraef-T|5H zW39VLvZ`~2xG!M8Xr4|y7OTb8|yEtCkqaV?^#mN at Nh^?T_o>upRRIp z8Md9YIk=2KcZ?$LXP7fHtdDI%sM*`OQR!BmSG`^o+NY8N$-6MFIn1+$UW|4z3}qN) zp4UVM1i;9q89M_yb53%E!F$}IOEt at 8(KJDjV}YcunbwS&aT6sFU(RnutrJNvPqrI%kxYF zZ^M`?STgqLz;DtE9#=Dyj4^0A#E`d!Fob4vRRU(I!Z$hDSZv9XE4H5JpN@$x8&{5Z z<*vw*iP(5li8lxlx?AWV)|ly$!?%pKu}EV`bq};GoXNJz!GyH zHzhLNI3`J!^zguhnG*95(8{qQ7UsKR^k5 at yyH%{4YZ6sNLPSW=jil#_)$wVBbhMMM zc{&W$){yM?a(g)&gR||)(&7fNgn{2C&Xx`OV0bLRaAqjf7HNMz at O0YD{BAlk!Hbx{ z7{H)?4jy at U={gF8*Y`8oRoNTp#qQQq;hXy9j>rh^Y-$0wp#RHxIQilt$A0Hx4Kd5_ z-OC11x$C9juD&8%Yt-s2aBZeDXuUCY^qUyv%UDzBkm#s5PZALDl;N0}DZsm4SXF5| zGomTcoV7W4a+*uKB*d_CbqQh1rk at 9LBrRdpoR~okh#0<4gj->t$&y*^wY5z(El9ew zS)L_wqMUb_Vmp|JUr#x at x;A?5S78f*L<30?9b9*W63xMep39C)KK&aojp{8XWu2#a zwL2zbdC?GuSP+gL<_F16ImHKXxpMY07#`v at Ksj+3JK)+{OHCOlw$?0QgA!ub!oA$% zfWxT4s52LbIK&hp;dr1p~0~xH%2NI-^j4Lv3E0$BZ|hFWi^Sldh=0 ziPZiDE^+$BPX);2!lvaK;JLx(7|V z>OtUxS91Q=bGKp3xpM0!)YeAq`Q%TdH at UgC^cyZjXa&zB?u1Hh5o0A&HQeh=hU|4Q zObS;~Ukp53F>;?eRS{#P7b4?8+mUigbUY~{g$PW#m~u=kY1pQ1z9V@&amj7hpsa>- zuNTql`1G7Oq7xP&Fr02ca?RZPooi)`OsnI$ro)-7CJ!>jIx{_3 at ab&lE6av+Vm3yE#EX8{#cN|p%}#Ab zM_x$PgsnOyNu$i~Mr|g=2$1Jo^T979_^$%a6XA{b^jk)mT^Y%a%E#=XZQNVr`Rr0%@P zc`ci0RF^$BP)lI5I1+m(UH#e)xTLW<$i zDw9qe@|i;64UXKgu*OYIws6)=HVKS~d!kIm4 at w+;MHEkS7&q8jAO>(`&A$6VOau2I zI$2`Sw(8?N{U+JU7Xs}HXJWu-n8FF6p+T=s0}`i^19z6hw00sjhY%D-jT<>ihPVZ> zi=!bDRDi=~9)^;QJrJdhB!-0vMRYM>k%AKST=nm-PDRTcoRx)xJnk9BNj4TXD8+Q? zw@$pVx)hia3RVUMB8n?Rp at hO_eV`n5-SRuFiym*hV0TjsfI4lF`ve zhiSlVs^|th4tFM74MNu%tMGDLo>`OM0glO(w=Cl6&$6OENV#q*JK7dj|9N~H%+3cyadGp+bStQ z`;i~{urx#c>z1y+_3pk67(3Q%Jr;7H49H*p%#5f)$@3}cA@|63qhjb(%NtWm6%iK) z!_Q|BvB$O@?B0rP5n@&2iV4K`0KYjZXh*4{k_RXd`H(V3;eX`a_?i>1 zkVEhwy+bIB(SiGaX%Rr;C8vX0KktPA-jDI4`PgGYsE7xBbEq!oWtoT_Au1q>N|LKs z1_Xjc6aa}ROpZa4RM}unm^2v?6ydCOEP`kmaGI(}2PJTVjFSl%6C)t2e#3lWAFPdy z(FO`n)u;CorUFD at lIAL@3ZV+j6Xhf&{V?pmmwcB3cdlAWI!N4vq;3Q(qmW~*xm%@- zqK=aA5W^w!LxPl zV;xm0ib`ik7>%y%mTjd6qd}X(3~GppEp7>KKib%smxZN`u)@h>nvHR^G_fNr^0KIm zAfkem$ubHYB^0Eht&!$oxA;z`ruGZTGcE2r(_{Zjk*4X2YyUQrgNKWmPP%hi=8}KP z^}QQ53N)p4eW at evlE_ET6F%hu53qqz#b3OEB2%6b{jf|4?@0Pph5(ocVaNAW?8uRfZp7HVMQ^kGQlT#fVE$YIGJ#w@~m4^Za zqnPAFZHjJZ4>c70%T<}O`|v-<{Y+MV(RQBY#~EEA at IuPtYnbp2TWTT zvvexR==C>}ip;r*)jumFm)~wQ%C%B69npg|wBtz&6*Cqj0h!5$&=W)#; zn?{j^ypGShytz6NXreE$&mAiGxbQ%mec-_iJK#fE9!4h(d-fl2g9JH+b)Nls at v}gj zN?PoB9CUedUAVD<9btB425~q#VA at KKvxwke@MiG zMTC_h7&~9Nz==u-)97?dO+ezf)(=Dh~+FhlebtLjEACIRftGE)+j)E$-vS~Uul7G=7rBdzrm za>t~g`3Fc_&jZ-Phw{ll#PxXCR*D5f3lc{o6C`mtLJ--kDNjC+B5@(fP at fr3#I-RR z3B4iF|6HNL4t!3^;Jy&PAPfHfq*c at clL`<>A?SrdfiMvFdD!)*!pJyY0Pzq#u at H*l z49GBP>c{~|sY?=2H55c5$N(BdN85<6wbY+okwI0El;AxOwgNA-AC3?`i?oP^FKR~s z^!r~7ayKRR8#MM!updIsyrN=F|A#NhDi64i(fPHZ;^(;$lCjdL+~VG9ery1 zO`c$R6WmXXkD*hc^QSaUzkgbpi5*4`wGc8HQ3dmohsO`8f65=0kNpb^u0ySC*QaDr zIJC2i`_KcX-;JM!;x)zj%_a2Yzob7UTkEsjND&bPKvPsk0f!LWM;vaEqc_NnRie_>{w5@!lbk!NbMJl1 z{rEaV>m69fmBL&WFm1~N0k}LYdqF=kPh1~$r=opIliydzZ5pWBFv!(aEuU}Ol5AH= z2)fV7U`#oM?1?+BbJ0nW|h8NU(2G{!+i4x~@UuR+v4KBRpsbyWR1 z_BM)L>1R-4)^RiBNuayoa{(E at m5hx%Tu9Arl46h{G2PG0IFBN?J&TJiSjL`iSa(+A+9w>%`b`u z4;*|tHo#*XII1*?ZUVLjF@!b7Q;>QA at TUfpO|b(LI1ILf0tK2$<82j!GejgL2&IHI zkbsFR2Fj at sj02qp%2944a=B+TOl=73$(e6?jGX%%?7_@%l`R>LNWnzVZNZjahIG=} z8)6&p&8^x_L?^@4a}la3*y+TXPEE04{zHWW!S{WVdFyKQfb~LwuMZq at EK|JD3y%lk z4Vf6Ds(G?zLjM_Q=2_Xd!40GCy9I+R3L!*0egGT at 5Iqmn`u9w32$^VvO5#XY)d-}? zdE*t?VAJ_x52Y!k7=}^;f(ar8PlKcObmaPT at gOZo6^E9=hCY~9k)41I_JI}J%1q&2 z4|GG_K`oM5aU)Eqa6fO+-Dfuzbq&x&d=LKGVCEWx!47gZAdysEj1LO(sB+5J8O+U# zGMVM7*&SRY$mF1rMf0k2(KPEVtt#Ekir{@NcI4XI)HT;A4c~(qRWmxjO)~jhQ$A)R$rLcj>t|K8Lxnm*<(=};G7h@)$1qn|# zGHPuP4ZfylFzn>P-p<^Xnxu1ut)SL~&{T(b29g`oUS~W8_whBdCC{LxNj3SIGFa9Z z-sw9Kz(8yYMi at ON4g>?S}f;vqRgQyaNP)5>nKul#y4$|pjShU&@nY&JE?lnd{&P{4OiEt~U_Z4Wy- at CjsqUN-q(;A0J zJ)_o#ZGmB%GY#gF9R_Xj5O2wpGaI%pDr_R-?IU9j5`j5>qA*CIm1guoW*oR;E8>CV zUhYwoft7ESRX49kvlf>ewC47csUunh!Xl`nY+(;GvokVDFkmt;kSrnsq|J&Y=R9g< zJ&I_ZkmlMhggQ;|Z9%9LP|XTHj4N7w1kdQ>VM)n&H!Dz6A at WXUBg#ysYFqAcl7mdM zh#`TUM2L6f99lt<8at>OvBDyP(b}(@j+JtAI3AITF{~m-Lgwbvo_uC{K4}D*SqL&G zmv$UQnw&#ucBC1qH>zj=lsaMF1WStVQq2y;_jyIu5Is^WJZm$?+8P+8R3<1`=wi&r zLc=RK#&m~bj62;8qzFv`y|!$#MKgjiSk^<9$mSXB%;5QvOt*v{Y+e#pT}X&8kGB`7}fCTS=H*y1;FoGro| zb_!-n5=zO8fsp2>b0nFQ1>k_Yc^*0c?A2Q7J?!`0BfaM&!QkbN{j`lzJq(b^CiV=3 zlie%m`A=Z at ZuRsMavn?ZK|6^7@)iLw0X|grs_6rA?ILIwx!{VC`80j0KLS2dQ=Qak zF%^21Miwl~@+pNDUBrYKpv*z^%E<>D=%I$UKjYpf}u(mmU`R+R+^ z+5WLH!;X77oC01eI{=>?DM%40QZWhuMDSM9K!AGoWttua7$HeymXw>_qcSTrtm=;} zkJR-&@7Cro(n<(UPeq${n(glJr{7>pysFJTA8kVc{LMFy2T!G`uK zPUk0syqQdlgVS6m?K*?>)z at 5E$VT_J^|V)QVwtaeU^?C7w7CKvS;)(`L~xFE#LIU^ z5rN!VeyZD0JDgQbxYQV+#~ULw#-y$jtT2ajYC{}k%nfJ%)ps2 zMYculXcp<4ESa1^3AJdBE*!}-jWxnc$&2qL7ZCf4T)#Y=a!w4dz(KLm(3v(|^G6IE zD#CJ`uOZftN_6KZoF at GY)kVZ at j*MKew!lb;luGQL89ES(?Hle%#i-m}m=Vt9 ztXx?vebepeMio>{KFp$#6iiqdL!BxQ;*gdoF`sHjbfP_4oWA|eYak%0y$ z2tbcb^u$)Ehtby=C_z9pW_Dzh-8w*8XxX&}>|z*KYX$JcZ*5UFB_kqja|K9}a|4{p zbEiq9cInA{X8ab<3cBoS8K0*|ExV at 3=Fo18@`q{@yY*OI+ at pcFxuW7;L^0EgLm5Ls zo*>2h#S$Sgb;53zd)n=N4l~huEUM)Ya7kkBQ*b!sok=(@Y}W|j(lkhHD_r1>+qNH= z=N4*Yg}W3cV`kR}===-X?`-JT2y5BcUwDqpgxQoFIiwQi2B*r{gi8bn1njA3QjscV z`r@#r)0ZEh^vxXNJ@}PEXG2Y=R%DI?#0&{gfjFRt9lIC(-Z}VR#w1IDX>_pZLKvcH zkJTo$7^mY+B?r^Uyb5hoAU2Lfv&JMw2{^pVgTbg1VGtyN!Xy~Lh+_uSh?%Nc|7A2b zz#a$WqVfgoJR}#)QGs+~GW;{>K!kkLqxH_V(o5m(Up*pc^DxYeU`33!s>+zC(jgX8 z8o+V|rG4hKNFcyyVHiZC0*Ang^#rg+BO+HgNL0oekLg4XK@$WNEh;6U9A72+AQ2Ol zgo1XmBl$ZywX&0AF{oxvDB$N3gOU at 8N9l|}fl?|Mic}CeBmi7r-;n~ZB0_n1s(q{X z2S{-h;B$^8 at dk|1ZdCpW{D-{KyC|Gs_3TRFw0iJiLr85E4_Wn(aC-jL?6abv;rGJg zu?%5G0n2H#lra$AVX|T$id+ht)7hhd?BH;nZs3ziAZfFq0QcCsy_J5m3|C}C!YJU= zkT)2SvXzGrjNl0zcp;UIlaMJ(AS)o(Da4m{RyW9*fWxTsP(=}0BR0i45a&WSOgJD6 z?i?dYQBGb02~{!xi6eMqS%$-(=Ur^9jF4N76jT0l(7!)NlJ5PO5 at EPjF_ zLtl>~TFdy>OIXbsLrusvQX>`o$+248aj_oZ`CGUgP8em6(7J=;`Y>dv;C|j^)B4ID!)l-;)dK>MWvuZVe(l zKZ?3wFZJB8(k3TfxM)tfG3JTPK4dQ;dz_|Q0;OV~xcQ9|>3 at z%zif!i+x0c_Xt(mA zhXB^`OlFh at h&-I4q$2|(=(yp)PG8luW0ws1?yleDPw6_MZ6_8Jq6(qndW<`>7qn+{ z(%&5(QTnjr1!7gzF6?5(&uiZOTMrf4iegoW|E9!F at gZZEa}Mk<$9AT%NB^d>Le}s3 zuu+<41BeVXJenhJ0W|&6g*FZ`A(I3Ylmbu`iIS#B#h-%?mHus~D>aTyG&D0UCp|{S z^xl+iZ7V8WnIGL}`<8P^l!JlXJDP~Mc~KEuX%%5aN*N=8#(YjgP=*ag%q5t at x~FmS zbcWR*OU(Y3okf$`RXZfF0!IkocAW-*n+c>(=pK`k8d$FPK2BddcKwbH6;-ooUFq40 zT#zWtIT(c619kC&FlHDWh5;@|(quZ-Q=eB2A4%^mC%k=Cv}8o zdK;=eKE5C4dHs9uxi?g0Hd9p^xnU*73y7Ghh7K+-92p!2Y=sv at D)1?&QmBNMU?K>} zFxJysRTiEm|3)Zm(9KPZqtOt?A)#0|Br#2m8dZu>RHChJ-%_DNNoHUXnX73ASP;S$ zSkq-%PR1L9G`PJ8VzBohdggG%{Nwv|u`H5%4ueLYIlv>sLF#8NlsZnfdg3Re@*(@` zPtMG-%a_r^f6IsKAPo>=_VhFwT0>^pB*~;QuR|K#VA&RNDjb_P-mID=vPVUJ zIy$InzD1$S{0kFU{J7ttmB(qPJ1X-#MUCptQ12_rr)-^Dwud#6>#G%ztAk21V=W at p z6-z}0W?0&R>bi7BmPyTI%eyH}&1ZsU)S)hUak;~n9bBQJhzVd*449sT3EB+Q0Ny~R z$c{%?ZaCo-9%L)jrAD|WW+B-usuHOpb8H-It_ESxWzua^QZ6q^!O-E2 zLmU}`RCc~Ylf3bIaP{Eqb`BVs+A at l%912630VF!m;NgS6O|lWZiVQGTXd|D(iuwb>TTN>fcnd~E@ zusBGhrX>bqAvIwTXh3>&Ia at hddig64Mz=Den6OM2V9f_;9 z?jRgX9c;*myhP&@Rt>5JWGrl`d8bWznJOn?+7u|!BAAZuIOBz>rP3-(Zf;d7=;fKA znyEQr8QYuxW#aoc5YEG2^6U9TjKmZ9Ta+7&io0zAz|8>Jor(-vP at h}X>I0VDzS$ci9wN9Mm$Rq6}|96Hujqni;OXy_aqX%*%lOB6eO$SK( zO*f)gcbMn96z9zp{BVHP;?jy1kSgr|B`XrJ6oMb(hhS*%16UwZLJQhU-b2UEJaT%V z$;%+QXw5o|b&g$toS;J;h6sGY)P3F{p1I!ma#xn)y$PqS#l(c5fh at djM*w?~PZOzw zm{`F$6$e>$1S2OJ83?hXTVoiu3ozK*1RhLmiJ<+OUWdYnIZ{aWsi38ilNLNC*zA#n zp_JMzkwh5=Aqydh$eAKE4M`T2WMAUkl5QFasELUKih-a*CE$Y~H9?Clk(xx-`8S19 zs!9*vk|zd6NlFqiz%M))kSsX>$~uC~22hb1$U`kyMyw!~k|5QL$VdaDCaW`fpWjuM zZfXxBGVa|eiv5mYVE`Gqa_E{ana-@#mWr|qP at Z))VV;nIrI8^@DM at XJ%-;;=W1O0q zHk<8&#CMoV1R^Gg+LhQ)Q at Fl@`(}X1$L1j5H!@lVz04BZ16w~bItqtP&`mCq2sRq$ zk!K^Qajr=o-9ny$FlizfA~_6K4^0M|1esb6kQo6H9P{c)I&qshvNz>g+#qOYL$BR4 z5)BMnX^kW9JwZ`c%_^*CK+euwNZ3072tfD0WN=wJ at rkJLK4~{n?9km^O$d|+N?j0! zf3cLr_I8Hwk~}Yf#SheWh~?*iW-qhKAi;wlk=cwX8n^1?)QQFRw-c?BZm7jHv8=O0 z5Cko+XbwNAZ#?$t=S%#mxvK>R?fF(Sb;)eG%fJf6|iMvHCEpdYpIGQmBKJqZ3)~1LycpHq{@t6S!4Y zG$F7Mm$>JTt4R1#`XfD=RbuQO4F(iqxIzyBk)IYX`693U&Ug^OOJe0q_~Hp61_}}; zLJSEUW{4nQCSig?S%4&OlEmw}#S&QS}g#Z at TRoM(YGHsE^G)S~QaBh6^GwLUl at FBOJ^99Y=zk zN9;EG4ab{YX(#t&Nc^%+qHiShozo`a$p_`28U at e~x;#GPq4s90;q&ts`>)e+!uFR) zJD{KjkoEh|k~Fa|l!nu^rbq`82zxMBQXZ;cDE99#9W;O4tiSx{_*xjpkb%1u0oagG zfcx0Y5*lm@!vUE}nNXDnU18qDVUPqw#^e1dy>UDrWIH8KV(j4dn*)oiIUX2tYm<`H zTiqa1W?-C>qs+RDhkC(sIE2k$J!KP%Qk&-#4}^wF8 at O1twD6A409+51qtkx5!mNsygnJHQLxN1A>EL>|QmlwiUEM-_eIJ_qeI zs%&@(H8N9HB(TY(n<5}>>hRKUAUw_lKTM?|dW6A_ at I-`nK2ncBHY``t9J8nKc%B=N zMvZ|xq=^!ddw_Zg+t2rB at S~rsW=feR6);vA4_!>DI7TpNQjnp!{M3d$^>E|1p;S!* zk};- at 2E|2iXRx6dk1YMQ9UmW=#E>i_9O!BSK#?RhWMQ5 at dH!%rBirEY$7O`-;Zq=$ zW5lusD0f&TkuiYHm^vZvl~W;U2ed21Bx$AJK*UjrM8-NoAGlUibkN2 at 5Cte`m5Bs` z0n~!(P^}up=0wR2Z!$+96Afk%qC`OqOAHYdu at 4Y&AR3hnVo+!SwTGFJ`#3jLH$X#L zRI!XKBV%l73~{K&G!8(jk=jw08cx>-4*25W+D7nLIP^br!t}WpxN%Vj5(qrq)Ge4H zF_7pmqER{&AVx&W(EX=GM2s7VG%)O;5)^ct91IsyF4IDZUydx<_ z6_Ai%03{3qYCYaV6++li9m!dU}6tbHK8yyXqKev8!-+#@R%#y zkRYOGE(GjjQ8Gsj&fGR`Qt)ylr_KCKM>`)% z$MgO6-jt3TuQ;q585q5ve3YLNn=gL1tz+&S23&GUbF0Mg;|s zNh;vP5|K<$h6yOrVJq(^7e(qHz(O3s at 1BBRrH-J5?V=M2?=8Rkqi6(|w*Cpatj{Zslh>2U)rt66@*)3$4_0$%D2kdPBDsg!dJffU4=Hg-b%Ti^A=RU$E;y~= z%k^c;D6$wOh5?U}`!sw%F3k}+AAcYZ+I#TXJiwO`vyw4TkUC5Z!IDdx at rP+3`w(Uh zKxD=5n8ODlC_tka#9>Jw`%P<6z+zF=6jl{cLdFaj!3qhe=4dk`2#N`$j0VNP3=AR2 zA*yK!DIx+8*fpU&sOT`!j3$8EFzcPM=^Z9!tDx?RpfWEIg$l%?PKVY_f;f>U*C87x z&w2MpMvpX1{1?~#v){`MWPub!CqoF`79pnLAV*0VAyDdp2CSnY5D(NiHVYaEG?D(v zf)onUAT$v;vTHX&jTIpX-Y$m`bZF8~vuLivgJ5+dl3kvo9MhOlXDh7+a$@RADTrN? z;|xSW8ev?IANrM!^6v(ZE0Ag%nkR}aDTQRdSq`MW0Y*P+-l{cuu0{~#RVUEo(XWPt8Z8P^Z z40GS*Eau3SE4d=fusLUrT$TG)?kM%neV02@?u1k->8|g4{>1|iALVEdh=XFilj1+@ zh|OnY}g=d>a?R5vZEXR<44yv5Xo8>J7WL>KPyf;?JcJ%o(?B03k$Y8r=9j zik(Uk${ZHTZl&uqiA3c?*Ra>EZjhhe|BHLQtbMeCyWbCiGmO}Z0L}=gvxmpIL=o<=0CTwhNpt}4-5FoU at h|oN9eV>-DU!bU`)L;m8zz6H-v;=R zYxurcTly~k5^}qx7Ajj_zdu`eH`}|@huF|I=`_@@87S>P4vq|+$lMFJCfv{JoNr|{ zO?LpG7^q4TZP3gfp)I0o6ScZTIJ{b@|BlD|)WX-`D4Zb%K_y$I$b&Gr$@(T}VO1TK zsm?(U1-{Md%dRnDrLVNRkkq~f at ScmRxI{eSHmr2xcczv4`kB*cAo~B;ZOk;oOfQ7v zx+~4)J1rbwqhKp6!k5F?)M%01jK(U{X+wM_>YldY5}3#cz9E4ab} zJ+fhe0l}zt1&MkAi!!fBZUqcZ!4xL%l}sp3wn~na2Nq7SQB28^krR_cIG+;bL1wDq zkW*L;hx28J)DZ*}a6_rVBr~^eQL)zb>a{}Su*+uxv=TDijnqVO$WQkYGAF!6LliS9 zE)C{#(%I^SzoUqS0}WikLL~C}^-_v6T)@j)y54s<-`}5JGIx&Y1;n_3JHdg8a$_Hc zmv?xwRw|uqBbuLX46&fCrIA1-rW}4~K})3XwxPXgj+hb_8Z at SuiJMx|6fHuYV@~G4 z(hWDJUm at 1(JM9Ne)od)>Rjk#NOO-@<#n5#?6){*As@(_B#O+`Hi!&;$_j7pmi<#Ao zz?Ul3LzAhA_yW8!h}~*x>--caIvtrzqSaM-cR%`*Xss~G;t8`X3XwLIrj=ERGOlMrHY!qzU8rTQP1OK;GOdv+=3Prh z7LrJn)fPljGqjRSB_qtsNJL64xu`l#Aq}ozQLiW#Ae%0)P!|M1ln`a*CH1a2c1vvg zFO{U6^QjQ*s|MT%U#olzvbl017dUkd#KsRGI*_^91C7ccfZ}A{scuCvkW at YE;GBN7 zO(ujjJZg|4y}ek!6rZ}Kp5e0>mF0hbS&i at JbDOMCij`fO6?8kKrt4e7fu48WfPq(^ zNLRpoYxduh$um!p6s+S-g4>I?GXm1+>Lu`ZzBWX;ZG^;66_^+EM=@Mu{w{$^p{B8O zZka&w=$GDbGuIj8iUxx)Z(XZ0iAw6D*Z9=uaVcx#Kvh&kDRr!tl)YH`@vxvJ#VPSZ zfA0R&@h+oalgQ~CMqr{$Wiv0xBUPFsNsz5y!Y^}CRY{=Fe;BdTAAe}`@J3HW>{M9) zTzR|9l*7X%)`5_$&J$e`kG{5&v-lVE;aqV7mty$vG^|OwD3kt>ap}^XJIP)*t`5Qe z#E1Udwr%tT82(y;D5P}D!b at E=l- at 1Z?EaAh+pioC3ix2mM-Wa4J9}wg?C=Hk~qfsF3sj!?ESsc&)j#$P9Dgj z>W`wnoK?ISBr|vy7SssTRSloXACZY-A_4=Fzr$*{y4Zl?%hlANF1f-7X zN at xZ);n%tYZWykD7PotFbD7t%hfb87LX!W{CzRXSRaKZQRaDsQl`V=Kne0J$97vGG z$!V^R9WI21gY%i0Nl#G<3N at jJO(ZI^*md3dz zmPbCV>R3Ne23tezF$?yPeCf)cTUn2c57frZTn3PYA}X32%fB!*b8^I(uF`sn_%wZHfG zbjgyh*iW>G(zM*|C1)9!gqdwr6z))WKh-E|MqmT04jpcVp~uuO*443~JG;XzJhG*c z;C6C80)q`~1K#KhWhrx^!}z3;>JuHXJ?Z3cjl>&!deeK%yYfBC_?nAg_oU6>@A}_{ z(UVqznXet!x(GKFms>9GXQ?XrU;>BDC;R&+^Nzziypw0MV~kCq#&!cD{#lv|y`>WV z%#~F6(Af2oRbPLZloa}~WQ1l1_V&mSvS48X!O_6X$5V}ErSusyXJ*Iy>wL>LIvSfT zlY=DZi;ifv{Ll#~3ap6}2=RAdWv5yN0esT9wYV|snO@)P3&d-(j7v^~-KwS)3S^}2 zdx+hypV(CeRL`g at qpSv7eQ4MCWv&%@l=yp~yP(Goj>t#3N8{w$Xcx^oHCpSMuUD9< zgg9IxJxw#4+(2?cVe9KP{jff01&N^?71Z2Bd%9S95%!apcfaEr)~M6=EU&gZLEK>~ zv^Ll6(L*PT%7=PUyOVorYD$|TqI8MGlCVh1_tt=al| z%PBS9&7rCeGJ*e5%F1GFZiL&GyIYWP8lF&n$E%CW(4~S?R23%FymV!gVvWCDG;xEC z*3kS_`j}TXN5C9r#a8S?8%A2hcgLmYeoEA4%M>@fb_zCg&A!`+zR%Gu#+|E`+Vu6& znZjp-H5%bJGB-E-;6}EWqDt09OFu6C1t8Vy+z-Idr=hP|&d%8xCC>i19fL&P9g3eu z_{PLb1;W1<(xsl<`=}?fBzFv}wDtawRl%V9);{@j_s^_2Vq)E&fARttl|#7$F%J?- zq45tHn$dWNaX?%sur37?K_f9Eh6JI$bRyawqmYH1 at dYobK&MJlvbm%=(8g(GQTZEw z*a^F4Rb>-a+{pPfps at _W_a2}wyogFD10#}6S%mWyO8nVn3c)57*#ZE2E{^zmG`I$1 zWrceF=FLlG)^=wrc0QOGM+)fIPrhZ|zQTMqt{Ts_Tz|h(f8v|l7^};>N?#S*2ff;b zh^uy=l=90+G+A)+&HtjKPQ|g{X<>RSRau{7b7A9JP47K9sF6EpErs>(rzbkIK<>f6 z&*M&xZ$8)9#VVtJBo%vn at czm5>*V=UV{euvfl~g~xu5?nJm!o<+#;=ag71thvc{Oe?Ui-Zq!}l zN3~0LCO1-Q%wM?|*rix|>$070NLF62%8N8?wYpr;SiLtRD|_HC_;1yy{J^IVVcbT~ zFn1)GE!c~71-V_anrdM;pew<*cQ=zVGGZhJ2M%yP%9<=?Ros&GQ)bi)d3WygQ#_22 z(b*ecg|~P0-$%cY8ZRIx+G3tih&spA)FYRTs2yx+{~h$&<0q5#&+dw;({CK_Am97i zj2u8-BvH>)I?jAa9`t`2n{%H`sQdRE`bzvAqQ#k1ihRI1g$^QN}{= zt*?oGn|xuzF3yYb4%T#ab<}6ejN-6f*}oCb5 at LkC6_VpGwFoT5#7gf%q)*$S0(2)l z=u at 6#)4-Eoy!^7<|6T6=gT;dfCG={ph9^GXz0$fB;U~|=1C;Qi^1J`&-mY8|yOJO# zigkwxV`@f9zpA+{ih0RoyF9;Guc9c^SlI2$psB`N>< zi{&9}VOZ)msrUAZ?PJl*wX?}}#|d>pqS8iQF0~Q|v01K{XzoTHAdhW*}Fx zZWS8`?fSwPkL=x7NdPyV at GHPq8|r&wvyAl(b*<@>(7t1re`p`m^BS?*VKuo48j6A! z&7YVarC)*+c&qPL at Rrjcm>Tkj0}km34PTy zF$OHxYq-Vyt?kieP+CXpdt!4no4kbYXP$fyo?~Hd^~k0N8E{J<4Hb1((iOMs^w6~l z*krZTS at o=Orf8cPRzz&q6$Pic91hQ3ylrE)s-O){WHUlMHs1HDJnS;EH~yRp>*wlP zS&ER<92OqBlwXSv6Bqx~bI4_2K at 6Lu+uwA#-fCUzMvNwX9mr-k)EQ`*#zPNO5=!g! zzg%@1t_N_kVVk6t=rhhQ>7mDj&P9sU_QkQ&cb$FB z;hvDvw8f`jPwm#!#Pt@%We;oh=(kVD1iie%)Y$X5%RKKy);*T<2#hJ7Xq(+LKTfpD zyH8lF%rQ_(dWt9)jF|g7-BrPIa~cLM!-&q{goZLZP3)1Qlt`U8S?Om{_jK&sFjj0- zDjWEX1n&&OCpMD(LZZas_5NoM^+MmDrJs3UI at 3$0JL){bZI)e+n6gf at capZdT4Q+} zM at bCX_OZA(o=ThizW?tMPtf-;6lM#m3R>7z&R*n#cMkdF>E87 z6YP&1?yktpTR7CTewATv!SboVEVeq+-r337IsRPnvkm1{nN)f1p!h|h at O%4;LAvQr z!)MeF{NHVq%00W^qj^6F`A+Mo)1{;(-EIPuX5L$z^m(TW%C7(h3>7C-p?NFsdG7 at 8 z7ydR~%4M}r?}=MTLXW*DiG`~hL`4{f}Y*5k#w{ zo{+%z22C#lA7 at 9n@<>~t+KM*A4-PjZy#=IN9Vy`tHf7P6k24yq4^qE7QoiwRF*f)+ zv)W26qE+{WVDC>$Y=YD-tA`Xz?-v^gRm38>hxnXc3!=OHwC>+~a*HMao+wdoWVLbGX6CEcPP*;pG87!YJ+R8o|g zk0`A-w{U^cN#0GR&1 at 8T2Vr>%B|r?JSPN4?Sco~)(&`Z;1g)x)Oma1{pjT#AHu4NH zkQ5hLI-Ss1UoDP8)xxTq3IR=S9 at 0E$EB`B&04TXQkh{(kx90$1qgktgTk)2t7N&Qm zMOd;CcG6H_5I467b~Ir|0|oH3Lh`6G1CMo at X`izf7-1G*&gV=^g~f2#y9HlY8O(fRAy z1XUU*m{iAePJ}IunYT`Ik|odUFVzT8N at ZF%+#odE`|;?vRY%nzC1DrQU at DzSmRlDM z!UL15D2fQ*QZYpei&v$Bmu?7>C{z-tSZs%Y*12ZN1bER>5FFmfhH126VKO%h7ln(I zZUpv)DHy_=h;(8~nGB3*j>`)TT|}m_nE(~=smt|72X< z^DRZ!WqCcH at dT8VPwY4)sKtgx=p7y{ zD+ob#+0AY-|8bF at e4LUP$yG3 at xsTB1y=umrx}3uP!3bv)=d at c7 z-kYfvZCEj{#JY1P{k*U~@rv}toTG^U*##y|`ckJ2zm2Wr-q$6cS^GB)FCEO!vDJ3i zwT_5o8kv8$VYQK1A^a}1vBOt&=dIF92tk)oJ!Z4=$7AFRrktkCzfSJBarKcR`xH+n zB+vDeETd&O&L4~5zLwk;lKL5(Xu^E4;0~?3A`kK7VYyPu{RBBeNckit_H=lwYDtg3 zn`u96F(qmH>cj^`)%Qss zf9{(uJFjpXT*_QO`^5Yw7%PwY<2mI15tt3k=Sx z74was=ej0OiQjrp9`{A_#%6jSR>(CRYWe?-8bg840~@h7 zeD}5v_69VXYS6JQWB0?wujyv7p8NQ9Alew39_%y5)3IFnB7{&x-ho#OAeB{lREUF< zS^20^3Gp>C!nO>dh!2hugJ}tWI7tVshgPVNht%@KR!t{Xq*^zQTw)-2kX^lZoVVLZ zmb0qWCEm{3wrqb8uVD`LsDAd|-an+evh|p8cm;pugaCA}vcN=S)nVTTYCY8DW3?+m zO-Duder$O;Hdl3Lws%UzCQp$u%{0q&JemuO9IZ`DVRB0>>al_gGHcv;0;Cvx5_os! z5^bn;^AD at QzlP58(+^^&Chv|e%^h|uXa;l+OPv4sD$%SZNZ05q+m93mZg9+N4zYFM zaI#R-kQwkQ~LyWA4L ze9`G;X8X0WfcbaSy_hZsgaOydjXUt3w3^zj<7828zGzLX&Zr;1Q$1Tn#8*|{oXVd_ z+a$s#t*05vI_A&J*P*R1m$Q31s=6o+bWjZtApj(DU`5PHj)CQ>)|Tp?YNQE8cQ(9E z1sWlaLMSCa`1rB1=IwU?1_>`~Y{A z{Tg(4eAVhPY*-?MQQxQ-=sO1Og^qft0+mFa(SFShzwbLZ6m=Ic4)Z^2cfJ^YESG!K z at FwHKEs+Z8`Q at GJ=e1F_S~`Yn0u!uCS6>Z#T)11l>_n_ at dy}7+RbSsCDdP)b7qg~V zXRx-h-*{v)fX=MSa;MCc%ZgwP!&e>&SBT at dnM-ljRnD6ngAy|O?_pjY=R347#ArP& z^!%~k^2v2nV?e{3V~Xe+IFI&W=YyvMox`oHfh%@6{eD4-dD at ObzuuFN9Iv=v=jX?{ zl*-9?wr1PkL9JXXGeXM%RJu(8F6qH`Vx#PUF**%e3c#M4yh(SFmmJNnc45e?6RXPz zm582FsxOxr^rom}4xdkstH{TwVGh(*a6z`=CtbyWBb^0}VHvW}UBBdZKw%()4OU_# za}M?3W4f#pg-ukHTH>wFx^SrHn2SVy6`y_MEd&16#rv5US#!S5SE%!inw7I7fQqsN z0vKZflu^c{vM`${0H2$K=%#m*uUQX6ng>jg*iZ={aTdWyVgxZnMIvCAnCTRtQHkwj z<q01=(EJuE^EuTuLX at Hy_IqT|+(~;oYk;9Amu^O0kjEbDyfUPR=r% zB*=yWl&HBX#3cW`?hn)p8=lWO0fX&XdCJ}=w|RB^D-PhFTE?$Rku?iQVE+ at 7R!jE% zZ2pK=*E3CjDSL#(H$=S!nXwyey<53kH6$>g7RFWu~BwR(R9 at Mrc0Z`V!xxLGz>|E$l* z74R(lz-`CBN%UnCAV&drYXG$u3u9K4mIbEwb%{J+3kZ3k_bg4CvB(n~IK zCb%zzk*{aOg^+jIlIHhK!gB=Bo(ZuW|CV@}a^Nf1%d}a3O!dvB`)1^)bt(3TnyHSt zO1!jdxEA#5-;-yYV|Knf38Vj>Jh#YdtnM) zj>Ufw<|)_Qf5yC;-8jfvm6Ltb28fS^Xqtl1muS+ED1Npd(tEb=g2}h{NA^mdaP2&P zphq%m*2|{da5}Q!7)*FPp6s9sfBaf7F98lj-&PzEEhZv}nK#Yg2U!(^fu>m1D at -R#h&NG}0=+3T4rW at t|h9BIyLWg>L(>sbXgJ zIyyLo>nkylmK-U6Rz&IoIcWJNooxRZkW5rSq5zNRX3W(WV^YVeTaJvlu|E8FjYL z%%6Q}YdbmIddI2$&jqo8_a>ADsO{T&t>n2-yf`SD{sJ7S>GOmwnN66erz*}^?V=eOoC z at L7i0+DE>sKgR2g&5QM%F!V6{`*lXFZit{~t at G$6$NCmoG~;@M?MtV~b$)&OLH4tf zYh%S?ERFwsELUloUpRfjjQ1rhtoR;C;+G%ZnX;|d$rqgVee$;^;6+Ugy(Lfw&X?1z zQN6}9()0Z8KYQpX9~a_rLC at A0+f_N>>Eq2~CG3lVT~A)mk)SN+A} z+3dQ&9A4c!$iprZE2wJgxaW1H%uzSYh;QLLpQrW8A67>q%;k*c!60_vvKp7k20Wib z(5c;204>bIuRu0j9sIj!3i`xtmZ{zHY~oz(-PAh$=nTczk zmn)4dEDrthk at z5!@KJYGZV0b`;ZhT)@6GZ~(<_gO>c1ng8QTUzWEIVD>NA0aPpOf- z4}T|$R{c2Ai at KNl&rH{}%Kj7GW;N)^^xp#K-(ooy=~L&5$M4^K at jjoXUhXHUJn|bF z|L5u>HE4pQjV7dy;?2DSp}AkNZOnLXbl(k<(o<7rLyKeC;YOxO0hg^c|mb3D=gDm4$gmje{MZF%Qj`*Ux|$TG{Ra zrda3BTfE%=xI_#e{5aJ%+#!8hoSEDC>|15T;&5)cR;Ty6HV#QDR{G<6BO at 9Z!p8j{Q at D}KO!Wq9)ZXQR1*y4ATn%#=|MXowm z?7i8p6$#8Gk(pcD-Q_msZ<;KD;^yri2BcN<;XAAd15w~-Q{`lT(3faCb8~>F_>IKf zFT)+7jFI2_d=?90JOc2Zi?`)h+mMd}W&_iUXT$JflzCI0dwnGp0IK{}2_b+impm#0 zl;-gC7jP#JQr_&=hR at Tom8?6xnI0h!s2re+CtC6Dn90N1C53M7c1rTN%Sw))7^b9H zB%<*$oR=JQAEl`t!t3uvH#Z#Uehivt5pIrTh2)ME5Ndm5vjQ%v8`10}7*@`_7LSU; zizMI at tyWkF7t2^Sva%W`#wKldkmX~EDYg2isVd^aaA>AGlSqSh?G{;dxcl`HI@#2a z#w`Mu@($GS73Jg(=a4Ua0tHfT%S;-=$TXx@;93DXD;>TKC*6 at bM1U>02=-#U85{T9 zPJ%y4Dg3%3$oA3rHq#zD)!y91PynKeRMtM7aQRG8C%pdgL-}@>>Tl5>>w9YUsm`>} zdmG9esMz`W^wukimhbEMG&E}p{B^aSUc-NFO)|ADJ7^YmSPO|?HJ!&-Q>V;6p7Tw* zTkInE%(FS#2!g!%O;;^%nr}OvGvWG##?*6EVcnL1iTrtRZQ{j9GNXnno5iwe!Mh2wlS-v^n?Qk;SC)X1mK90==e;ZNGoB z-VKKXB#af{EWcyJy`cU)!W{mREZONI3X@$X at HqujbZuCy*DTAHSND$Qu623GK;|OQ zRUy^HqG>SY$)#uN0`sZtRbF=yu#zZ^tQ~h3SB&MEi9 zThO1RYw`b4{oNi#kDVoqXJ)~aYN03>baYcRfvlszjO~8ue>Xod^nJmdp=^t%uI>is z#UDwiX(wF^-P4K%i`r#3AH4}FU7dK#G9cKIai0Hb)q3_I*4Xj>(#^NYD>WRPT(zr{ zEXK|PfF;{+-<8f+^<0;*?X3o?tF}IxnYu@ zRLabL-8x|LOkJ|<=@)AfQ)UlrhL$1R153 at 9YV&4Re&{@@NZZy#;a6FUkeVO)l ztp?Ppc86%DYz=hI>N~g%fSL at 4)R;#sm2?MXi-t$!cJRsT=E%>LrJ|C~E|52^CrE2?gD9TN?fOb5W|G(@Z^IL}HGsV#2BYr|tG at Xzf|D5*^JW!MA9v^JG` zpluqMAr7{Zp+e#E%cT=Jw$b8ZHV!BsfdXsHhoPq(`;oDau+hiPBeb4xm^LDr^E!X%m%Ym8? z%X>^=j*O4kxaT?{>9IiJS+UCBl~jB{$W7FUC`q01_wJ<>rhT)*@5xr2KX0~+FD%#H z+QI|GH)e4CxbWna@)ZvHObBi6nly6b1~Au>^J>Q09o2E$)5X(Ucg4_X-^k_r z$_SP;qPi)b ze&NS!ONZxnCvLW8cBJP8`ha>I-flfVt#kBErH5H zJyPCldW)!klVHx4FyU#I0ne0Rl;1HPU|g3fThm<^C4vKGieq6Kfh5X@(U`#15<_Z= z*eKQr6j#277x zQD=bmNCo7mjN>mLx-NywvC=qS24Gjn47MlKS-EBp=KCuMBEb-?#%u at OV!^B+vqT*Q z$q<5lq{3VAIEFe$?w7gP?V}6nf{Ah=ik%I_OKSxN6KxTEJu4UW1GBA^9A;fXVSCOi7ND4pkXrCrf=x_AG0Lb(>&hkVoaC*EJ1;^^``&+! zA5sl-ke{1a&t*eq8o@$&*^r>!7Y=+yGFWlP73S)TNZd%=aZCCXzEbW`B6I-=;1Iuux#D!Ip7<}O=|o>A{cJ%<^W&s#lAK9+(uJK{j*FDPEqcXGCpTq0 zl+S3%VVr++aYXA{F8!CrE)Wx7pwYF~2Mf_vLDw68-7o>*u0t4hIQwo!N_wLJ)1DB4 zs;&O#a}2A=6-#=O5f2EuY67qF|R305mW zG}TS%cWjr6VLvFSsHG-_>jKSV9yGiSh6E6j3|}9{MQ9A!c)9qEKW0rDle&DhY^2cj zJ9A5259?tY0OiXgRqP}fw9-(*I;w5zi&n7rkyZ93jz4 at o5^{P|Y++oEkcTk~>E}Cl z#ADN&u`_;6v$u^6taA9);)`ouwLxarwdZ#p=X`g>I0b74#|h>hQ1|EDAXjd#)97C!BlvC at ti#XWn5)ziFqN<`ni&mM+r~>p% zxr9ug7I{{beqXUmRVmLGdk*VF7?Xeyjgk+-X~J<)%Ag;&BQclJ at lrN>`+dAkve6h zXo3?0C`oexu)x$ATNXSiN^=ExI79#i0)lv0+52~0Ofy~E8 at E!yNN!6BGwbw+{=^&q zsq%WV^2%|jrDo at q7Y+@&1g4kDiXaTYTC+V}hR+*w)LED&W-|}4k>!KX)F#DKm>B>j zqv48ObA&f%MIx)Jf|34kwZ+s-*KYPLdz972j=30fH)9tAxlaAeveqKLMQF6oss{B-=5EW%w}zSeNsHxj4b%~vQKD8(cF$eqlkt#gt~-GI z6MOlFMwgs`i(;`jzc%`cK2XS5DP{D0bxJPAPLd)Wg4LD(1jc6B0KS6DoO z%Zz>R%iQ5>V>4zPz7n1QqASzk1PTw^*871$liqH%O}o`+Vtk6C>{2&S*C#7ZSL)w1 z3uA^_B-C<{#_0+}II(WOtjgAB&wNvwlgZGPwTTl9B=R^`yST${*T3D`YuPl7&8r(9 z=F8I&%;HtxjK6InP}MnX1Gaot%&Y5GsI}zX>W{@LY&c8AfQEUcqF%ZSPpA{{-)o55 z2f?>%c+v79BRY;NqA8tzSagnBo at s$4S3UILm5s8M-$R!|ALde8+PHP!x|=T-=QB6a z5vQ8aa6Xw4zR?%$gK{+RC};-!WJRBT-0i z)j{x~WyhIIE!~D{{YN$h9&m2E;UF+tk&XZ=r;?K>q0ZJy4mjBFpC9~X-6o!H zq3YwR?zD+1pY_h>VEoR8R&f|{tK|q2B5`!t6LLr!4&jT3qKgTo%6OQ|aTEf)1HfONJ}?O>*m7F|}%=5vz1qUAR$pevM9!ST|=%dhZRIijehzU|*Gp zLREGvbN3!c5Hhg3CvUJ(rNBsW-DHiqn_V+geN+BcsCuc0gu(lNEuTX7T`H8{*&W&= zoNa4&7b`LtnuHSc<@ddN!%}sk+<_7+rsx=#+2WRkG0#|^TMmK0)U>v+q%cTqu(@L@ zr(;N(BGEcaRAXVI-0zL1 at 4Rn#m6Ov9aj33Oa|m^f$J^IpLVX4|GUe^t41$M8a?pkG z^(+Baql%+)S+QFht6STr#<9GbXsrt7s6 at u!W~pqj^~xW@#|aB zXBCLx+0LPA3Ut!?%01J23l%zewZW-dmc^=LichZtr3*Sy7+;Q;+LHXQ!_IHo3_QyG zS#{aMG`U&NJh-6h_3TKTy7f3D+wLfIS$jQZy`4u?|4JWh-ny<9r3_8k0e^<^ckW)`i=zkTHhH{x at nTTxbk-a@B>P$> z=DEpd#^yI*)r&ca7ft2o#}2*+@#+l5XZxY^mP at W^1GnAf^7mEyx@{#Yg%NxFrkn|| zy+BPo^DseT!F){Um at b2R?l`mfWxc=G=Z={3Ug6|V{w42*q=mPYg`%4Gqkafw=*b2> z;o7{bzJvV`SYKxSUqw^beBao|%bz9heM-uA+1*aKS<+nR(5SfFas29f;o+yzq01+| z{ZyQPZ7CijoiFqY6K-`ZMq;XB>59h{xk|7VwTcUBfKA~ayZ*c3yf;rw6hdwV$=6w= zr=GZw0TEbD`F%@=Z=fQM$UA>{{r$vJsR-T?=)7M?qswCWqb2R`*8CC<2uK3z4aO?; z{Ii1bn?vMrh}v_Fejj50qL)^M&x1bqZxIvJ7&YO|ceXdh8B37DP^KZV at ccOx^w|*a z`Z%|wWhZN7Q-Ad}qflch3|PP?)};uV{xaac5un}Iu+xGSr<{s;aJ0caj5K6`@#$8B zeGsmVN%dE#G3VlV4^FR}zaTznO&hrY_Qp^8d$$R9)2s;f0&fI}6C-_= zeUT3-5WAF4x2#lEN07fP(p%J*xL0|IhTTvX?NcRy$_EsJi at Cf{&_g2;7>Gm3ns;o8 zs2Fgh+8gj?96%uXv&bWR$4pSeXaQ+MBL at ayQ1B6QNWKXmPxME?5HgiDXpB`hw- at L- z&}DUxW;kQZP+G6heiHl6TTAUNQ^C3L zwXx*0Ae>UFs<@(vBk2gI`AHSPUJoGPYe|ORWFHN`bdq;B~e$y^s00}_$zba}3_xA8& z at v6mt!j^EG#r_$bU+WQKYO+66EU{oS#5u6cv&uUtq3pV=1R4dIm?4%}6q7?C8*XMK zpBru@<*UWc4dMc`3ShCQPQb#EF+0u8#1uWrRyE-fuL(^7C-(VobG+nLLl!)-9V;%c zX^ent{<7ALyL6TCl<~{%hj!^jjMj2`W+y%>C>7dUV1JOQh4Q*zIF{@c-FI~ z)#MmiEi9I1?iJ*m at 7OwgSFjhi8OH`UOkcqdpQd5>{ z_eC+U+o+5 z^Q?2<KZd-K%(A3j>5Jl1~)ktAotRb&7S*O z+7ZVlzqJUQ8s8lo7~QVuo#K`@7|>y#hULRAP9J^FAPggKO0w2+0rA6ylu z&D?AtUi%=NHjpWIUyId!_~H;X!sSP%@6?lAx69ru$ithJ+}OBXzM4;@Mye(!8c z{7nf_I=+Xy7oOwVorit08AVuI{`>4;`-&^b8I44byc-lT7E}-zG-L*`fw7E1{$8t; z6a<3q!s4v~`&k<`GznBSHex7cZLQ1VN at QuJj3@Z`J;~1 zvChC+m;PVJ!4pOm-(1$raMj3C?GUrIq8jTD8^g+lQ!5+hI5z7e>DY3JnL807s*0?E zRP;pM-+$7ZNjsE}4_}PhXAYU0p(p1X!Ac2UwyRK4vX>G)hkFtM7$K!ebMPq(FxRLx zfCA!2V$RWS-`i774CpLp40{x$&y}*3Jal=3DV_}92*B2(6?P?X5j~?Mgc}!tt5?>c zCql)9^vXN4u})?KKq7{Os8l;MSsK1}NzeBPk%89+(c~%P5*~w$itI7~wb*VPgKH8^ zcL8T!XGK^lhYyX)3EC0sQ;{>5R8a^TokVxRpt)I{Obvl^gKU<_bPxAlcg9A$xDARD zVm5TnN39K;Ny6h^DmQvJ0edvbxB}KzJ$FH9QRNP2g1sDK2ajzy2aOU(;{^^2XV%;!KkJ3}8&T8R-=y0S_Q#Iv~2OA#D(#QJ+wz z))Bi03*Tao^TO*p=ZMKtm}-2{EWo!3e(<+vx90ZN-TIBA9zXPNF1y8 at wUa`fDLK_q zg;LOSd>ym(c{_jV`b2}c2N?zhVd`C4)qXqA`&cD~K-yOicpEFMqY+!) zU4r^u1-B_<(!?j$qTq`Y+$=j=Bd)^25%&fk=gP506**sY^XT+zpDJl?F~Poe({P6E zWu61D8y$jWxGh?-Jit_O~FOcMvBn|7D zx~$NRs4~&BfL+s>I;UNvEVKzVbvz8?#Y$c-Pw}vt at nS%~*F&BHN{OezXrO?RUEUV zNwqh85|fhTZd8Xea?J$IRQduhJ!0amE^s&@GLvFRpfs|$LjPrq+U=Ktt^5)E=;_G7 z4`wUZ<`wO98*{byg1VbbOfKBeteIZ45x4TXn~Wt7+*NHL0K=vLL&d`n$#0G3o=`)-(ITHBt3`zB zbV%SFWU5Cc!_OkDJR$Uw%WcuThXV^=Sx0a8`qZYf7|F_Y1Rh_OYnmTl)_r~UEL)Tz!^lv5@~Ur1JLbjJOD008tjd)LVK!|N3e z-I;IjX-6@`MP1tc8H}~P at V<{(G`b=3p=+_q{=hWrefJQL44^R|0H>1+aS7wS-Y|db z-ZMqri(@!%kia#Uhs!9uhITe-H at Cp=<3F#y6{D5&~4FF`+BI&OOgWD z3)3oYNt>vDtA*$nD!mv_&p?d=BN0npGqxV#dib?c(NrYK;b}mG8Tan3f2p zVph7Y)cSFppj at cLrh(Sa&93mThM$UZNBQo at DL}u0A5h^atH%t@?@#JM0&FF*pQ9zJ zSlAHXb<5l0F`n^BI!7C({kPtS+&>>|(ByTq4l(X6?CnP_h^!8kLzdUXb(%`DVX66D z4SA`x__y}|+$NDhzd!mkV+~$B`X2xbK=i*D3AF5c{&D?q_#)$P)c*2lZMP4G&$@H8 z`GLzLjf~|%hb3j1Amf1KBX5_WKX$fcgUvB|F4RJy2_yJBlwvby%_hC%#v<@KKp0{f zHIrC`N(8|(K(Qz>0YJ3p8DAk93ua(HBi?hjgopa1$OnXriy;I8`4Ajb5YbJR4rtSJ zcUHTzi#1{-%40Izm~11F*) z5f6hzQUt-kHzK|uXicRIRfQr!*AP>HA;eE&dt3D=eNtg8Xok!ni2PWO^+xc)6H+e3 z=#LTxqDqK at CLm~P5=v-DfuKqyQY3S0h{PzkF$BRg!H5Zp zV5TQPjEnwaUhWYJXA@=Obf?usDxP8w&Rmix{E|LLF7{IKe`a`t`)Mf!w2oNwttP-jxsAF0TUi1^%U;?KD;fnFF zmjh`?cD}S#?tW4T?~={FMi`0lj)O{lkff6m zK`ToJ at eGB5mS83of*>U%Af(C2CW$6vF++MWeCNhaAPuwbC2%fa5t0iaqBaxT4Z^7& z9}1n-3bUO_d^TzDV;J#bVf8jPY-YyEh6FG$AT-23wta21A`sTL2}5SVh6K1_3=F_# z45kdhe~jyi4F^#N-Lzp&Im*A6^#;R-z%DyP`@E8hupMU;IK^VUjm!u$tt^yLxYA}1 ze2x15j?feIq3`}ADXxXsI48X60p|e_B?`$ZF%+O5A6SV(96{dz{$fGMDH>OpOloMM znNXN6x-rVDp^=e*vUw3SO>rYWi!mlJhZcf{C%Z98hLc!eLzzf!)yCAp9i|L=Crt_` znIH)P05DGAT1G+z!a+!cLL~tqfk;RefoM at efC>Qke4)O7U(>F}_KZuPnKC6OwoVeh!Z<>t zaI+_^UsWHFj0y~_tX5_dD=CGPw6f+jjnW?PMLSg#C2sQ=vqa9TOEHMS8D050u1X)VK*AJ41{Bq*aLm zicrFGaFGathMbg&r?!6t{qk%?LA8M?yYrZ3xf^7&61TAK+Z9$O)karw(I=B|-#ftY z=}|@TNkO2ZhLEC`gbEZwBxpqtHbm~85I9Ma8Dxk}(4mdfEL*tbBB?HxAC{m0= zxJP7DijP6nJjDZ|MF8+n_H>*; zr<_kWlS2 at s69B@M5?M#vJuuNo`Pd-;w;GZ)BMd~WgiLaK6Q`PyWxdRc at uyt51|%9x zlPX$PlgmbC7-~;E!J>vDkSP>>2pfaaK0}AoHT0dP!KA<<2#1d!*?(r9ZlXTF%yHBC zbL;+DGf8kg!eG7<9|#W+mLp}hehbmZqi4GD35`Wx4nMg_ybmE8Z346&s0kiS^uV62rUwA?V3Q8~s*Vg_O zpAUk5U$E?53w-O0!}XgbNwKkphdFB#D;PpStod9j5^g0bTz(OVY~vdvrZHu)XqZV- z0+6M8KpYm!a?K~1ftPj;Q}Nv(JFI}*h#jQJDJ{X28zEB+H0GJ-0LmtWB4(tBJ4UCj z at DhZp1~3AYrbvU66OkiK(64WXw2aHwG3z6(2WMlwNgXi443AN+Ed)tRNW at x7scJ6_ z89JohnT*i)REFn%bm_|JrWQKGgf};d-?LCvE2vyx;>HR-ax=Fd!-y0MqwYp at dwRQ*=;zJ^-CY7PJIm`n$fP!Y*d7XIi4o+XDbPen)50j8^O*eJM)FA9^Y^h_S<6yjUD|tl-fa?v3{FY`NZ8;H>5PEf z?@j=2Oo1s;16*dAMdQ9e@)M9{LhWP&FaWJhxLtSrCzCzDjWC&RQ*@TMaDErRm>nK% zLnZ{FN+&nf^Xwg69r=y)%xGhZZhA285Sxfvf0q3=O8Ln#lO1)A_`f+S9~fWZ7ZG4U zfdu#H0_*AYW`IeLnD5mqq{WSebw*RBEC^3JQ{Ggovhrp&>{XkvzxlBb*r& zo5X9At}3#XlP!x^CK{dXhu;U*mkLBlZehB|GqcWukd=p0` zaJ|y}34bgYlLlHfn!+Eqi2bMA&#%y-R2R7#An5KWa$%?+LpUlhYec6Q7z`RP#X%mn zt~DbY0&T4uwo#)D!zhX at o10OnCj(uhgxS|79W-3V5Q$|Xp~xC%G4GLo5YfIaCckKQ zA6coWfFk;%6(HndIRu$T7_a5b`-H?WH^0yMwav*aa*r=Kiz1wULTF9)+hM7O*}GUH!->91yPztB*xbbJ?Uo!&}QRUX0T>D&`LgC9b?f2;AY0S zd114zk>s>{UUSGh1X4h2eY&5FE)9%g$rC~u#3K%*dm;>m!^rA6#)fh<-42IBhp=`O z{?mNDRPfItVkXwGx_`0Ci<{Y4I3;p~LI}$z3=9UunHm`|+>-8I|MjSDRjG%!{1h`g z`*wUI+u--dEfpWkf|o`Sle>cuASIj}i7-~Ag$vuP+-~4J at UtJI$n`qi2`IE7>wVA6 zq4xV&s>{)dav-uwb+v;GK?0f{G#9BtjV0=@ARBdC&>ekTAkrPsz;Ooa5|y;pv;0g zBB%=Ej#ULv#3V33fv0->%VI|nj1WIuk0gedDh-j(M~6of&5*^-kU>}CA>o1wp<)L{ z9`{;#A|SaA5VxBnOl?UfS|-XoiwcOaL`X2%!GJ0U5Ep_!V9 znTUcSp at 9fUSdmC*mt%+<{7$o{Uyvm0C3^%7mZ&lrnSqU_our5nMP(pr$n0Ti*KXbN zMstJIAcI(7(?UZF+VyL7Q$}PC=kdK*Jcs2$-lPD2P3laFLR1lO{^55ic=OAFYWW z1wM6bZk9wLkPGcS5%dfc1 at u<+8xI0kl_1~l4&S(+moVy)?GJW`sVY(h2w&5g3N~UR z0umq|4yH*O29S`okSxVxDk{Rt?^HdGYEpQ&O`|gxO-v at G!rF9VWBhm>Hd+v%B3Fk1 zc-j-ZK5uN*74hcC{y9SDzR}X`S^LPq-OhaJ at h^x#Q||~s`D at N3C6GV`Kv5C5^#T6*ak0ULF)SvlKv(iIF at b2!_ at zoOO`6rtsAnQAJ%QK0pE6+ZJ?G`llE7&3CO;V4jPAY2~Kf>en}f+R?kAb}zpSw#X&;Fcqt zZn0G`$cqsCoPg2CujIxuLpxs#uL!oX&H^XTxy#|`NJVPaU}VBUO$%KD$RUyuT;bKu z=r3dUeWUezXGnOYsUEL|InAV#;Pg*;R8?7I^~oj}Y%$=m(%?kULAIK}n4vHUU`b4z zQA2OXBY4>j)(zoNg)*Y5Rbt(_%ZAN at WTqL4eOu?K(QM|{wyY9Qf6W-|m0smLDpoXU zkDD5)rDCvSDKiEu at jyPzP%opt{yjk~6GR$b_%Md4A~{A7ZufQGDxc{m^gyrN0QjBm zm3w0E at 7c3?C(nL{Sz%0(^DK{H2BlzzNSPdTI4O-oB@)RcM3J>8T|OOoW at e6GAy<+RPW$!9WS!FeUUJA+y>t|KCs`U8`-KJR zlp61X!&urToR~#eM%p1Q5N+Sn!Y++-toj4o8kzQZKTv4<$X&3%&;6>>xuUGG;3xc2`xcN51^d{qbhVT=>6MWM1fz! z5i``i- at EhQOcxgjpRK{A2%*`#2yM_boyUNMuwzMH{(ruLXADbSs6XS*K2_5_l zP<&usaC_eX^uqz~)AO65$K}#&YbOX7;fGG_c6K|oxGU|PHWLNlb2^it+zBdqoF9X< zf at GYr#Qt?QKT-KZ%>5q24Va2-vl6D=ZDS53FePlRD_0tIh%){vpgQSJ;ksL^O940YDas+xs za(!KpMj#x&iT(*#oOHj*Y(Y?`5^3-Ihz!t8B$2IXTGXIR6?MR1(wIzPaYWj0BDI~utfW}Ro4Sm! zWD*R+fb|4&VZrRxR?8ZxLx-7jrvo$ktl7G)nApYSVHrfw!m?mMI0n;Ww2(rBg&o6^ zp%~k4oE!~EkAh{BhRLwr7j~%>s#-*_pr^H^1egMokiIiqAt6Sh&613x;HwbGM2HpI zA<^_V*ft_>Z3yJV89@`3VNz7F02vV4#u6-Ckj-G;hDDjeNfuDYz?&GDBztBmM6#w} z4Iva5;MmC^G at 9rk;jrcYOy4ZwYdu9*G%ZAoQes)LS!A?WrIcF%(&Kh>2%6=D$S^SV zVPVousj>vZZR5lX6Bx!695T_MFmYf+;%c!GI;0vzaWiS6Ybv#XKxxRL85AQlj=}dX zPzsr at 0uixr(TzqLtvKTq61pP-LOk;;!ZA){#zrP3mT<793Z^WTOyX-rtVG7vY|v!2 zYX!!VAxa2 at iCiNsHW9fE)?#Q4PDM8GtyVO~hNBXYinEqgBO? zzgewoGO1{(GDg9%v|36=NgD{o2N(>iRmGKLG;1MLTH6^-YZF66JJGDoQ9BGw*@h7^ zGXexbYQjkjAi_#mgsM|{g-S$GvkWHI8x0zQnGJ?EtWz;il*S}AUu^*vF_0M9B7*^p zkeQ$+jkAigM`c at zK3zGlt4rTz(f+%(m at 1_0d6=nV-XEROB1YY3OUg at V3C#qT1<^;16J|E z3Me7MOkzn&+~Cs#sBoeb=*nTTz$)C#7K*!Mnz~Y(E*0fEay$0IYpxr5V4Z)_bohDj z8lA3SVcZ5Z(xoXwkd!7#gqmz12#qkIh6ZW12smaYjBqG5G{H+c%$l|JG-;^}V0 zOp*Fq7#wWGa!pGSkVmdEN+9!C2aPl2YiTStlh(<|s}w8Ih{Z&zlq#iFHxaG=g7e=V zRi>Dkk!$ud!Paf*f_9mi(TKzZbc4HTu)PhHaFEkN4 zX&`B}CMOsKw6nNG-Wzn_LK8#|0i#0!tTkYHLeCr%URN_pr!z4LpfMQf0+|9tO^zlc zpwk$dG at 01m-Mhu=1UnEn1_}*9lu&G}D(IDkI?|;A9V#Lsb5fWG1vrpH4M0$7*ZA*x zgQ6-l#El%#;zsc!v@&FEDY&VrBx(rBBu0{xq&P+p#B4Dma#f at oQy&j0|987H48QkntHnGuE0|`{yfg$~8 z>Jx6^Ruc&`Q5LkK#BVenE2FKE)dXz=d`)(lgfRoB+FypXZBKlX1fbr-(Jsn65X3*w zgNLE%*A)@>%Sglm6oDZlBmluK>}4VmFeHf+AxaUX5q at 14W031OPdS3jghDM;#fJD{ zwd9|sjD*@xLgoyn2|0!`m};ZKa|z2gWfTVLQbR4bCQ1&USBglXVoh#IVHO+-9+VqlV! z%Sh=!*3>YnY={{m6(R`IRjI)ItVz9ZWmw>{Hj2?JLL?exN~oO_QmbQx8CpziHf4Db z$=C$M*rtsb34 at x-G9a@Ju-RS=Bv6AAD9B|hQKVT10wV}=aEzB^;X1%9Lg6!Y0h3T+ zFo}#+LMIiiK|#nl0jlX8IY$$zaH!Og4uf^9Vq-8cY3Pv0Y{LxtI<)SMF%5(mHjSr2 zHSm}*w{UDNEee%IN(>|(!Vc-aCtI*J6qGzsWMhe%u!Y)AY%n$iVv!3D!!$b$gc9o9 z$se5lWM?CMKHKd=$xgh(I5%>(4 zWFTb01eM#|LBQ4s=7v8oemlcth3-qAXQY3ynu!ULl1U(_S`mtX4Ma;rpJEuPsSDb2 z4_rA3rCdXkqe%oS_au+Y?j~vy0Ad-MQX+^bBv3gWLCq>ZYNfi&iR<6!K9*$qoy6J%^krpnA2UJpB$8BgbC~L|r#zUVj%gUzJY0?a z_TO0+W75-NJ-*brD&XY)p_~Uft%em$G~C9MryI_3E9Xm9El-Wd#i>Z zKNZy;j^U^^$S}{^L{Wh3L|&obPF<{pQcXh;_QL96q?HIJ!{iJDP*AJP%tlsCG(m(L z4Uthakc1Qa&9!#!LyD-xz3oGXbeOH=A;Xs8_H3OOpj(Xwh#rqbROoB-A1~*&tFWHg zh-8%0kj5AVn2tPXtbqqM*&5x$W zZQC@;8feHG`17_p{K@?iYowR34nLtk%0NVm(o+wXh$GLl^6IpO^|JAiF1RxYIMZ?i z|1nI~LNjNvReU0vS_MpVZWxi?rc~N|(q(^2kudqaN%Owf%VS~rIWf9NlXnr$!xQ&! zO?`g^e$Uy|(h_h+N?}AT0?{gx0}#X?{bx<48`nv^?ly2yV|GT>`<7*@lM2#oZP93`)o$yjTxWtc8SrREs?Z+5~+y-20|}sBBcl!Vo42$-6Ve*x!xz^&OON=lE}3r12RCf z8yUd|J^vzsPY*!B{SUmiCr)WQ3{J?M{P`*7zczT?JOtE=j--L at CpC#bl|#CFrv$YH zl{@d}L$l)s{3dQFN|Z7}q?-{?0$4PRfHZvkjlJA-nE5577|1bDwg``z0v`;}4KY0g z&}G1C9vwS(QLlVwJHn0qCUGSdB6X>V!cD0C086mFX&V{zAf^HFmEhZAK|OLqfMnhS zB;RJzNfiPS{|Fco2u4+j5s6ZPLv)(nkq?PBX5%&(1T9i!i=ma0!J*dKEPypx28z{* zkV9L|oIq|zNeoq at M*!62#vy~4VWg?6CRg;c)6cstvE|I=VB;`3w9%3pfYvx<>E}5N z<&F?oVVE?4G$K%w4GjpCs!*U5h1d#Mcg*!mAwS_If<7G3=9IOB%2XL*A(91=ic^?p zS|+BXWmT0mSkZ9GiG@~76chzW6d0Hpk%E|#IIi?B^o(!lIKx2#p at Aa{Cj<)QioW?M zaEnqX5(7;pz-T9rfVQV#^v)tUQ;@jENgPAlBF`NYWW#*y_dNdhok#MAR^>$8^obD{ z;!X4)4cn;_ at d!RqQh9(p4IsG7Ep81GI&^ejnwks-C_o!VhxkGKb|bKke(%eny;~EM zkm)4JqdAl7F}haq@~~Y&h5h22$A)2HpZ&2b`+KYCi zs--E5F)cSrWW;MR7Gjaa6p0jN6pm!yaDOu5@^x4x1ENlcxARDc`EKm_e*D}qFyH4) z7n}VFDKtm?A)VR8v1ADS3p)gX_rUDvrp$cYUIE>sX{6j*S@BBf z44r6q)bM!YzJ&1VY6B?K8h-+24dA&A34_m|=Ye6a{X37j$rT#kB+g)XZ&2vY1|z$> ziEs=IJkCh%19Tv=wc(A%>%F;Fj=~Fjy@=Nsk!rdcklRH;xVo1r*@>noM)dBk#JP0I zn=g~F^@XS at Z>KjsPa+wBzjVOcSe9!R9N~|Po4J>gvpJfj?YwfbmyZqc7g6Ur+b5Jz zC`GLWA83EnV;OhPvUhI&zFZfz5|%nC8-cxhr#*-?YA(l^L|{C^Q6&ZlVIplP2?}|& zS~c=|7dT8j$3ucbkT9 at 9Ulyu6b=J-a6{Y5z(2~XwRH5vmNSh76LRy>O>~`ITqOx&g zn`O;L*$)V?Dr)QwK>KY^Rhk}Bh$3k^&SbKodf2qJ*!rD62$u)IVY31yvy}8WcBvlBq0du z)@kT63`EolEZpxDFIE zaYeCu$4_w(lE<*&%JSuF)Z=sFd%Z&$U_vq>Fsu+H at 9;D^@a$g5U at q>>rJRnzWNwWy zc5QKoqNZrA=JTtb zqfM~!w665I*$g64!e2Z{L0K$ZqCkWsjbOFD1+F2khIebUfZgopZ17-YJ9Iui;h?O{ zRyxT&NMdqnhQm$!$ z;5jmQpz>N>M^bL^-<4 at VaP4Lze-hAeZb7q-9NC}B`$451?yU77bkRD&+Qggk;TjWh zTpNX2+a9mpT$7kLC4`J>H)2?%IqW^mCb^jwFgXyhG+J&2H? z;8bwkdO^-IW#kq|h;}eLa-kuVvv+=)9Ig3tp((f}kJ$`TZHG%HA>eUucpg24!iu8gxT17!JW*&Y{ZP!-nKY2*2s0k{ zc&;{zM?$pF+Je*Nz6`z?uZU=3BbeR|CS;q;hb$YEIOl=%P{c7{K!QjeH=@oKEOEn~ zmGiQIHh$D0S3{X(&s5y=*NkpI))f at xt({F=xd4G z|4i1?lp~`qOUp=f4c-eN7b(NHVm%$RIBwk(H17~A=+3$zlRo3XJ*L$4vEtnfB+2P8 z?XHi`DWia?oQjxJ+7Q(_7y-gy@*J1$=LIHB1P<5X&pOy<(YsI=MDZzCs3x8y`Zw at Jt4_rXmx>v z5k-znmC1HC*hw zeK6JIemZISb`%b)hFm~yKb%&`Q@;7endE8DG|l9V8xCCzf*b%=_(x$J zs|?#l(~gXYCwLVUHn>=r%KW{iZI})Gpw_{#{R*~a$LbJ^Y|tHxSn`q9>5L(o>eaLA zS=Pz~&Di)CE+M2Z+;)Z3?@cnsSD1qWAoP47v%6f}v}dkM7b at 2((Ho>_ae`^d at 5otJmoOsl2H=OZo-iU=S$*!d%e?WT4Wmuj(V7HdRS!#sy?B)tK&Iz6{WW*Y39c>qfDVf$MJ~zua#=4lHY225yn$x;Q^j1jL581NT>c|R4 zUT>=C)}z#$uvmNVp!1j9XlM%-?ApmpU at 5`HvN}_>IoNYiEh(CqDL8gOqeiCuWi(2A=`1>cP4Q70F&23r^_TL!8T{8$lG%pQ#7{R_o&|WZ$2ye&4RTz(D z_61e1q*#|oLA at +WHyLE66ho3&(0KZ6=YGk=`-g?MA)dkYR%*v~9bP4r(1U`p=Vr?WQ89f$| z=MMRFOIkR00HJi?X4XXw at Jw)^r*=&4?2xJNBXaC>pP4e$NA-JfN}XlAuq49^Ke`1NTM zM5NppbEFI9)#aUYex1$-t;E%f$r}@^TlyY*wU7+K8u^}nU2Ob%_3MJadqaoLH_V2X z%h_^mUegRZ~<;o{9Uk{;%hM5 zh`MWN{2`ltw^^1YOrPHnyRLLM0a5~s at j4M5UD+K~vtuOmzP-BCqJ(Tqp2E^Hwq6|D zaoTW*6K%~y!$yhX9b9m^8yT3*-g5M8EEh9e8C+X7D6JyK!PUaoPZs}u+eB6;!|7EE1ywdZ%v4Y_?OoL z`2G%IDK#E?9k;D~CgTnsnIxObrnKnIHe5_k8o`TLrF1$jnqo0LiI16knIvl;`vCV5 zNZlE0l1G)J8)%CJLA(feGFit~Xf()ZYoQSQh&mz+IhlLz!>p!kyciTLEha9RriXM% zhdxQA1e!yZGT94FOd=Q)<_H&-2s+86#>VZD5N5-Hu$l^V-HFXyvB5NAxK9bXykoz2 zg&E0^j!@;Vv=Sp|E at 4$rod>Zix~#f+2wA|mg$FGR$ACJ zA2^D5^Mn$j$HYf^?-K-xY_XogR&Q?;Gm~jPcgTL*kB%x)Z02fDGwj&3Fq6+Y7KBM3 z#1qNAhP%M_7<82Ae%u$Rz)4L0w9fWA*w+yn;?Md(am1~`Pw;=KXAM=!P#gu7kc{2(hLfh z!1!h%rGWmBI<6?tz8x3a^47IZjwJP}Qki0T6S60EjO@#;r-1jl=6c6aQQGvl+lO)Z zjKVId;iTp~)fpa5mu`9)V1voB67btC&!wks8m(%WaX->%&a!F#UYogleOgoD+u(Ww z=?ExDlo<|+-_H}p7k_5BGCmoaA(hl9%76pUsR1n=9F*8ca)%RP0PH at XMv&x0oY3(! zIyXTtr=XV{oXnuHc`X)`kqq~7uVk(EB$pYpZ+qy)$*Hg)9d5y+BSJ?AYzF<~3KcNm zQ$d{wa$*(&-d&?aQ3eOA4J^)v98I;{V&f210?9cJWaJ_}Ny%-N&gRVPC7a`!ldBV> z9a;_gFoeo8F*3W=9VJiPQdLhb?4h!d at k|m($QdI6M8U0)OA7 at ewz~)l41)~DPODIr zffB(e4u+ycA#Bz}RSAWf!N#K3qLVGfhk~@VTSgZcs;e}jw#CLYsI}jZTU)zFig2AR z%=a5jE#P}s7xUg^LF}A}l3TZ?`0{hvyml-HC~EwHKW%ZHqpldzAb|e?J_PRRBsCP~ zjkF at BAi+bk_QAowaj^X$$ODucScJ3BYDapj!y;y3d(qV7awn-Ark}W3^kMni?%jej{5$4m8!@spRy4rVg>0}iiH~8v=9*;Pq}-Jr6;D6# z;WTH!pSePy`+Adsqn}RgKhi&6x4BDxU-N3uu#o8e+ at tcPQeQLy&}~Hzdkkh~Wg_9; zMVxKPkU466bKSr`mm~o_lHoM8O$3rbHC<=DHvr;FFFE=M zK*>NeFcTmtM* z3PnAi7R|yH?|nJKGi~J7i$vyNZ$xAt8LDGLXE0{cdx|pcXgJ&rwFcN6 zwA$0H3}E*1hW8f(0C!bEK0w2o44msXyikO-Fk?i`$ypW`KxuKt(jG!Xd;voR)aYuL zVYhDg#Bw*zusGhGN$L=37~CM)0LK7i9;|6?hW7=tNYPv;3RVN4qDX+STsS>%&jNOz zASWu7Xd)sQV1fvUh=!M5$ux(x?KFY;NE{EiVE!WlfuN9`C5Xnx96D2Frke}RidqM5wHH6FHpTQ+}AEh^U)Hb%3VhX$US#f>EEmOKheGsRD$=F{<*d$fn5 zLBN*H6ZLCkHSQtAj<-Av0rd3bO%irGbmnLtgc>8H^5%47ILuwWHQCYJo192;p_UuJ zV+R6$*-T9_(JQgne(^q(J;gfrcTL6evPCFmN2m2-dxVrW=Bu9L@&2nGNvB+POd^W4 z>CNG-bGAd%4;EQ2LFmwMe=D#$Gw?iuw+2T?Ogajb%vhNfL4a(_5a at +>eJ9W0={y79 z=jsvs5567Y>gapjQv0d7Ew<5V$TyLwbW;&zAutR)jp2-Y_Idbb$D1&h>^u{(23CL% z%O*?cLUN>__d69M)#M4GeEzQ9gl(c?j=~B+{6=51E}?@>v5sRi6AiF)NxY5?9O9~q zk6Eqv84Vm*0I#+GBqYAcV82(>$)ZHkTDxxNQaj% z+jlaf4w|hdFuUHdu> ziu at K>q=ieohYG6hzk)n@`rAF>U$dm2f~5Ma+xy at lxpW@kRrwI>fl9t|5ezUq5$N97 zF{8ljR$_cdAQ&+eR?a6HZE%0Qnl}yWF4|(ojZc~X5Uq{q6cS!wQaJEnhyfFdR9&ov z1rkG2USo&OoZYZkhd~`Ca7#uzIxann7I2o&?z3b-Lc%E3@@1M3+m>AA5SL~IXfF1@ z)(-)*YOs_7zR)v9u!f|?2pKhood#5cF}zSM80I7;cBDjsJR=3fDJ;WY05BuCls~!` z<@Rtu9;7!j<31in&my*m49oSID$&OyD7ks&K#7a-#Ttr4Fk=2g9>Jtik5up4W|pCr zcCA&8Zs!THQk0|`3^X{<+Jj)gfufAGHYpWTB-lk3NiiSD?uW_u_%=SM`H2B}A}8O5 zboAkwr_pxRFT>CDA5XH~vNjnXq_-FmE9^m6`7hM^leg@{re!OF)I|*9GjCQ4 zWYld#7Aib0v}8_jcDL~9)VkQ2&4 at XyR5B!(pduoRMTA=D7%dKpfHp=Dxh_mhtP>J% z5P*<^s}R5`BGrTegt!x_DMZDALs5oC5Xq2X0#Q)8K6a7~D*~8zd(hdWveg%(@O*on zQnqZ!c=_z~PCTN9upZuwPgP%PQ1@(YujAJygA;5= zxuh|&4IdAq3Lk;=9^mjdL`xm)Q`2Lh4+QPqt1V4rlZgp!qy0^|+deU`crV)sC+`|7 z6ADO75 at CimU@kJj4}=-V1Y96&Wki#r$ePus>C}bnlOEx-pB$-me~GPYrgNGbdN0CA z_n48=qCemIeREBfo(6*tl6AVUHC`^h#S_OMNEH4s3CcePLF;n*BCL~JMCvMuLNKV4 z=VaxNMw>g$+ADS>V`GWzr%nH_v=3YX!Q-1N)=!_s_eGhRi=gP9xKFYb*3NIBo`0dS z^g_3<>&c0_v11E3 at x{zP98%b7nXFHTrZ^yvW;`r~cf+=m)NV3o$)cG)2D3w0 zs!JS;6fH$ashCw%Efp4ty$$_1niFa;qgd9V`w3;X&(}-HK7L&H8!_Pedl%YYQ45fH z=1h at LX=x`4ps7ZIp$KULL(A^j(`UBg=dU%O4`!cPBRW}RjM5DU48{RUfe?TYM*+_ctT~}^YX)Mood%1bMber#y08)m z;Tf}JDWTO0-fjhI8n#>!4Y?z5akSPbWcp6P*=r0q;R9?qE)pC5b2o$<8j2i{r525e zv1kbNxHpC~Y>EL~)4LY44OSyJ0i-%AOQOZQHs+Nh6c!uO7KqG7w#{5A4TJ?q3Q>Yt zv!g?&35>E3WI7fFF^h&h>9pO@>t^Q?ews%P*G5qI+b`_fZh}YRL^*c>utY at -UpK+jbt@AJ z;kMA9R8@=T>7)4k(*uM!_1tm4cC}PgQbQC)Q87|UTMpy82x32RhurLUiVcBU zX&+L+`BG`Po)$^PyJpP9wueTV26_ty9oBPVAwsl(awmw!yiSyt>QHoFoKM at q3L0!E zI8Yp4qFII7Ru~BEQ6aDLY%;n(9;as$RFWNOXye#?WF(&vVd8W at wsH@wp_*VvqZU`b z?9!u?;<8IU^N22BL?#STkX;7w*hxID7 at VZhocn~=NayqJ(v)SNz6lx=Wm^iRRi&|C zZyv8LboTyDHDjODk2i3&C6sk+j{P(b7D+qfbj4dJieQZB^&zAVrkN(umpOA}liA_3 zfRiF@%-R?%u$;Z5DOSIQ`6d^qk>jdb9eDR at 1nxci-YTowrgo9p46X5;7FT(F797>Qh0|tUi$&;$O@?W1 at x#VB!acO at ZsD^u8QDodcqx?HRdK z+vJnN>kndUS}|%id^YCM^&>FR8Q_>7y<=$;;8 at 99+0dMkKUwDd;?R-l&>&NR$xRqK z2AK?$vMTA3ajK1k!gW2QkUA7Ey at zG<^+5RL`2!9z%reBCYzZVT37!D8&zHOwvn(frkTa8>vJH6QVF$g^Y}N&{CCy z9|i#hOpkLQ{O#L$ymyjMuEW6*YDbHMO0x`OOA4aYn9rk(+WmhYv-pm-QkD3;_xv9* zxbg?I7&*a={IfDFg0OcPhq>$TbD;$T2EJ~SrFKjr*l3>5Xv)SZ+;p)cu{gEr&2sMI zbL`N#Z1Y++6XYMeG#iMcYbK4~l;YZ*UUwX2Jf#}#W=Ha52#jQbEGw~TCJ;kmm9 z^W1S{!#hEeZh{G<637eVa6Dub+a=JUqVF5u3Y=GE#5>mj*Fm( z(xNyj9F{~L0y0D%ip#+wz36ms9;Z+P6RPzk;|(XFBK7Tw>pFs=j?RQ=cENBVt_&(R zXERLqvo?+YL8B3*G$sQXn;1 at q0*6uH{At=n$9*xi5SE&Bco-8B7IqA#tnSmX*zz!J zb=yf3tfzSHA{j6(ot1#HA%R)bLnIOv&e|xdy-0H8;xr_i6~dtgP5GN_Y*|cV2Z?sF z5OQ)j)51aLQmF*1Ad2708cC`ot&W{>&bM;Iq&XCK^&EL}dS`G&BFGCT1Dm%ag=Tll zJUgb8z?`8G%dyc_zosV%Z-yf(V7u2IYrDnQz;m*4k|mZ(LOAyE=;q|oZeB(m2ZJ<{ zqSZVKCDL1>(1i^K%|UJG+Z{O}4D%lL9*aTJlD1zk*as-0ktSCp43 at S8TTX$iy5U!3 zt5puf3x`OzdtFxFaqz*8?aK(L5M3)dxX z+ZAS(cP)v_5)Md6IP60Z9lzMr*v5B}OI*(FWhJceXhSk*M-LgSVu9G`(m9hj5aMR# z4NSxe?W+?6uGI?D4q`9W_YOlffzSpFv3J4fxxa2JNW&{WsjSU-70HMnBobp4P!50mn=*GtKwn(tr z!OFG{GBO+(aL94ZjrYA3K_Lx#Hp7)>CM;ZfvrGkQ)@rgMz&V*rx}iI;nF|=f23Tgn zL~KD9HzeB}^V0hD)Jw;hOQMNcMs>|p+VQ6a4CfCqnaGVD&ms)O)IiMj%^3l+6i9&x zN1b`rs at Uz^R8^99E^_lxu29JB%Qk~s4kT~LS_czGwuc?9n7+-S_|a^!0U=wLjY~G% zEEdh?4wk_TuTDTk%@xp1$#AzW*~>a5!SS)pv4O3=i?XokjSj#N2H$b2^R6MdW at cez zi(P|KqoB_?|?t%v27A#iw zhhf}fO?94?*ad2Kj`~v-G*x{iZwmR&k4qd_50f7GTGAf5Qd5TlNbFurUxOnz_9kG)x+3=jD`G~t&$~NFnt0&G9o<>nu!}S~r{KHZdo}hl zCM;K68b2cqhD at +H%;e*TGK5!KSgf2d3CmG%M;(wm5X8Ho*bd^cLnR}S4nYRUH|jR* z=G1gH#*H|Z7Bs`Kskj8l6E&AK<+p+$#s^?Vo&)6Atr`~mTzEYk;k|5`i?s*A-;PbH zEwGz5%ibiuOzVl?X1hYz-x+O+Pi>8+^sLZip`hG*G8paB2mt6 at hK7k# z;ggx<6y7eTz{JS2X2^ijyX+5Dq$* zJ2j at Cq^3QQ7qbd=BpGP4a|Dx{#D>g*5qSw9Id&ghLr|r}OyImKq~JtNKE{>UmcVrf z=wy|HS_aS{Fm13&+blNtaS5{T4lw}xi!{mcBOPHpPTWE9g9CgV7hqI()j<5`CX)r! zkRzN#*|*ktoGz{+SCHvA^X*qxv4ragZ2_jRO6bHBP8R>3D_J#q5?*5fK1;_m2*d4GbG2`E<#JN z3FAniAk`V)f*8+QYOrH5$;I|@t1ON9&c{m at qZDP)ra>{YE;6F)9N)Kig}lb{M(x~} zEShj{ds^Un?>X`z3r5-u!S2GdWRl~!Lv=}CS&dW#lB~*Qne=BDm?(B2y9O#PBhZ4* z{e=P$i!52%&O2d1j3KYRB(X&{lQ$z^VX}tv^4{4(6O8ZsxE~mW$ue^HHptF2acp%t zDVTtehb?y~XieF66EvH4bFqW(vCKObyT)8?&4|{j@;0n6?qxh2 at x5@;oWk*;%L+*L zL%@{IBb=tI=7+BjoCc1Gu_SkgJe^3#s!>G+uO9w0gpTS52eXHCVif2E4h&%TTiE6c zJBLTVTG~ooX5>ijV`o7xBBD at -wyD#QI=HSm2Es#@HDhfSA=O^$%GR}&i}njDRq`1`B?NJ?DAK0TP^c#SWn$ zqH!UnAn(&;G+Y_gkr+yp%NS~6S z7!qWak6n=YFB%(#mTcVEc4)y0a<-Hy)WHz!Ym*LGLke(f9{mbkhSvslkif!03zD4h zYmu8Q4whBtc8ndBbYF7lD0usQi`5QO9~i6c_t@&SjA~6+DRf%>AXa6 zl+vX*a|#$)R~_JBJwP4drJ5ERXwkTf3;Zmtw&STM)c}0%==v?Cv)#5rtW<&(9w9#CpCwKL5r7ci zMgo;#C`q8ACBrh1hzvm;Ka2J}>=U;RL!L*x3h+K^@nAMy^#*EvUY~vfQY2pj|9n7vP1K6Oywqc(#}L}eD2 zESMZ|?G7xsoRr_Cxv{WH>Wty^G at E!x>B6c?vW{cW1d9?BB2rN#MF`3?426&L2FAf2 zW80V+>DH4lSKQCXfa@`;6>5sV#|q at 1gSy2iCGWjfB^3{%e at CPp#0!XXA+s8dkH at g&_C zTARaNJ&{B)2e6FjkbNZtptP&_;6F`~=&~ZT*`rG&no=0B{(Bq9KDZ2!47b9&66KQU zATm?RQB9C>aMTCL5cCOk9SKK73BUdk%kugNPTfvX05 at p(KeQ zdaaueoHTSoJL!s3n#@>~<|4_Gr%5q at s^wzVtRpXNm~BxyM|MW!^gD*4?So`!B{m~! zXK5k`hq_0i!@iEqE*qy35sIuU5i2T6z=M&qaA3_rK1##aTKLW zK*WLxAy+3!qQ%5XVoE`vOobC#VkDrlBCI5;fh}J0PnyI@@1ZRS%a#KyGNhJ)LMD(l zq4@;Th`W-JMu4V}p-`dyFp3AdM1DC&BPCOCr&$04ArPfhss%u$L=2Tj;w5~7L^mPn z0{w6 zJVZyVO^QPmyh`*~bs{Hj-|Epe+oxABR9*Qccn;>RF%m`szg}0p#td!T(kP`BO zQm6+%Mf*JSBVvQg&I0?ELm>B}es9n89|YkHPC`#1^SY(BTTQYy$3{LnMMs*GK&awO4-~?Uo44Z z>U>&;?tHb)vauSI*m^{dITl8O;XxW0hJcXC8zK*oz|SCqy46H#6E&!N7r0B5)vJ+Y zUm9{`xMlSPYML3F0HC2=q>&;#U>>GSJw_R%r!eGI at DLEvo3u<3$OjG_>Z3CYds|nv zsx%RfU>Fp`ILX#^nMM$i%1FgnmYNDU>y`~-O at vTl8)HmjcxELYWr8DYp at AdDO+gjv zO9+VLV{;YC;%_|UCyn{O zhrLC$EmIN^6+m+l^}!O6C=DaTlL-m}Ac>ZSQSM2=B&9><5=C-A3Q$eL>QZJ^(SSsN z9?22>wJ7M6Hv{!ZhM_2l5+&kS5u0*0^cOA^$YI=`AGR;3 at 33SVJ1zi zP#Sdus8dh~c&HjvXVuS}vk$z`4Wk#-m~asEDzcB?0C@(qjcIzF3FI1lL z6zmUg0dxl$nEo;OWcLz(b>MObZ!|=}-XbOlNJ1Ehm%Hj=^cV*6MiU5Dq`yKFz at Y*d z`3_p8o7&~jO%z<1F9j}*4RH?hNDd$~4hxYzST;f6Jc(j4{99)lCch7^;7zGo?Knj# z`yvY$)DXyn`;GEhkx$2B@!DOJ8NIf4z2uk?h1(&pE)>TUiJ_)SMhu%2XCe1XZx6M4Z0XKj at 3+#{Y_VC#1SolVJ at Wx#_22ykNn zh at hL5Rdzz9M9gRu5bO!c{&R at Dg;SXjg}|Xmd$^?q128DY^U7;9&^%NIdzlmR at Dh#C zMC53rE)z^f15pRJj94eCFT{6yA at p4)dH5I!!hx-oq9TylIEWt~!%)vH`LhWm at iZ-x z+ at Er~*Rpo(J|3t%(;MtLiHuHWdU}%peF^WtdD%L$wh1dP7KZYtnybXXecWM=>58p!8Hv>mdP3 z6;B6;DBeKH!HM4|x=D;XuznHX at nRMf!^z at Z1Xq}1Y2JVjdM9}cBNBu{okwE;q0NZm zPqRt^x?4+-kpI=b!%y$;4=PJ37D-_&1(;-lNRWC`7%*_egV*C8>C>=F5})|^kH9$N zgas`%GNbC_EuY_?o$79nDv`6|`JwmIUJ3Nj_#nj+ghle7 z^OLyU!DM_Y4z_!U{puB`n{g3EG{>^${IW#9~DIp(mwCKk+0!5A^+Gl#iK-U{8dO>9P;5Qj~ts zfZrAky~^hxx~zmwIo~`?_!6ZoEnx4FWLrqmFVQEMV!<> z4wM}xK7N-zjhE;T!k_Jtp=qEI+jzLunF^PKMhy;m2p&~^J18t9qclF2g$r6PmO<_3{3raK at _gR)tv&+ at S&p3&& zqC?P8Jwz5b at gdb|5GT`Wjw&(3AxbFQ0ti^>#8xRv8%kuBQ>j^1OD5I~B_ZH(MZt); zfWbFe7$;!xcCy7MFZ%g7BtxWGCeY_LfY_j8@`w9*)S-7R`mqN?DF#SA=Cfg<9`ey& z3v#9=EKjDh1&@X+!7d|)6&u?EHsr{XAIGe5GZTf5Gq$9O8gqeg=8Umhrq6A|*FPiZ z(e at K&C*0c)afoD}j at e46Rie+HM6dnd<1<4s0T53kFl6(|)=SncAIS?to%8J<{m~8Hg71$)z#XwL at Sd`Kd|zq5a<3SesSYmb(noyXQX}g?nmvEX`k%D;Y4kYQrR?U|T6= zIIN at k5|uX0$TDiNn5>v(!m}E-mlg*`4hT_*1d)&*!wpWc!z982h>}>y%OJ3ZXBLj*QAig6H#d}C^e8oOtBO+l_JnYFw7AYG&EBSP$eJ>90;e* z8rQpF#q4;@*J*KJO`A>a2WH5E9R!Zu;hC6HkyN{2GJBetzRpLdPwn;#UOvk|MZ(0~ zKOd3E={h=W=`b*n(RNFVtaWIvupQ+>cn6A}6e>|fSC?^WidhZkF zK4V7?{QcZP;TZuiy)W at d=|3>I at lR$?@`lm_q=rm at X1+DJ>4O`N8{FRXw>Md$1tA2+ zELJLrq;XM*YPW)7 at 5xcH{Rc3)_;9~k5ddox9=qCmf5(6~2Y(L at 7#s^r{Uxky3m_d3 zIsR`hI~*n;`(HjSv>P(6$1|sJbM}Si>OoZInFw`AWs&a2NYw{+w^ovsRIC_X>OLP& z>- at QzJG7ortKA%d+QBq2hiyZ%lyF%o6txnR5co{Ukts+lRFNYk6ADh8p1&dOqa_Y^ zhDV_R?DwJ2QA6v2MVNlh*r#j^P?*#^g98@~Y{))txEpd;Zif-%4Tpd+B?LsXM9517 z&{6(~U50tVf{-Uy>_0J}<&Mf~U4N!<8NW#e*dopEOQI7{TES1p0ANcbF2gZmKeA1c zsMrz)^%z+K%PCB#OsPvY!-wHnwOcYWGP5az0}O%;AxxMiXoZ2S9mJAg+ESQmvULMO zP>t7CBIGqbmpmzwuu=}L?(X%bvi5VVlQ1eqMI*vB9-AHf^wtxR?fs#0k(%@d2vPoexO}HX^xom&?*( zxE23aZ0Rrz!ipvz3gwr0$TJKi5*Y~%aH?Ze(@ZCecJZlb ztYMZG#;h at lnrL7tz@pG=8c;BhnUgFRDK^x=WZOjsD2<&iqd^uIIsbvg;> z9Esa?OG3N#{yq#nOX_Lqm!cR{DiLJmADH(hRrqA}$eKYuUNV0%%kq9OM4d_Ijh at QI;lf at U?d{>j$FPOSKA?q7E z6-)%M7>o+~)lTf$0Dasbyuv5+xsL=pl>TS}f{HERKFQQl6t=L;!A+zAxd%|W4ophn zA902b!ve!r7m-Dl60IY_7rAHUaNWsO$)?T6h?G}!pnDHx;!r?QFqM*~0NVu8tE!@` z1F*Pek73^bK{B5YSFr(W-u*%b3G4R1(E{+B0rVjsv}8r*5VZ8LnL>0=nxw<_H+wiz zeylfaF^tS^_2BgPao-dp3d9Jg?8BgnzI2J)yt#v zudl=9KIhGbb>>KZ5d1rt#@V53BJ=y;VAvsx4E+kQU_S~14a7h at B8HNeIA0}8VqFG= z;Ek65!a1<(zBy5){0v9TQ)cGQ=T6wRP=O00$qWem(>@H2HQaHC{3ySt at v~V0-G1}C zyI1S|{*J?``0^tX4 at Cy)wc1)(+Q7KV59A#)qfe2A at 62MYI0yR=uc at Q?Zp!+{9Qije z^;~8c_3il at D#PTv(v7#~_qH at hx>z%DvNOr3e6l9lw%=Pl#^sU*;V_V at 55gioH(luU zXyo+==q;H*Kz2Y+}Bd97snYZhidm7jf*n^Xt_C_E{Z7hsiJw=}W)t^{00+ zyp98*^^>xGgKtO*Qf-tni&GxVh0_urg?dtQdi|L=k6J8zrT|&s+VTuKBw8 zA27ujn4`|bW{suiQOcjCvN$VnYZ5S-PaMLiO(H$s)7@(vWX(Hbf!!Sn_*axU3IZA z>U6dxX>%kXsgh63Q&u7^Dv}}7hh0b~RR>w+yRbewn}!Bv8QW+mesKBBIU=f&2nS3i z#F;S?1gQ1-zff2`VsQ~3hEWY-t!Wu2JSkUEI;Q9mUlye4cdS{NH7-13+%-Jpq z(5$RnHAy2AeHgI=4KpeTJ{Ndbnq&<$Vjv7r5|Av->j0w=bkVxFlB7!{gsEi}4$^>x z*iWb;igCh^dA4cXJ7lSgt01#l^#Jy{i;DxBk#Tpuw1iCrQ at lCg*u)KSa^?kj>|v_8 z!{0;VWkrZ{BwBJ<%LoK9%M$OMjpAR;N^nkt19wfZyGHyTIKb%0CbOegc?MkLBwZUa zX0RlJP~?mxcQlCtB-C*1R0dGH2f%dc zqmigSq!nrLhZeG!!m^|`)RLt--TLP0>6MvnVlVM!F#;S7lW=OlcIV4QtV2rG7SCF(b!K{=T)v(Jfv9RD-L8rLk)j^Hny&T&} z8N};-J2kKfda)X0)D`axa&Hqcv1qy~8pQ|@*t^0utE>W!38J)Ci5F^!b`ED&cO*5D zD$&a@>AAC`k*wa<3&aCv;|HLVJn-_4gx1)aj_V$V2VG;v`)7l^Zt at +D(@iKk>Fji+J8j5vFG z)FGILPl5ghyS5$Oce8JjUfr&8C(r&VG zAUl6lkpRt#kpBbZ{#mDH&fR+L(VKLb;l`5!6i-SKIBxWtwnovXdPMMzq)7E{Y&;G( z6Q9G^xL-F1h=57I%ibzv0t8A89<@E>xnVR(RHNLKpIp&r{l(IL^vH0nE8ahu`Z_;} zIGc51mecpsc2($s=0b!*g(yU>&+^76;y=z}dB4(9EhJhe%iHpN&xdx}-(K^L>9O^1 zczGWhEGGaDB>@l85Q3kRv%1i#eBr at Cen4g58;7&~B1_rX$r15{5|Wi``IZC^D^hxM z1(_*Agneh22)$JPpKS;`PYGAs36R1A7m(PD00)`}<-|R395AHPR47nPBMh?2i9(6! zDtf%qY*mNh_b0+n&itOZ11T*oPs&#pXRYz^(68Z7%ODH3tUUggucX*_K&L6H1ZX(s zdZr>qg^#BY%7CG08Uc;5vq6N+6pR>uz!;&VhNvW8!#ikK8AWM(SwRg1a7MT#0anR1 zV at oiJ3X(1=Q7 at cRDtJM}P=or;4UYmi^33I$%tDP^q5HrXKY4&TDx&j)gc?T?1i+p! z#6o3Me}MuANh+dYr1(eNfelR*RDmSYK}xsoEI^P)IYX`0+YyZGq-xt0AvUocs)Ry< zxl$BTP%ShRB~w6Bg((s=36Nwh0MPjgFqtE`MrJ68sYxISU^)sC;VBUk24;7d;D!XP z1q~omftZ#=IFe2fIE*q0Za_7m$Pvs848aYu5t=RRyaf$9bhs_Jy7dB9G3M;(yt+(Jplc| zqLSmpzuFk<4 zUCi?apZ9%w`3qn at BW8m%LH%-xr9<%Q_BHU0GjY(*Zq0GB*iY|uel9$ZEaFKm>b>(u zQuUtGMX`$mWh_8{l$-Uu)-z at JnbiKjbvPPGG(G7ftg5)A_9qKSDpVLo9fS%7U)ZlE zyc)*WXN>;$^osFv{tMV6-sb05NalQv&z+#C3NxWax|SZ at n?IlymbAI^ZKd~(FIkSe z9`Nf-!%$(IOo at A&V6X^e7pBqwf#*=}`t at q748TKGdHvR(giU4W4*c674)q#Bj$irC zgpi93#N#4&^2cesRwfJvEu-r9hy&s#ObLWAnS>WtWZMq9DJdP z%zY#bL5t4w?r{C5db&JZld0bNUgtM?jl#Rieni;)5b;>i^F-L-;oQ^scBv%)|1m*X zCR177lI%wTHrW5a|NsB}|NsC0|NsC0|NsC01VDhp2ptI}nm~|5U)puFwv+hy-e=%0Kyo6d7!`3c2ZcIaIz3GM5iPi|ejygRy` zVed&wC~k4a}mbt=?I$eD|#B?)Jeidyj`o z2fF*+_EWvR?tMN1YhJ$JJ?^ia-q_!L7roEDw;AffS&8%rX*+Z(?W}4Qow#}y{<&{;nT+ at og?caBL_2E04-tTXFZjEiXw{pqc za4V-(%Iz$)YQ5R-V4-tqcXsVj9XqkT>f0Xn_PWANtxH>Tw;gCRww+z>055xov2L9P zo0bK`X<*PqRbIk?-I(63%R^hcwC$~Q+nw8ui0*HC0NazP^!ibtq38+?bO$8>KKpk) zJT+Hi-)#B;B3o}X%=;B(_s?RRWgaR*hdy2)GJ(gE$2 z>F#yg+NAEv00X54af5F6wcEEnX at nNm_I1l%`js!c!1 at lH)&P(K*y`swcGoQ(VuG)y zr>ys`^w9Ot6nAx7sUy0pS;5e{uFliBUf$~f^hAIFD3n#O#2)u>0BxJt4u^AS00Fe7 z?X>NjF0oJpYS3eHTR5`Fk-2xemvA0gTHU*}urxg-QmIlWW^oJ(jyG$yM)n%-Zi=0N zd&k*DvmsUW(BuO{Ut7JeQn()rYMu9aUAMJJr$fEocssz(_QV@)Zw*8p00HY-K^(H$ z0q<3{)YIJSruBfq-kcCY?VHze0Ju)T}1w9&!MnDayG-Mh80000=Xb^z}O#+zGG^e!(s5GXY zH8nJCL)xdQnudS at pa9x{0000001^n25de)8^fa49dYMm5B|j2vDZNwEYI;!Gng^&o zN9quIOo4)Iq57jB&}cLVq|8u4i6I04fJ}f2p37b~y(l&3kPZMK^+=6(Dy!IfnEG}Crp+h_YYDF8T5T7Urnz}HS+E__01;lH)Q5AYyFOf`sQiLy=|Am z#=i-yj>yolDm0Lc8~wMlWB}0wmt+w0^E^K$HhH?X?&Nc0yUT_}nUfHu?lB}rAzQp+ zXvP!W;^z++7jIDLx_iXBoK7(mCH7`#IgPOytgu!X08Gpz%r^+HNrMQSUCda}8#4gF zK*G#Yl?fPuGRROSRF)$f1coAHiJ%EmP$bC(!7)J%Obtp(iX#aCC~>G+krasqphTi2 zV3Z;fDgiKz#YRUIz##`Eq<_c$<~y82SC&9RXm!pGbN~SR)kQzpk^fKqTo4rE5e!8| zG@(dPNGvT$OaAZuNX>$ZAE6k>Zr|$~ebLcDNKnKjAktGn#5AG$`@e?&uGKV_$aH~# zhG|q(WC535UfQt z3`B$lC`>gJK+qLcLFG$Qx#OyK$MgzL`Vdc0T5A90Z}DPgv!v&(0|IY zDXM~Eh6V+sK%$mng at _>`Ss|rjLI at b5fvG}=G}J`JLd=BJ6x3BqRaCJg%t%nuHAu>gOpp3X%F8I> zLP`-DF{`jCLxH4AxdaU79SjMDM*4~nkFF%7=UI7h6oad zN|Bicl8`8=nxqMZijkseDo}`Mq-a?si78YmLZE~wh at zsZ8l)*IBg7bllq8gqFpE4hgF>t%BQQl!gop9}PyZHmoNh*Q za9LdQk=&r%V{6ef8^S;6ADg$!M8*B at -2R*u|C7)}lK$|}RCCUxQJ`SJ#!Ww^3$0VL zh}AesxB$`ti0&7?8K3SHATT2$+xcx`L$`H+EB+3yuAMpH0U`$EnX{C6KA7jjFe>L0 z-|xN`oNIb>C|;M#dN;$dVVWOxjfZN89PUHiorP>jN$w7g(rY7b$TMSYrkd52)oA6f z6x}puA;FwlGuKmz5DE~{s(bo_V;IO;p+h(eV>{As3y?lt`d39WvftNgE>xZIj}>Q# z`_^CL#MkmaOaPi5i_=m4xIC at Hk0fA2azNgJ&nZbC15ISD7w4-+%}_3SsRJQ zxA)(HwdZZ>q9JSPczRn(0&`FZMKWr5uv$~c2Qtig5{B9!rKh5+1bwnZO zd{T~01*)yM+NS_s^vi!Hu8 zf1t}z5-%sD^SlVd$xXwU$ftszfn_}UDIZr`GYoYdT-$eY z2uUI`RN{oAEMt{YQLu548yhP+^W3={&wk+)feaMs+MvKSbN3mddCo*|Gb73~bG#-^ zRYvFY_)IWVI3NrH;fl4LFzt-S*&)Y)xx8-9&tZ6=ZF(jhq;&2agzVd~wyW#N~9!f#8v>c_mj+ za42dF?T0xg5YZWJRh!$!9B7XCVHMRS5amQ>I9-nTY_MxtY%9!$jj-(Ft|%RV6;X|- zXpSAn0`Zw8b1NaSqC%)5v$k$#E3;U81nw_`r=m)aY1oCtY7QSXND6Cjjx at S6wGDdU z+F<)kla*I#yuAMc&b)S#GZ4<_0|NqR*yI)K%qXDyu1HqIOCH?IGZVIAaNH?rXx*}t z&f`Tgn;gw)bcB_QY|VC&NQOxUk1%L#ym0K>qVvFP`dJKqt7R`)8!C~=+dA(Nyb>3q zw`}sf`3F(LH?M3LPk>c|{Ge~ASU zf3AZqbpYR13fp`i#hgJ*Ok~)A{b%L^FM_k5_9RIDriq%xzihw9rA-6< zYpP&@vhBBnEC=%ReqYqIA|QVu3P}W7i`4#RZWM0p!W1QhD`EW4DR^6`7sOye)!31B zV&90+G6D4!5mf-K&7$ehnN*P<(|bit!0DFWBWVgmEg{jmI9t!ykW*Rhq2e_ at -eOGv zyeRgA at DhJDl=LBWam+W6dp7H^$qk<0)h0Vs{{FUq*SeXB?hhEp-clb}n>cF#>C22m zA<;Bb^5`YRz)x1uKYqwu!{&%?{RRdET~vrsK={Z6QzoIOl4%k;rUAW=azA__kQL`k zL at Yzbs!Ncf6#}o4a)oGyhw>?^mO{n}Wd6kNGgD1 at 39{BCQmb{EgoPwPn{BdNfrNW4 zwmjqwjU*%}sT at ftGW{mR+8Qa8%B1ET-nQ7k%zLXEc6Pg1c>o?3`UI0js zwEyOi{sPZAp z#fg~^vX4aDYR$0@{uSYs=S9hs-yg;NUym7LqgiOyz8OhbugGNm?IHs$t^V8~bv9sO zwEFUPlxLIC;eFNx;T>SFIM9o)9q|FQ7ImvUPVb0F=VhfNttrP2D77M(q(ZB*)0V4K zP^1`MS;0l>q9-H5wVLu7X7Z*EOE)A+{sCcV0#vUBzO?%_d at tjW!FsEWvE zYWdLZCFj2ex9R_M^E>cVAMAEr`KqtiU&TI{sGD)AH14x#2O*%?vvVc9iv~20bwTD~ z`R6w5TQ at QKIDUxF`_sp=-Z$+$cixXw*vNW4*T_+PuN7q^D~oilG=>3;Gp zT<*u!mJCIoy8}F^MUfZB>&M5VQq+sn&#q6a^`3v(^heric$La_($I~wUf6|1ME9L+ zy|Pj9^?2s_&zHx_v6ibf=D|y at jR)Xvs(}&q;>~KUoo!(K?;G8wW=2!psU;9p2$60y z_0<9;p_eg=+?GIzyk>a=2uEkX&CPVZ+WaT6Z0(<9;rbB(0$1-4_SeF2zt%s;JuOM at phh1XC*+jt ze;AL&*O!B{u^gSISN?acVnQT9T@*B}c)%dtc4a&NZj3*icK#keuEWt~_2%>QckR}I zt;Jf!NaRIo1)|6io;z@{Y>Yk^lZTa!RD2?j7xf1iY at 2{4bOu?A?)$HY&YAl0 at Xo17 z!{$m1MwnXxQ(iVa!K`NKG{^RIBO4oRV_Ksn1fn|cORhbz>F at cyFfYFgd8HP9Sz%n*UYz%^PGJd at M-@n&?raNDzGDpBk<8TTd zry?R+d5Kt&Q4<6bRKX;X69f=U#FbMuML`Wg0aZmbKL^yW at vK*Rwvker076`&I`#x5lTu_#JUz5q}p_$7_?zkkIA!|>^k$lAj8|M9c&Of4NTxpe z at Ai7%Eq``GoKUU8Yq5E^?d6a6`usEM^Ali~*lg^$mK1_ur7TK#L(Ii73^7e5H4QON zgoQAW{W<2R at a7E#=~YzKSyok65M?4|Rfc6zhGkbQ{!X)Ms+y{*rmCvjDw{8TwN+I| z$NDHWiNC-r;GeLM)6T2!rf1n!X^UcZG<5UhF|?I7CJfgs~BZ z7(_-9g`I;eaVQNUBMT}ps+>D+hLMtC5fKsY!(wF8A|gce`<{=thtt|8u(BSmkB3gq zs+tL!kWQgEHYAgUNK|B{sv2otz8Ix84)irGmC2Gspd_g%aueaERa<5l&<0s$@v_ at 0 zs;aWe%PPFQ#ZW&}tJ)O#!Wk|~6p2YlWaLqjQm^Fz_{>yeYzXUBfEdt5f-!y<`6urF z))oGXLHz#4xa`L(`NWXFRR`K1ks!c;{g7#h{ctedG-AjNVcB&6BO@?yZNei8fB-M| zh(pMDhUNY!sy+w-0RaaRDCTEmeBU!f at RtsT09{CbGfLqT`B(`A1d&R at d>`io)I*n( zMrTy7_lT&0`PisB5mk{#=YZxl4`#+pYzYww6sCfqJhXNhybXq at _$#e*44fz3HG|R^ zC8W=3hS!9dj_pvZbWlA`v3eqIrfM`yC8oi4dX{ z3wXQU4gr9kbI=xc`Araf69f%O70zt;M)v&AtfS%(#tn9&(*rbrrQ^7uGNPx_+)GrV zpP%_NSO*jBkP>m5Cf|yuJowPUgnx+xU^E=TQZSR#_XYijnf7N zzZ_zjj9YA)10luZU5jQgX37N6^;)(SDFPxht-r16&O=G*hMXa3Ubc>LXt*R%z{q|< zXPm5&3%MCuBi(nZ&o$SQ$10);z7~Ts6zjrSdE1#f at 4w-e4By}E(%CCJbxpZ$&u(~^ z-Yn1{94s6_h6PnwMzhqx@}Ani8ePU>xVC@*@DRlJ7U7b-l&dg}`r3{ojW(U}bWv8E z3^Np_k+`rS z$n-{gzDNlkX%AOHi3XQ(8f2p=QI)3rYd?QEP`HJCBFz=-Xp1W45v&HyEWNo_F+>#s z)jrF`f)XHm(6_6Kt>sU&d@~;}Q17FL6(h}XkLdivf!=EzH(ty2$QmagDWOJ1BmSPS!_JKsv*82 z9BQ45pFLBdm^F;&uSbXwloNG*sT)X=f*w_?*S2|yh}|$n*g;~^K|X)plp?(J-Kwg& z%KNvfxp*fDjnNz>g=uZO=t1w0ffply7DXMh!~}b0mW1Eh!#WFnSA6u3fa~c25E7?y zg20k?PuH_PeI;?Ku71ydFH!jYKWk$($f*=TFhmny=T3jqGbjt$>GoW? z at YBi_xKxN~|1_DLSlWM$HRw{EW7=Ki1On5Vlh z!S%g@(-DGlO+Ql&VTEmUP&Bj$5+!aJ&ZJfh1Zb~)inlcfq`{l7StrDW{;QGVcC24r z7bkvg`xe(dJ}I?tjzp6wMSVc#&}bi^*_6QY&G-=nJY=a5aiHF?3808vR`+`jV?3or zBN&^q#uLhdql6xKMIzTjnK2A+HvyeP&NJVQq2C&s=}HC?n!6>EXrvyfFy?<|N;C)i z0Oh`1-lF4|Z9}V#BK*@)2a8EN-^CFaWX|AGxn$jrKV at IvZx%>~QP@&8W at 1~jW_d}I z at t2<$e;OL<;)i=#Uj-Jj!^t7ABkWxTU%nmto=6R5&70^+brWSg08Yu!$ zIC_~_)LU%@sO{+H#uqpxW9IJZ;r=3-e=!p>q)r{7!4x)z+AJH at j8=$ zo6};p6H$vSCPK3eP7sNcB~n!+ zP0Z}>3>u)f;SIIVMV> zRYI%@`&caX&SJfP9GW2lgaP`)&&s9(3ijBAgkeNP1eAne5(orF0BkckIT)E8b!dd%a1vy18Gq2Jb04? z^daZT*S~+4W_tW}%!@i&A>=XGR{k9RZ^@`D^`|&4$xs!>IGg*!eP{sU|?pD2RoPw)Q at RN);v-D7W*^s@#c4s z(|*vlYmZI&^J6*PGZElMmH=&t2c9m|&s0I#1uq{_7i2X^dDZS=)f1rU{6~nR at 3#6V z$NBNE at R$@s&O4*EGfV_DF%rrADVK>k36d5P1O^aDxS^Rvg!%AK;QZ$x<=f=3`MlhR z(|5~o`!`(uJjXX+=fBQ3-^_PQb#KbfvL(wO=iQ+(AYmSmGJ8m&0tw^m>(5(t%Nx78 z?hr-Gse1l7DxEm&^mNEA8G9|JqKvpo{%g{(r~YNk$&bqHM)d~$qwxb5^SypiK;kDM zKa@%n*b}5r*d-DBytzM(9i?WWsL$n~=gJR^Vf>n}Q&;bM^ec3tvvG^5RAjw!R%T5s830F<_xs zFvu^+j9uXP`h5DnJ?8L#bYwR-VFEZop}v3|eMe$+Orx_oYY5Q*wN+WKkuA6^ov}@= z at z8v+!?|-rOiW~3a&Z)L!;!@mz^i-=Z$DlnH!H!MgS+Y>Mzd^+gmf=*u9sVxGSPZS zwgMczw3-{RS>xY9oH8cR!I$reb$=v`BoU zJ-h-6AVkODv{SRsqYtK1e)>R-pFyw3eNP{T+gyRooep+2BFGX671Rq-{99}zse>#T(W{eQR#f}U?C zo?iY$MSyZ5rp`zC?|4V5H;TSNFG at uaB6I%FhMV66DgDH%{@MM%K&XB-ih;#u4f&um zR>l&Dhj({>ciHuRnm#~~l3kBeKXe29ZUe5OfFvO$9kOqewS8w;MU;YeJW+@!tJ0tb z+)2 at dK6C5pqEH;dfWEjs;cd`@xz%YXL_aYry&SQX*j6?vwJx1<^&mE zAJ=e|WzWH$8^M#R`Ft at lN^~$ib_!_w6hHvrQ_&;{g+TaJpq%;|1R;IYdpB|_U^6OS zdlcGwOnC3#Y)C>K`ES8c`g9WLzd7P at Hr;gsB%!H9twa#@2?SdfX;C_6-jy~A_lcp- z2kSIADkrE{XNv5G=s#A_M|_e(LO~>gK_u_Tsvt;@Q3$9zanf4KGJg22A$-~1ix+&? zov#KaDkInftbc0U!U~LBQCA3pn>^@LeV*?h9%cPg(Lbl={7Q(bJ-B_cY_u(~T>yc? zt{00IMmlUiF>7=GoZ24pqI#7R0xqLIXd97Xo<0&uCbdGo9$DJQ142We%QI3T)t6c8 ztVG;86V-R56V4`LS at usRd&hy`83OTMvjl$-07kyO{#cKa`}+7XV!e?~Zmz*f`8EH1Ie9+AFtc^i`I`*DaZ!JPW)60ea%V1P%yHOFX*j=N}9s z)l&NdbJW8`VYR4Ihyv!Q1RQROrKmUKzd9k}jUlQNDNp4z(Sgw&y50bJl}xor34tak zD5f_O6Ah`vP`s;2A9KGwcJ0AhFM64otRa}#Pmvd!5FkJ#0uTsptudpmP|W3IU2NOO zLRwd at x|I at 3aNCo%>VArjcEh36{M_GRG3DGqBqk1JnZ?$eMVrmQ=I-tTFmV-3?m9US z)&MCGk#LYIMh;7C9ou(IN1l$W$P&3)cxc>fG0{Cmyp~A!J;76!MAACyQKdY_jiD`O$4= zfQQy{sw7$|qcAf3))MR(WrWd1Q3XUqmS$fZ!J3Ar+1yN6jRNcAhLq6+Qb8iKcEh19 zqO;kq-iWJr+^&c^(>WM<10DLU!jOiSK#4kus+6`xgEIv{#(fkZxCyf;$mXv^9P8{!MJ=T9wLJ?pdsbi+{hR`}d_77})ZHhcihGvR>$erbg zq$0lMA?gI|3NBDCMk-R2ZTdfV=RKn%pc5n8ENU<|xQVzD;ke!P<<&keGJki`f8$aK zrlZ933A|1UT~3Ho-a!P31foO`N=U>II8kvkSH_qfurh=``Pe^Q!)G~YBX-RL(kH3v^!a;C2PbLRZ|qCAAn=9C2N~0J2-g zGc~0gziS1hB||tpRan~!BtuL?y1G*+Jl%4Qo-(BDAZXaZo^X{R3GCFYTK0uBSkf}l ztw|50=jfePA(|2 at ICjOk#6CM8yV%Mdj@|x0q2d6LKxnrDj^c zkn@`0+z_7oayeJ4l&OKiaR at t%RPS*7x6)#0vtTooAmLG$dR3-i zhr+XItzNxGV|mD{bk*L*%}Oi3pr&ok70zxX3J4YaWCrS=>>t1s7sT!Viy|Omi&e+HSq&=yBs;On|F_q1Q>&&qObyV6)if(Pz7DPyqqKN_tXsZ?< zvkgM*p>R|=8Y~%az#@iV;vL(W+iWnD#*8ZkkyaazTd|wxk$XX2G4$-wIjnMoSx*aA zQ#6jtv^eekbc|DX>t5-hca|Bo3EztDS7V#)xWy3*maANa(y=R7c4j<2Z7I z&-WwIu0^RN9fAo(CyqfI%uk_Tz3-7YWr%_|_vv>=huxd66W9AucwEWq5ctc#OS$Q$8fPUjHGCvuj0hlrRkYZGP(W~Anqw&Z)0Fn=lgvZ0hV#*`T+2ru4jNU1+0j(5iC${t zj7muqL97)Yw)V?TQQRN{^#mIzw{MeZND@&4sCBk|m~bFQr5y43ZLJE{s-o7#)UY?3uHY&0n{dnLU|fzwFzevieuf1qvEyfq^rHYCvGs6nW>)DL4D0GKxu{ z7x}ek7ZdI7BB~^?zX{5naCcge66Ip~t1Nq>7bQ?d%-YMO7qZf5c5g3^cAV71*xjMw z#`{D at 1p*<6RR%L*yfI{%0t0<&Ak;7%^zCoT0U8tp9p{sCpTnjxo`sTYv^&L?A7)J~ z%0?^?B_E(73+WsUOlE_}lzV2kzKF)Ps^PxZ$95%pu&8mv-0;s1hD99^2*nIa5uiv) zAZAGdBwz at TB_HfSK#C!6h6L>|G(g-m7OkW`2nJ|?iHH6E(Ik&ZoiEGJ9Ueai_&N+H zMy`HdU7gtI%})NHOOS&Z4?+h(?3hfMut1IvpS|(0=PRaGyu`?&v+eGuyt^{BNDT#% zjaF8Te!|&rWmM_x3*GOkkM*Q4eN^=ztk$es=ObB)O7q{y_H*v{3jq-%5fm#djI0F^06_&B5z?m|T>>NR3LLV% zda-#64Arx+O0EV%duG_kz-R6d9?{e at WzIW;JrVK(#yn!0edy5#_-ohfEr4nnSU6{dt${ zqKwa1KuIGAB=N2AsWJ`+8OI7~2%-nPIB&#DtB4cfXiAWQu_R_YIC9Y-DI}W>Rsj(# z<631B+&$y!z2l}#z6gVH=lzLi8V)F=cJ1o=Iei>bPhP_tMPerA5E6FRl$p7}vt<8H zQjEO1nJ9AERX10Kv852M&u4-zdF$-ebo$}h-f#rsouiuawUQK*y)u9pndC*1j#Uuf z%ONbNMVXjXzQ^1?2#66`IOMV71sO9V(*#*ebYUHYMMhp!oC)!K#Cxrggjb>7nYZEy z8fR#n*#ue}I_D_AWCVup+b4q+1Dmjw3-(rE2EG4Q!%F0~N3Vj8(gLx4fJC4HMeQu6Z^Ry><8_5-qc_;12uj`|)Nc#0L4qn^_+h9= zP!4Z>!fZcw4Yxg1uIrTLS1;vl`D{nVvS|!@p{O23B at ug`-*NE?0EL1Ve;e}t?sx9f zAQ7_f{5TzwYfK2o+0^I|LjQbcZ(x;?1M>j|UB7e$9iqujr0#x$c-)S=ssP8e*h-K* zRr^=K^nX)rxdS8wq8{+N0w43hdD|~cmt7bQ$Dbjtf~q>|v(QR!rwW#UBcKQi=BiLq zE5*G(E9USxwYSA++zoFk6v_L2eqXqbsQ%&gF}Mi%;lu2$Si`T&5}VFSrEL zcX7aWPh5l$^xxak0{{*}Th(aM&luY6Ju90DLZ3X9t`v1}yFrt&s8a9?DK)R7cXdH0 zj+s8>Zy#@mJw7mIp>vwbbD`&dwq|?r&ox>Bi4}76?iI;4U(yh1NpGNNn%9f0mlu<( znuxoC>V3pJiU=!d2J0`%DQ*a(jPZ!j*P7-W396n8Q<+8;c-B;$`lSUx18jX8|0X8@ zY_;DS=bO8>Gf?5Z!mrUGi>~;97`>JvLBfXTm2RT>@bT0$OkWw>7Mwp?{0(&OouK{J z8vdJCtEi9F*1S6LzY2Fa$T8~i180B}9uL;8!2~%oawEf+ZY?gi$llTSX>&$apXr=i zhRnoh7eS#6-?lO0{b_UQo;#hmc%1{t^~T8;M}>5KA4Jz;H2cB at 9HScBWV9dj8JoLW zEMQG?1)>y1s=4i~gh2A0tCT00t7ZA&2uvwNj&~X7?DWYe^K<-=ww~2W&})pV*#U~F zgd;}Rt)JFbLX&V?;JvzCBt5uR7`Ica-Vpp^zeRkYai(w0Jwwy;4Qm(n?FPzfpDZ&Y z0yv10YAoyUuKiUs>dx0T+o!)7*Tn}fvn>qg3ZTgmFMj7Op?VVU61L`#2A|nf`0sn; zoQzDYRtwd4ow+78<9n7FpGd!4!!ra+yQ+WMBI|#CTWc(PeS7wTR?tjwIipZ_53n-G zwLzpQG}pse=7+VS_!-k*v64$CW7D0+bw4p%6V427mF<~Z>>Ir z2$_9(&|OV+_|O~s98Qe=eoZcG;fc$m;4P8Sg<+A|-XQEYBGz>X601Z-wHv!=4LEnD zN5m1-HC+wj-L7g}wg>?Ng7(-Hn_!GGw)Ew#o<`qz2t8qe0DwXeWF?q!cMQJ$>gG|; zStzeX*=+7+)EuJ`g_I=?xC>JsB&6c{0D|I0Cj-LkLwd$g3N%s7!UB0ROVUKlN|`c_ z`SIXm5h_ktint5BI8GzNUkzk5uN%0m!lED_ki<7j at ztIU6Ln(Zir0ENzXw>60kd=v z0u+v~S?4YujIZDoQkmS=c70{zXsVKK9+T>;Td!4Vf^6*A$Tn>hWV#s08T4>K9!La& z0yGV at B_(9;%8v}pb&fIFJk%8v));mmReI%ke*z&nbA?BTmrGt}o;P_Z at SIvQ8xR7J z>jD|kG~yp$s*>TM$Z-zLT0{}2;sUn1JAOi{<$pRM`QU)-DzzJ3(2uzm-Rj8B^ zXr_>RjB}lG at a}JE-+M9}B{&3vz1_T#20{tpp9{jk%4PFZh-Bx|TSfbvVoahZokd(_ zKi-7BTiK&Xfk2s|6%hVH2kIUcl3LE+V zA90#>o;aBe#{w;x@#8rkT9f-m#+I?iMl$qzyBHT)@@7Al~gz+xvwOE&D77Iqr1 zoHmND2?C at P;uS-Fyw%0xKKvks^~4aHtMEy2jW7NMJA$B&VC zvDb(b8b?rp6Q#RZ0wsAzEe<%t$UqDSIInMu5><$ls^J at cLT^&1h@!5sLV55?|Lv;$upOA9(yH9W)Y=aImQjYJ4_~xb$Af;wqFdx&irz%z$^I41iJRq9BcDOL!`BHIAzpz)X))AXbs| zor6$OE)n4vIs^iN?9$aO>3W|>K-Bk;_FJ7k at rjPj3u z_fl#gL?IG~t;3(TVjeu(IEiy!NIB-q8qB+q0VU)>x1&ijfgu-W*6mIUgAA}}7aj$ zIDJf$TQ}ChOomOyF_f1=7dGLKUU}LacJ=Kf4zGJb at PK>!IUD1e at OKt2>ZWPWfXYK) zl>`P+5E5NcT~9XaNd6r1X?P!vIvnF^rhf4?K$;CixL?V}?xG9fGyx_uIBmOjqQoJ` z9H}-o+RJ8S9T^W&PvC&9Erc_C(825D^{zyNk~TXCXdH9x`ypPq;-f|VeukE?W>X(4 zI~FMrxa at Irc=&@htqU`E%y?2a{AXKl;;$K(DPSMG&FnyoYZ;GM%q6xrjBNIRNHI{l z?`O at K{#JO`gJ+Jm#b;mC8H6NVX_&HnZe*jVkSw9H~RxvtO;w-wU!pE!}s z=IlKRw~`UL7tJ(NS@(WEdu96 zjHeE_qXXri6G9}Hwg&$nn9n>u^Bxz1*iDDBA+qKS~-^$%Aqr- zSGgnYL_^wdd)ZiFo6!!0U&}i*wRe__twa*1ZB_ZUOJ?628)VJr}iQ1X%l>J#p(8Rs};(FwGoSGT0)1ign$QxUMu*|{}fcJ0~^g^Loy zdd9BGB@>2*QBVm21c&7(fpQl&=_3z0`tq_#TecFo94$b at N~Z{Pp9)7aA}B$tHZhJ> zu*z!3BIf&K-EJ!Uxzd!|Oom;Gppk_i#E5`60YnHn^RQm!9ka2L?)`#JT=q2 z`NKcRVvOL$U&D6*gajkkAv9-dC{j2Pp#!*~=l3nXxrGFdOCbs5z$)7;L1iJIp~q$< zNwb03jYfz){5NRvk6k+7){e2;i`)SHmk}VJz(eg!hSkH3rWc#A2lw>PX~D+&ZEJ|- zq-h*UW+LemA|}O62J7A-=%r`*q0Q;UPJbcF^b;0$rq5>xFw6sQyQyg^=(zxcT(aK& z`YJ+X#R^1w#knt at PO$`Iw~x75x_rLsHd`J2vqKoEuQK>q{UC5))zN5k7+*vpqM{ED z273&+y+~uJiAOUkC at Z2cQzt75o(+3O%R898dFqHn&>~{%LCTacjfV?+JCO%Q9H+iY zLgfZ+b{oO4IA>ZH?vRj-H7sIeeQy}?Wzv6#i|V4F96eYmz9?5&i?Kut$7--dN)$B< zDzxbUxrN^75gjEx-<>MlG_a|`gkv}lJz-zn!K;!WwLpklRZ)4BF5(S^6nEMX$j?mL zq3`LooU(26wvi$RvXeVx)4Ik>Rb^@A`C=~hG~-l)L$!mn8%7d!svu^3wSNf-ye>rt zJUlf^r5Pb7hQl1h~Wnqc5XS$6`1Fj%e!|GhSj!Bw_%y&ibGMv5Jmv|2YF9o3#TBz|@IvH0Zu{m6h&HiUF+(Gfk9h6vE40TJNB5%jkxy`!1xt-2Y9dHDyd zV3IKx<^UG+bd1aZ&4D}_8bJFWE^s3GQr>D z=QHkF4JTy#xmZB)1Zg)>pPXXIbApY?fnhZfn0YkjuKjM6C(>@2upuxY=&JR{oM-Uf zA at K8iG^JuSl=A`4GN55}DN}O_CYEwr^W(>-S%IImuMWZaCR;5kPZMkq4uGI*|oAgC>Vral)0J!Dbu0i at 8BEQj@oN zkkZmZK)1kA5_kbfD4oD9`x%A7vJyx}t{g5&MYxw-AR(Uz?P*k2WFyT{-F%55Y5dD6 zKC#ThE%3C(B_EBLbxlPp3Z#-8^+E4$y~~FVgMi({!|)!XYt?*kE;ZrF6;TifK8zur zhlV+2S+0yL8N at t_N%E<~Nu^%SU#mpnF>{4gJ#3?`l{F%TNcf|wAigfuGIA-QU2=Wa zF!9E&I?Z{~Xlv5Nvr#8!zxC_#Qck}wzLeH(=aYG`8Eg)l>%}+6=NKT1Y%v4G0af%=wvf&U6zFeyNOTU~6+Th--FkS!-CNC~gGA zWAnNsv>+l}0EsQV-97|YC{yb5`V8&)YX2HG+Vi%V at a4rtK5Qzn6!AZWML

5zLOi z4ygmiFf%T?^ma%#d6#PnrLR5QQel2~k{uNj!%%9neEf~sL=;(`lkbl|)6IZu z_ at e|-#1#}Hwb2Aw(hzM901VW7G85-O)zfH}`Ty#F<}> zP2Ad|*Do48RDgU$9 at wgVq(6Ycr$)aLyvc5lX9GIrX~n}a2 at 0yHIx_CUgsU!nJbrt| zJvFKpmz#bhaR%Iv7&(fvkY>vgA5)H9bq{sF!yQ`SVtG^mf{uulBPEm^GLr(+b~(-H z(AIX+7^snrtnK^QYZ&Ezrg-6O(YA4T#HlFitum(g&OxBt-ME~Hn2m&WF7{(6S3AyP zj-D31;B=_F2QA+12`gM=Hh^$Zrdlt at R2?O_7RNb`7RwGd*^64bBK3(#w2P>Vu at +7L zPd6L{T^#Z{;U@}(VXbY)D=7#kYw7wX$bg={jR(gGqJGfWX9|5g}U z`(W033i%qc^Ko#-nLuj`N{H&uQ3fr_Po<}Oz*~gyT5`Lno4b;g>iK)h*7)NSeUv`d8H>8Yr_oQ_f>eC1-&wiO~9zb zk&4L4B{oJETr&)njcS&hCrwWBsJAILlZdHacCGgKwe;*T&h897!Nb|*!kJc!wKLk$ zGtIZHZx}39TUgdKbCj1mGt#QDZK#6wn%UuI(l at fnt+|NHtjFzbI;UZq+njoe4 at Dyc z+dT8H2HKfVEG^+Bn66fZWlGhhUc3pwhn~B{w`;C_aaTT}nAUhrkzAsC=VO?4MPeA zA(pEH{0Hclzta zfYcHRM!l85r4}p|?;`t4Xvxt5RP+=~HAml-*czA^{GQBuBkQU0w(plo0RZ at 6lyWy9 z9`pLxMZA?;lMtY&T?kp at C-Iqv3yCt)H`1>Grm!=I22D~ zab08Crw^KpO_$}lO|w at i!1IQP4n!m*K#Zc4KHw1SjR?hFTrmxKh}H+g%f|e9&o6gq z)7Aywk(ggzm0@$7tj<p+O&ER5uX;xCL2Zwq^b&HU%tUgL5sNxTm` z5e&leL6!hCA}^Y?d#vnK$fHE671^rcNkZx>e;xh zelBGloyKbQbZa~0j6hl5wN7v}?iOU{kc0812Ki=+;Pq z1v6oJzl7xLOTP13Hr@{LwEh%}j>xEPE5p!%5Wq^t9D5r_UR=KaUA>ovV!bP8T)QWn zA{(2Bln34rEnyFR>2e?HoSpo`#P;;WO}MM}uv7?}*`_{Vg0Zf5lp=u>yf8t3n!W7z zvL{6~M|K^T7?aPi{+e*4Z0hwBNu5vF`y*HP at kh%|i48*7K#B50tMStE%F-EF%Hzl5 zui>eMMS&n?(0CLGhUYSz3L+q0y*dsyfrtPNi7-cK4QeDM8Tu7=p;{Cvq^cTBg!>^= z^})zW57`95Ziw}nd^vY~Ufv%^0@$B5v&ZLw?0kN`?)U+QL7iMYHsxa>2y=A*Bb{qcncx^r>>VrnEz1{hoz at C~m at +_ysKd2Y%GDFG!H(Ex+W)Ag7%@o28MpvS3 zFX)_Mlre|KMd^XzIi^OQ@|(jPADl}G#I#J0ZBt|f_>al3_PRdPm*g at 1TQ&0Fhu&p= ztm?Vb`x*Z+N9+Aq+q}#aa!ln0Kv_+gMpjjhF at b}XEK(kR!_PyL)TR&^O$K155?KwH zG%+if5s?K1kYzsUk<6lcjcoQ)vJg%!avd;&az}!AK0$l zU#Wa|(}vdi^X~%_ at eVQ}Rv;sn?7;$rX1vX35K zpGLXws`E=8O!KmzIp|E2xl}(0Kw!cI5-AWMK|N%DLFr4oR=VSD{#*XMvjggYb{-m% z&?4NA606q`D|f=df+=920sG${uD`nuu{Z`n_5wTp|5q{N$M$*tp~C}jhwin%Mxz4n zX35RVzsHEkerf$s<$5X|(;1{N;vR{dsJDk6W#G?*2K0t@$54stKYJ6wLP-Gs-<34Y9n&=ibpTG&yYqpj5;vLWt z{V at VTYXniCe~lu2;9;N9Ac^}C1Zf8(K)T|Wg&m*6XT}4R6v_fJ1cG~@Chu|UHM%C! z0V0}XL!qHWfR~QwZc+Y`B|-gJi0u#sDh&vs*@%N62e?F4kyR?xD&VJJd;$F6ng(Iu zP|OtRNPr-LNK#W(FcJj7NEG*kE(Vww5a9$sP|!3qL at 1bv0)&8+heZpfr3lFYq^t=E z2uUFP#c%RzsHVzn>7|j)V~Ai(?SQ zEI=Z!GMXbs5c(2~1neaG%EDA$0XkPuO at px%5QB*T8;JzO_{~Ls%%_e39-%Q4C19H} zLXgy5T5?e>6bx6Ss{$!_Dx_2AbTG5|W8Z9x6m(H5RWK;!#*FRXrySh=`m`Vt&@@3S zA`F93KumDe4wa*%!;~yVBH|IKrBY-}5d<{VGOR at f$P;%FgCG)GBqGrbVX*D>9Bv7I z*UgFR_>ZHz>*JUo1VsMO`7_X5f${aC{d%JhY+L7de+6LhByM6LecxTC at b*ehpgRw| z&5S+hiD_=1u at NNZ!k at VGdH$y4h>_)wOcs?sC;WK4t#C_^OAU&p_t|_Hu0f6uE`RC& z6eiQq^#b;bn0Bs%gg?w(kv&8bB4}`F^P7+ at k+qM8{(FVp9L+j-bY5R<(?Bq)Dt`2< zQK#-J?ZFVS00 at f&9&o%Q0+0&n9<2jj{C46JyJ0|!7=F_w)1oYc)b}rRX>2=!^Z19% zrKDnU6s0MA_kU4M`}W6x05W^rW?|Vg07K14+aVf3Ae2&rAgCH?4%s>zQ9!IcPtY*xBoZm!D8TsWS#ZcN*R->}uGw at KroZ2Y5MGLi-WigU{P8L_X)+F5YZK3WehC zNO*E6xd1qiCBk`=1(TB|e6qH$L~H7L#x*38NRHFy2P037mm7}8%|`x!BjXJhi-VX>su+Wa>@#uvGo}8BlM}#kVw0@`A5fS<_d-nu;J2>Zc2atGUx(hcw ze!XJty+k?S@;L++d|v?!A##5tC>N{bQqPt!j2%^q+1wYhSquRMDNENd2n*oLeHc=X zbU1yC1jX1 at E608Rz<)uGD4_!I4cV0;slcQC!dwUOZ;x3dqEi|02atD9*f~Hus)Iq8 z;*Y at 8lh&Pk at 3QztuK8~{mZR9i4?77~iHB9)*9AqwbU=xEfN0aO+}rGC57x1xiFtu05OxmxoytWH8K z!SFpRUz^hYcYA|-D&mMm7!Zt`gaTOca3361`X#fZy}v4j;0AV-dkg8sd;3@$2-Zzf{g4c1%pnj196QM(*%LpVIxm+S6gj zhP}|*c7-8TK<-#_cYJbiEbv1Yn)+LKXn~dzZw at Cb6h=BMq8L6YjQX$D-Si`Z(2T;V zmqbI$E`boDwo_oiu*cLus2Q3%x;|eUTGsh==sB667=2?PFoKMNiQm8Y*e7BmuZJ~x zr%voI5THiFf7BP%vBpC7y_j`E!|4+bDR^VL?&zJpJw1BcYHSnff_iMbg5pJ>AV5EF6*u;;qn`+M zUZLHGr^)L59sgE7kG;})J1Ry=B$1K)kgdF^w-0dT-7NqjK;6HFKO(=EKYHodkw1|l zc9*26M;&hHy96B*x1 z)=ePg;r1nr(kTlxI7dVw}h)}JR<6?XH)><33_D* z1Mmd4m#hPopkhR#TMZ=DHS$gqd!9RwD=pKyj$<(Ae9D5Wt at fM*#CJ4eUT?Y6>08bUZSxBY>e=-_w at ZUABC1! zi+GyN_N=!2{hIwMkJsuGm{RG at WqrxuUjX~jZGnN?@%AS44ZElE{G}7Dc)fjNG&+rb zC*%qg5Z{Q6M>2j-z?S*R)4c=YbssBqd;IYBW2d^fL(JnZ)$p~l_^bLJ7om1qka5#R zf+^TSDHafVH}rFL60iY5sCg`ht`CoP!8Z3hjZwMk55rbcE3|O>hFk4H_D|jLk at oUu z6yW(hA@=;u{maBt#~z?_LG9fJ0Lx~X6)$Up50l!&w<0g-j_;ALA5|RRryTL_?=Fk5 zXn`}V&vk%s8GVHiZR&9 at 3fNKUL>JB8gj`Ay0SD)TSf4*%hBqI3ao8{*XZe|gxh+M4 zh{+Kpe$g6n=7>GU1F)3OAscUD}PAB$~O-=e5x2SjaU!b1MiI at o9R=0$L24o#)6h*6gzKBSR2{Rz{jr(C=Kplf5HF3t;#5i|%CM14YDTa($ zlyu_(b2XCF$6T at t2;Ps&)$|B{0ifHQ!*VP8S%$#;sDd5|pl5D#q~(4x#)K?na~b91 z5137edQlH-XgeEc4Tp^XJVt?Wd63Y91rRX)anU zw18xl6RaEDWiPV3 z=K95Tho^Qmo(iMLNpl*X*vAig91DfSx`uK!%r}SP|ox6aHySKb%v=pq at nFjRA|ei*II=3%|M>7(WooTCs40+H0c z$yhHVL5gMuGRMekG3$qJS8h!E7-Etf9$lor=^Z$*Bmz$u$v!+_IVj~GMZYXpXUumv z-wP!X1QhgVIN6{$0_K5zfR3Dm6L|F+S&Hzc`LB#EfZeFeExatO@!ZEe7VcNq!nne8X z#mS2<$9qyVgd=Fk#ix*A-YLsax(F0RTH}zjy~ACcU|_^`$T&1Tq)^a_uNspE7-i;f z93#wO-$~gxJd-hnM3x_6MpX6vN;g zyVzL#0~YS&O*20WHT{#`8|F^cM0g?o=pU_4`=kFBXemW7MIaExW-6+RW>O|@vmj`+ z!wD%6s0kKQhJ>I(k&$JDsZl8?85RbF3J|4A0HBJPmV_t}p`fCbBv}%KiBgdwXa-SW z8Wbb~hMI{G!VoAizW8z)AJt_^G{UkhRZ+r6ATX#hJ+gu5e!0;3-FazL?IuMg64=?8W^DDir`a3IWS;g6sWXC zu&xjaQCdM6ghf!qw5CECrW+>AK&e6tAt at P#(U_}Qp_vP?1v?0g5`@S$;)Tu7ffTVt zKe7RUz*|9A at O&{7-SGkHj3=;#9}jLQuRB^Ef4_={eo?i(KC4)_q{Hzzy&E}^4l2i) zPoKBn)W5r4UNm}(5{o-M@{Ak_2_gBbNlT*eQz`L4Kv*vqL3kgtG03o$;?`ky0&q~{ zGKBrETjXWx8Au7hz-5TyFE5a#;)yXEacXb&?NiYJAVA;01>nRUq0mwF6*K3Zi5)pC z;>jzYXE+!{o=oA?2V&ex-22_&4iw3t at B{_FMnhi&?r zs&346B+U}*^#P5XM>88oM*SSW590cW at BGJ;Kkc|?rTdcuh{#dcli2V-%7X!-L=-t^ z%#x3JWtL!~(!Je6v!(jG6hdoG+1PeSW#jhm9Zmiy5i#u**aS>C;3i^R3g|RL!=#jF zjU*t0`1(s(s-VK5yMX1KZlQ*RaqB#NE8%sGJq+8WSb4M$paBp_OGyWG*Nmb}3yDPn zZRm{TJ;Ncaj>H) zm!4#S`?`5g;)m}1zTXdoJ2mm-onMHbpikN-`NDldc|AFP(KQm+I=#N#+vTm%nl4{( z7suLYP-*4Ri*`(3){E){3-^({eIzegSg}U(tw1v&tGEut1egvUmlqT9sJXwoKv~Gs z!{sg%{TqdOYtU({)E+RqJ{J=|k1XwTje8y(!GQYMWntCD(?AU*w)P5M3f*3^Rp zxmx--!lYqt;)YnV$VA~oyoO+C77LQH=8nb+)^`6B><26A$G at J254ipA{2Mc5K$8l^ z at h8$*{nQv-yT0SchbA0e*a71>JNkRyF54u=*IXE9es~K^VqttV&!)Op^}e2OtcK;L zy?GAMbp{qHGE9$&LFMVS51R8ibsc!;!Z|R?&R$^V3yS<)jJ*p zmAT)2C9timqsyM-2>bBxtTPxyAXJi_G;G>8yc!J!y$2FD3%4u815vE}z`z-v3>C+?opL;TZVibFGV z`*G^3cxx99|5VT2M0Dl+M=e6*ipk|`zb=pElnOMFfG72Z0Tj z6+3;#4n$Q!Zly;Lmwr&+vduh3x^S*{4eGGVK0AnoZ2qu=HQ+f#r3dflC}d~Z&o0sE z(*fuZ1;8A~2US<@eE4;EW- at l7r+qR_qI8lFgc%UykzA>Y4g%WV?UM%sB;zcjBW4Vv zE^76>dR_aeR*zf{hpUcO>EEHa#I#0EIAV!$J_A6r~U>8F`-nPV=m^>N5>r*3^F$tt~21BtRre=iFKA zf_20RBrbp-Vl4t{w*KT^1g8PSU$5cY=6oN%DWrNL2pEVM3=4widV5pgmy3doA3=tF z^i-l1EJ98s`9LBD8}00DEokrea{|+9+n8-ubJ=4-(}NbvL^}YrxJPBE9f&QGNg%v| z#80ExGcY^n=SadrV8YbWwiy-R7@@e|0KQ4S5JwE*pU})gE2syi_U&GoWfVn`kXB=r zS%rr7v4$-=>iw=|RvFeI~fiZ%(ebp!1JND^;MJdWlfpds1Rdi{+QF#~8iIpf0- z^-j;l_pLG9Wod*uyGNaD=bVL`ogR?)no#MPg~W86{@-T^Iz_S8IisT705wtoEWXzJ5uS-=!R=tAOq&WkQBi?C{J$-5fbp<(MMrvdQ z$1#R#MKeP&$jwWEkfkY_0f~xIms0q3aK89r5McwvA3-dJZ3e*aIwuP}j$+pzBP9zz!D+38a#qTHe&e6*Pl1T*+^!$oEZ)cyOL7{vgJh;iKF zCEZN-3IXL8VF7&mdT{EnA at A5PmxhJ4;*B8T;32m$eef3 zqDUZ=>+D+d at d!AC7+3p(3U7bXfLb;lc(oq&D-2Oxna9Zm;Nj=gFCC&8g5#3El-fQ9 zb9TC~#d#XTD1|a1i#e|%`bBLaP2mRGbXAl$J6|uNwxz8*Rit-!x1~k$URUmD0ypqN zZ)XrdiNOLQphR1WM=fvVE?qy~j6y at drEia&vm6XiOEEsMb>L$2)_I^rB346>Mi+B9 z9U$^k24ES$n^H8T%2_t5RE(~kv7VT8day| zjaaJF;;k(x!)e)R`lH)jJgHr_zVm}%jAyhSvWmain?$;-0-!{9%Iz1dp=V(cYAw4A zN3ildX#!eb!u1X;+0pV4f#s3XlJT8n9G^PiWdUC~c-B=_lpm7Lia5X+2dXRCI+t^DD=7jMOhl%@>_?SN671L&I}f-V#J;N at U=7OF^D zkbOt1>--463HDY6i3>`R)@4D62?jW&W*`T_PNgqZG#|3x&KZwB-izRTVt#Wc$&0IX zQEpozY3yMUEtz1W9Konmi^FR>SKF$c^EbBA zD8t@pjidKh0}*U&{bmf`R>!DC)rSc)7ao(R at 39x&0q| zEe=7KFW3B at EBE<0oqlf4q_>yq&%LRAeBs&I!~8A$a<7-4ui5KQB=u$J66(MJ1Rv0y zg$gJ2G%=^vTiNyf>;ii3n8(OfQ9c78ia!9P6pBFu0>SAE3Gfk6kYC7s2dUhwf1_4r zL>OddC5}cJ5j7DJOc4YWz+|bEO9f0)091hx5fl>8!32auND&M$LnIVKEkIILP)tzN z1VsT9z)LV6^XCGgB8g10WF~cU0Zh~p1trWJzudMU#9R$zorJM) zjGPP%45E^#qM%Zs8xv8Qp|CcG-FF%WSco3s{m;a|=nH-?#eO80Dyr_R at n&UsE4(NI z2L2!42_DJNKG*2^l>PNO19tuS at c!6Mjmd*Zd2*waQYi`dLx}lCu_$Lor_g`ny}o=q z|4fzC^?Mt9AJ6&hZWB>AqGOuUYjMG~CHaP05=#wGNV9Otgp$KWOnSX{?u8_+S%ou-nub&<&qpbpv6|xS{eT)gPn8I-YFR!tu*%uFKv{IzU z$cv^8^3LX;qG^%~S^*I;*|PyJjdnWfT}JhHk3dQxC*PbA;jmdEK{w6m==Pqdy-EjZ z0r4S_08pz)w(i_L at 9qLXcjffw4`d32Wk^uVYqxhk>CVZnR&PcUt)QINy0cnu?|Z+q z)qLW9{E_-k at cch7-w&qKuOi0n#;o5)2#m5Ws9VDin-=6ENA`jOi_&#W at XaXDeh5Sa zWMWKe40q(_PJ69<&0$FZL_ at nfj}N zo43^6<%Vi4hHtXr*#AF^QkG83&g9N*YEVH7u{qZ7^xM&fXJYa#o)KG5mBnFg`evug z01i3mgG+EgtZ<;tx|SU?vFZEcbjFq2S_uKyEe#@a(>(ahGBN~!fwCKX@&#Z>d`TKa zh^C?#nizznVGvmcMnp&Tqy_9DZqGH3+vpD|!i>)y`hJn>{ykHrR8sJJ7C^Mc zP$GRb&;0rF=+E{*M{gQR1XG5d!du<0Vq}tM3l7=)LxyQn*_K zJJvnZzw+o-s%mCq{Q7cJ-c#H<4G2lji`~>3pw=BcQFn6gvQ0g_ta!i3fO!e)QW}T5 z3uUIh7-2HouhpIiyWSO#9&AdNW>D3~H;jbkU>ef{$U^D=l!ASCbr~Z|0C)gL14)3sJEf2CIi9XS80LHGn z{;i*9cBb&bFV)XWDx zETE`9i0%@>dD}`g)l&lIc&Ar(yKLIo*kssf at -Y+iOaO1dz#87n?B$Bs+qv}XOC4wY z(}}(~a>czmsN!LD-bJZKDwombv>kbGO at qnOHx9)Jj_6(B&eHm5=~`)H1~WjK#;sXP z+n)&#f+NF2^<(#82kt}Y+&AM at Oe^=j5)Q3n3%f9iX+q2OGr{y2e3obiWek!+`q89i zd1oJlOm~oIwkKJ8BNb0^d7CS5nYqTa!!&2W#Ai9e)@oWB7#-G_fytM|tBE$MiYP28 z+0y_IEG0}BtlEgS49%Itwj`)vp;frVR9xLV2ge>gH?5sI=j52^W5!s}KZ(dRZ#Sa; zc8x+=Odolny8xLG1fW-Xp9+~ApI^J_{8;?w+`ujN|A{pFw*5K(ll5YTfdXyL^KQ({ z-87i|deucgI{fMS1c~;kVLpqA&X7;!OJ4P1!kHfe=R>{dkuwv4p3^k7iZ!)V!|$5~ zA|bSg#-vi|5-0X2=@YmWyNtG0U`kVr*7J5Xy9BY7oGpT!;6*3(KU$j*xTG(%<)%0xijeVTpYhT<#Y&W40*inAZu=>6K@!!-shvD`(et`OeGwLy)8ds)R!JfR zz-2=}@GL%l_@>`ar+Gru8MwR^CZTi+5N=V2dW7O{!BIFU;V0vGv#mgePDBQA+y;>( zARF(lMZm%k1^+Oed0*WvPMbFX$axpP*AIXr5QxVnVBu?T(vU(BEwL6rCtCbmJ89MM z`-H`Kj%`P%ZECS_vx_k at Uc8m~?5QII0#LK5ES~l3Y8P#OSVBEWsyb7zcb2m3X*O~0 zECJ$GIHq$9`b|U;&uPbg(Ray2<{<=7D&3ohZDh^dtIoNR0?!_HA`?ww*Fnn7f+3v8 zI at t~uq;!Jf>XM2y^xpQh!~LcLs-SgMtm&}~XtgC-2TTFfL`tb3sYOa#qj>aLQYAV( z7zvbQf(}C=jLK9uDStCiL%1Z!6a1=jIaR^>h at 7>R? zUYP^@-NXj at F#vkiAc51e2E6d|jVh`qBhoef8JqQGuX=;q`DaT=_mJWcbb-X*T7K(L zT7-Vq9J#l6bP1W0Q~6$OPKG4_lHd;DqMCLzB?8UOo3&M)iw}^7ZSBp#bbu2XxU2Ps zTqq<2d8qQD^ls^1QK#Abv1t0x$4bKnUyrIi^I{(l0)-zg&hG?MwO)1ZvgE^^o?uMw z>*V1#`_RF#j36)B;bbTvYsFWRjmU1Yyy)~& z;&B9nqO7lr3ufZ+$p99>`^4Dg0Lj$h1&DOg>Aa!78fi!=FXb1J4^k9ym3DMi`sHIL zwiG*46scE^IL$WSh3BMwF-MBhk9bT~Lb&A1uKeCTikpu8`TFseI@c}pPV$Dxpr;<2lrGT*rY!st-8WU%;7!4-V;_OxiJ0R~zRs&VP0V$` z8y}Y_QJnVjl;;Y#(`|!eej&>T#LDaKBP5{|h-@#{j6mWMhh#gilGgVT`_5^zoK5=H zX5?JR_8LK4oGg!%@o65i987>$hqo>k?jeD)>_KJHP=y6Q$^*?|aNvJw6bxvxwQ8B- z!LaHOJ@|%RE+z&?n6deYA2 at L$tp$(VI*SHsV5Mmo7*Yk4qJv at 8@+<2nM=H_SY$0M6 z7qB324t{6hk5wa=>*~zF;c*s z#e--j#9rCA*yNd?unr4#VguETk0cMfb${5qTIQ@^*r41oS_c`_qVEHv|uy z_jKzIK1aa*pqhXW)J2R)Kmw3qwS6?BWP%36_%KEK at 61Y4sgnP=LNbu7q?CSe3@{LX z&4p@#F(nU3IV?i>Aw61(4Iz3=gowd=Q4tjo3A=WQ%(MKh+881L*v49BDWi%ZnKpO& z1_)sQ8)1fcG{S*Y4;PQm z1{Ne^jTpeJtPNrc(lo;*fU8l43VTDa at B{nhJ}1cfgU$!5 at o$AW36HAJquRKx!5^VW z0ZPSppIB<! zpbE6O>eXepXJs3jbhYxHs;tzl4|SBw8qPbt#(%;H`jyVOiTH^~dj#H}oJ_M7vOXa) z69F<4$|sOdpigxvKA|FHCPIF(J>q)=u%Foygvd*YaW at HJJ_2A(!hV4)Cs?1lCPGk| zCm}frU`vSzJ^>&S5)w~%njtCQ1V5Ql%J#zo9tw}xq7V>iASfR3SS5sly|7P|L+KEp z2i!%~284=#A%K`d3=izUOdw563=#+t>3Pv;PPABCga(CS0NPbR89U3Afc6~ZG{7MF zb|?pGsjN at i$VpKc?Kjn>i*%@K`H$J7MIBXQOD+&WPuauF78_kuOh+?QoGx1>0JFpx z2>?nU1(79S1d(0&pVmLza1amb{ZtcsR8&ezEo)T?vq1s06hnE4qMbsZIt-5hB>^;9 z5exv4AJ!=lQl7?D`T(w{eE~jVbhr{A*;p!NM_yEv5EIq3$`@2Fs8XSZI|3p=t1sfH zeW=f=<;!2SeiICm}WH+=}7W3iWE2)rD z7k%@<;Fxr)_u)iCVg-eZ5-AffQURcvE7T<=D at jR7nh)kvP~3f}bs{GXg&HP;Y*r^$ z)bjHm$C{zitII_RDG)f(xDp=oQc(PP)S>R7#Q>FvSrp7;5qboIJ0Cd$f^||E!V at q; z37{b&|7h%BIAIYHQ5EsS3^Op$+TD at 1;Rr$ygdqq*A51sM1?F^w=uP5gCO}|Ji3laI zBvVBwNQqe?uz&!2 at IZHw!YQkgWF>^;n8IiYtqp`#Sin>ag;*zAp$E5|eP1E?GNV8t z5TZn(GNMQ#0jgY3Up^E@{?7>(fbG) z2nPh)V&J|W#K#JuJ`l?;1fi|m{7rHGS=z7bOHpO2%U7g*Botm%>57MuJPlK=x2ID} zljOw>Cfgdtt`f#Re&bhq$EcXrGV-{)<9rr3F4Y)EC`(hE3XejfJ~SM}E%DMV5C+>5 zQ6fP*lJ`2)I?+r-#ZWO5vn3BC8zxF2u at veeBKrXi5e(gcJ)_)!W_k(e5h)OWp^@|o zb5w#q0Fk`P`mz63U(NGVp-4$cNl1+i!J)HWLUI#|I5EzrnhKjg;so8&9pu;dX?z1t48u406x-Pn7)yxjw;9q6iOz1vc^jNbrZear$eI%1ByS#ptiB z$_jefl`HlM(3%Ob*mUOnNo`wqTCE+0u53XzD=Rt^0Gp~M%*xAZIF_}Gnr-mb+7oD( zh_ at 5k;+dMkiLx3?9^glKCg2l-SySmMFp>-=-2?=rc at f>tgNljK;3t5Yid;t^!x9LK z6i|&sHvgoBEZ3BV5)g%r6gAY4 z>i5AY3Q)vVIV!|J&GD7k5g?A($WR*;FgB*tMKQ?=08Q at 0vE>+AmkB|@BNan1u+xeZ zrkwU&Ho``nl1-y#whszGgdp;_QBqFQ*AszCx|IyU+P#ijl>Vd&h`_{E4HOC&eoM6w zHFm=h$r#0YLgfj8g26C}NI`)i1`LppWC%R0%*Ozk3?Q%wOcjA8Z-*1$`{|JXKOaPi zIFJSh2xn6qXQLF`kqJU^F=T*{(I81QOa#$2GgQ_Df>WJpc?lA6O}t$ZVxmDH276dV zp#;?sqXqlVA|b at rP=!MZ{+J>iz|07!McKe0mohF;mP^=}Kr~^OR}~K7HH+LPLC&T= z3VI3PBnTt~f_HzK8p4fge~kA*yL*X20i2$W!19L2c$O at q%7h{r4~yj?Av6Xf5u<6Q zRn!^sQC5hHWr|$(vSuarl^{T#@_*l z5VcJXLPVrMC!(zcMD7 at clOFBqJyA}Nu|&?E^7F+_QB*`# zBUMdR4K-C&RaMfisC-1IAtUB{OmJZAQlSn64`X7QD5i=ED7ye203HK)*cFTv>Qhh> zi;-z4d*`fx;E_GMo1;+_B#;$Q)n3tG>uA3GzRxgFd>UX$g!_8&)gg4~(Nb*kTFbe-!Q1T7J`j12IL at 3>pun&Sn z;Fs$Xz?y^UQ=5%?dKU~SASb^dPs{dd1k6Yz1llIs4ToraM8K3K2|`doKSc&gTuKij zm52#ivL$`6#Mar=*gqQXE#V-)eqNRbe309;I-8Q4;) z$H3f9;LIsj<-(K-F~V*ILpU{aodMqLa3b at U5fgB>RSt)JXF>4`c3h17KV33fGsBY=cyBs>sws472P!=*K(2l1BiV7`> z5NRK{;T^F1Hh}1ha~T&PPC_&$3D`;!2?GEq=BR8lMIgI}LuG`p;sMf-PXzN%%IDNj zFo+dJ7||0~AI(aFTySB+g-5Ii*+8Js(iTUMkzkMm%&6~8>Afz6gs`QA$8o(ttR at p7 zA_EZz at gRUrs)p`lV4!=1zzl(bm at oq%U8BMvFk+35vP`9DqUU-Y5ukB*H at pFS7NFjIuOX1Q}2u zs2htC$^O!|AtX$&Pq<1CWIZJw)c#l|5!_t}x0BtFe?rorH9iZ~FOhdVL zq^L76s;a1&D)0r9LrGP+iBld9wTDtFDW)i?h#;zluND+hR7Ao!Yu|%#z}bn4 at 42N^ z(|69+HduJ(_OU29Mg^oP-YO4Lse{W)%eLb*lO$0~2`f}blwww$YFcOZq~#Za$)Sa# z4)P4yG`7+SagCp%mw5YdsD01yqfClQEXFwEX&O$-MHE7W!>mrIik?uWV0g34kUXI{ z&=Idlf+(3Nf+&Ivy?%6lAG_0;@_PpDbV<_O4zTPx)GP={1(J~+Wy~3N3X6b5+(m&E z2hC8_XfQJcNTyK?(1-W-`La?Lh>QbV?2>}8~z#fO8z*K;c6|$4&;-!%X zZh`@{!~~)Y6i#(Q4&Z at 1oq`6cdI8;vi;|X9FecckR|i6vr5ac)JJl18YJ}aowg6oA z2OUQ*`1QTVgAyX(hoTA{9frX$2aI%7E=H=G85szQY-grEYbN>qE0 z;DZ^Ol&D_sAWb at jbW;SuFa+qNI-$&@L=VVAwfyp3Zp6lKs1tK zNG5`4CwM04C_dwsL^z4&CbdJ^lB1HYvuJ>IswI^T&ZzAl2>KzD+#Al|+30W8s(IoZ zj#N()B1nWth?J)s&~*X^*oeWP;xvf%f<*$u#6Y?_7`L+pK*Oae91jq6Dimb7su46& zC~YFbLJA64kVqhakOQIe=Te;PCaN4Dltal&fT8Gk-l>l8eP?51zq1Dji5o<7yBpZW zav2C%=sM7879p4b2vFgb!Gj_Q#VgCbr?+~P3}5PQb)0YL~z6UXdS{LA1y#kcqP^;W2DqH$jCvDbIYXOD at TwB zDcNBv0FhSX!$AAT3_}?l5+opDfd(O%BBW%IMp=dcr2rx6eh zI~0{FRD(03R(cl`FirxE)eYiy9YSg)grO`!IH?5G6Rc0FDjPsm-Va)*YS=*BFc5?7 ziEX)4MutkJOF%=3&IQOQKovOgM^c$6)zA!r$e6_{l?n;YsB3q$rP*M`S70C#NQgUB zUM|Dk*r;$iL`cD0MDqX;NGF^&_UutZcsvaTh9;cOYV+?-fdHoPRV^HpLda2|ltK{k z2&3VeD*KfWu+bo7UjEynSad{-wkBd)opsj5;d|c2`PG78SR`UQ;qDG1atMU<5kAOO zqL2>gph);d0 at J=!eCm9(IK at n1gar#r1O$Wx1I~UViTxIlH!2RnP5_6sL_Xw2HCS|3 zlAxdox97Q;00%tdXgljZr!hzxRsRT9*CwS!N2&jpwDlbt(hpeRq6DqKIlSF%vBKCtGgW at v}m4$fOgA1P#QrR3^X^frAID+TvwXLy%L=5VQy74=9#^ zbGu15qI)X6lJ}yixlk=cAkC;PD- at Ab59-VDaWOJLfsi|dBZ?m>}iixG5C*7}#9^!S2*9P8eDeuY1xd8Ml=mLn$v_cg&!lbEp!eWs6a zAmJUm|51G*nW^eiVRjq}!{VoQE=vUc*?UWd6iv`n5uzR-MgA5g3KVan?a&c at OOSZ0 z$I|61 z(Fp;xk!mGDsHBMF`M003{T#S?I0z(G0Tm&C<7J1 at 4L(VNsz0?m<;N2K>LB(a2>(C4 z%9)zvts(i?eH=t0DfKQ-DDFPNDJsL{nEEPP-UlQ>`9Hvyeh2#+L*8TTu?-HX&+S?K z-QU{B$LyVss_}ZQ>iCKF96E(ghERwbL4Yz)(NU@ z1f?KO>S%EelTP5~v_umG!Jrh(%|@955&T*hI!UrVb|T~WW+zic$fe!!{CjSd%1?(Q z4)Yqh+V*<77Sj=o#p!)048f*G~dWj&Zy-mxDLxmZ^;Ar!k ziZ-}s%ZpH`8Ic$}#H-X6_&2mKwXYy}d&jVf8`R8lI0?)oNTfu9c?2n4naJ?O4TK)) zju>Y&goGg*nW-17CC=F-7~B)kgh0 at F&V=!0MrN51Tk$1aHRi;aJ at VUjt(~G7!X6F6 zgm7eF^z-PjCgZSXRywC*_dX9{3SLKAeogRWR0P}^Fe8#c_CS0?!_r#= ztad1dFknpt&`2~~IXDhPU}%L%6Nf&O8W=HK%-pkvuCHHCtt(09`HX+E_%>|l>x>S% z;}7;l3=#o&I%X2jm`!bnJ(f|}|` z96{ZfQ3I%fVs+BfP~V8q&!YK*rvboh!Z92Ytji1>!vG+|Aa7FPz=S>ybQ}j2{PVLg2vOfg^HlLGF{#aR{bVlA>#?LIcSl8AGrgkD*q+(}4^=Yn at w(IvT7F z?l3_)vc-<_Jcw+JCs8PDXy!=HlhiXqQ#}gYkj0IaYZ2IT0PG330j!pPjD%X&n%Wyo zSZDFmH8u~3L_q{Zdk;9gJUSx`6A)rB14Vl)kH+R%&DHl_gyQTms}@HBEv;)v!!RiW zEWa^{u+eu8!}3qgD1Q|bgVuH&r4)C&a}&9?s6dcNI>)E5(vI-sdyeld>Ep(IkD)Sp zhd_Q$W0cYN0ok+{v#c=mq(pdrnB;M}B$7!al1M at g!bv2P;tz2l5gcqhCev(^L_J|X z0w~iPJYm*laLE at 7FHl?5!SwcGG6Hs>uVvfdQ zKy+LV@!IdqvT~Z7-8N5Z2cUj|BiQ&p3elkKLxUPNm{u|pB_`2BY4mecaR*kZB~?jX zVSze>j;Z^+6*vz)?4%+gAqXKH(_$O|&%D|&kfZ>>%t|I~&T9{*9VgIqBf&sukfG`f2tp7bK%$B$ zg$^DCgf|ql76&(LnC2IF0RhGF24RcVCSNIRNeK=K904pGUHTda^m=hMEOExM$!3`Kj+Dw>&#CLWn*+cpc6}A)rEgF>Oi& z5D;{bbRbAbmeQe^adXw!_*cDg9;P-<7qj19iLS3>$nrj-FoSJ0GLk&QP#_%$bDa&% z{0S_W6YEsorXXEBT9ig(jE<`gkrMsO>Kq7&VbIV*><6h2$ob(Ngp-;a3z%@yp|Bv$ zEHo&XqSK#Q*pGG?gq<=G6`(XnxO>XnC^$p0#)0Xp+|R0^VrJv*eh}5``fI9qR&^1DHTI>I|@I^NNQTTgQ?f%>fgcqPB^sFxgHH$ZiNQ zA|asEeNNhS>+a4XA4KT|=R`%;N>Va0ZfO<5QM_Yw&$tJY^oK&L$wJ1EiE;r%glzYD zU#b;G#n^MFQBJFN1v_5Tof=MOfRrXmkm>N_#^H%IwW-wDKB2}(SZsxpj0!O6qh8oX z>v8WrT)rTh_o__e3+g^#c0S?z#eUTD3WEE3-HWnWI at y+T!mSG!+BhA0)93 at w=y!h* z!UTeZAqGN74njuQ>G7UM?!iM0E)OLHVU`=LE?mKTB`Q4W;g?ZVQ%MOZ93$fV7(Vfc zQxK2lX{I#()bSh?`jo(aVSR=mej at -O3IzKUxO=S2{U7YvTokxLD1ebd2?2fd0E4h0 z)53uD!qPyvA8kM6Ky at S1MM#q0!U|8}2C4YT;aw9bXaJ-FAp9lBh-Z1V)ixbez7 at 4$?Fl zeU}0>k{~fIJe1ZqiCHpNp7#y?eYdylu&zml;f%dP;Va%W*S4y3V-Qz<96+Su)+a`l zj*{RXwu&y8mH<9>@z{2A4FGjS=@isHW`Q7of=v4`b4e&dr5I`RSSeM%VH$Q zNj$c(@E5H&qV%PfNFRQ(s;-bBovCxfiaik*+}UIp=3r&+(8vvoFhD6mgo+VDD`3P9 zPqTfB#sJ*&MLP(5Di$wq$q_*My~Tn{O&=RNia1C{s zPpr!Z1`C=TWPe~VIv6trp#}{V?=GaN0|pEjFw-d0DccIT;8rg{y^!GgS^28~V+j1G zUCizltfEq+AF%CD7dR+RRZwSO39T7D-PCxTvxrGS)Lkqq_Yn@@EFO647iKi_i@^2< zA>r>O^E2{;4KkdQ*`9{9k0KKpNHC5fFsUkeG^lg>JlnkeLy1HxA{RR~e^!l9MImXA-hzlh1EbP}o%vsB*i1p)|dKhn)8 zA|eUbr9|!ZZ)Q* z^NSOfW)25`Bg==Jb2<|zN{WZPs`xZaazJo6SfT`wtOO#aDPU_N>sBc at 4?$gGc0ny* z4m$U{qSUAksB>PoZPJv40;OyM6nUaFjJ%=*atsfBiE#;VT(Cbo%m-8?B_L%b5AZ1p zh+!cT4^D~SK^;E40RspkVrYn>6z%5~G2Toe2&rf=06E9wde5(HJ+hn3qRh<9ilX+L zq(;Yt1NX|FI`Y05%R6xp;WPw`fTINFoLxO)bE>R>ln6q5p)eH58?E?UR1PUAK%FKV z$V>i*N+ at T6KhOn)g-h0|tGb?ENs;RxM;+X(6=q&m?=Sx6Mb93H<@edQ)FQx zn+Z-z8v#IUCX$9oX3AiZn2SS`LXK|hnC8yN(s#?GQ8WtY at F){s%>`p5N58?kTGJSD zGi04b(QTgl*mjJ-qHl z+*CMwuS733qLqFXn6%O-OiY5#)`A^3EC5H29w6J_C@^9nU_-hR3w+Y_Qo!DT5fNTd z-vHkhDkvxjiI2WPdPsg?ez1`3mKwRFIC0W+8EdxF3>u4RkQ&C6MmNrbkhBh*$J351 z85o9w2Yop>MduW70_n1}eWp&qu-N1jf0)-unW&c>9FYE!BAzf#fz}~e^v=tBF>F13 zksc+Ef&M^)&B`9XO~6YiR1XlyXUcTX-ZTKamS*9KB+*2B{wKuD41+TaAr?x($d!CP zg?c7NW at Z(ikdw1+Q$*B}lUUJ~L at 8y2WbPPiA9u_7-4g%N0gx3RYH%;jQUOGPQD+XW9^Ga;FO{uwQ*8sY z2 at 1P1Sx=cJ?20H^tYYN^W^)A1Gdk<2xkgTh4!Y^&5>N)PV$z+3^Z^`&fowA?S!fNB z0~uLH()$Y!h-|Vq2u{swIp73t^ukEt3UKKzkil75Vw|I#yvcTmU;-*=q5-rQ8lz4_ zX-pxXBe~mX*Wz;{7#>?^Nr$Z0<$C0tOU=WnH?CxJ3dMHAzQLg^%0ZWz!J7>>X-7wB z#=vj7NGF}@oVQSvS0tRM#3hh#q||CLXv^1()@Iq3N)q~7)+x5|){6Z_YrmmPO~qS8 zT~2}ML@@yA7Lk8-O;8WgALnBTe0M-Sk|@AO>_~uBq`t+t{yj3lj1DAoyacWTz#_&d z1-q*SNM~ZLKowMrhl?#tQZl+=!+r^_$w|B)twfl{GlNiV9JO)hK5s6||3h6_{W%7n zg_0(yh0FU|-o%WJ7-WvAySS4bl7M{+05M+h`2;r*QWQR8pF|IJ6v+}KiQ=a|7EfHD7tcj4c84BbfUt%X$ua}=+wgc$^ zdmjLvTBnwt+KLH at h~I&B0|W`ELwNOra4-K8G)NC=1|1aQqqvGHLYR+TS3`h`^KYqek)di z26H-j9Kt6v2X6>+R4>NmUgEe(Bx42=QoM3pB9~KSA#u;h1-QjX2 at yyS*d1`BC-mY` zPybx5MKk6s~$gpR!ZDA%hSW3URQUAOYnhRWyL_ z;BSjiG8~UCcZ^MN>2IKP;B(()Qeb^>hCl?tApK;X0QNVt$irb`p#p(8a4%$JLlq2} zc?aGdkJA03jFeMT{${xcW8egP0fd2s^Aqt^5)uUV9?uwCaNId?Nb|(58t_PCjl6iW zMmAeZTuHaF2}Uv3u*L|f>v>689ln!a^AM<1AnVxsCpthOaLEjbB8dqQ(0ELo;%gOk zwwHkR4M`M5=8WMx_4GmC#}5`fy3HjvL}U!xlLG9L8p7)WQRIzeTe|SEatyc!EI28~ z;pTa&eNZQ)C%AxqsTDoss+nRyC&(EN1w{fdDh6=hh#N&fbhu}V_pAqODIxv`nIEHR z4kIZDe#IjQj8nF$Y?n-}RY|rTkaOd at MWl$QRUHJXZQhN+^TQk+wYa5Um;EG#L-I5~l+>U at G;B5`R0< z1k%hdju}K~W}6r*#U(LKQ;*IQ=2wOHTEw54UZKl{!ZI@?$QX+ZB7y_HA%Gj^{PQp} zF~@(s$9YXR?2pUl@{;_h0?=igaU>&_MlJA%MO%s-e$bDFg~;@9f`I}EWc8WhX_R_# zxt#@IOawmS6=Sqi6UG{`>&hCeCT1bX(kq0u`0IFoGgBmq=k9 zI7WwXgNH#7Mg&0+L=glK5eMQo&OS$j*_^M7&|H{9WCvt*#J-b>W~~(8%}CrBK&Mzf za6TjS!Ljzra*%=wDGH=NOkoUWe1qZ0ur&qKK+(epousBA48YLC3N-&$U5OCUBziU~ zv7(B=poj=`({zZ^CJJD(3j+fZw=$x_tf+`$g(9I4MWf6fM<_OgI^aN%$|4Bd_4>Wq z8|(udC0~cGWP#!hgJ6j85eEo;?upmmh8i7z85H{*)N>TJIe+Xj2`^j8Rq7W&| zG0H0CAq1ZKXo*E861g~5W+ at VhU;&v at C_oNKFi4NoTreDV=4B#551eLW7Uos5WfYKA z76iCC4TfeZP8AS^NTj6+NQ6m4v6TiudibF9?D6R1=SmW_s6As`hQ z4T7%?#=wF$6UKvZ*(lu5>_V1MWFV$x8wNr#r3es4*(oXVjSYkY1jwk=2U-d+qQ1#n zp{$gHMPOC!iDIPWG0%of)x~nG$G8(C2^PX$%?vx&7%+i at 9Zo5M5McyzvaSQi4J(#r zTevS+2lMXWdybAm{N3Pg0P7$%pdi;6Qa?|t()?2aIa}ppdd3D>mfID7HJ39Oih!bm z>paZy!=tPZAmOk at RXuk$D7KkZMx+Qxo=QzXYYMSpQ at F;Mi?eb;hiH*KSkPNRiMq)1 zd#-Odvh9Y-{=1o#j9hWD&8d1m=@l^`@HNs+C4xG at VmnduoDIg`2 at V`kVcW~Cm6FRZAgasJbhSsDTD`k!lRiu!n zam?L7k}p%IbeuwJ@5;uNyBi+ z%Xc#oaKM`FnF&=#cybC*d39}f!B7nX?aXo>`{EabJZNV#ZP>trpm#L^d(JJEOmVr` zq!C7u7KMn4$l0?ZSx!s}!&w?lvJFW3YlbXk?8Phy*#{CH6NV@?n{AD}WGK^vWMKql znmLQ(nC?zQ@^F)Kt1BYsTY8gO#|SlNDKW%0ILmOgv$rTndX1AJC7`IMAPQF at qz!0{ zYz{>^5zWoQBDCbNg^nbHtxN(cn0Yuccf&G_BQOozq9O1 at tZVFvWnb8avbMFdXT2^B zVqTpVGEAFHQ3xIbJ2%!KI%38=AU5Fe;emz`L6CS17&D`XL3sG1gM486W9v;GS+J6u zfkv1au(`q|j|XVtF6G?X*_E4Ww2{9lN<-&xkSjD05`z&5u`?28>LJp*bu^^9K1|4M z)NLalMCj|b?Z`O`u{t#el!TH(O3X!z9$TD^pYr_Gt^fFo!RU{Wu0f;OE zr(R7BDcmg6PD9EISer*jAjnSKBHV(np at arA1&0R-NHI5nX*XM#6vJ5X(?WP;Tw$oP zy_qD)(q$=<^vY`@Y;2m3AAlIbE at TdYob3@c#Ep8i9~oGf!uAc at 6v4p3#KypfoZYel4byy znx-Ne;3*xA#G%+-N*o)JCXkSFIt_#8PgiZ^AtB1Bhmf&BqB;ixtm2Rv*f|!pp*GM_ zt++l>kf0)memwWq$G*C3B?M7~5RT21X;eOF6$o%jpz|X_9%LZxxL_XKL&xjyT at k^n zvSYo3N*OeUWe#ZJGAwBJXK&?Vh%hG^&_sjBcp_t0*yPW+>Mtk z at wjt~6Yg^yWlI$(3?q*i zt1r at gd7%svL*DX^_aZaARd_JMH=SNO at ux4t`UaK>i6v49B7aZ9$MgWC2PFX-KuSMJ z)zySprb5At;F%~61R at bJ)`&8LL~2M^3lg at f4Hq_daM0M@#I1uIlnFq9U=jpY2$bUP z0ta+WV^MHe^Fy at n3Vfjugkh94LO@77WPzcyo|&G&wQe15&v-9Za`TzSJRFZR9-D32 zkuc&&53$LDl%Z)5m>8OAB$5$s1W1di at CbHpR+q4gatEA*8e$?MA{U=A595yfZ~2PV z03`@Fq(4-5m-J0gkds9;Ng?SOoM6A6C89xL+X91;s>mHBj}mzkRd at q0Y46gKVHv3ZrqZ8p#}>>AdUIs z;+!-9?(MGqrCP`w?}kkce^!s0D0FY?x9yLZ+8te2e8$w^x z{|ER#wex at 9|IhIM>HZ(z@%<0)|LgpJe}9kvkNxTYkN;oAPy0Wg_rJ%VXaDcw{vY}O z2i*P-_kWA${x9$TFHiM<#rd!QpW^=~`2UB0$^T#SUw`rcC+q$L^-zB5|78g?@~@Ba zYhxR2zuAfr5+sEx{}0oml@%#SrBYJTQc$$EDHM)FSR at VwQsO2OpK_R|ppX9&G=WG- zIHnqK{i3p?6n}sG;*<5(Wy8OYzvR__t^WwF|I-Ws0veph+Sv^AhCrr9U#DkecM2x% zbCg2u_=oa-x-30dQ2u(MPw0YigZ9BqQDd4+q9tWjaiX~8`iuj##OebHNxF(Zg6r6b zexe8?ASx7>*22I8iaM>?*PrrMG`>>RU9fj5>5R!S!m~Le=yA5&Y-4)f>6rB?z(o!J zTDCSc-*m}6pO5!#4_~ybr01A4oY6K6af&mNlrc+q2hYvSikCs11LQ`@U>H zzkTCN1+x!IlRd}vByksI9rK-b?H$m;sQ*(93&OA;Y#Jwy(KQCoGyXHS=$uy4(<(?4 zFW^giRu>Ds at FTL;5((mTf zYs41|W#7a%3~LQxydoD-8M6lw$vZskX4CG}Y#F|%V_Ssw%v!sO{6!i0=E!JmIOB zV{k?!V==La_0p!Rim0jS&apvfO;^XtqV42(M{$AJ8a+JpN*>V!oW^4pV?C3HBrr0L z+7gK33YaGRH)F-=D7rJUW0!Of%fujarttHz^c%+j1xltFk4DNAO73f_lV!7R?S`7e$vAt&m}Vv8SVrjU(~88`XLjiaPqI4CJet>d zu4#y9N{U8Gtx8t6&vB(?Fh!R#D_Gs_Yt+yp0U-#>Bf`&TFtQw`%a^KQnbx__Y*L`m z%+C%a4_rY6xet|w8n&BdhcK4P;kng8oWvc|9k!*;s%)Umb{n#_PE$OFpcR!1ylski zM9iu~aJtUbEG^5J^wC7yZqPlt?+g_W(1awCM~=^lh)x{x`0d2wws9!MNI)TPMMK#Z zoMQYgpl{CZ+kkAGgIYXn8S0>^%BpqSwN~|{Jjhg$6TI!s5LvmuL8=w at 6AzYT&lSIc z5|Oj=no?S7D>z2dWEfbtGcLvO^rj3%qfEzf$6O<_fed3)+>qHJdCOJ^gr(tz4NL4- zYfSpUVKx>bvl3=f-^U}F z#x)UrKIK;N$ZTVI$}Jr&`(*IRm{K at fW-*+^@U9WYPD=CBL7ePsD9k%dNHai at eQMf- z5V{kU?N);@zh!xbnvbYDYCds*+N1$)k at Rd zIfR76#$b$KADNGFm&!V>JvfHlJ|Y>62$bs{Ur20jI%V5n=%eL>@ zcm&3<-6n=KrS_%bWkW;X6-Q7v=*hR!+Md`oj;|*@Q_~Mf#ww;Ua?^y*vx1JFHFRed zws9SUs5h3lvxyWtCc1`b=Le>qy1n6 at A`$0 at Zf@-1IjNYHxjnj0u$wYCCHp81x* zibZVYY6+2sGIUIF#z*GX+mnkxZNlF(gakX)aENTf=*#*uFnT{-V=}=4!Zge>NF&oB zzHoGm@`Ug1)5Tc at -Zvz&JH-zN6U3p+TIbxMnlrMg at TjH+RVc`=KNht at B5?{2R`o|C#Dcct;Atx2Us z5F$bZQY$6HT>>RtCr23|*+Ju-pFT&LG0Wehhl9JGok=-a&KcelMXKR?%X at 4kw>!5M zj~JB_ZKj|Jma}DA6$ftx*Cu&ECeV+E%n#EsH+j++Zp%hM* z*zbZ!)*q?kTXC-g^9MHU8D+pQ?W<4P?U?T8r!Q>A{WGHvwM+?Xki;f4Zt)b3&#_94lJTd4bm_ia6YYB~$raeU5S;oL29CjcwvCjs! zQ+cH?c*$tNJAtjV0f{EW&4FZ4WO3d-b3%}P$k;H%%ww5I#_O>bP;{jlti at D#pvz;# z`#gIdb_WX4kb;LW8y|*rTgpg6No?{-g9tVgKCEH$PjDd!OoSwC$e|^IhY8#ybe;EE zx3YwR+Hwkv=)uyAvta^xc0w(r$#U1LeQtF+Aj6x^ z3~0k#d+$>`x*#|6!ajIudgPprpan2JPL z0BEkwE+)W8ChKWvy ze26$(>kc%kWGG~Y#y}Xrl1O{PI3f|?l}h+Rne}>ad&B`<06riP0wS3pFHC$x-2bB- z_3QnYvM6|te*6AjWVjau*K)>@1ZRc at +mpP26p#8lfJl3=5 at MJD@N}_F0tA5kI*>o) zxF>*IP|7^W?yg9?!{9;tl>9!z>Tont1mKV)5SV*_JpJSzS}A~bD5()P4k%ENFdWE4 z;sFx~VxWWD+xgzQ#>mru0bRAQY^;T}l<~S at wu<`I}F)1ggk0FTTkLdW^%p|^c3#$FG4BK z(2#`uFv+0_10yD-X^vfi;1&hHN{S63sN+$(K>=``(^m6WoHh8eaP zMM_LFJi@|;-&Gnu*L-D6g3Mc*sV&AfRO;h~*A-p5e0^t*@7C9&dwmP&CGS*Cb|Tpq zc6{44=H}g=5X))`o3i8~1Joi`JEF+&W_PM(hp2{P9M-qKD at kF{n(5+>9k$!FZn39y z%bmBHDVc&u%smg`RNFH!24%0n=9^v`_0#4S4FEG+ca7wzxP+u at QA24npsc+%9Ai~# zr*)vzh08&jmVOE@;rf?2RktzJ< zw8XbD%oP|i%CTLsf(^FpqAA6qUN&Z6c|>6wZ1RkVSJia-tA1Q{RfLnOmkHex!wsMy zg)j^j#zbb*5^U`EyiL)ajE(D2fYgJ=bY4{i+A-^1LmZD-X?(IpIpq4&Fh5yy4!b9Y zt7y-&baO-lg3f+{)ov)(0BoE*3e6cXo8nswlo&oF&N&0P7-~ zG84(rQ$#~j_IJbK_2;5#X!B49iVyD6$*@%IfB?acL~5$-8)^xNqpUjr5TY7^^FEE!s=W|Crqyw zQoNn+?-Wlo+d0t<2WM-*iNGK>BiBD(y+GO|gN1~SK#mZuN(2EBVBy)%U#-y-q(0&w zG+7nNP-r-2A%fz{|2)Q-M4#@dPT%$~wIU>cF7(ylu#5o0!vtC;n21^flr^r`ua=ZP zb>(hp$J)pM7A$Jb7&qPxw3_3Pc$N zK%|^9?GlZ;vXDu-{Gjn~iI-ZUYGN#ivXVl^FO8SIepWW)Y)huw_Y>hwS|H&AN`t(E zT!fq6cAD|{=4gk++huI7J1)}hj!sOjCNa)B>q#A~Aj-fiZ6nCEC0AKo0eR0F+hN+t z^f$}H;$IWc_B)Z&6fqLSU#A}WY-X-N33Gv$vLo_U$u58#kyawn$Je(Nw*Zfr9LQww zQSzz;3%0W$S~47g1E+pLbyUTY0v1k=iMG2nw?x8JY>Oz9LMTE*&vp87{h8AZ`T2M~ zAmkw+*$6@&I=GqK)R2qD#;i?GZotPo?neFA(+?7+G)z5j1rbvDC-OD?lx>ljs9jyX z;K8%cJ5hBQ5qcjPFj(stH2F1Of-tmmzEQe`pl37{M413_RRtTXR?^c((QlAd7zYaZ zO8B&sGzkgui8Pv&JF;S3KXaNB%->0U*Ux7!n;E5F4P!0`}peY6q+u%%~s7w{Kvc z@)jy5wJHRHA$=s$w^_pnBDNuM#5D-J$J7uAz}`Y>n0XLjFoht(uu=s<7R!W$DaUf5 zNwZxpZ%F|Y!_N{%z|XbAbjnqVb0P%AQ!71)DyFtw=f zo!Qv-HrA2`eP!)!Y89XX^wU9L`6Y})cQ)vPZxtD#=RLi1>P9iCs(`MTV(X66HAWsg9NR9KX4ZQ^=7PO3eRE{v zgKByMinZBcF8!lgt_-shE^YRA*H?M3w;OAi)!=Si?o;0%oB4BBHIz6plUO4 z)iTcMfRfK~KkY!gM~L3v_f-?Kr8IC)=S7h~*hj<%-|^?JD3J(_5>V%}4L-=iFK^86 z!;?V~*vKf1l46*!ShO at KZvD@8G9DfXTHc#m0*K$B4da`kP$=2(5d^)9!|zc>Ex=xL zyNh0E6!>h$_GPMmTgI^Z#pxGaGR>?chUb$VvAMgvW~ zi`GAyo}3KTm3W1cVG9jez_#HmVqsJJT{!nwoq-qLvR?z?u;A zH6 at 7oNdQbV!ZqlFZ1e$0-pV)xGKzo%kcI&>{`5reIm{H?LLmUHHmHOM5ahVD1|$$9 zWRiX2r#qNIKh5P&zHlhHad8l;h-ixgWDk4?R1|L!HXJyX=DE9O7%qF#b_E3{;2(B< z9!Vhzx56SHcu$y55GD at E@?HV_Tj;4-hYZ08S=Ng*4zZs`M7!-cN4NN7`%dlQUnQ5?xbU;V9a8NjtRYyRs=ZoL>muR)fmt>;jOLh zKzT4b49?R>`|n4fCI*1c;mqq6F?LNmiCvAr1ECCEG_FMe2hk$37~sQk(2qj1X-*Y zoh#stgzd}_$?T7|T!|7rn0+NpqiW`v3_iX=LnDuj(_H$T6v&A2`rIIuVHC;!-U=Pzp`Z^E`euE! zM`dCG{Jl~!JgD;!{ctH%OOIRuJtm3MsEsYRQj6yW84wDH5*=px?>rStfx;>GG3}iR z9!Sh)U_8D&QyNe`@|g&A0%Hmp0??E>3RK8YQxKl{1mvRZ0#KnqA;}zo2BwLad3H>k zQw>X7O^M7fvh%o$_Gd=8hDA)W-A)FKVWk*gQZ+(DhGKTW(iq%3;BMl^9FtZC4@^P8 zn175N2f7+N#s~L`oDUtvyF$f9A-JAIShB!~0t~AVVC~$W93iOGqGt9 at a5NtaU4g=r zf!ocBftV2O`Ee6s8=z&DWS+8P1qHjNT2V|CB)}A<7?Y_6w3U6BJiWe8p$D=T at o=S} zKru(D05FS;@Xab(olreH}PZLz2AD_2>p}Wr${@t-0iaS<(4gsI9t{z_xv*1Up2gKY^rl2VVDu=%iHg;Ub zZ5DeBdYcF~K$RAl*kP)Y5d zdt)MuiWz;$xjQ|VYOgsFZ=}dL6AqIV8*^|vOE4K!55VMhcr`MNZfjGf0+LAxq1Qnv zOdzrY7|1XRp=`tqFLXF;;EakG2|`5iLx*V92(+T|;ERH2#D6UMp3C$Y7&d$673Bb- zLF^#F*wH?ibi>s_L2Vg^W)@iOnKn35vV;hvkYsUYOi8eKFfpKxfVqv#-f4!%CnA)J z!%drOIT?lx*l-i9*%-u_+e!=oiEY~m;4?7^S#pXEMp{CkFwucR2#Ka3CO0RpxNe&p zV`j7*x?-py<8yk`<)Q=8BEJ#EEo#Ar7fXdq@*nN`b({ ze~u7>kVpluUna8P9WBvOF!rk9iUH=3_8;rZNK;k%qz at 7s1qdWm2^9`TNYPkIESZi1 z8buNEz%yVR0MvK0RT&e~5-OsQ%*Prg2ylgZ!*0Me2{0v-S``Skw at _A+~$_Z`%J zQ5EjR!GsU-%xq9GD6=YKcLJGF^poy^{$V(t3GK|>$Sh#wI;Ardbp)dr#Nw?FPtXKu zS}%`j6qI+`3Zkkirhu1iA`*=>r14Z~ z8-PJWid7L*#1t?xK}At8RMA0IRT5A|Q8ZNzR73N< z66Y(byX>Za~1i0ncFFvuh|Fa zDwNoW8o)?o3GNc%=pmoGvi#*KF$94|L}p>qFWJl#gyGn7>COV7NSJ)mSc)yo3oy$% z%(wt6ii^oj&*(*yiirWh8@>Qx=^_RqZic9ifO|0K3jqT#!XkJIMi56Ke&`;Dx1L#E zwi^WoT4D)WppveqK_8s~4f{b1g62=Vy53sd`lH-;5dK~zj62;Njm zl#$I$!$?!O-1YC|uz@?O0~{D{F=vGV%@yNC>;jhhpMTc)Jzr)y+mEgsKQf;;8?*{ZolI3hC)*+A%6UvS)hwL>>Z$<+N{ zZYt7<6e2%>be>Zd*i8pB#^tR3Xw5EZyM=lVS}GV{7+!cUC~=`!zyc&=n3g>2`ABN0dRZW^#jbI& z at Cj={QA3J%W1AXC4G(gkljQ$T0#5r?4K%doejc&#~`Fw5T*e`1ds#&E8NhK_812h19R;6dPZVo6B0xP1bMV2BD9ng1uyZ8F+=>T z!R!am%ux0K{7xFA{hfQf)Ao?Wu_{F{k02C29$-0HfHz6jzAXyzzh; z76bWDIE?#2_1=g(qw&4a=et5e4I at 8(c7q5{r!dILUPGE-)uLJ at x_%ZEwyX($7%U`k zMIz5!11V6~TE->gtPGn_R&?ijRgqElVqp=DCAmCyJDdg#W%+cXT`)xB4w*Wis!W4= zej2j}Rw$te!3-RYQ+QX#d-qz|s%;GPbWP`&dS<6M5^B*Q#7}szNT58zXb0W+hg!`< zH9=GyRC4qZHwn at u1H@VPeIj(a8)j3&b3^RmMy+}Y>g4Uc-8L1 zHeqI%rZZYZKwVX&6&p&6329Y(PPV(blpX>JdB2Y4(3q!pa}E$siCnhvM4WEJHgQ>6 z6G=7At`%}_%q9_};Yepm1l7Yz1B)?upcNG?cg36;omTo*qBVjPc~mkqQ3V=`T#`et zU#^;Y?c>Th6nEqi={|yN8EBH9D11<5h#u(s^-<|CbdJ~w at xIfeu5S8BVLT>5i1&tR z at qzpQ+B*l}2LaF{;Q=HtL@`173?I%&Y7?d;7zU{+p$-_RYl>oKDS?wR49+Py;+Q6A zID|_UI3a-+VX8oDt{P|>;+h(fxTR>Osr5J~&-01WuVLbt5}%4DY%_p4nqbOD*8|N1 zD7*Bb at QIt44Jk^@X6T7&V2cv{{RxW at kR>f8K?6WiColttMkBEB2}cZE*Vb=O=T)B0SlbPc5tv}>1z{WAKi%7 zu+#A1Tm=(psNP?n^n!W`&Xc$FVf}{mFvpN-WL8x+*m_Peu2l#4QQ@>cpaJ#sIOzkT zL&gAi5z|B(1`JGMvs6nIJYs?8XXD_9pRXh~$4U5Y0o=(BR4^psU^!9B%7ZaLBpb+& zax*SU$|#a;m<2uKj>KU=J# z;Q3HTrqH-2j4?NXJ=Kg|L@{v)nFiHn7 z3{Y`k07ChmoP57Ou&GR(K<=v9QBZa<%&5{fWAnvx`lpo08kB1Ft_s48Vbnu#WfsDMdXAZn$EC|Zal1__9W1tOq{A|fPz zvnB~p&_z%rq*9YbFjPzwu*D2eK!(74w+e_N846*RC$R=aG4n_3gV_)6{kQueM9bAV z0CU5=l+$LP;WV(%3VWa5_SwcY*SWFm;janPy-Xh6rTyH0ndx{Juab{d$BCU+zFeXH2U5 zgga|fK`Hh9MqxA);MgSn!C%f^eFf|Fp8Ck&OiZvv7!(~?Y<-A6#@E#*mwb>n=x>%z z=u}Hapjsy*soDG>=sBr?cszv+%va?jcGFZ0{ENdu(AKS*}h{F}5Y6ocea z1 at 4t}VUkc^5}EiXKf3nuDUnDdNEr)|z5Dlz3k)sxp$ehVc4+8u4ul>ao~2s|>Lz~) z4?z!jvkEOAT-Pm>6j8d;-ps+=aHBDcFtczP9K6v_SL>5N)^=Ti#DPPoz%Uap2}!Q;L?AiGrN-DY1B_#(vZ)=4kCg$Dh75doLMx;}ijW&^w$x5W$rCSm zo12)1Ndi7NGRA?Cwv$_st&MI{HCH5ZA{Io(hMB at B3}YmO11|}+=4{l1-tuu16= zQBzFru$=poWFU6Z2SAAwt at W;Pvn-5~LK7maFg)kMm}vo{Wf3GazE5m*WPJqk&cQjs zGs~n}(drrC1p;ghdb7qdAw-v&bR4iHMJARiOodkwL0-_IB!Q3)cDKupxy{^)DJGz* z2KL}37GkGV7Gu_qNG8=9irW+d#sskm7-zF1C2jq6{%(Qh*I&vB zAsTb+sX>&96S6m3A- at N6+t{#RENVJBz at 5{Gy9QPc5A9$eGv0Hrrz3B?NQ at BKu~$Tq z85q(KMNZij`JCgZ%Z31^0+C#iL#?Y~r`Bg;8HCTi2(@=gPtSd7Rm_hKX24pAgjgBxBC)!E&3@>SL#(U^(&=jeVO ze at JwWeFV>qf}IkQUsZ}hubOH2pEBkc86=re5lq=)PdWr2e`JWJiad;c<*tiiOSyph zLtr|0iD4{*@(F(=PaHq2J+5vAFK!iH;|wE at 7umr)3B}_tcnTi`28?xv5=^oD6~xRj zGA|q|yo at l9PBb)7fbdV&CV5YGAY$o|V4q+K44 at YF(YE>_26hJOKtg9oWe~&_y1Z2r znL+kE{6Hh at T~P28FA!fmR8z;NBNrh2m)j1~22ffOTa>gY&&Lnf-`^(=UaN)A5)@v% z at kh*>Z8!Mr_C?_>MRa({)i@)}BnO!lm5H&5qz?yK61L9tP3^Y4I& ze8HfIF+ey$CUC(~gBU~*cL>0UA|fB!e~aXr_OQE)vGzMeX3p7{kvnPz1z)Ein9kV( zg7*Yp!U83xnj)mk!u-eQ-3MVa43Y)S#WBG8L;zL~2`l>5 zM(kfTF{bEgAQ@>HO=M#eW0RXW_3 at a}+=Aqq(6J(3fu2f;8p48Z;43I?gW6$)K;$Ow z=xGYXZn2_VI4bm1sA|w1>MTzDRLlAUy%mM at T!HlwvTfcncDXET&fOb|FrcI>r`0)WaO`RqjEc*SHxk1rC{oT%89SyV4@ zzcAX7&D(z*72tHDv;eOPNcdm1Gxi$!cY{v&=jgfh}&SSFm_i*W$_0C#KbzxQ!zN9$LX481NB$-}E zCy8C}7P|*A%i}2O5(7^(jr3Xo2UPeRsQ^mK3j#C%q?VQm{5Y>T_0|J|K?*_j?>gi( z{@M}4i4mkZJyIlK$YdICT&YJSIO<3SSdk+)X+dH{B}SFCuN=bLcXvQeIM#afOD-lg z$OL#M$rFt;=arrxEice(-tm}S&jF|=h at Feo>S{xUp{o(j`gVZG4$BTeP*C{W%R<&j zLnSq2(H!QUV|5fLvSot76g=R2FhF!PNI9e2OgL~PI494l8X++B at h&0f2^UN)Mh{1_ zo5(;lLh6#ZxTZOX;52k4QTz(XhNO;FTp|VjfG+vPx;y~!7O|~SreBWeH-4CDuZKJm zph**=2*(oug^>*)@}2dmeM;6jA!)_-+*N<{o_SvneB= zam_cL*!dt|Xi9p=?k$pTBlGR_BWw!#^4zw at +@_0b$(fWea*ei{fU-t`u#kd2sERGm zz+KKFS*81q3;;2Ftu}Ei&O;M0ZzL6niHjjRxay2FG@%Aq#|>C;*K}Y6d4aby;D{J2 zfuOju8en7DNJ&d2_GTxx2eaRZO7w?fNsI>N8+Hz5`Hc%^>=Gtk`h=b>%}=qM-duT<1t7ws#r0) zf;daZqAn(E2o)xU!lYHJ3ielMjFsYGhn2iR5eXDA$qqdPJ!ER)jj^WDTh;+7Ki0q^ zt;}_{qa1{gC8D_FAEHKdGH^B4Ha3wbBelOq(#B&(h+?dbOv0Ejk)yR&Bn at JNvsDyK zw;~ze4KM3xh^}<{PU{0&5=}KIH9KZmgjN9yc$qnc9gxPOYjAL-QlTpBLQG89rc)!z zPWz9VO&&6H9u{JQiwOabO at ZPVxbvr;A&*`=S|WBg8yP!Nf at 4B_i(1?w1Z_zK<~=D` zEPBc?Y_3ePiZEpmyEUlHflj%WL%y6d8hYq*Xc?_G#A*iA*LB8IZ at BmEbrq9dyj8m~$UC z3~de8d*)P?S%BkfnZPZgrZmHZL}|?QcM3HlhARF*`tTwe>H`6ZVwjwV;Ar;<>Kbj7 zvIK3EWME at h7$x7&4n)D{7}Pa05e*RyQZ!W7Pf+O6w|avkzejHQ(w#EMJ(z-a9gNLH z2@(h$nD2Jeov?)nA+_RBpml>>5KtQ{YH%^m2)qs|EQBCQA|Vqb0uyUG(NGpKB~(I1 z7MT0XgoknTp3fKVAM22 zLi&e^ECmKAD2^pWY(PE>f)BWn-0BbFATqZbJGi5awZL{o3howLZIFe=4f)XS2Yk`3n!7zPy2?is9%vq59L5%`74 at 55N>x26a z;ey2Wi;f?|#uE}jOto;b4csV*qKY7zC?6o02=aAG1{iC?N~FaUh6RK6+C#jl(io`f9hSL~0X*LAzU6Xu|J4iQpbT(mP);5YGRdQ4z$9?Y*>a0z zFbyTcWCI|fA&}S)C^iI!c3psS428l at LC77Kl5>YGQ((vfBW at GOS}8dPEr2tI0kWqK zX at tW>LKwiCF3FLxP6qCBQ8S5AM5hu#$#gIjV-)1&hv_(gz!KC at q!DWQcETM+!>a>D z;7OpX$F-15ZIa at eBq{cOeG@=<8QxNr4%`SacpI(}R0if_hz89R(7>QX#blUN%q2|# z1py at rRZ&w!hwR?@$(*SYB8l2=f at V~q6(O7 at 006TJXKhnlVnW=g;pEZsF)o@@uPTRU zk>j6o#1vRdlL-L?6onB4%f$qc!VnOaaY(>0IRuBs{^KvHG%^nFqSeS at K-vM_7tR?$ zP4RJ14&K`s#I%2Rs-Jq~p!9p$+VT at NvgCnCAbQLr_sD}nlqvJv6Z04tphSTrB_vG< zFp#uW5)vf?Ff=Pe1O+KV!3iNy0YJnvB$EUbLn9CYNGy`2N=pPqK!l4Z6#+3pBMhuS zhQfT&J3t%>Lt;}H9F?Fr3?~D<`2}!v{Oj}z%Sg_QP-5-34VwYxQbE~Ob(I^cyTwKd z_ud*Q=EW~Ka0kv_AjVj2A7W^72bbO0KbB5rEe~=VFerUvk=>5)s-Cii2(UoX02mS| zmwT$O_1|ya^c{A;06pSS(*i*wV6a=O+1eR(;JwyrayDXADOW_b7Fb1qilkUl zq*+Z89>{S=oK6n}K at H`r`oF}v+lh}sx>ATV0E9rT3M7cD`QQOEGD8 at s17F(_Gsai8I$;UjS4GF at E$c&aGf+hfiFu=r|C8uREUl2EB&7v+rms95m zYzEFM<}MHfBnmUyc7AUx%WYLb);fiz`A>#E)k zoXZ@!#4!zz2?N4TfDWYhVXmetfy#u~G9KBks*M<^eQ~5|i~G>;^a&e@=?f-AJ(THa z1Kfj*nt`x-U^`8~5l9P$g0Xl4A6x=Ktg-X?K(|Pl-r*of2TwJ4?@GSZ{3dN$ogF*? za9}J5q}Dmh3get4pieN6Tc#$;toCG{q`)KW3Ame;obqo!Re=2x=0KbnplBMe3G_K5 ztO#Q%WOhM9qBddAdssfmNrxJc1gIS067V_56lF{CPqtvi4pX(-_DXHyP{~)yeKP{9 zy9EKw=COls+V0{!o-umFLO>)v14JJJ4hm^HHnz6tIKbNy at ZSw0TX|*Gvp!(rXunfX7*_90c~`kO at iW22R460p?Ci z%P57Y!v%X=rhAWEHdOVzpx+O2DDuxdG+?7#Ewrp+h^>vf at bMCaf^?u_D|*hjNtr5_ zu=2}~!~xPCw~D9<93dJ?5aNPlA*VZar?Y)Hc at g3IC#rdf??JDrQp9!+3)~KzK=T54 zg%lv&fRtd0U=X2zeXCjt)FVK^h9&}JK|&Dc?*Be_<$1E!nsbbIymQPk#~ABcjJ(}Fg}{?ZwbX6;qE~D4 z> ze3c>3*N_V)xD>G4trn?wG4kvj%~_?)2-|=<0qxuczR<_&mX at Y9!~5EWl0QXS?J(+%lrQ0C!tteL4X82q(l+W)A~$J)FnOpXuG` zPdY2-TybFZZKn8g$iV==oXm_K2$1Slb7y^QK=nS`gfi!V at 2$nYzO31GI4JT{;^0iF z(y6Z3PtQNDN?(OMgOe=225(U+5!l3N>F4 at nBy3Nq3g(DZC>n+4cQrqIazg8UCLg#J za9NuVmJEBmzQ6iG<+{6b!9~cK=NkG$r`EJ1ISpfU?I^NS;&_I%Lhan0^qm7 at y$?2V zsM9;fV^$fJNfLYWqVDT zn|N{vg;8v!iXFU9$g_j{Bku&aPe<|F6IOJgJ*j==Pg!@gwOr!QU4`5!?rs)lQ}fIy z=O~9imfY>8E2l_hcMyDTwcnN97I`WzOIyw5$p@%>7ITLeGPa$=@zK?>S(*FHPwS%A zg;ds_d8 at X@z)GSJVwo2rX{lOSj?<%{XR`K%=C!f|#D0WQ8qoFa_hhkg%B>MY7bOeF zq})yqh6M32U?19KJ4`izxy$78)h?;cUDrIX3llNix0cBHwgS_!d8%jvHPDVgpYscs zpqcYW!>n at Eu1qp`)?2KJ{8(%{TS#3fYrH7K^DXIKt{ZFH%`oQt$82{*I3#LB5~%FBeO zhaVh&uFR0!*vrD!wD6hNoXfJno1fd;i_#kE;4Ov7;58bHGvegTer=kJT*@<{e2M1R z;a3vparM%6XE at LUQ_S(c at C95-?}c}>MJj%04qy|qM($ef?At$hqcb0*(OyUxJ0s3N z)tFDAmnbl5ukIOB3P1{Cj3%{v5RKO2bL{r#xkrWuG4Q0fB5EBcC7l)`)si zqFtE1TXqxSQ&Ip=dMv# zFxa1v;|CiC6vvY2k`Z3_mD9_bj<*(bSEj3rda(4bqC@$ZH-!8h)s85tCK!q_&H zQ=#`Rv>WjWP`zXx^G!*vvSvX_e*k|7NLKuj{ZfZ+pISKIFs`r+aZhK*H|^O*q7Ktt zb+ACzUdc|ug1#x0iu$2}Rf`nANMi|z4`{J?T-GfLxIwJaDef}Fm4c55%ka@^4(>I2U*;iINm zYEfqN(uwCYExcV>oqJkuXdod4BxPj;k@{|-#0Sc-loj8>2!TwMA7^J at ZzTFvt{=bR z92op#cnCkBxBTK&;^-C05tX8#o%ruMxzzI${4hp1MTm21Jc8ctvOLovQ^qN3G z`In(yhvf&;)|riv?sd1J>w;p`tZcnkl}AHv@@oY7! z%in71e^6?j948_;%qkh8y7k`avQMEKDh*=elqtBRn`r0izw64bH7pc5+gKM}X?AYs zcdDaYE%t?9dX3C=zj`pgO!Fa_*^1q-ce(%mH{sxo&v&64*Jce&a|&}(g!^8$k7Ppy zM-5c+xhWd;Xd1HB5;(;b&KRh)_Cd8sRGul0_RL%7TPNzGHW7I*^hTP{XEL-H)}DxC zVAcWo&Xq>O0WITTSic5>?j?N_cQ;pq-g1HpJBCxSl_OuJRmIWZ)Hx1KX6Exu4Ce;U zaOc2CV^&pS{{6C6P)L_I4f9KgxaU)Thq_ at 9`uJTg5_b#pXx)bE=m`c54o#6w&bmp! zXvVI1GtCmj{EDGbN~%r92@@J~)#9qa<|=pU5EwgOKcvTS-- at NaJk85f*~C398-d_|+&`<(&YzU% z<3pr9F8oxSMolcu%J*&?ocCSSyW4T#XlO61av`7NN;jNUNNrUEg;X at LMxkgde3a$j zFZE|WB&xEf8 at g7*-OSST1Ple3;R&g%b~uDQNXo(Cb|a`7F2WGY&F*-n1J}A|a=egF z-B37)##q5^sATqy4PTRmy?oh~OX3pACu_8q)iA!8B&Mpq&FG8#iheMpyqO?Oi>B=Eqb8y=ug%y2}JUd#zRAr_1ci@%=`{a@}&iH`s-5(YQr1AD+)lKG&(hm`j^| z_StiXJxgmZL8hnS-C3rnI$kdczV3$g$}vJW;sr9kl3tj(WhXxolrWrpMBtrR_u+4EZNyCG zpX+<3pxUB-BE8)`Y6$v>C#$m2DQ~Dcglk|zi$B(`knc=^j at j#kO;h%zC+4#0CZ`jM ze5cz}Wucpv1#X4NuPN5&F2B|=$xd>aU#1IADwKNt;PtaJUeisBoU0-iSFfbRExPNA zB{?EW##v~dv2Q_yX{|rS&ZfmgCCSqboQ!VqOt3thm(fr(%n at vZ^V!ioHL2-xrjrz1 z`fkGNK5P`26_^zE}nr-fi at GfQrj{F!0p^mA76 z1e(;fZ%HknxV7vYiX0QMDl@{{MuBo3VeB#Rvd(pSN^xH~;+{jcutU7M6*w~?*0;)@ z25A%?wKM$c#M6bgRfN&tvfhUK1zQ}pjMT+C(Gh%7ka88q1BwlOeeuTiRHqosOOWu~ zpj+I9`N*Zg7agbMzg%D)foHva?uo8$RbYfY7!{(OI at z4EHlyyxu_U33SGhV9f4D0C zPV&4j0;Uo|!wT7D$&MQvTS zhk_XdW9Mb8zuPF#ET5_Bv5Qh4BZ57nA1tx<^Ip`(7Zv%(*N%5Yn7^D;uZi;(mwurCSeCn=M?0BgB1Y^qLU!If2ku}n!l}>B*_G2lJ(n|!@Zb~2b62>@Tj*XRx z46(!3+t(^q4HlZk2hA<;6oQhAI^sckb9LHWHiRa-{(p|Or`OTM=nJC zM6XsnF%O{lye^fGnvPyJzfru#uwz42u zxVBCA%5HmJ*DO*()Iqx6;F^5b&@?92*AqW8lb0Jr_=b=Z#X`QW0+V at zb(k8j$qI4rlYgH6L-%Q}TSqL0l7kXo~{=<}^#@ zkz0?ZAvd$;^)GfP)vx?5G&&*>e008{R9bm+V0PG+Ed;*xqR?m_!yo=?VBd~YlooTJQ8W-L$t6Yt1GpOe1 zJ1LC54PMfOt%SAHOqf)1#AlpucSHrGtvJ{)niNKAt8vUrD2XtY%Llo0={0cG9OiQg^Pp zDt=1z|MEa;WczKbocoZzwl}?`mI(W^=P}#Q3@!w=UiP*b#R~Vg+TB`WDZXdvHlllZ zIfQDT?{>>pJ0ZS1xNNKT5H83iYU%k)BNU~Ie^*Y*?Sp`nons;Ccr|( zkP<5wM;^%nZ4JXT1#LYW-fP+(a at Sj8>-ImsvoA_Y3=g_h at +DZ7*9=idM58 z$Mfvduwd{C=hRi1Ss`aT!B)Y9g!JuAi?8FSOSD|GXA?H=+2Mu|+kA*3&Gyui7ZzQa zcIu5%-d2fDUhtYYarPot#%r4c)E4V5j`K6^6Jt{rim1V;L8_jBak(vlSdFOZQT{j9 zcMyfI@@1biBp?iv{bimAho-TQUzqJ1?|b!@$2=|<$$Al6U(D$<9v5PJyT_of6LN2x z{p^)ZPvL|QF9X5By~-tdoD;F`H40of#S;;WY1ORn=Pp08FtLpq z{}dkMUhrK*!QAd+qx-Sfi3RG~ALSw~&eZE%2zyXg0Kg_(E2A(hec2fbev>EW9yx_O zT>^hQdG7VCFEwp1zo{BE4V8MZmlR$q5_uHD+VP5+z|YtoBvWIR$}0bHb%4fPK^2t at Yyi+0@Cc8u3yQVrhBG~WIFVu}IyUk+!*o30+5gRpOP1>06 at P;^(gcWSi zgmYq3tS#Dgm?WbJ?hSj~k!lhhG~ncZM**~2)uf}bRC?vHSEvrikMSx`zH(VTBVVca zrTc at 4&?n7P^sDOk8vA-wSf`Np6&XF{uQS^x=&ot;v9UL2sVo&W*Al(ys!TUEWqyJ%Ufb zf)PS~&zG_X5P6fQLEV;5*|Oiw5}BqcOdM8XPf<5`S4=^K??WGyp5 at JJ)t`R;xu zW)21Imkp=EBem!^Ra=!tU_agz84#fDRSp-Kz-}jpR at rw>bMlU}wh00uPT-rlfv<$K zne#b;z2$gNf at C)z?XX8JpRt&UiLO?7wX}Vc%ZEfh1F*_R7*SZPI--3N7STz5mxPjaL^;%5=|X7f%-mr~U+vER(D3chGMz7W3b7}-{zm&toVibxvn zyl)lxF5=Bb0X(9aLzI#0I54lKSi62}Y?t9N(rjZ%|J6zCyNR9Xf_gC%&To6~#RuBP z3^^HGJG(81JshZ$Z%cGXlsxazoYxt$GAl at fRFHglDY;{Ys+rAAGSf3(gGO97%n7o- zl4TvujYSY!XTdJ*{knaB9@|@+8xj$JXeHhVaF+I~t17+2_30~hn`xI#v@!*Y*j?Ez zpCm!{j&W}4w8VNpc1xwUh%J%8w}*jijWEQ>?mH<}n%5WZcy?uhSrQLcn#!1Cq9CC) z`Ll(28&RP)S;_qx?H0QYk``BXuKS&|N*%tZ#kNAvzCyuyT^7bY>&f>LVkBnG&0=H( zugW{t0ZWpge=)?KS3LR1rJ~`so^Mv{Q=BVZXmQfvJP(v&>B;1!{D(XfVa_(s`G5n% zy)o14R=#y9AdC0R1mf+Y!dAzTUP|Z`D8AZ2u(xbop#OdW(;q^w%(m29gZf5OV^2hTZxZ(~343*9MIjUXE^u=Uc&7x$rt}4HN`RZVw(b8(b zdf|l3onmZ->&F~#mPS3?#+&EO;-9i+r@#U$fs9N{&H|(CvX#??I$k(i8?Z!qHqM at K zg0T~Oxz+caiB?g!Twp+M-WwN7Y-fvgFNCJ8dt-HGrfGyqUffg|e4Ie#SET`%90#Oy z)r}cm89-xLe4w={{dXzgP{4 at eic>HxdmkSI-cpFBdo3WmkY4=>T at d87suh2r8@;uV zNL;re{arSC3Kly`v5B)Q8gSa18 at Ec*eJN284esr?wb?RxmX=&;!kKwhNzX!fsh-YA zUokVY!T0Z*NKouWn;LLtoh3Z+QW8%|$(RSTE+U=3x-v-C%PPJ0gBequcx(e6CCcI= z(~H-il$CorDp7O^qA}U_W=72mIaC7BV=bG at 649R?dQ9HVB6y#FJbWr{H`aq(T-NRe z0b|Co3 at ED4Epw_cI@Y9^(6n$$Tj`a^NLso~A7wG+_r;jIu!e>PU*8AbQe6Sj43cRE zBvr15RZd*Ao}$NBc08`tCWUmpl3w8Q&GO}=yv at LUX!)wY|C6B%Om?Lwc8#S>aQNx2AHFLF*w=`kZfcq2lKXbz8Uh zUyjw*z9^vnLi4UB>SS at Rif`PI^X{0lcBg-$Te}W@$vdz8Tdy5Vq={b0w2n?}4j*nU zjuONPVVe+ziM?!IiN4gv&B=cmojmhSb|&y^njS>Aq1Ujy*XI2#i*F!M#Aoa&i<#+7%k zNIMa_ZP zP*4=NSt6AbPQYrUyuMcz6JK7#ISj%3Be;u)q=(yWM%c6h29o0RC^#C~7^4}+mP{19 zmrmuUHJbHaF=QCIe(c^k3e$p3uB)8AUaJlWq7RsdK`#M&lpO~9` zzvcL9azX#mX0O`Uv>^p~=UrE6#Ep;GSQ*RGDaTU8S7cC1MD&~KaNOhhmxGMAm_s^? ztE2Ha%Yh43HLG4uPt<#zxe7KLgAFL-<`m9FJi9)zaKhD_-dkF+3!X9z_>@%aEHPK6 zYg0>a9-Qvrd+SP>!k;z!$~^o;z=8^&f#If*Myzu3Yo7>><94i8wp<*F?`A^hlytS- zUb`;m60rqe#1xyZ8Skbj2Pw=>5fRCJ<0X!_#SKpMZr+t!YfwAW at u0Cs<+}nZ=#sw0 zOO~$G6Cp2_9#{CXHE*>jLA+Rm-gAS8pZbb1Mm7rM~Pdw)pqZ3M+L z4TE*Qi~!BIiIZ(F)=k9n4M$!k1*iCQ2bE&QKLuY-;)Fm})bLz|fUweNI_c z#LLBj&Kwx_9I$?4RvtCwf4PRmDv?vlRV?L_E$ z;#qM42YHaCHvsr8wjoA@)4qEETYe_iShSf{4<$3NeeU)S6kB at Pi?cGd_!8~I^H;{R zsRpWp*qJ)^jY4*Kp0DXL^gU7eEL;=F{1ixTsP_-B$c7;fbQ5pI4!>D?9`U2+{bQkx z?GJ%ApGkL;duExSDN_ZW_it&vQuZJSOSR|RQG at uP!j4rvo^Ymw?R?@dD%ds=+5xfh z2g_W8#c{gF3E#t)-iqIYEHJ_k52gL at D@lbC23 at hb)=@|a${1;$zBC6P`+ySxFVOK4 z;1#A9-5lpb&M4CAbwzX0(m-O&Dj4FlSqCmjU^oOE3`{hKCODOX~Hix&X1GBZxOU?&ij3;hD4ftP%9j6urR zdvX<$fPh?CkOf1_0LA({9pm`zvIH~*MQFhG;%A#DJI+WGBCm04n>1t at oM1G+jOZ@x zp14C~wq*|1##F_j#_4^B8||fX8;ze_>7HdY<`&G#j^%nhADx%-n$8VGBT$F*x+H}p z-SOs8NlU{G%y!FK6fmjKQu4*Q>2KqW+ed1Y#FUncSYI at bN#;D*Eq!o9MJcD5eKyf( zJyWA5s9e26B4#B)u_W*K!c at Wm$K4it1 at GibTGrC(nAxVJTPx6?lMSoGI#IPaO zQ62^LDdKTGr%&l}becCmS(k4hIf<;Yyh`&p?yl|4V0mpC@{*XCb^q&gdaW;_W`#XY zdp0 at I<%YMYTS_n4?R|+I{seWGhN=h zjm0h?WQ+*LqT>SkXJp_qEEJ4c3 at D1kaS-J<6g?w%xvydUa>a+8ERj*6`<}G>yMJnk?CW}uhv0mzMU%BP(BVxEV?0*Mg5L)QUAI3 z`ellAFr04M+U9a-#WJr-1;Xiocav#UQ-T>Fzi~{lN(f+j;f7Od`RZ(`_Tf?+PaYVPDk*X5Yh5(qR#tLv$<1FX?l1eS{?;Dl4J5dYy-~P#@6&*V{RnB`Ss*^?X*8;Cm at 3h|JP3Ula;515x{&p=z0s at iz*~Jq at wOc^Y#E zp8cYmA<<_ubmhdYG656Z)N(sA3J%J32} zrZhb#mU6wZuhiQK$4^P?jPlfHsR7rEW7CdHYQ at mbtmU4lGcwSRnzB>2Tc9V-mCn^` zuAk?V-}`QBgh>XV=^2)|VSa18O04U+V^XiFyZ;=0#^gZzh+{Q5 z3Rc>p*y}Gv4r%(_*7=zSvm|`kEcLgV!`;P1)1+TSQBMx>M^%YAD#LuEH5i+2^oXW` zv;1!iavH!6^%J3LTsfVwdy`CKZG%sR%pE+{;3}TMnC78eMrt{I8R00{F=e^Biuyv@ z{`j&EsSiz?Yot{aLw&|1(lxK7YbsCo8{xitnX*A)S!mfA9H_J2?^fuwPWYzU58M$ zg7z(hh{gxhvL8*!aFcefA57qL*Bku}@kRz0XNs2;tdy$khc~mf_ at x69qBQ6d7Uy{m zOP(0$Um|4(QeQ at 55cwH#hUzy!QS^7<8+3qz+X;S&< zao5)Y*Yl>OAKQ4-esqLPP&?)qYM<9%Ug1Ha;?TspFtAQFnt`X1?A3?+O9MXS z;nrX8hv^zVwri#^Ge~tk8It at G@%~NdO~>bdSX9N`?aZ5?2#z#1nokW+Us~g2*VXGg z3DdjpV9Mqq!3M?&VT;q-P(2BVf&S3a-!y?7K4(*+JX-_{@f6{WTt zdvjn0xjF=Q3?~4v!9 at +81O`qnPUbK!cV($vwE`i|>vbMuym$fGwqlG at x8~Ncu{M4E z>B{wV&+VvGKq6(wpzE?8tGik2tGv>YC02dMn9%*)R+VTcV+)w0!R+bW;C*dN`q@&~ zHlF0z7hw*P at U&|Y{jq7Ju{1`5h=*FV>QU!K#tL#h^geScy9++|yb6-iMSa`X1`VPY4&uFj1Ivz+{1U`eQB4(Dw(H2zjB=X#H_ at cWXAdF(ooDPswiX~>-C3;iziR|diLIfZ&FEL zEn|{f-v3cCVCJH!w8ZlGqFTh=j0GB-l-CCDrUb{F at 3XIw$}WZ3ws|f>1?PsZm72OF zi0}3ra7g7PXVKZV5VkI6B;A~49#uL-wuGJOrzS#? z?W$(1^hdU50wxHLf|Pd^5gUGyt at S`%LxWgh`uHEb+IQ*e4M}}_u}hdSCnxvSM1R?` zoS5W_r$a4&FuC774(W&LfAa|JoeB$_LKz$be<%`J?%rQ9y7)zpIXC)7XZBXKja^sC z_OZ;zbKQkv}R$>b&E&%cv5$m7eSk4`Udq zM4p=XF4L#N9w%i&=!CK#-rk6OrpGoWS|~;xUGZ+sahRisO}fs?O?sFo&y~NzI+ek| z(B0Q?I;s at zQH6siz^z_qJ?bpy(!{ntjK52xa0 zU(Z))1AteJ9rv)f-T54)R}eGy)D?jw!@zEb68R({FN$;9d|cFXB00q7)WsJkD5{rF z3ig#};SWP8r(ct+tDF|^$^2V4d)caCFiu(etK5|UL%3%|2YX(C-fBaxKW2DG>Aqex z+m#0Yq=+{&dKl~BAAS#|xG!{R$@X^_4-9#MY6PSzbl0}&#rwFU4T8^zzXdJc*K^cP zkg(*oq-i&QCpN)+Xnz{Uxy6QE?-hlADaq8;IazRL+h)iSU^+{IKxabKE_>xz@*p6&)BW!IeHpC1y6UWIEB|DSgwA zoBs!ISvPRH_>Wx!7nTI>5d)U%LrF|ZLPndRp`jV`l*z!}YSfDYjxPYiF)BHUV^^1P z#8dQes^B~U5W8GfsOZH at lW)E!j{dlDs(hs=^P1Mjp}1-#x4O3#=2z?1hy&uiJFv5T zvya{?+e)`Rv|o>?O)-4df)4x~n3xFL+LmpVw!!W4w|;GV3{l}*5ajOF;xj)DiI`*)?Bs8!a_7Z+$8nhHS+Zf8E#|-XcEPRSNGY6ycKZ z=yTrsen61sTlLCahl<^$uc&HWP>Hr{R)RY{PYA;qSNh3wi5qlo-T!vyE|<$GHgLgn z={|<^;XXDhOXlJ-oJD9!SU?Ai2#pOL!hAAI49RR^tZ-|n_iNKDKTuY6I4!qz+?ygc zEY;9vH{ERU;cJufz7oxBu(n}#(aV00*u-ni at +@G}*ID3lqXRJWo6_nOOT)dLPD8C(1ued%(0mHi+x|QkOa}btR#UZ+=U3K8H$$ zLf7{}UG&0+Rkk4|j~?~sdRk1(<@Vi{8+-y;))`evC5g73&Ka1Z4_kT^-G^b^k17|= z8JImqXuO@@*Mw6adYXPy%gV_~s*tIxNJxkgiw78->x^SeqGY*a;0;FAmn5OGH59uv zQEcxxv#7Ka0tJN(=|wc_%PjODfE7(SO?(3LsVx`z$v>*9wnb;YWL$wS^0juSA^8sW zgTCp!8J*Qs)>+-Uu_0Hhe{9*v^__hb>SBd`F9WOS7+a%Ff0CIMhupPqHfb}tRDS$s zB2$b%+I$1`azg3so~=^h4**at^F6rXAD=sNWP9SGE1X^DAt%T`CZJntpD(Bi4UJO$J+A!w4*M^9-hV!zFhmj(f%HWy5(Cgk3i1VV2*P3! zgfLCOh&)HYcm zz9dqBrjk-fNQmNJ9PtDnC6EI?h=4>xGy%zQ9GOFef at FB`FWV7{Bm_ax>pTnuN05-d zkiXGW_{mCvh$aPLP>KOZ)4zeBWGSQv5ltY2C}MCJ((iHD&vB$bhD`0(xIqxcA7zin zla5S#kgx${7~cq`|0fOdBjMNbBNV)jln7*{-|FbVAPfOb)C3SgBs>;z4h`t)kX?2{ z{^tM^kMlknfGMg%0{(f)tNhSAXrKHz6iSYf;fN34l|i_5>Jfg}GAkL>?#;i$jG z?Z3zDUyl4sS_k-RBzezdzVu=58CI*M}4?1!tiLC1XCpYpNf&^f&0siP;tjLo% zG?HwaOc?aroB!@|`JY1n>00t$9R-0uIT~;%Kp$@Yb8R3*K!866i8xXw0wEaUPgxQk z2qL1%yY*k!D-V$Ug2AECXcUON6x at -@UnCI#k%T84EeeZ9`XX=`BKD^v5&sSX{5~2- zn?*nefFzI3PynD1c(i#(rwmXCfC36$fa0&iMV9*+^Pu0+i#d-56cot7$fG+F9}ose zNdX9?KOTwj|M~bk?m_=L5ef(lk}T_k3j!`&0RDDV5F|3$Fu)gqB}X6uB>C$U1QCvq ze}*qwf#{1MkeB;YTr&Km2N4_Kf%W!B_z;hF7lR{`5dQuk0v1ppcmoplaBB|(Cvz(! z4|`i%$6s2%WdI{%2S+1wd!7GLNWl;w68-;QWkJ|;C=7w at q5m{RP9WrCbi|)LAE87* zDx#E*l8hn>^e3hQ7C51*`b*Kl*2&(`*n at 0B>EDwT;06MIU;o&`Ul9Mm*54A~|5sbT zApUn>kki_cDhMPFIN^RDHWc8E at h2brqs{XEAC!LUD3SB8zXyq&a^wRr0q7$T$pAc& z1p1F;NsbW&3iVgA|L^mE5nbL`){!hlKkae za>nyUkVpg>1^J^g_3KYT7W6j)XltZ!s(3|N^P8rwJ+119=*M?mCs at e@Q&k%(9P35`YkGDW^amHu-?{nrQz zK|=iE_>Nx4U}R!s*OL>f54lSD zfaCzjAT*#KKmef10J$Gi0W9!nz?MKFCs=Ji{Lx*HoUzCSL=lO{>i%k<6ks5Qzjjj- z at WBJWvMbl#Cw~fI3R3LTi{j2o-Nx&LG03fhm1dRRtG2|}53XKM|iTHpJ49WNJ6v$Wv^bs2A zNZEm0IZe=B01Wy|*~A}Dz#wpd6#}rwAJrf2-y at 1vh@S(0LZQ${NZ4RN2~9%&4m&_W z_t)#+6Drw=2?6ud$e&yAf5$Kg2Uw69!Bqc=e<5UF!W1;f5J%bM-cU?z41i3gLW!Yc-$G;r=(=z0rTUP%U`G2k41|JN-)Q=#P{}IFqG6XRUhXjuN zLJkvhdx1t^HGj4n3Pe2eTsS at ihbG9AlP#7QL1z8;q$vlSBFD#1lWPCmq}q{5Gb#gOV>?m5E4p+f-lU*8~qI{gS0`hVdu6*8(fCKQnTRWgqD zl3Z5tAxC`)f!stN4G at EnNOC9kGYB9*gW$-RZxCP+c-_i3|75Q7-kmmvr zL?U^g(7(M#HvL!O`0ceLlK||Wn>^Sk;VUH??>KQFI89~|xV&8>{>!C>&wH}(GkWZ&+lFVxsA1F~B#a=@qnAk2dmUQ|A`DX)hS6K}ULtz$At59>LG(zps8JHc z=ux*QgWzSCy!$(j at 7@2_v(}$yo%ea&$GuXczzTrMY#_<*pcu_{Ck5tXQhR`@&{3Z5Y+0lu;PTOzdG%E3Ip) z6t#?`3Ex&t{r;;n^L5d*7a at C)V1X>72XxUJO%%%LH?l}wjp9JLLJJvcBdWRT&mnOT z>Tvb?c;~U_TH~jvlcb1mZms2>_L|ZXsDj!zAOKMsF#jBTH9IXB(59b8W4^0zYaaND z1I_wP(jqtFZS`er3>EqAUDp{~NX>R~T<{EjXTblfldSjzLqVKTfqPpUI&BBoLsV*= z?{ig3d%|(P%;k|OSj>ULN at OlwAi@Um=1U%6Vv9{@2H8QFT|3QTyeun8TSCo!+_Vw%dl zj|l|Y37)$i1QiK;^F;!^8`A0l-+S#Z}IhgQFoCl#)Car^|Nhq8h zE?dD_J%GW3<&1Oo1A`?QZ&dD@;0JQM<&6t7tj^LVjDChpOf*x#C_|iob5?r`NwF_W z*%h?4F^-h4CpuI6_yVtfu*8vEfjOLYH8rg&{b2m$SQ5{;P~gRluqWAXE<2k)7W0E= z+^&B$JQZN%=VqDu)OP9vv3FhZ5rdlLoWRYzmf|K=OvnE*QES)0I35gHOpL|uVpm=U03~ld-vH8P5Eky|^ z%F-DHMLYa568^W5=X-FpBgzJ6g~7t at pz;{3I|A*D3O5JS)BfdXHT%Cg8rdEg5LdC_ zUaSPjB)hj4)Y55FOIm|N`EWTlXwKlq0jbZZdfn~`W$I$-F4g63_xZ<;UAd4c^&-O6 zp63y+H96pBnlCIZ^R=2Qw6 at c{zjriLN&D_NT7AquZH_1|&S3p^0ahJ&^+xP#hXsvD z2$#SMt`O39!336>wP1%yz$jCcONZ>(th7+%-6-M~`8XGT>wvZqcyJ!BdO3Sh%j1Ay zj#zEfgssFH(MFWvqv4*2s%>yn4iYoJO{$5#h%oOT5f&8|hdNtg{@?8WLi3OKR(t*% zn%O5IP20SFhFL;HXIK$(XU?xt@{Thgw at 5`+B~Oo$iXT zTu%&VFYvGCqwv~D7Gc%V8u?}vs*ibsEtuI5?UO|sc#FW8^)Wj{w=O9Uj#?U=>2 zK3o!E%zYll+qIxH0>o^4G(0O5llM&@Siz9(RF{;8yuJ at z=WIm%@JBzXO)SFMy at 0T< zm$bBhVp(l!m=EHlB>+9Tr4vT{7#4OD$3t$uGxI32oxP&&LCFaKwADM7v@|Zm#NLOh za(W+y at oiUh-@RhqADN1g-JHJvn8At-Ly4nRfphpGTC=WJ^-PgaHhM@^eE{B)Ek>?t zWwa3p%it|Bd#7IJgcxzU&XMV})(sYy_)F|0QD zd4aF42rJDYxs+z~G9O1rD at Xe$?RwfFesj`*6H+G{MeahOj9>`|>G?j_^nk3>qm%S5 zt#AE)+1AtFo-A05ATKnayU;*XL`E3ufc+~N5kT!f3eI&=@LD1eD>X4Wkl_p9f0q5A zOaA4Q;zIKWQL@}Rep$kGbHv<=reiA`1GjWXt`5&^L4Ke)r{b481}lqskcpwk6`xoq zjFz)mU4_}9QN~ih$F36S)5_ujM6}&HRCMa0lq#d^z` zs4hWm20(vUwQ%Cq=`6CT?Pj+_L#B+!<3-vs9(g@|qjfvx-sSFKeJ;(@iLpBI(*ZK6 z+s9ri8 zvHRAZ@=vzIF9Ce-1dFSR%G=^xJ{K%2x6SuUjA=fiH=faDidYZ6RSu6HtSn5b1X1se zka5Yz^bay~Y$Q7#u+%=j(naUg8DKnv at J=|qb5f2K*0{`0)3m^_G(5VkSQ}>_bw~jw zN`cIa9wUVt>-0|V<&%H)L}zBM?7Z|J%3!S~}bP zw_D38!9-+4BY@(+TF^Pee``VMw5+#K(cAT=-DH65q%eK!p4)mE-ENTZyA829D)`;( zS(7+R_-?=rm5q*Y3%`k^yzAnxNe=F0gK~OEC&bIiQk8rL<)Fco9;}aJr6+}7YL|Bx zmJJ4B8R=>FjF^^9OOQYwh+zLU%@l|6r)jBposxps?>UYzFYnLdIwZ%`DjU99ul0|^80Gy!v}ZR#hCdRbJi&z*?!C- zXC~o$N at dA@1GSVTxV7|h$tcDTFtl%xRdnomBI1XXb_w>2>~&?s6Gjb2D7mI8&0Y?u4uG*;4_)TtvfhQ(`J|E;o5Z} zy at +J?4(=Lv-lBUD$p=zi*8B1XYo1)ZFeQydxu+%Vqs>~Ucs^3V6D*63+^)PDfX(Gu zKu)VRnB0GIE>O*fE;}z0O5dQK4QFRR48eDUx`*S}&UjIq9PeMEP6naBF*;f%kZe2? zAjsS_WB^6`xaAOC75(Too$dJL7HS>QR;Jo6DsYUx at J?M-PC-LS5&BC%OAH2zb#y^v z9h{*mx++j>8-%4R#s$iQ#$ara=ck at fge4l|47GH2w!yi;&`1Zg15(4rllPxeo&yF^ z{BJ2s3k(l^_1)vsRY^~IooYk9pzJRKJ*}uwd&hLI at Zo-n#bt4V zCky9<7gFXU36R9}*hb}XWrwSrEtjEjyAc$aRlp$Bv$8|S!SSx~IM(aFq=8x8L;lpa z1C0psy^V)73D)z;=?;wIz=N-NsRZm{v7-A>qd7Go*@BWl)>xhVuzWB&H#jcEs=uO( z{`z3&H{eQ#@sH95SzCxQ_n`95Obb;jGK-x#>Fabr`kuw at z5ccni<)=ZN>O#Ms>a#h z>NX#gjU0kxf6(wEsJ2{=1>ploo$c85T59aw$Tr;P|bTm zeSY>Sbdo>>rWmef+k&D(Ggiy6^t-SW{~@F8D&p#5$pAMw{j^67maf0f zjiq*&yv;Au0j9FLv1r5oUj9cfB)`Izso)YTVxo=N&ajl2T0q`9k&ucETD&S4=U2Q0 z);s7{OMk=u_p}L3syv at I+h`IIB>L`O#bP%hw?eF4Pl7tTtMXi7xM&_=LzHmWPlpgr z3jX at Hx^ubJen#$_10TQ%iMJiJcQ}7n2t0Y+#Vu?kbH%=QY79led&YW6%R$wdiPGhS z!}^eT%sli)S7-Kc@;7^|`tM>9Lo;1ED+N9UVNn642wrYg6NVyriHqDpe?1ug4%iDa`cU?}y)Bi?7Dqlduj z(FI+Au77#~x)%tBz9;6*Lb@}*-=leIX&YSMP4R)|6Cr6Nb41y at 1HpRmWYeHgX~a$U zXaa8U`BP5`SFEqAq;YMggRB_BwcmazvK%EK89vTO9s%KG_EY;#_tIxc?fWrn3K{dV z+6Z|mGY>v#fTjl{;Rb0NTZLWI?3Brg-1C`Qh1n9q5`N->8u&?w&S8h=fSJ!xlK+9S z7sLwsCpG&gU_f%xS>rKm at T<}h)4Uiif6*UXdMZB|pQciyS62%%rgQylE9F+3YiP{l zOBjp%tdBmT$d!r}X4clz?du-f_FXe>=?E4~&8yymHzP#kl2zc!#Bvh)jw@|_w~Qmc oc=IInz6HVkZtr}LJHZL=B)Dr~g*(CB-Q8V+yE_DTcZU$105 at CCzUQ^} z?*5x=uD{Xx=tE`#)DutyP at 4$_1Z0f8nYtX~U0+PUz*tmz-pdMaI>S<2&AYTUlG@%5 zzt?6K3L5J#$ zwrM-|1Qsz*%c*9hi!f3xdrMG6C6Lfm^(q at a_gDnouX7`Dkog#97MloFKLG$o{Q?66 z-R@`X5f4LYqu1wDjD&jV9ZOoWh_w{NzC*7Kjnv6EQb&u;uVilhakqeTR~t}s7-l0H z_B0#K9Llp1)C at E4`z>Nj;O=E<6#YX=furcA(wm1JCm1+YZi7HNPT8c>1DcAZ^c*#& zio|E$8|@6=<9E|?(V8aQvhAUEXu%`=x>I4n7SeW%oNqQUtYapHJTb3A z?q`Lty|wyj#Gc$z0Gu8AqG)`YZ{nXVddoziEE%&mkf3BkLh at Z@gyTpes`kSn6)sB0 zc at N`+`_u&l*C24dEiqh>(lC*9(tZj#dW_1xJ%6TM7EfDD=v_lAe8GjfR>#%e%-A-i z8Uv57z6%tY%vkTmEl^bi)a7hdV9k!Q6!KQ=nX$ksS>~H--!BMjTQ~7fW zwO}2+lCltpnTdrBKTzJz5+~I>)nrs)Cr|XKhHIz}38_3erWsLixJfXsRe%b at KtysA$1bO|kZK z1+oh-X3q&Bn@(Mou2wQ!>}Gil)zAPy0 at UJf%v#={*en7c`rqL$3qMj8vrQb1lZJTR=eG2E4V!=7Jus%qX;MQl0nG>s%xT z$dbU$Mc$R2k6LM)Tvm$Xs9!^lnFeHm at j}il6IQaIB at 9X(MyODndmG!Q4C at L9U zF+-Un7hnupqza3RI`I-!5O zXP{fh(|9?>z- at 1FP`D<+G>~loB^IFxw<;Vi+F_KcqyAI1EKF=5dwt9QuVep#@xL@% z=lX9jZhQ at ByQKE|lEDD;3SmaHCw;vpv?Irr2~JP at 5HEFp!R1)QJ~_VI2HhKT at +3*s zYy=;AiGxN{yUl1ATOFUPL>NfzZ>)g5&oIB^Zs2BG`jz1_2qpcy1NK)FhWg}WODjCB zmuqJH4~c$_y=hrLcL!W%8yAzOC+fm;eoDI}xs3ZM$h=liB5WMEDgTK$%<6B%8(^Ri zvU at k|x-|Bgak2}7sQY)tW}qxDU7BO^S%p1QCk`zUuIfrF1F8f-r=+cBKfS?U8{!M? zEQ(QyPfI&{zT+RYHShS5V%$Ty406TIcwz5e$1em>c4)a2ns2<5azQ2q0J at uON;_K* zlvxJ?B}m;LgJ>>m20y;j9uG~nz^zRNXXOwnq?`#i(jqKlH}Go<;mTXXGar8EnJ0|ywqC!HilM4C}cwUVd=0UjnRN!*csc4^ke}B z)yCx;!4-M0LnU9xiq1EFC0(`YwhKzM&351Ef at uL(o?){vFyV$n=5BsBQLzIUR(E2+ zJEL(hpp3zRS8Cd at YVS0S5xVy_4NlWfX_T{%%M0Pm(uVY*eev{R0JHpIMl3>k?}z+o zs5!AJirBAJhEQ$~N6KoLFv+JT`8UF@=u_xsS^n8mMNohyqeBibP+`?!o`zAe)Ezj{1*f5iZ>q6`Y_?`qY%@!PUMF;@=HV#_dP<=OcuH8pb=HYl z5Nv)CHgN-B%=F1K?=bCIT%fa*Q~vqQCNapacy;KGzDCV93?=+ov<)lUnLG)BpDfrV z+*VPj5=b&(?=iUtlQ93)wxM2$t-UV=sBr6i7MOq;oFqmlM0hihscKh z`gN}ArVH#YjTgp9X(H-6;C^Galig&q^2?3~N)R~nrOmxH%Skc$p_){FD;zt4&cw}h zR}?YBho^uQ5D_eu+hcgp&{6{`e7r=>_43r?(KN{St*>W9 at R^yV8RHyU%vCN at csK}W zeL=|&v{iit%DakLS}5GF7ZO0ZXx!b86CQ{>h4w!M>j7G%O(e*+C_i760;sz35lG%- znWt7Q3`I3P`U=N1zbkXVDQi-ns->PT at vbWS$c>sk$WTctm^YKvEY{2R=$xOhyjQIW zYL<7a5mkE(zS0O{!ZrvYR)mJ9vw=cMV*OcBnl}Yu@?}rN=BZI9%E`df8?M9r!(lAU z+)N;A$lrA|1WN8tTao=?s|FZo0yJ1eDC`x$|LLr|e&Ld3(cShT7KH3ZTHgB)+9q3v zGB#aA*yLQj+458ydAYvYq at pW)Du;VHsEbp2HQxyqRSz-=9GQqgVQQREdXDVim)fEU zlW>bu5X_dB_XOMd8{^#u;yR*yp zWEh#$O1*3TXx`t1u~4$QF27h$Tjn?*e4>xuH_c?4=hi7&cV~Tk2)>G60N8$+iCRj@ zBhMC}eKM0&pGis3ejCKLYD2SxC7Z9IcJHgD6{b~liv&JmP)=al;NX{4Jf%MpYvx|> zNk+SRJnEnR%cCVg5iFfzCfRY=pOewSQd?KR4LS8^tTYUttj$lTg+{xXK_KIe7 at x!=IZL#;!*9 at g*NDcoQVT&x=G1uCKUL^Qhkg at snHJL5%5BzXwfkuNLk^BK%-hk8?0G zr-n*DF3&_X$MKAhvz_9N0t>#e1&rfj<0_Hsb zYFNv+oiFW>NytWY+vg(Sv!V2JKbQE^8S%u+=edg!@TIK~9`rUZpO%1&S2dd3Qr7NJ z=ns-|K#<(Q>j|bB-iB95)0B#ZU$8Z9eBDfmly()-)1XH&B5 zr(fh)zS5~n2+`#(W?yNs2y3+1ey&bxpNAY`j;9{7jk`W*Rg}(jCeW;mZ at wWX9ild2 z*l&3f(ami9!SV z#7F4*OvAc0RI}7h9lL&>pBiGo1Fdn*nSd>932 at A#3^ViEn37~s4y+EaQ~l^1Qbv!B z0g9B0f|&a^f3BXi#`1MlXkRluTeL`P8GJ?52E1D%xa`w!BobM7bNIwMNtbq)dghg{ z(A0HkPV;PWDI?91t!x)x?p>M9zAPUA+?T_ zZ`vB0&S|c+p{aatl5)Tm>cb}&)w|2=hz|GRk({f{{5ca{m20buCop){T&F@@%M7AF zzV!A?2pa*DlfzHzHk$jr6z&l!>AVHmR>fDgYkYntJUh56+0 at 9y`YJVHZ}GpQhme>uGdD{J6x%=D-R}(lx9%<# zgMiU2{IW at N5CM>x5Ts-@!lRfr=!6>lu{knJeET|cLp at sG@Y;t${H!-v*K08$=L1_6 z+#O#gbk+#`B3mUwvY5xD5Xc|dg+MQoYl*2uu5vKHV$u(SgN1RUihH10Y6?Y#Nq`jAry<|u;jFXPP zBz at cBfV61w`n94q&j4^YdFQDyns_ at R8?SsarD!MEgHKXnS>F>JE%vGo0z_xRdJMXq z(A3I#&I-)rm;#eKe;mbY2=WoL;?dw_og(X)4`(3a!O=va=o3?z?Pt(m?3e7TMtA|H zZgn#XDS!NB#WtV_oYi zacwrnj?}CbluKbNVMi7Y*7l9zj=$iuBh_qoXrFF4Wl#)BB<>}zccw1nN97RYbThgY zRylDcqgED^ju(n5?U`+tec7gg2YiL#8w*_vO9F at -Vtlf?IcQQSSS5$+`0l|wlNh=2ln}~FST0pBs-34YHOy0_}) zxlTb;^uTCll{OJr0-4=VP= zi7b01y5LkEx&KAw^LK|P@{eCvu&WBy{qx*aR5WC?Lp!S z;vgeq6MaWZ2M`t5($d)U_g`0#i9XoU9;9z?Z*1qF3^ug}TboK7yHfwRMSmLs8tMPG zXj$Hu2TvvE*i;Gl7i!yvKsOL^$(f at 8uE<)T?I at nk^f;rP#8hh>HmJMRZO%BzLy at TSRI4@`AW;|j9-*}^QKND)Jlj7y$zZ&uAM3+p z-zL;yuiDSDW?pmDPvn!IuGTwIx=b^O at e)?od{S?8d2YtLpLOdy?c%nC)FqOZIJZ(UOd<^Zjn at _w|AuxSWEvyLSJ80rN;LL#V!Ul;J z2$H)^_S7g&?EYcVI!Ymsy|E$0+6d(MJE|bC(O3BxKQYGVgn!{`9n`_~MTISXtY3cY?TJs9u(EYRDmRX8vw#b$Nh{w40Y;9`etm=AavYWI!l|H|!`oogs3hqhW#Mreko-`L_e)d6!wUpHTI;^SQEoj%d zA%r|}Us|R0ltXt^hidAZr7t4l_yf9K^^~3^_}Q~PSl`>4?UX{W0-WtJ(4a6Mt?SV4 zf|Py{?)QYkpc_$^eshRe`KD)qPbN!F%G?qpM0>*KPB=wcAzF?y;K>uscaz#grw_hL zMsI-sU`7!?|5Y*Rx9Z1MLiGq(9Ieg#v;GV5u`qo$&^4(4+Lt{hJ1nhS0y?C(NYJd~)wKA-MZ` zuF@(tKiO0U= z at e@H30^Wh-3}P7`)po)J`iLdF6KeO|kusr7A9q&hjmPqlviW^b+%G%7W?DFyzwy>C zFR~yJuXxr21zdR?_#BHj0FnY47av40Z)n at I3WyLWy-)MyKA?rCQXwFnhq67yxs(&y4X!$&@(%+V{?^qW)C8N%%w4uPn1qPLi_4?_Al0xDjpV)wN-K&B$hfpSrltwp`FqJh^K9 zd=gCfwwR$cNY!H4ID9E6#U9?z6cZ?5d|9NKOgdedcc-+PO`<=C52Z+byA&8knQIok zBCvx>(2D3eDZCe)1;qP}G2)*Xk3thb|2MH#Ux9$cgxA(5<;hjCN8BA&yWBl*NZE_< zQf!KNy<%env>K-4yg8>+L-rRnD$o15(0L{BBT_bJBom&TqQcnV+@a{5BMRgf!+2H0iXnm*sNMK5cf`z#WXROte(8UY|8f zOTIGYTp4fanpArKoG(UbY5c`lQ%;(}o|&ef&%{ZiI0KXiFl>Z)1q4Ke#6*N`_^qr3 z#dw8zMfgRmM8INT02C1w6cDxL7qsQQWd9)r8$J#Fx7TjXRdSQt!Lb=?|IPUcNoeUv z!Wj)B@`{m}OsHkDUa7Y>8{05i3QoqjDvY#Vq97+S#%A+DrKkK4hfBH&O at Ha_;^HC> zCHFt{%7unT_6c}PU+IA?^GYvhHo$}Pw`H#Mh|(*@HT?S;#=nMFuHifX&@(RZkX*y- z*RbI=ym6&x^uB9&^J+i(k861QdVTF0-nm}ix`zK;!{^uV{uPXI>l!||hKa7>!>jW# z6t3apEBP=?ui>+6eE2ndehqtF!B|*V{9puK=>Q8QE8zMV5!W#8HT>)v#=FLkyN2)fGM_)-_Ch4TG*> z)+_liS+8Ny>-{KK1m>{BuJQY>Veu;%SNR%Ny4p`BaJ^soYCoC&H9soXu<13ddbOX- z<{CD=Ubnx7&92v7uVM3RIPsdk&GmUNuVKhF{N at _Ay@u0Kusqsd`3UVYAEEK1$%5RG zP$wwT8)OTEgWwQnm#QBf=GKf%-zl&gm8seLv5iD8$25* zfQW>_p};EG#pa3wV0^`Zr#%!PX%F^*fLvfmkQD at Av|mVMQ%Nyui+`P7qE!9zX$X|8}Sya3+9;@OE~Bx;P>@L7p%d79_{UtnbCZNIpT{U*@PGI=eR&JC-OU+E#A?;ysZa{WhZaBoo9*pF+ zhB;rQCwUoNKnN8u5~KljarZ*IhB!g0F4o*=$`3XEeFh|9k3_nP^YB=?+xbA9oWTDe z`1hO?n%*VTAVn7sC>-YE3~@p7$N_GG_V1OB8wrMU+xeh5U7qYEDr_Mvh~kM2h*K)v zCpk}YF8LoJsFRwpr-s-H2nYy^SwVRDghg!y_-uu3cp+j!ynOt&0uZ29pxFxZTk}~7 z3JVJf3RsJQg$4Qft-!nx0UJ?KUMpMCf0R`a90o(8{g*!%*FPX2)ZngwJ*-&eVa~4b zOFvQq)Tsq_h9KC`1OUbbx%kiox%s*IF7;b)DK+xLQc49hE2Y3`DVc3xDM^w!E3s`n zE3wjQDN$8mDWUIW`Ct{z@<9w__Q5k!%Lh>{vkzb-mJj4)EFZ{h%!;2{&Wg8|Sc<>b zwG at 9OKP%RdXDL=iI4h>UJ1eGCJS)azH!F6J+)|80!BRBeXI3;he^&JAyrrn$k)=r2 z`K*Yk%&Z6j^Q=%H`mA8>!mOZ{r=@`Kk)^;r{#ivQCnyjW8W0Fb0s(UcJj-6 at Uq+Kc z(}X%gK(gF`jp1-02qc{CF9-j`0bMmFd}vx=z*Zmyu)R0f${q{_Nw}aU{tK!opdO+B z9F_k+$h^7y920erU2?oFeLfG}+MEU<+{LfF}`;T!2{s*}H0PaJ;n~*@Nl!L=O zT|g49R;c&CaryrU28!eVgkJzpNWKLY)x6SrX0&&;J)K`5$1&1MZ510+mh~Y>UE_u)BQ!PtO6nJAsse zA`YwqLpxw3V+FOd1#;_uN&v$`%f*}8@@PEtsmuRTg2awnf@>ib*R34D9pcNDe^k23J z2omTWy+L4`%XSISgS?W4tg;>+2*jmep&_fQtf-`_p=hC}ZJ;Z!h?=_uwUw0g6!k6S zRQ2^Fg#|%C%K>OdNLWmmkL_;;0LFkwMRk%ekQ2-fbvg(r-3YK9s!2kin!wAA_k={n z`1pZsHV|6?AN2xMjs)q+scI>x>Y`-S1Uo`d`vJ{EU94enRI>%Lg265uL%`0iD2oC< zX$$oN=vV`)03od5P*)^s2E`e_h^P?TzpQbo6d`UePLMF_j|jIH2m*(LkzfD?)e5>g zfvq7R9thH!2StqA1`PC+JbyRjTqtz`5`saDE-;|=LokA1u1M%*^Ns3Cp)U4NK);Bq z4jJr(YIOg%K9L&`M%4w#yYMSxqfWF2ax?Uj5WqL8vqm|fCK!&q)PXh35hV*RpWvkr z+Q2RwZ6w?qrQxf76lijh0w4#NI~+)pe>K0r79oCql=vuZAiZ5-D4qU8OTda11c?Of z0d)S>j+cEiprF5)^#-T{sR`nO;t-{B6oNBK^I!lUg@!srP>_#L1a*iC;Gj;Be`|>^ zeeNGlhd4l06f(Vd}I~+wAWlpFQ7^Q_PjsfL3!2kp#Ko^C8@)_Wg1ON9U zuH?5tSpYa3kPiu at DUdVF2Eq-HdHAn*hPc3?)-Y~_HE>1nk%79{qOKcAs3SMhoeL1h z*#(O9;YP)?lM~DnfiekjIf4U`1p;Ei9p$|!b2_6gE}S6is~0LH!KmTC6v%pyPfSRN zmko6~;InWD`05 at 6Xc^@Zf8XGMF!fbY19BEI(viE z!AKvFgd_0RLFV!j;EHPAxM6U+tNiH+gFB)KqrC0%eg}*JMje6U0Mo_gPIVRVK&l-YSy1am0PJvOE zM=O{+5X?Xry2By=aCaA&3)g=eSt$2Hxu`43UO+ZS1?^uM at lwH;zRQC`K^gCQ3WO~osJa)3I at I0APP7X&v at 3So|@oX719=61jKa>$icZ2{9= z$JcdSs{&`qp9+m+UL~Q6YxX z5f>NWxFB06upK8#Pga*n1#ua^NRTIw4a^#W66QKz0T?Kkx?&Ch5Ww{WYI_-^s2D=6 zSwR5yA%A~vxXvOrkgL=I5CK^GOVfzU3%T{*_cc^HbqCl1yulfY%1FSmAOsTb4!9*y zO+^JRJ?An~Q5=JyD1`%5JRoo at 7;rZ0J$?aEfy<=wkP9f+m-=S~!pX_~(wzWl00CTW zfDDQvr0)p<3Wd$zdVQ$+!7$84EQ-5dKp*B ziu&3Q^)0x$xv#6!UmqT9pf)H61#&Rh#T#i4FvAI?Ga&AOD?9?Y<|Bddg1EUuf%pc_ zxN-rMVNjXuTKTBl;ttdzE66o%8*Bu9{ z5_zEN2S2~yWtgdgz|NQX3)R>-1Hq339ta%ouUhaIW#H6*=k%&3b8!J$hg|s+Y7gLu zs2~AYKyd)fxInET|LpJ7A%I(h9)TSkfgI=oykt=I0SHtiw+jR*1%m!vqtQ@~?)d-R z7hGm$cO=-#36;SBA4Z at w$Hy)7H}5E>fW`-mKmelSU26%LM=9zb7P*q- at AL)e-W6=^ zc*QL8Nl1F2?FH+(B75f|Fft=|Ej{*Y=KaN0N{t-7n>{9c><=e1AOPd z`@9C?;}sGV6%rK^LwyZK!^FINZetSz4++{oe*gV9vA<@yQSzdpS-V1EXm&nOSF}rN zm;Z3MxHv<7a3-RUzW!eWW at -(#gu%rHws{vFRpi6xWG(F#2h#$+q0!&!O%J9w>K`eQ zMkz!jytJ{gd795krFy@%kTj~WG~wliSmpNb2j`7}GZ+36D=EGmt7mU|NB-=2ib6+%b^jK47nic5g~<~wuV z`sFuwK!&gp7BI18%`r`Ccj zfnz=iH^PwpkM8oJ4gF0HdeX-Sknbb(c{0)seCukvxCDE?eSM>&DutBd^~RG#mQQe( zn#SK}m(eD?9;t+tt%6(Il(dW2$bb2WZIBn-kD=mQgKv!B!^f&PHOgO>bgrz^)`&~HjL9*^h312bcooPju3^*&XLZ{MnqSz>=)r4 z>aZP)!fJ|b=MiT%n>8HyWi1Emyaos3=dNRjRx9`{H-NM4=RP(a9%6Ul=EBKIWCCxx*6U#w_QAz;P&EGk2b$m-+!I&XTEeJ#CzaF z5o7xAb?i<%w|U>2giXG5YAWzB(aK7fl&$7Q+)bU=-C`vMvK8(Lj{KhC^86la8(qkS zsRLLeo%PRd at 6iubCsNz3cCn``b#V!@BPp_#cR8xhR)pMQ^r^)(;!O-`_Ns4D-gjh5 zxRs#)^gYeN at Wehr+dQj5ZtJwTT^rS8wba|9qrN84kJ`}wGwbL6y73M}*~)kZ=i>U2 z?3!jFy3gKn)n+E&BQSousmW{Yvua8<0M?@S_TycaGjb`&t{b#~hN^|7r40!NkOl_M zAf7}d2r89fcg}Pe{ZvZ#wn#TUUFrD{^LO at -mt-8$Runrbh>8t1q62bf+C*i?|fDa)Z2gSG%5KACN>Ju_BP3tgmAc z`h9z|C}P3kxJ5Be>D-RMdLR^ zrg!jl)s`>%xeo=`GLxIkrcTj2nf1bSGF0O$ky_i_e at ScnncBdG^MBs&d3CvHc% zNS!GGL)x70?ORVx?%JhMrbN(m@$LEvFQw8w=l!6chcDel^+2rMcrk9TjgK9wE51om zfIKZA=TI-3>$)LJP?ngfdWJ}TLaj|zRyvV2CZuz_UZ23&lw#Q4cUy{BaRsLtig5+Q6|6mtZQvL-Q0&-A^> zH4$Hcale~Vv6*^7{G|Z4*`Kh+_W2cDvtOq~TRNs?weRQ+RrTq&N%K>U)z#;n{I}M} z-iHORAKqY{{p2mx&Fyn9+jS`>B)_n1FUpWz&;LCsqE)KN}eyss7( zHu+k*AF=8a$OEK}2TgL^8r><9-tMo*t0ucVP$5Y~J>> z8=Hy=6r!EZrb at Hi>yWi|tsXr$D6Zl>ueaRW-^0hhJ0=I;?j#+8PdFbaYMX(${Z3?IO4}!A8DsAMn!q8->^!m7cP> z(#>`4s;JPwI5T|w28knnt63xCQB`AacW09$e#$pNyq9*eS*w-kw7hs`@_W zDPh`i?hK82;c*0YwfIkt;(evoZC6RD+?J=odhZt1q@=2S6BCSTz{}rffBxzZQTTLd zlj at wCc_yiLhsXKzOWWzm_BfA^l$^8;HVuPk8l!v5$4>(2I>>alz}qccDMtlPPG6vp zA~%=~=zidjJ=KoVnHox;CY)krzcsg}-sAr at nX;vkb85_fg0$j;W(i^a6Uz!a_aAD! zE_3dMHA3E>ECtF%Nua~BLUUel) zHu at HrNkjYwB@`1}!gK3{$KjB1X;Z$b3X3j;Y+l8BJ{MGyHgelu)ZD;*7R&g9+=cLP z1D9o=%>_gBCq0o^ZWYBhq(s5y(zaaUw>?YysU~VIXFS+)tKMaOd@}W=k0=eSPBaRl zri&)G!6(&a?GxqkVv%A#^OFCQ+-LT&*RPAmBIA~7dic9PR$SZCoZ zpUOS!sWIqNa2H^DF&|Y;q$y$Y`g1Z(&Y>>#A9rbw%Pb=o(scx<`%7|hc*G4?*; zlPaL zhgQE`X_AeE6E-w_^=_A-=`D7P*t+vl4S#<}?ud**iFLBjDJYW|m&97QGL%LCQPXR9S+uGD=%Yb5fD_YA9BI zV_`TrXmi!kEgk#)=&AH54CotJk1zTtVokQFq7$jZVO+WH^yW*Q-0T_u499N3_yC^g z(PrW&3_)Qi(<7GE`*;OMUj~H2VYFHw=}aO!+u at x2P435D#74y5vegYRA1~rJa10l` zA;C|QhT$61VT?d&$qw?n3r)-2bGI0u=MEqV&`0N!wgI(}FDbqE(=o+tLmX8XlxNt=UuE`o zjySfv-u at Zh(`{DnhOjXie8zK&{Lxyi8WEdsd^Y%_ImK&F^%JesU@>i|Y>zL-p6lOybSf&(Rx{oNeCAfAHBGoWwa~z_MV2_yjl~m{w#vo+0j&*v>C5 z!JZ=74Ai|=s9qF{z)eO!J!8hP+i+DJ{TW{+Te6BjJka4szrFIk*+t>* zccvCH&TjX&YFckNiXn1;_Ubo?`S$w%Cd>1lefVI3n=YMJgm1o!<+;qK9BYK=z$q(L z>3$svqXK?=V6o^D*Lo?l5331o!$26jnz3H09Y4Rcf2Y+))(NAU=N=zY>gZxRln56N z-sIOMlcguXHky at nr`f*5^)ZwN`>G#EvpRg*sS}ibWGnE++iS49(u0mZnOTq9-RU#` z`rxy`*QL<35XgK(t69x+7T at h`C78Kwfs}nC6D2~UFm2DjMIqyTIrj-;yYqHhC}`X7 zzA+^G1-8+r!T<^uYYCR+#g{9lw{dio)o1)%zbtS(SuM9r4zb+#P at B{wVW!6Qm|h>X z=~gvAJE6FL^ljcF=1n#{wEEfVG?hxZla~EVPK~xiT1dUaT0!ckRjWS%+;&J<--gm6 zzwW`Ui`4$lbiOM{7xJRpb38*g!bs at xcp|jR@`Qy{sJMJ?br0tpO+wR^F{1<85Hb4p zJ*(wQ%bS53rD^AN8~NqF0}w8`a1MLqNFchN-G#Sjl^-t at a@zmY4rtKR^AtrP>0Dnw z8;VrEPaMW7;|}mw)CpD_#E;ZyFg9)cq}1r2*u;_l9g8AwVxv#0$eJcBlhk`h!lQMB zpmlFsy*b8rCwwi{6(KmeLXtz_9UMa3S at yDM%_}A=G<}3Y^KO}4KeNmafB6uOa8sA} zk~-t(DaU7exxs4_ZKmn-+X^d at BY7e)o-{V9qf^CC+>C4ns?L_wJ!E=9izJy8FCE{X zDKgYX1}@BpW9o>|kYxAb5iIrGtV*Ra@@M;#zpS-dS|arOG+|G7_xCPiK`q0SGC5%i z_YU;*e9cRTZ!fLAc;X#RgHB2Z+FQ~3jYUm#3HO(z3C?;a^1Bo7tg}x(()irAe5GD} zST<(S&%dlV$$3pAobUU;3d&6imEit(Z>@XPc-MEnBOVI#K`TM8JWIm z>&{zJ;^167Fobw~R4plE at H*~0FnV0VeoMk|DS2bLS&RAQ=o+EHK{nmjH&yk;+C_>8 zk(Wb0FWtBK7TjtA)Qni>Ux(yvnyTp+ZF+6lSVe|Oz0y6oQC#~fc+*@?_DS(vnTSZ} zw5n<2P+Q!*<0A;`s*&oT>DXSF|GW=^xx2co!_Qx@!7Y*0P4A>z&IQQDO!eb$8^;#y z6blGH at qAa8*xa#=wceol)?~Xj-P{TF4xNK1{h}bz7XPNn=Vio4K3_5X;O#A=1?{wp z!Wr<8^_j7A9IWtN)uQR|x}D$4Jx`0Bj3}%ney`{vmY9n$PG`M)+Aw5aku~Hyzkdk} zQ!drP1*Yx7uIzA2zBWXR+ENm zjmVgS>2g8Ciiwf9vFSn8O!;t~ZTH&+gTmD5{xbQY2oqe|J<~?$uz^yDjg(RG!r?oc zQM6(T7d%HdXU*5d4|yoX*exe45{o(AFP8P|x-09%vvxar zI9$7{2OcZNo}g8GWGW zFa_c>bhJ{8j2(y)b;-TYRLNoW-N7sO-p0W6_Z-iFS0&qco6DqwHZMyt{8p!lyHDcJ zB1~XCQrZWLky`eAk-5+MPJJQ{?aD07Z)BwwuqM#Ps)%D%cV(wS)FN!yN}$75qTTT3_4kHboi`fS8<|Afs^&Lx at U;+Rc7Sf0=Beo zAB>#vjQ8ipwWZg))6Pn>_xf{2 at 4j4htJ`*c_G!n!=$TxF5Da~Is&nk^_HMr?Hdd?x zPMwC7TkQy%>fN5Bl+C?b_r%FKE00(UW0uySC~)o{tr6U!kT!X~5|*$O#fdGSOat36 z>*MJ?J7*^MX-9u?pZ at mX<5$W;vGkwkmV-4fQZlBiF%6w0lher#?@S1YBbSm_O0^Y! zc^b8=SLb6(RjDSXF70=q{}H4hOSxI)!8Q$}*J{jSAm1ro{+c95)vi4zp^;>)yFNln z^UAKx8%8JXagr*4Q--yY&m%p0`W?L1QwNJQ*H?ldFm+t;D z9(a9pKA{6Cx26uMNq8Ba1SPNL{H^g=rTqcLXR`EkNei12i%v-s;d}dSpXEmdUPv1D zat73QG_w2eI0r#<8+d9JHJ_f<=8ov%!tq|lU_;BaK76LWBjT82wf at w;I1NTvN6SfL zc3;!YP{+Dnl-5J9R;hNPc at NqiJ|C1Y9Wt1}PDc2yH}$``&*^vo5aJiLvI=2Thx- z{b}*0qvxnS5fNT-yfIIqhdvW!o?CrSs-QrNioJy5+|yhiw_7+dPiRe|x-o7z?$`|9 z)4MroeB0G_)7UNRPyh6k04|NQAs!av2SI}e)5A(qfr7q2Gg^KJOs90bR_;=ZQgrL| zh~g|CF(hyr;1*1C#4p^q!wGE^5Uj)3fN&u+YURmKvrKZd`M{UjW}LnJ*tr%iPDGS at xl(vBp~y&dJ}? z4VOOBn1<{%n;!RNkS{RBe&UN%Uy>AR<}5_a3^cFv)`3O z48crI!%Ir6qwZ`RIq?H5O3JlbS*)^R{vY at HE? z2QTbeZ_-q^f`N;TvQv*kYvV~>32(-x*!Zc!x$cS^o at vUl5z>TvO at APvN}L)_=*fQR zpv}|wP6$S?CTXgum^4YdmeQr`W?*eQyC3~=-sJQv-Xf=8*MjCC2bx`c`wh;FBJQcc ztrzf~oQ~XRVhYS?3<8&q<{oW at mPa@IT{`eyNq?TQ*Y+b#zQ+tH^5!v?&^~*w^Q0nG zMBaqALD^$(Z;QFLThmO z*5sNyQhf+!U*)F0D=r|qc|3gQ`vh at zl30(Qs<$d^-FqTxjkj}3pY%>`Y&6cfDbG54 zYm`{qov8^s-!Pss+P#khyKeLSI%CRw!es#`^Q+(8Dqz_4zZPn3F*K!C^2*5wT|1|p zBUfJ=)9V}}Ys8ivzwJn9hGnS8mGUhMyO237c6%8cedc2EIl7g4FYUt`x7r!G;cnS9 z`MimjsT$^-g|Tr|nRuVvug+huM%+VQxa>MRwLSJJVwiSv*qHWv`7!bE)gYf%SB~Cr zX6-8LgdWvNPh~JA=5SxmUcL9ZNQL*)8fPc7P281wB3q9wcxDp(r1;la3Z~b}EWKyZ zuKuRXAfHL>rrDoD{u}!XnR)kV&~sH+^7u_gj_~T=OH3N*IPxHLDh}A4v7>1T at Z%ip zFbA0LY7*)K2Ddg=2=gyy+{eWmi|FpSal}c46U)2*dd#@Ke`qD at n;1|k;$*v z6N?XKS+=hH+Nq_yfyakm-Br0RyyZ+%$M2>YQ*maCbH-WC%%ZQH_?yT?;)&pATpEF#CMhgS8iE{xFow zz2$y04_?%NgYwg1m^i+nnL)FJ@&^y+HH8ekXt(f!F9Zec5o!1$T^5SnHp=e_c6pQ= z2-!7oeUqZypFOvUHRx1hc`p at 5FQw&H<6X?+*|VSPRp at xWSf)pIcih+J#{NKuw-vQy zwS+}i+XGIX;?Ix$0w#9NARwL|+n(HE$)KVckB_gK at ONMEi?T;CknN2r&{umDQ&fg5 z)AW1RQH=c=|YAJu{_*U5;I at ll8 zB*Xa<&oDJZhYmhiCzQQ at o5a`ETH^ID1_hNJkpzx5W9w*z4M92fC4KL2;B8R3h$<&O3wTh8-LH3c$DZJ83*`NB zSL*1Cnw*=nfCAdrZ(ld0s7 z(|7%&c0#>o9M at TgnM!Rw5rOtu#6~4c)uSBadAdZP=yqe4^0y{ zEdLa_d)gW!)X|aEsTF& z?thpIwbVW+T9d34N!GP6Oy=Wo`^;6x-*3%N9{Z~=_4Vkt?ZoE8SH3~}JH~=^k=q%@ zC%-5gYd1|7oICeHKUOt)cJbJMzxB;nRXkhU&3$pN$c1+L2PvUn{vWxKyiNn*WV<=G z6P7%myUJ{YCFVQD`MRkbeup3{3$%q6xZ*;~MndT35Rdo`Qck3Yoxldw7dN_BS)JUhhj(n+=(Zy{2T!-NcO1Ytv$tT|z zMx;7z)CXyEZ22|TtMbS4$^7mrJwO(G`2x<;JbcA`Z%S$b5#dd&pedMriI at wNuJxj!MT%`PcmFxzk|7^o(qE1bRv-l*#gCE!@usk9@?Pyra2bQq` zMCZIsNs5INk2?EHcyURB*XJ_?o%LnHGm>%TZj2=2qMq zEuus-#e}co98Mw9zT=hhs(Dbx5A_IPuJ@< z2`Vugr)v*hcX!JzLbOTGT`s_;TfvOa68zhnX^Tv3(0_3G#q8hlHYeIm7 at NtB#Bj{3 zebiDZ;guZOHTU7YEONAa)hXm77B^&KD&IS<2KgA08aLbAmFKlfHDj!Q&hYqCYW#~` z#sU?19pC(_z5K5{#G-nvY3KV-3}d_F&4r@|T;X6bj}H4l)ol-68~)03Hf)^8+N-Z=#&NecDNR$X_Qj)G z#mOgo7DH}UKSLxn&Ha%I0saTs3(aQZi}z0|^yVg{=41~F-cj^Ev#K4bTrt8`_7Lgu zoBJd>rJ{FGwES%@w1vTF?mH#PeNVYhif=8-HB6LtGsh$EyxDK+Q;sC4d8aPl`SGpb zgy7A$c&~Jh1552(cv8*sXCIrZI681}-X3l2`94qjPDpzDexAsJ-#l|Ci at wNS%f#UE zvv7E~&nH}j=G3MFWtA&FN(V!??)eSv#~y{lp199U6UR? zc6i)D+Z&HG*m0}Sn@~#rdD9syQ+(x<_S~~7F)N)Q#Q7qmJwxeTq(+&LaG2+avtZxH z(9a1U1~^GHz+8_xbfPc~dtP%C)%y=}r6f#5Z9S4su_w1)XKd>!?|atR;<{~K%F|HA zbnt?1BI3n{=W#cAQcp16;K>6hvqAO#gd2A`J^c`y!k&$NRK!Wu?NR0Z2;3T}UmL9l zRlQBxT_L{wOv0E#kvMf^bs83xX8IC!|F4V7(sm1inr^Q^77LjrG4KkrZKoC%PSJIfNbNO at 57^ejWu(WN(H zbhR*kC$s4G(S)LLY$;|88N%3-;-KEPE_n)U&-r}OB4480R=DpzVXrxnopqQQDt>#+=%Tu#y;(_z=ImTTr?;gOntZ*dAkR zWmlio8q&8jpZ?09G>fD^Jg^(&M%vGDE@>Fs+4m%`*dGPDd!^U2Vl9=uurlW%sYn&Nd&AOIo zyKh=_gQ`ERy$}&9(5{y4&tYmACN at FgbH+XVwTWv|ABFFIrOS0d-wQq{&G?fT#qTrx zfD1yKRxZ-$dACcqY2GT$Y%nsOl1CCc^6}g(_mHWP$AaZT?Dz2}1ig}jm|^vPulShr z>mT=3Gz6G$RN^GQW1l3He|!+&`D`X&2P^FvU$;-Y0QB|d`tjZd7u9g*GgsqSJ)SZl zw^MG6o77rM5%W>y;Us>NF}L|S${LZTJ}be6%kOWQ#cUSScs#Su`$g;;h&&)U-?nOJ z?5#*_k;cOBV z&MeJX79wIRtA)h*%MV{$*bI%W#_HF=a{(~?%D`e%uJi_hwP z?i2j}g%!G(aU7bcGTrac at fm_Q`$oR2R#HRJGvClUWNP>{j7>WfiAev-3U}3(*g1QV zF2T6_xoz;BUe^RRw9~uv at rpNiVY~lxo_b>-xdllPY#Vb5W?$^O zDC?cP%d@?`)64PoieFQ{L at qu%Tl^zj?mv1yR(aOD=w&gupG1IHo6d#J`Uv4`_%zR` zVC#h`rm4}A9PHy1?J$}Bk9zRqDemssq|!HXdogq04&`u##>E*ED_Bp|Ryk(0Xd;tB zv*#A`_5E%sdTlzIk!wB2?pB6=Tlw}eQc-tZkoi>jlGczoF~3Ja$_>Y1?&fs;R6m0*=d4jA%4!jkK2 at SX7R2Ix!*O*1rhmPABh* zymGEpu`6GCN;-Kj-OZdv!@9q4~Xc#93 zaj1(P1r^9gZcZ|O0auAO>C@%Zc&?mP7f zpMzzm-^hHI`c-)F{wuwj`K&=C6=aD+nov5{ahk%_8`c3CR-fe32m4KHp9ELxCGToJ&GZKh zUAB%xbWM``TPwab<<>u6GrDqFC`csNkU*+`#mkD54?%u+oC@@+NcP@@ zxIFw4(Kr2j0=&?NAkWpXKvUHNQtQzBR9z_o-bcjbrN?4b34q%tPWM4n|}>hV8}iFfmhwz z$&+i|6l+t3qtN&oepsUr$dvxxd+U3|m#K(m!?(|=5mFBz)s7e2?auCReT5#`z

gRWB#<L957<wQ+as%@MRA*h0GRK?^C%?vmA58a+y!0(+Dn( zXda8}m9r4zOFCIX)$C-NZp^+!UnOG1{Y|(G#_Y$U9j1 at Guj&wj at w*(ekCKOj_Bj?V z5;Ljuo=U#!*9%f^Su}c#JyEhCW>91oTH10fv(G?3t~Wzu8kS%hmXv~Bh}^mGT%|US zN-x?&B at i(Q22ngtFbm!;9`22m#SjdTa#nr63XeF?!(tX(WtkAqJC8royI}POdhy|e z-c-yYdZL0BeGi>Zr7x*51v{e|Q8SZ(CXo=- at r3)}riy(a(=9ZofcH<CgJC46cPJme zL|fJ*zP*Jd8GQp|>msWuc9F<3`Ae)U_{LOt=N%#}!9#Am at a<!k7REkj8{QWBSlOsS z)!^GiXGx0wO+w9`5e(;)g2S<_x3T6*T9|?;pN6 at -fW&40X?39`Cy_h&=H;r{7jp5N zIvwp+ at YaF>B_*Z=dSo66Bcm$1m&`n=S2HFaN2+uSA#Q-IbtF at tjd-v$NOtSyf<^;= zyFkh*jwJ*P&fZ^wG#O8PF&+7WSGTORD`PEtqXk`{$Rg?Vj&$)UvBuISb^tch)&-*p zvFc9ZE>;=AuTZp9rKLy^`pU%!NkXHZvVlRbT)Gy2kV at RsR~Jh+O&{R}HJnxY;<;{_ zgMwum2t%KyQ2yk|OvB+_Ws<FLm*r}b7ez+qFl>OhobU6fO0~~Q+ at aioR^iQtW at 9!H zj?=i_WU508ppfMWn9ae{Z+I*|eo<QWRK(kVOKc~4+FqQ8mRuG4^<3l*c9{<6;YSVZ zRz<BZ7u1~M4DhzhH-D7kRIu*gx-H*4lGdq_w%UuW3e37;^zrcIrpC5t74hv=;Fj}b zfFxGdi&62(g*q20#%f60tl#o;OyPXACQ&qem9OZaur4$o%3IWhYI;n>5AV-!Sz2Wa z;;QhF24nqcJNhuF8pMV_CYyz6G0c*IE}(z|y0^Cg5fDrcHs)<2k_83bC|HSQOh09t z#l}-p2?;K*t7{4_?|5<)%dAFiJT8vbh9>d&HB)|aVCOxe7__aQ0x*mo`Vs{!OEgR( zKeRjOH=tEnS*)yGQH3O8>Nq$h^Yw9P!H2$V5A_6yD)!>(f_AkvyYE<{W7gp4<FsIg z%3u~eUT$e*E5Ld2GVa9r(+E#AgIG|@1uORXN4OJ4Tu>+rE*dF=*jD2%Et3lQ4(-i* z=$V_1jyG=*bDzm)5m4`v-Conq5G0b=z93grK*L>%1u at j0CxQtY2-``xFD@)@+E5vV z?Or%gC6bJzpL;!|u2ww9`Z5#W&KQFBQ`&4Zso&TO1H|>Gk&F3{GRAqA7>-?&{;ur# zik{^P=1wV{J at IH@ovgx5Tp668EyhZGURn2D&5B!vvd`lB+y#b8`TGV%_=gZ>pGztR z`a_E28;zG&O;*RMpfqETKYaa$L5D77Io|b^<Ewh~IlPK*v}_jd;_dmC^e*-k#yi>{ z5pHOY2jg~vq(?RHNIi^YkdsWTD-KG at N)Sxhu<L%ifDy=?E at +9nFG&0tG>&CTME<bd zSEe&CS|~zqmaz}LP<r5FF-B<nt+fYzS+nx?g)Gm}ghZP#!|#b$Z#hfoPiG)uNZ#@) z$^26C{M)9#Hz~p`Zyh#kg81U_=kTK7S&cg}oST8XzYKL{8InS7L}Tvs1>%)hqN##F zDiIbDvaddD9l!%yH!q$vT&Udqd7;Zs3Wqn{`5AN%Er9d(8m2-3nliJawAgLxXt|Iv zwbSNGBFYHiGwju%Csxl&m~sf`l;`BSg1cMsHOaBWDdwR>J#QFrnBYiBw}80T+#JZ) zhpVCY28D?ku+*yP*KX!$Fi|y8<dlI<&$4u9IpD5`o0L=`B?Mni>)Y|9 at 1RGCuwli9 z=&r{~94 at MqK0cfY|AMK?_~ZsU97kK4e=}sXUHjl^kVl5>aagDyMlR28kaS`@(NYjy z$oGVoFJC<(Ix9O<Rlj&Vb5p+;zfV5oUI>L{Q1cN9?o6G`Ple;tx5QzfQgGab+|94{ zf3Qza%Tpo%w~)ob$icg_g$I&hmsJey0DX}-vl9#HKTc=I!z-u2p~%oDR$!NTPoxwS zLNI5scYMc^hqYZMrkOHMW)4jyB(5aTR0b{R0_^-P at QoO;x{U9<rGj=`So*Mnbv=Wc zw%kdx3OaGdx3f?715nBz-}xv_iSDTByP`1!%@PJx(V)rFCjLC{a|PY at Iv15O!$eO# zC$YJM?y8XZg7~b6|7o=}W$(=UTNpIxk%xR4LBBD{MmCR#B(U+(b7agTh!~G{cvjTH z%>CsZcaGKI1C5T&)REXZUsXBwC~jex($ktXO)3PdvN5XtT2aCBW<Tb04jPdm at QY)L z_<_*~N=EA;!oqn{FOzqwH=42VtQm9?j>e2L0F at acJe@HlG35tQ5?1(7KA+7#=O_Xx z20lz})K3h$)pF+)<PM4h5qi&xEt5(=RP7_cb5-=lQEc+S(|mazCy4{gXAFsJdARaK zpN2FTWXCk0-o+%M?z$30`FvM9^t;Ubt<h}0hX|cLmYZjGox#a_(uW~usw6q*>^~n) zX2`W>+yT*@>e*RVbDY<c^6dDjyb1}jH6b)b8*OQNMI<A0{v`b|?W6fdIiaWML>P0K z`r$W^Ew~a3BU)xScA^*K5PuGCDRRF`Ps#w}5k2`H)|zv{Yeu at yGJKZ*=E7MspBd@& z?E7v2X+6)l_8=j;t|;RzKl~-^PZY`0801?dyK&M5UuZOr(`C-e?o&O%bCBPQZAL4w zkEOSJV{T6`C|awQ at zsn#6OVz1 at E*&(vFeK&nxM9GrzoO%x&F8~wEi1g9U#B9%m}Zz z6NdOnY!#Wf9EzS7I44PW<n}iux5}hLtTOS=!FLWrakLC)Wme8ebTfT{GH&~G^KPC6 zIE*~;IFeKx6wCO80sU4})D!r0xZkO^4fgrum-xd}X at wnb9MZ=a-errPkf4K$q?C at E zLFX9i!}J91zI_ToDvVe`5tz4aqWFLAFpJ=R$wi}9yXX2_hN&47dz_6CElDTw&KZQ> zdp&*sOknK&e6t??mc}6F`U1i8wnmz~TrAQ12`|*J+H5q?s92hap4{&6ee&$4 at gy@2 zuCDIKnKsfL(ds9s<BK3qEu61zmM&ht_r4O`7 at s`BgfRi-q6x>I|2y^a*yG3JRn3+q z=@N${pA#s?@e5#aBn%}COr-hGL`O?yDQ{**MPLkJ^^=d=VZOzx>WHliD<yEKlYW7@ z;(!jK*poR=T)&Z9ELANn&qeh;Hr<KtX&3#OQL-P=dAxK!y(uG(%`s8FPY~7JpwQ1X zGHl{MWoPi6Mjnc8fxJQ9A-WQ>WM39qV9kL8x+O23OsW0e1n%)M*f9(VZFO$wpu|%h zhl|%|8J}C)(u<B_>6A96EzQwN2k at e@F1Exue|Ud+LwT>h9^IN2PhH8!f^a5~o)A<_ z9)#VEmHOwwM6-j^69H1A<KFiKS#eK@!b4giflbs;fA^uW<sKd={jUIl0e=1)0SJF` zr-cSme-PG5;6EhcH>>)k4rtrI`r`+EmyiGx0{Npnpm)F(Ay5MKhSY}Y3+;*gbH~F3 z2_gbuhw$|SyLBLV(1b`62#h2c7D-YE(7);Y at 2Y}&9XX7OfF1$u2QgF7Evm>M(!be4 z-ZcDw-MoV+M*s=ofkFNYR>=H~<e$6EfHX|*w`U-e!GRP(Btj7PGjQN_D0Y9QB4h)! zI|7G8D=GYctn|aJzq=%uZg_;J-w$7ci3g?}@O$Tt-0+0Ryx^Bo;K&D7<<Ip&76pd# z2SEHe8lZnM@}E`!cmfVw?KcO9QkKlYW&cKFT{0{1#*snk*Jkn`JJtVefa9<~Ih{YW z_Xj|&^7r76_-*q)dpBSvEB%cIKQ_!rzjz}7?Smpgbk!eCB$7et=fYYTJPHAbK%LSD z1!n%wZF>Oig+zeZh642`{~ZL0_4*OVATW)5z}oub?>T?-`P=9I{p0cL#Kua?$W+gY TkB<-fThjjn|4qb2z(fE5ka8uR diff --git a/test/archive/use/X b/test/archive/use/X new file mode 100644 diff --git a/test/archive/use/bootstrap b/test/archive/use/bootstrap new file mode 100644 --- /dev/null +++ b/test/archive/use/bootstrap @@ -0,0 +1,2 @@ +sense disallowed +buildRequired False diff --git a/test/archive/use/builddocs b/test/archive/use/builddocs new file mode 100644 --- /dev/null +++ b/test/archive/use/builddocs @@ -0,0 +1,2 @@ +sense preferred +buildRequired False diff --git a/test/archive/use/buildtests b/test/archive/use/buildtests new file mode 100644 --- /dev/null +++ b/test/archive/use/buildtests @@ -0,0 +1,3 @@ +name buildtests +sense Preferred +buildRequired False diff --git a/test/archive/use/cross b/test/archive/use/cross new file mode 100644 --- /dev/null +++ b/test/archive/use/cross @@ -0,0 +1,2 @@ +sense disallowed +buildRequired True diff --git a/test/archive/use/desktop b/test/archive/use/desktop new file mode 100644 diff --git a/test/archive/use/dietlibc b/test/archive/use/dietlibc new file mode 100644 diff --git a/test/archive/use/emacs b/test/archive/use/emacs new file mode 100644 diff --git a/test/archive/use/gcj b/test/archive/use/gcj new file mode 100644 diff --git a/test/archive/use/gnome b/test/archive/use/gnome new file mode 100644 diff --git a/test/archive/use/gtk b/test/archive/use/gtk new file mode 100644 diff --git a/test/archive/use/ipv6 b/test/archive/use/ipv6 new file mode 100644 diff --git a/test/archive/use/kde b/test/archive/use/kde new file mode 100644 diff --git a/test/archive/use/krb b/test/archive/use/krb new file mode 100644 diff --git a/test/archive/use/ldap b/test/archive/use/ldap new file mode 100644 diff --git a/test/archive/use/nptl b/test/archive/use/nptl new file mode 100644 diff --git a/test/archive/use/pam b/test/archive/use/pam new file mode 100644 --- /dev/null +++ b/test/archive/use/pam @@ -0,0 +1,1 @@ +sense required diff --git a/test/archive/use/pcre b/test/archive/use/pcre new file mode 100644 diff --git a/test/archive/use/perl b/test/archive/use/perl new file mode 100644 diff --git a/test/archive/use/python b/test/archive/use/python new file mode 100644 diff --git a/test/archive/use/qt b/test/archive/use/qt new file mode 100644 diff --git a/test/archive/use/readline b/test/archive/use/readline new file mode 100644 diff --git a/test/archive/use/sasl b/test/archive/use/sasl new file mode 100644 diff --git a/test/archive/use/ssl b/test/archive/use/ssl new file mode 100644 --- /dev/null +++ b/test/archive/use/ssl @@ -0,0 +1,3 @@ +name ssl +sense required +buildRequired True diff --git a/test/archive/use/tcl b/test/archive/use/tcl new file mode 100644 diff --git a/test/archive/use/tk b/test/archive/use/tk new file mode 100644 diff --git a/test/functionaltest/Makefile b/test/functionaltest/Makefile new file mode 100644 --- /dev/null +++ b/test/functionaltest/Makefile @@ -0,0 +1,13 @@ +MYDIR=$(shell basename `pwd`) + +test: + cd .. && ./testsuite.py $(MYDIR) + +debug: + cd .. && ./testsuite.py $(MYDIR) --debug + +coverage: + rm -rf annotate; + cd .. && ./testsuite.py $(MYDIR) --coverage + mv ../annotate . + diff --git a/test/functionaltest/__init__.py b/test/functionaltest/__init__.py new file mode 100644 diff --git a/test/mock.py b/test/mock.py new file mode 100644 --- /dev/null +++ b/test/mock.py @@ -0,0 +1,361 @@ +#!/usr/bin/python2.4 +# -*- mode: python -*- +# +# Copyright (c) 2006-2007 rPath, Inc. All Rights Reserved. +# This program is distributed under the terms of the Common Public License, +# version 1.0. A copy of this license should have been distributed with this +# source file in a file called LICENSE. If it is not present, the license +# is always available at http://www.rpath.com/permanent/licenses/CPL-1.0. +# +# This program is distributed in the hope that it will be useful, but +# without any warranty; without even the implied warranty of merchantability +# or fitness for a particular purpose. See the Common Public License for +# full details. +# +""" +Mock object implementation. + +This mock object implementation is meant to be very forgiving - it returns a +new child Mock Object for every attribute accessed, and a mock object is +returned from every method call. + +It is the tester's job to enabled the calls that they are interested in +testing, all calls where the return value of the call and side effects are not +recorded (logging, for example) are likely to succeed w/o effort. + +If you wish to call the actual implementation of a function on a MockObject, +you have to enable it using enableMethod. If you wish to use an actual variable setting, you need to set it. + +All enabling/checking methods for a MockObject are done through the _mock attribute. Example: + +class Foo(object): + def __init__(self): + # NOTE: this initialization is not called by default with the mock + # object. + self.one = 'a' + self.two = 'b' + + def method(self, param): + # this method is enabled by calling _mock.enableMethod + param.bar('print some data') + self.printMe('some other data', self.one) + return self.two + + def printMe(self, otherParam): + # this method is not enabled and so is stubbed out in the MockInstance. + print otherParam + +def test(): + m = MockInstance(Foo) + m._mock.set(two=123) + m._mock.enableMethod('method') + param = MockObject() + rv = m.method(param) + assert(rv == 123) #m.two is returned + # note that param.bar is created on the fly as it is accessed, and + # stores how it was called. + assert(param.bar._mock.assertCalled('print some data') + # m.one and m.printMe were created on the fly as well + # m.printMe remembers how it was called. + m.printMe._mock.assertCalled('some other data', m.one) + # attribute values are generated on the fly but are retained between + # accesses. + assert(m.foo is m.foo) + +TODO: set the return values for particular function calls w/ particular +parameters. +""" +import new + +_mocked = [] + +class MockObject(object): + """ + Base mock object. + + Creates attributes on the fly, affect attribute values by using + the _mock attribute, which is a MockManager. + + Initial attributes can be assigned by key/value pairs passed in. + """ + + def __init__(self, **kw): + stableReturnValues = kw.pop('stableReturnValues', False) + self._mock = MockManager(self, stableReturnValues=stableReturnValues) + self.__dict__.update(kw) + self._mock._dict = {} + + def __getattribute__(self, key): + if key == '_mock' or self._mock.enabled(key): + return object.__getattribute__(self, key) + if key in self.__dict__: + return self.__dict__[key] + m = self._mock.getCalled(key) + self.__dict__[key] = m + return m + + def __setattr__(self, key, value): + if key == '_mock' or self._mock.enabled(key): + object.__setattr__(self, key, value) + else: + m = self._mock.setCalled(key, value) + if not hasattr(self, key): + object.__setattr__(self, key, m) + + def __setitem__(self, key, value): + m = self._mock.setItemCalled(key, value) + self._mock._dict[key] = m + + def __len__(self): + return self._mock.length + + def __deepcopy__(self, memo): + return self + + def __iter__(self): + for i in range(len(self)): + yield self[i] + + def __getitem__(self, key): + if key in self._mock._dict: + return self._mock._dict[key] + else: + m = self._mock.getItemCalled(key) + self._mock._dict[key] = m + return m + + def __hasattr__(self, key): + if key == '_mock' or self._mock.enabled(key): + return object.__hasattr__(self, key) + return True + + def __call__(self, *args, **kw): + return self._mock.called(args, kw) + +class MockManager(object): + noReturnValue = object() + + def __init__(self, obj, stableReturnValues=False): + self._enabledByDefault = False + self._enabled = set(['__dict__', '__methods__', '__class__', + '__members__', '__deepcopy__']) + self._disabled = set([]) + self._errorToRaise = None + self.calls = [] + self.callReturns = [] + self.getCalls = [] + self.setCalls = [] + self.getItemCalls = [] + self.setItemCalls = [] + self.hasCalls = [] + self.eqCalls = [] + self.obj = obj + self.superClass = object + self.length = 1 + self.stableReturnValues = stableReturnValues + self.returnValue = self.noReturnValue + + def enableByDefault(self): + self._enabledByDefault = True + + def disableByDefault(self): + self._enabledByDefault = False + + def setDefaultReturn(self, returnValue): + self.returnValue = returnValue + + def setReturn(self, returnValue, *args, **kw): + self.callReturns.append((args, tuple(sorted(kw.items())), returnValue)) + + def enableMethod(self, name): + """ + Enables a method to be called from the given superclass. + + The function underlying the method is slurped up and assigned to + this class. + """ + self.enable(name) + func = getattr(self.superClass, name).im_func + method = new.instancemethod(func, self.obj, self.obj.__class__) + object.__setattr__(self.obj, name, method) + + def enable(self, *names): + self._enabled.update(names) + self._disabled.difference_update(names) + + def disable(self, *names): + self._enabled.difference_update(names) + self._disabled.update(names) + for name in names: + object.__setattr__(self.obj, name, MockObject()) + + def enabled(self, name): + if self._enabledByDefault: + return name not in self._disabled + else: + return name in self._enabled + + def set(self, **kw): + for key, value in kw.iteritems(): + self._enabled.add(key) + setattr(self.obj, key, value) + + def raiseErrorOnAccess(self, error): + self._errorToRaise = error + + def assertCalled(self, *args, **kw): + kw = tuple(sorted(kw.items())) + assert((args, kw) in self.calls) + self.calls.remove((args, kw)) + + def assertNotCalled(self, *args, **kw): + assert(not self.calls) + + def setCalled(self, key, value): + if self._errorToRaise: + self._raiseError() + m = MockObject() + self.setCalls.append((key, value, m)) + return m + + def setItemCalled(self, key, value): + if self._errorToRaise: + self._raiseError() + m = MockObject() + self.setItemCalls.append((key, value, m)) + return m + + + def _raiseError(self): + err = self._errorToRaise + self._errorToRaise = None + raise err + + def getCalled(self, key): + if self._errorToRaise: + self._raiseError() + m = MockObject(stableReturnValues=self.stableReturnValues) + self.getCalls.append((key, m)) + return m + + def getItemCalled(self, key): + if self._errorToRaise: + self._raiseError() + m = MockObject(stableReturnValues=self.stableReturnValues) + self.getItemCalls.append((key, m)) + return m + + + def called(self, args, kw): + kw = tuple(sorted(kw.items())) + self.calls.append((args, kw)) + if self._errorToRaise: + self._raiseError() + else: + rv = [x[2] for x in self.callReturns if (x[0], x[1]) == (args, kw)] + if rv: + return rv[-1] + rv = [x[2] for x in self.callReturns + if not x[0] and x[1] == (('_mockAll', True),)] + if rv: + return rv[-1] + else: + if self.returnValue is not self.noReturnValue: + return self.returnValue + if self.stableReturnValues: + self.returnValue = MockObject(stableReturnValues=True) + return self.returnValue + return MockObject() + + def getCalls(self): + return self.calls + + def popCall(self): + call = self.calls[0] + self.calls = self.calls[1:] + return call + +class MockInstance(MockObject): + + def __init__(self, superClass, **kw): + MockObject.__init__(self, **kw) + self._mock.superClass = superClass + +def attach(obj): + if hasattr(obj, '__setattr__'): + oldsetattr = obj.__setattr__ + if hasattr(obj, '__getattribute__'): + oldgetattr = obj.__getattribute__ + + def __setattr__(self, key, value): + if not isinstance(getattr(self, key), mock.MockObject()): + oldsetattr(key, value) + + def __getattribute__(self, key): + if not hasattr(self, key): + oldsetattr(key, mock.MockObject()) + return oldgetattr(key) + oldsetattr('__setattr__', new.instancemethod(__setattr__, obj, + obj.__class__)) + oldsetattr('__getattribute__', new.instancemethod(__getattribute__, obj, obj.__class__)) + +def mockMethod(method): + self = method.im_self + name = method.__name__ + origMethod = getattr(self, name) + setattr(self, name, MockObject()) + getattr(self, name)._mock.method = origMethod + getattr(self, name)._mock.origValue = origMethod + _mocked.append((self, name)) + return getattr(self, name) + +def mock(obj, attr): + m = MockObject() + if hasattr(obj, attr): + m._mock.origValue = getattr(obj, attr) + setattr(obj, attr, m) + _mocked.append((obj, attr)) + +def unmockAll(): + for obj, attr in _mocked: + if not hasattr(getattr(obj, attr), '_mock'): + continue + setattr(obj, attr, getattr(obj, attr)._mock.origValue) + _mocked[:] = [] + +def mockClass(class_, *args, **kw): + commands = [] + runInit = kw.pop('mock_runInit', False) + for k, v in kw.items(): + if k.startswith('mock_'): + if not isinstance(v, (list, tuple)): + v = [v] + commands.append((k[5:], v)) + kw.pop(k) + class _MockClass(MockInstance, class_): + def __init__(self, *a, **k): + MockInstance.__init__(self, class_, *args, **kw) + if runInit: + self._mock.enableByDefault() + class_.__init__(self, *a, **k) + self._mock.called(a, k) + for command, params in commands: + getattr(self._mock, command)(*params) + + return _MockClass + +def mockFunctionOnce(obj, attr, returnValue): + newFn = lambda *args, **kw: returnValue + return replaceFunctionOnce(obj, attr, newFn) + +def replaceFunctionOnce(obj, attr, newFn): + curValue = getattr(obj, attr) + def restore(): + setattr(obj, attr, curValue) + + def fun(*args, **kw): + restore() + return newFn(*args, **kw) + setattr(obj, attr, fun) + fun.func_name = attr + fun.restore = restore diff --git a/test/slehelp.py b/test/slehelp.py new file mode 100644 --- /dev/null +++ b/test/slehelp.py @@ -0,0 +1,33 @@ +# +# Copyright (c) 2008 rPath, Inc. +# +# This program is distributed under the terms of the Common Public License, +# version 1.0. A copy of this license should have been distributed with this +# source file in a file called LICENSE. If it is not present, the license +# is always available at http://www.rpath.com/permanent/licenses/CPL-1.0. +# +# This program is distributed in the hope that it will be useful, but +# without any warranty; without even the implied warranty of merchantability +# or fitness for a particular purpose. See the Common Public License for +# full details. +# + +import testsuite +testsuite.setup() + +import rmakehelp + +from updateBot import config + +class Helper(rmakehelp.RmakeHelper): + def setUp(self): + rmakehelp.RmakeHelper.setUp(self) + self.updateBotCfg = config.UpdateBotConfig(False) + self.cfg.user = ('test', 'test') + self.writeFile(self.cfg.root + '/conaryrc', '') + + def initializeFlavor(self): + pass + + def prepopulateKeyCache(self, keyCache): + pass diff --git a/test/smoketest/Makefile b/test/smoketest/Makefile new file mode 100644 --- /dev/null +++ b/test/smoketest/Makefile @@ -0,0 +1,13 @@ +MYDIR=$(shell basename `pwd`) + +test: + cd .. && ./testsuite.py $(MYDIR) + +debug: + cd .. && ./testsuite.py $(MYDIR) --debug + +coverage: + rm -rf annotate; + cd .. && ./testsuite.py $(MYDIR) --coverage + mv ../annotate . + diff --git a/test/smoketest/__init__.py b/test/smoketest/__init__.py new file mode 100644 diff --git a/test/testsetup.py b/test/testsetup.py new file mode 100644 --- /dev/null +++ b/test/testsetup.py @@ -0,0 +1,20 @@ +import os +import sys +dirlevel = 0; +curDir = os.path.dirname(__file__) +testsuitePath = os.path.realpath(curDir + '/..' * dirlevel) +while (not os.path.exists(testsuitePath + '/testsuite.py') and dirlevel < 10): + dirlevel+=1 + testsuitePath = os.path.realpath(curDir + '/..' * dirlevel) + +if dirlevel == 10: + raise RuntimeError('Could not find testsuite.py!') +if not testsuitePath in sys.path: + sys.path.insert(0, testsuitePath) + +import testsuite +testsuite.setup() + +def main(): + if sys._getframe(1).f_globals['__name__'] == '__main__': + testsuite.main() diff --git a/test/testsuite.py b/test/testsuite.py new file mode 100755 --- /dev/null +++ b/test/testsuite.py @@ -0,0 +1,185 @@ +#!/usr/bin/python +# -*- mode: python -*- +# +# Copyright (c) 2006-2007 rPath, Inc. All Rights Reserved. +# +# This program is distributed under the terms of the Common Public License, +# version 1.0. A copy of this license should have been distributed with this +# source file in a file called LICENSE. If it is not present, the license +# is always available at http://www.rpath.com/permanent/licenses/CPL-1.0. +# +# This program is distributed in the hope that it will be useful, but +# without any warranty; without even the implied warranty of merchantability +# or fitness for a particular purpose. See the Common Public License for +# full details. +# + +import sys +import os +import pwd + +archivePath = None +testPath = None +pluginPath = None +nodePath = None + +conaryDir = None +_setupPath = None +_individual = False + +def isIndividual(): + global _individual + return _individual + +def setup(): + global _setupPath + if _setupPath: + return _setupPath + global testPath + global archivePath + global pluginPath + global nodePath + global rmakePath + + # set default SLEESTACK_PATH, if it was not set. + parDir = '/'.join(os.path.realpath(__file__).split('/')[:-1]) + parDir = os.path.dirname(parDir) + mirrorballPath = os.getenv('SLEESTACK_PATH', parDir) + os.environ['SLEESTACK_PATH'] = mirrorballPath + + def setPathFromEnv(variable, directory): + parDir = '/'.join(os.path.realpath(__file__).split('/')[:-2]) + parDir = os.path.dirname(parDir) + '/' + directory + thisPath = os.getenv(variable, parDir) + os.environ[variable] = thisPath + if thisPath not in sys.path: + sys.path.insert(0, thisPath) + return thisPath + + # set default CONARY_PATH, if it was not set. + conaryPath = setPathFromEnv('CONARY_PATH', 'conary') + + # set default CONARY_TEST_PATH, if it was not set. + conaryTestPath = setPathFromEnv('CONARY_TEST_PATH', 'conary-test') + + # set default RMAKE_PATH, if it was not set. + rmakePath = setPathFromEnv('RMAKE_PATH', 'rmake') + + # set default RMAKE_TEST_PATH, if it was not set. + rmakeTestPath = setPathFromEnv('RMAKE_TEST_PATH', 'rmake-private/test') + + testDir = os.path.dirname(os.path.realpath(__file__)) + + # Insert the following paths into the python path and sys path in + # listed order. + paths = (mirrorballPath, rmakePath, testDir, conaryPath, conaryTestPath, + rmakeTestPath) + pythonPath = os.environ.get('PYTHONPATH', "") + for p in reversed(paths): + if p in sys.path: + sys.path.remove(p) + sys.path.insert(0, p) + for p in paths: + if p not in pythonPath: + pythonPath = os.pathsep.join((pythonPath, p)) + os.environ['PYTHONPATH'] = pythonPath + + if isIndividual(): + serverDir = '/tmp/conary-server' + if os.path.exists(serverDir) and not os.path.access(serverDir, os.W_OK): + serverDir = serverDir + '-' + pwd.getpwuid(os.getuid())[0] + os.environ['SERVER_FILE_PATH'] = serverDir + + invokedAs = sys.argv[0] + if invokedAs.find("/") != -1: + if invokedAs[0] != "/": + invokedAs = os.getcwd() + "/" + invokedAs + path = os.path.dirname(invokedAs) + else: + path = os.getcwd() + + import testhelp + from conary_test import resources + testPath = testhelp.getTestPath() + archivePath = testPath + '/' + "archive" + resources.archivePath = archivePath + + pluginPath = os.path.realpath(testPath + '/../../rmake-private/rmake_plugins') + nodePath = os.path.realpath(pluginPath + '/..') + if nodePath not in sys.path: + sys.path.insert(0, nodePath) + + global conaryDir + conaryDir = os.environ['CONARY_PATH'] + + from conary.lib import util + sys.excepthook = util.genExcepthook(True) + + # import tools normally expected in testsuite. + from testhelp import context, TestCase, findPorts, SkipTestException + sys.modules[__name__].context = context + sys.modules[__name__].TestCase = TestCase + sys.modules[__name__].findPorts = findPorts + sys.modules[__name__].SkipTestException = SkipTestException + + _setupPath = testPath + return testPath + +_individual = False + +def isIndividual(): + global _individual + return _individual + +def getCoverageDirs(handler, environ): + basePath = os.environ['SLEESTACK_PATH'] + coverageDirs = [ 'updateBot', 'rpmimport', 'repomd', ] + + coveragePath = [] + for path in coverageDirs: + covaregePath.append(os.path.normpath(os.path.join(basePath, path))) + + return coveragePath + +def getCoverageExclusions(self, environ): + return ['test/.*'] + +def sortTests(tests): + order = {'smoketest': 0, + 'unit_test' :1, + 'functionaltest':2} + maxNum = len(order) + tests = [ (test,test.index('test')) for test in tests] + tests = sorted((order.get(test[:index+4], maxNum), test) + for (test, index) in tests) + tests = [ x[1] for x in tests ] + return tests + +def main(argv=None, individual=True): + import testhelp + handlerClass = testhelp.getHandlerClass(testhelp.ConaryTestSuite, + getCoverageDirs, + getCoverageExclusions, + sortTests) + global _individual + _individual = individual + if argv is None: + argv = list(sys.argv) + topdir = testhelp.getTestPath() + cwd = os.getcwd() + if cwd != topdir and cwd not in sys.path: + sys.path.insert(0, cwd) + + handler = handlerClass(individual=individual, topdir=topdir, + testPath=testPath, conaryDir=conaryDir) + results = handler.main(argv) + if results is None: + sys.exit(0) + sys.exit(not results.wasSuccessful()) + +if __name__ == '__main__': + setup() + from conary.lib import util + from conary.lib import coveragehook + sys.excepthook = util.genExcepthook(True) + main(sys.argv, individual=False) diff --git a/test/unit_test/Makefile b/test/unit_test/Makefile new file mode 100644 --- /dev/null +++ b/test/unit_test/Makefile @@ -0,0 +1,13 @@ +MYDIR=$(shell basename `pwd`) + +test: + cd .. && ./testsuite.py $(MYDIR) + +debug: + cd .. && ./testsuite.py $(MYDIR) --debug + +coverage: + rm -rf annotate; + cd .. && ./testsuite.py $(MYDIR) --coverage + mv ../annotate . + diff --git a/test/unit_test/__init__.py b/test/unit_test/__init__.py new file mode 100644 From johnsonm at rpath.com Wed Aug 19 17:38:55 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:38:55 +0000 Subject: mirrorball: branch merge Message-ID: <200908192139.n7JLctpD013970@scc.eng.rpath.com> changeset: 3f3ccc0601d3 user: Matt Wilson <https://issues.rpath.com/> date: Wed, 28 May 2008 19:05:26 -0400 branch merge committer: Matt Wilson <https://issues.rpath.com/> From johnsonm at rpath.com Wed Aug 19 17:40:52 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:40:52 +0000 Subject: mirrorball: branch merge Message-ID: <200908192140.n7JLeqVG018753@scc.eng.rpath.com> changeset: c693a611a02e user: Jeff Uphoff <https://issues.rpath.com/> date: Tue, 23 Sep 2008 16:35:52 -0400 branch merge committer: Jeff Uphoff <https://issues.rpath.com/> From johnsonm at rpath.com Wed Aug 19 17:40:53 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:40:53 +0000 Subject: mirrorball: branch merge Message-ID: <200908192140.n7JLerTG018804@scc.eng.rpath.com> changeset: 3ce442ff97ac user: Jeff Uphoff <https://issues.rpath.com/> date: Wed, 24 Sep 2008 13:21:56 -0400 branch merge committer: Jeff Uphoff <https://issues.rpath.com/> From johnsonm at rpath.com Wed Aug 19 17:40:35 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:40:35 +0000 Subject: mirrorball: branch merge Message-ID: <200908192140.n7JLeZxl018039@scc.eng.rpath.com> changeset: d5af50aa7296 user: Elliot Peele <https://issues.rpath.com/> date: Tue, 02 Sep 2008 23:13:37 -0400 branch merge committer: Elliot Peele <https://issues.rpath.com/> From johnsonm at rpath.com Wed Aug 19 17:40:33 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:40:33 +0000 Subject: mirrorball: branch merge Message-ID: <200908192140.n7JLeXES017971@scc.eng.rpath.com> changeset: f486d9a96ba2 user: Elliot Peele <https://issues.rpath.com/> date: Thu, 28 Aug 2008 19:08:40 -0400 branch merge committer: Elliot Peele <https://issues.rpath.com/> From johnsonm at rpath.com Wed Aug 19 17:40:37 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:40:37 +0000 Subject: mirrorball: branch merge Message-ID: <200908192140.n7JLebQ1018124@scc.eng.rpath.com> changeset: effdb12654aa user: Elliot Peele <https://issues.rpath.com/> date: Fri, 05 Sep 2008 11:44:22 -0400 branch merge committer: Elliot Peele <https://issues.rpath.com/> From johnsonm at rpath.com Wed Aug 19 17:40:41 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:40:41 +0000 Subject: mirrorball: branch merge Message-ID: <200908192140.n7JLegxR018328@scc.eng.rpath.com> changeset: 8c7a15eda13b user: Elliot Peele <https://issues.rpath.com/> date: Mon, 08 Sep 2008 22:06:34 -0400 branch merge committer: Elliot Peele <https://issues.rpath.com/> From johnsonm at rpath.com Wed Aug 19 17:40:44 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:40:44 +0000 Subject: mirrorball: branch merge Message-ID: <200908192140.n7JLeilh018447@scc.eng.rpath.com> changeset: 5c714570a46c user: Elliot Peele <https://issues.rpath.com/> date: Wed, 10 Sep 2008 20:17:06 -0400 branch merge committer: Elliot Peele <https://issues.rpath.com/> From johnsonm at rpath.com Wed Aug 19 17:40:46 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:40:46 +0000 Subject: mirrorball: branch merge Message-ID: <200908192140.n7JLekvh018515@scc.eng.rpath.com> changeset: f6951a0e5501 user: Elliot Peele <https://issues.rpath.com/> date: Thu, 11 Sep 2008 15:52:44 -0400 branch merge committer: Elliot Peele <https://issues.rpath.com/> From johnsonm at rpath.com Wed Aug 19 17:40:51 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:40:51 +0000 Subject: mirrorball: branch merge Message-ID: <200908192140.n7JLepAU018719@scc.eng.rpath.com> changeset: 8edd4d7a4603 user: Elliot Peele <https://issues.rpath.com/> date: Tue, 23 Sep 2008 16:31:26 -0400 branch merge committer: Elliot Peele <https://issues.rpath.com/> From johnsonm at rpath.com Wed Aug 19 17:41:13 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:41:13 +0000 Subject: mirrorball: branch merge Message-ID: <200908192141.n7JLfD1I019573@scc.eng.rpath.com> changeset: e6e2db8bc3f5 user: Elliot Peele <https://issues.rpath.com/> date: Thu, 30 Oct 2008 13:10:05 -0400 branch merge committer: Elliot Peele <https://issues.rpath.com/> From johnsonm at rpath.com Wed Aug 19 17:42:34 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:42:34 +0000 Subject: mirrorball: branch merge Message-ID: <200908192142.n7JLgYZB022830@scc.eng.rpath.com> changeset: 9f3228960939 user: Elliot Peele <https://issues.rpath.com/> date: Mon, 16 Mar 2009 11:26:59 -0400 branch merge committer: Elliot Peele <https://issues.rpath.com/> From johnsonm at rpath.com Wed Aug 19 17:42:50 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:42:50 +0000 Subject: mirrorball: branch merge Message-ID: <200908192142.n7JLgoqR023494@scc.eng.rpath.com> changeset: f47d4f09d3c5 user: Elliot Peele <https://issues.rpath.com/> date: Fri, 01 May 2009 13:16:07 -0400 branch merge committer: Elliot Peele <https://issues.rpath.com/> From johnsonm at rpath.com Wed Aug 19 17:43:07 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:43:07 +0000 Subject: mirrorball: branch merge Message-ID: <200908192143.n7JLh7UL024192@scc.eng.rpath.com> changeset: 3d722e13c18c user: Elliot Peele <https://issues.rpath.com/> date: Mon, 27 Jul 2009 15:36:01 -0400 branch merge committer: Elliot Peele <https://issues.rpath.com/> From johnsonm at rpath.com Wed Aug 19 17:42:39 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:42:39 +0000 Subject: mirrorball: set a rate limit Message-ID: <200908192142.n7JLgduS023034@scc.eng.rpath.com> changeset: fe9baa287dab user: Matt Wilson <https://issues.rpath.com/> date: Tue, 31 Mar 2009 14:17:37 -0400 set a rate limit committer: Matt Wilson <https://issues.rpath.com/> From johnsonm at rpath.com Wed Aug 19 17:40:08 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:40:08 +0000 Subject: mirrorball: fix perms Message-ID: <200908192140.n7JLe961016966@scc.eng.rpath.com> changeset: f9eb73a3f6b1 user: Elliot Peele <http://bugs.rpath.com/> date: Wed, 13 Aug 2008 14:23:50 -0400 fix perms committer: Elliot Peele <http://bugs.rpath.com/> diff --git a/scripts/sync-ubuntu.sh b/scripts/sync-ubuntu.sh old mode 100644 new mode 100755 From johnsonm at rpath.com Wed Aug 19 17:41:03 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:41:03 +0000 Subject: mirrorball: make excutable Message-ID: <200908192141.n7JLf3Dd019196@scc.eng.rpath.com> changeset: 6d9d60f123a2 user: Elliot Peele <https://issues.rpath.com/> date: Wed, 22 Oct 2008 15:55:24 -0400 make excutable committer: Elliot Peele <https://issues.rpath.com/> diff --git a/scripts/findbinaries.py b/scripts/findbinaries.py old mode 100644 new mode 100755 From johnsonm at rpath.com Wed Aug 19 17:40:49 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:40:49 +0000 Subject: mirrorball: add init Message-ID: <200908192140.n7JLen2A018651@scc.eng.rpath.com> changeset: f9402bc0d6bf user: Elliot Peele <https://issues.rpath.com/> date: Mon, 15 Sep 2008 16:11:07 -0400 add init committer: Elliot Peele <https://issues.rpath.com/> diff --git a/test/unit_test/aptmdtest/__init__.py b/test/unit_test/aptmdtest/__init__.py new file mode 100644 From johnsonm at rpath.com Wed Aug 19 17:39:04 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:39:04 +0000 Subject: mirrorball: add missing __init__.py file Message-ID: <200908192139.n7JLd4Bc014377@scc.eng.rpath.com> changeset: 733b04669420 user: Elliot Peele <http://issues.rpath.com/> date: Mon, 02 Jun 2008 21:27:32 -0400 add missing __init__.py file committer: Elliot Peele <http://issues.rpath.com/> diff --git a/test/unit_test/updatebottest/__init__.py b/test/unit_test/updatebottest/__init__.py new file mode 100644 From johnsonm at rpath.com Wed Aug 19 17:39:54 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:39:54 +0000 Subject: mirrorball: this doesn't need to be executable Message-ID: <200908192139.n7JLdsYS016368@scc.eng.rpath.com> changeset: 9c08fced6801 user: Elliot Peele <http://issues.rpath.com/> date: Thu, 10 Jul 2008 10:31:34 -0400 this doesn't need to be executable committer: Elliot Peele <http://issues.rpath.com/> diff --git a/updatebot/rpmsource.py b/updatebot/rpmsource.py old mode 100755 new mode 100644 From johnsonm at rpath.com Wed Aug 19 17:38:53 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:38:53 +0000 Subject: mirrorball: add m4 Message-ID: <200908192139.n7JLcrMW013909@scc.eng.rpath.com> changeset: e482df1665b1 user: Matt Wilson <https://issues.rpath.com/> date: Wed, 28 May 2008 14:17:38 -0400 add m4 committer: Matt Wilson <https://issues.rpath.com/> diff --git a/pkgwiz.py b/pkgwiz.py --- a/pkgwiz.py +++ b/pkgwiz.py @@ -125,6 +125,7 @@ 'logrotate', 'lvm2', 'lzo', + 'm4', 'make', 'mdadm', 'mingetty', From johnsonm at rpath.com Wed Aug 19 17:41:42 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:41:42 +0000 Subject: mirrorball: remove syntax error Message-ID: <200908192141.n7JLfgGx020753@scc.eng.rpath.com> changeset: 6419a4c9d369 user: Elliot Peele <https://issues.rpath.com/> date: Wed, 19 Nov 2008 21:14:32 -0500 remove syntax error committer: Elliot Peele <https://issues.rpath.com/> diff --git a/pmap/ubuntu.py b/pmap/ubuntu.py --- a/pmap/ubuntu.py +++ b/pmap/ubuntu.py @@ -27,5 +27,3 @@ self._states.update({ }) - - def From johnsonm at rpath.com Wed Aug 19 17:41:11 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:41:11 +0000 Subject: mirrorball: fix typo Message-ID: <200908192141.n7JLfBxX019488@scc.eng.rpath.com> changeset: 2e6b128b5e81 user: Elliot Peele <https://issues.rpath.com/> date: Wed, 29 Oct 2008 16:40:45 -0400 fix typo committer: Elliot Peele <https://issues.rpath.com/> diff --git a/scripts/buildgroups b/scripts/buildgroups --- a/scripts/buildgroups +++ b/scripts/buildgroups @@ -16,4 +16,4 @@ print "built:\n" -display(groupTrvMap) +display(grpTrvMap) From johnsonm at rpath.com Wed Aug 19 17:38:59 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:38:59 +0000 Subject: mirrorball: branch merge Message-ID: <200908192139.n7JLcxwB014159@scc.eng.rpath.com> changeset: 594e2b887f9a user: Elliot Peele <http://issues.rpath.com/> date: Fri, 30 May 2008 15:51:38 -0400 branch merge committer: Elliot Peele <http://issues.rpath.com/> diff --git a/pkgwiz.py b/pkgwiz.py --- a/pkgwiz.py +++ b/pkgwiz.py @@ -135,6 +135,7 @@ 'libxml2-python', 'libxslt', 'logrotate', + 'lsof', 'lvm2', 'lzo', 'm4', From johnsonm at rpath.com Wed Aug 19 17:42:00 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:42:00 +0000 Subject: mirrorball: branch merge Message-ID: <200908192142.n7JLg0uT021468@scc.eng.rpath.com> changeset: 20934770f9dc user: Elliot Peele <https://issues.rpath.com/> date: Tue, 16 Dec 2008 18:10:17 -0500 branch merge committer: Elliot Peele <https://issues.rpath.com/> diff --git a/scripts/mirror.py b/scripts/mirror.py --- a/scripts/mirror.py +++ b/scripts/mirror.py @@ -1,4 +1,4 @@ -#!/usr/bin/python2.6 +#!/usr/bin/python # # Copyright (c) 2008 rPath, Inc. # From johnsonm at rpath.com Wed Aug 19 17:42:00 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:42:00 +0000 Subject: mirrorball: branch merge Message-ID: <200908192142.n7JLg0J7021485@scc.eng.rpath.com> changeset: 666c2003c8a5 user: Elliot Peele <https://issues.rpath.com/> date: Tue, 16 Dec 2008 18:43:00 -0500 branch merge committer: Elliot Peele <https://issues.rpath.com/> diff --git a/scripts/mirror.py b/scripts/mirror.py --- a/scripts/mirror.py +++ b/scripts/mirror.py @@ -1,4 +1,4 @@ -#!/usr/bin/python2.6 +#!/usr/bin/python # # Copyright (c) 2008 rPath, Inc. # From johnsonm at rpath.com Wed Aug 19 17:42:01 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:42:01 +0000 Subject: mirrorball: branch merge Message-ID: <200908192142.n7JLg1n1021502@scc.eng.rpath.com> changeset: 6a7d2b029f1b user: Elliot Peele <https://issues.rpath.com/> date: Tue, 16 Dec 2008 18:45:26 -0500 branch merge committer: Elliot Peele <https://issues.rpath.com/> diff --git a/scripts/mirror.py b/scripts/mirror.py --- a/scripts/mirror.py +++ b/scripts/mirror.py @@ -1,4 +1,4 @@ -#!/usr/bin/python2.6 +#!/usr/bin/python # # Copyright (c) 2008 rPath, Inc. # From johnsonm at rpath.com Wed Aug 19 17:42:02 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:42:02 +0000 Subject: mirrorball: branch merge Message-ID: <200908192142.n7JLg27l021570@scc.eng.rpath.com> changeset: d9c2c5d09f9b user: Elliot Peele <https://issues.rpath.com/> date: Mon, 12 Jan 2009 16:57:04 -0500 branch merge committer: Elliot Peele <https://issues.rpath.com/> diff --git a/scripts/mirror.py b/scripts/mirror.py --- a/scripts/mirror.py +++ b/scripts/mirror.py @@ -1,4 +1,4 @@ -#!/usr/bin/python2.6 +#!/usr/bin/python # # Copyright (c) 2008 rPath, Inc. # From johnsonm at rpath.com Wed Aug 19 17:38:58 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:38:58 +0000 Subject: mirrorball: add lsof (SLE-93) Message-ID: <200908192139.n7JLcwV0014119@scc.eng.rpath.com> changeset: e9c24a0eb964 user: Matt Wilson <https://issues.rpath.com/> date: Thu, 29 May 2008 10:09:42 -0400 add lsof (SLE-93) committer: Matt Wilson <https://issues.rpath.com/> diff --git a/pkgwiz.py b/pkgwiz.py --- a/pkgwiz.py +++ b/pkgwiz.py @@ -123,6 +123,7 @@ 'libxml2-python', 'libxslt', 'logrotate', + 'lsof', 'lvm2', 'lzo', 'm4', From johnsonm at rpath.com Wed Aug 19 17:41:20 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:41:20 +0000 Subject: mirrorball: revert 2.6 commit Message-ID: <200908192141.n7JLfKIX019916@scc.eng.rpath.com> changeset: 94dda597a250 user: Elliot Peele <https://issues.rpath.com/> date: Fri, 14 Nov 2008 16:32:37 -0500 revert 2.6 commit committer: Elliot Peele <https://issues.rpath.com/> diff --git a/scripts/pkgsource.py b/scripts/pkgsource.py --- a/scripts/pkgsource.py +++ b/scripts/pkgsource.py @@ -1,4 +1,4 @@ -#!/usr/bin/python2.6 +#!/usr/bin/python import os import sys From johnsonm at rpath.com Wed Aug 19 17:42:23 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:42:23 +0000 Subject: mirrorball: build script now exits on error Message-ID: <200908192142.n7JLgN8Z022387@scc.eng.rpath.com> changeset: 1e5bb324a9b2 user: Elliot Peele <https://issues.rpath.com/> date: Mon, 09 Feb 2009 10:51:41 -0500 build script now exits on error committer: Elliot Peele <https://issues.rpath.com/> diff --git a/scripts/build b/scripts/build --- a/scripts/build +++ b/scripts/build @@ -1,4 +1,4 @@ -#!/bin/bash +#!/bin/bash -e # # Copyright (c) 2009 rPath, Inc. # From johnsonm at rpath.com Wed Aug 19 17:39:26 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:39:26 +0000 Subject: mirrorball: ignore pylint reports in status messages Message-ID: <200908192139.n7JLdQtH015328@scc.eng.rpath.com> changeset: 5fd973eae61f user: Elliot Peele <http://issues.rpath.com/> date: Tue, 17 Jun 2008 11:01:41 -0400 ignore pylint reports in status messages committer: Elliot Peele <http://issues.rpath.com/> diff --git a/.hgignore b/.hgignore --- a/.hgignore +++ b/.hgignore @@ -7,3 +7,4 @@ .*\/testsetup\.py$ .*\/\.times$ .*\/\.coverage\/.* +^pylint\/reports From johnsonm at rpath.com Wed Aug 19 17:39:27 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:39:27 +0000 Subject: mirrorball: import rmtree Message-ID: <200908192139.n7JLdRZv015345@scc.eng.rpath.com> changeset: 568408ad3828 user: Elliot Peele <http://issues.rpath.com/> date: Tue, 17 Jun 2008 11:29:34 -0400 import rmtree committer: Elliot Peele <http://issues.rpath.com/> diff --git a/updatebot/util.py b/updatebot/util.py --- a/updatebot/util.py +++ b/updatebot/util.py @@ -17,6 +17,7 @@ """ import os +from conary.lib.util import rmtree from rpmvercmp import rpmvercmp From johnsonm at rpath.com Wed Aug 19 17:42:00 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:42:00 +0000 Subject: mirrorball: don't use python2.6 Message-ID: <200908192142.n7JLg0bx021451@scc.eng.rpath.com> changeset: dec2b3a9d29e user: Elliot Peele <https://issues.rpath.com/> date: Mon, 15 Dec 2008 14:34:54 -0500 don't use python2.6 committer: Elliot Peele <https://issues.rpath.com/> diff --git a/scripts/mirror.py b/scripts/mirror.py --- a/scripts/mirror.py +++ b/scripts/mirror.py @@ -1,4 +1,4 @@ -#!/usr/bin/python2.6 +#!/usr/bin/python # # Copyright (c) 2008 rPath, Inc. # From johnsonm at rpath.com Wed Aug 19 17:38:54 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:38:54 +0000 Subject: mirrorball: add sendmail (SLE-90) Message-ID: <200908192139.n7JLctuZ013946@scc.eng.rpath.com> changeset: 30ff6d5e8a66 user: Matt Wilson <https://issues.rpath.com/> date: Wed, 28 May 2008 19:05:11 -0400 add sendmail (SLE-90) committer: Matt Wilson <https://issues.rpath.com/> diff --git a/pkgwiz.py b/pkgwiz.py --- a/pkgwiz.py +++ b/pkgwiz.py @@ -173,6 +173,7 @@ 'resmgr', 'samba', 'sed', + 'sendmail', 'slang', 'sles-release', 'strace', From johnsonm at rpath.com Wed Aug 19 17:41:10 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:41:10 +0000 Subject: mirrorball: cleanup after bad merge Message-ID: <200908192141.n7JLfAX9019471@scc.eng.rpath.com> changeset: 0065b7441510 user: Elliot Peele <https://issues.rpath.com/> date: Wed, 29 Oct 2008 16:05:33 -0400 cleanup after bad merge committer: Elliot Peele <https://issues.rpath.com/> diff --git a/scripts/buildgroups b/scripts/buildgroups --- a/scripts/buildgroups +++ b/scripts/buildgroups @@ -16,4 +16,4 @@ print "built:\n" -def displayTrove(nvf): +display(groupTrvMap) From johnsonm at rpath.com Wed Aug 19 17:39:25 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:39:25 +0000 Subject: mirrorball: add more files to ignore Message-ID: <200908192139.n7JLdP4f015276@scc.eng.rpath.com> changeset: aa6f020f7462 user: Elliot Peele <http://issues.rpath.com/> date: Mon, 16 Jun 2008 18:23:04 -0400 add more files to ignore committer: Elliot Peele <http://issues.rpath.com/> diff --git a/.hgignore b/.hgignore --- a/.hgignore +++ b/.hgignore @@ -1,3 +1,9 @@ (^|/)\.hg($|/) .*\.pyc$ .*\~$ +.*\,cover$ +.*\.swp$ +.*\.orig$ +.*\/testsetup\.py$ +.*\/\.times$ +.*\/\.coverage\/.* From johnsonm at rpath.com Wed Aug 19 17:40:21 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:40:21 +0000 Subject: mirrorball: remove epdb Message-ID: <200908192140.n7JLeLqJ017477@scc.eng.rpath.com> changeset: 7089cbcc94dc user: Elliot Peele <http://bugs.rpath.com/> date: Mon, 18 Aug 2008 22:03:47 -0400 remove epdb committer: Elliot Peele <http://bugs.rpath.com/> diff --git a/scripts/update.py b/scripts/update.py --- a/scripts/update.py +++ b/scripts/update.py @@ -17,5 +17,3 @@ cfg.read(os.environ['HOME'] + '/hg/mirrorball/config/sles/updatebotrc') obj = bot.Bot(cfg) obj.update() - -import epdb ; epdb.st() From johnsonm at rpath.com Wed Aug 19 17:42:40 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:42:40 +0000 Subject: mirrorball: branch merge Message-ID: <200908192142.n7JLge9l023068@scc.eng.rpath.com> changeset: f9aaff972e1f user: Elliot Peele <https://issues.rpath.com/> date: Tue, 31 Mar 2009 14:21:20 -0400 branch merge committer: Elliot Peele <https://issues.rpath.com/> diff --git a/scripts/build b/scripts/build --- a/scripts/build +++ b/scripts/build @@ -21,5 +21,5 @@ ./buildpackages $platform $pkgs ./buildgroups $platform -./promote.py $platform -./mirror.py $platform +./promote $platform +./mirror $platform From johnsonm at rpath.com Wed Aug 19 17:41:18 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:41:18 +0000 Subject: mirrorball: add missing import Message-ID: <200908192141.n7JLfIpc019812@scc.eng.rpath.com> changeset: add34f58002b user: Elliot Peele <https://issues.rpath.com/> date: Fri, 07 Nov 2008 13:25:15 -0500 add missing import committer: Elliot Peele <https://issues.rpath.com/> diff --git a/scripts/auto_update b/scripts/auto_update --- a/scripts/auto_update +++ b/scripts/auto_update @@ -13,6 +13,7 @@ # full details. # +import os from updatebot import bot, config def validatePlatform(platform, configDir): From johnsonm at rpath.com Wed Aug 19 17:42:38 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:42:38 +0000 Subject: mirrorball: fix script names Message-ID: <200908192142.n7JLgckc023017@scc.eng.rpath.com> changeset: 4aef7e72f84d user: Elliot Peele <https://issues.rpath.com/> date: Thu, 26 Mar 2009 12:07:29 -0400 fix script names committer: Elliot Peele <https://issues.rpath.com/> diff --git a/scripts/build b/scripts/build --- a/scripts/build +++ b/scripts/build @@ -21,5 +21,5 @@ ./buildpackages $platform $pkgs ./buildgroups $platform -./promote.py $platform -./mirror.py $platform +./promote $platform +./mirror $platform From johnsonm at rpath.com Wed Aug 19 17:42:10 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:42:10 +0000 Subject: mirrorball: fix typo Message-ID: <200908192142.n7JLgAF5021860@scc.eng.rpath.com> changeset: 3a7112c7bb89 user: Elliot Peele <https://issues.rpath.com/> date: Fri, 23 Jan 2009 14:15:09 -0500 fix typo committer: Elliot Peele <https://issues.rpath.com/> diff --git a/scripts/pcheck.py b/scripts/pcheck.py --- a/scripts/pcheck.py +++ b/scripts/pcheck.py @@ -98,4 +98,4 @@ # Commit changeset. if okay: - client.repos.commitChangeSet(cs, callback=callback) + client.repos.commitChangeSet(cs, callback=cb) From johnsonm at rpath.com Wed Aug 19 17:42:42 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:42:42 +0000 Subject: mirrorball: branch merge Message-ID: <200908192142.n7JLgg6g023136@scc.eng.rpath.com> changeset: 1f06d7f32d01 user: Elliot Peele <https://issues.rpath.com/> date: Thu, 09 Apr 2009 13:56:44 -0400 branch merge committer: Elliot Peele <https://issues.rpath.com/> diff --git a/scripts/build b/scripts/build --- a/scripts/build +++ b/scripts/build @@ -21,5 +21,5 @@ ./buildpackages $platform $pkgs ./buildgroups $platform -./promote.py $platform -./mirror.py $platform +./promote $platform +./mirror $platform From johnsonm at rpath.com Wed Aug 19 17:42:23 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:42:23 +0000 Subject: mirrorball: default to no full trove sync Message-ID: <200908192142.n7JLgNBb022404@scc.eng.rpath.com> changeset: eb8d44d02495 user: Elliot Peele <https://issues.rpath.com/> date: Mon, 09 Feb 2009 10:51:58 -0500 default to no full trove sync committer: Elliot Peele <https://issues.rpath.com/> diff --git a/scripts/mirror.py b/scripts/mirror.py --- a/scripts/mirror.py +++ b/scripts/mirror.py @@ -17,4 +17,4 @@ from updatebot import conaryhelper helper = conaryhelper.ConaryHelper(cfg) -helper.mirror() +helper.mirror(fullTroveSync=False) From johnsonm at rpath.com Wed Aug 19 17:40:16 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:40:16 +0000 Subject: mirrorball: disable too few public methods warning Message-ID: <200908192140.n7JLeG0q017239@scc.eng.rpath.com> changeset: 2faa9e1eaff9 user: Elliot Peele <http://issues.rpath.com/> date: Thu, 14 Aug 2008 18:15:42 -0400 disable too few public methods warning committer: Elliot Peele <http://issues.rpath.com/> diff --git a/pylint/pylintrc b/pylint/pylintrc --- a/pylint/pylintrc +++ b/pylint/pylintrc @@ -30,4 +30,4 @@ include-ids=yes [MESSAGES CONTROL] -disable-msg=W0102,W0142,I0011 +disable-msg=W0102,W0142,I0011,R0903 From johnsonm at rpath.com Wed Aug 19 17:40:19 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:40:19 +0000 Subject: mirrorball: make jobs bigger Message-ID: <200908192140.n7JLeJhB017392@scc.eng.rpath.com> changeset: 9a9b4e6eca3d user: Elliot Peele <http://issues.rpath.com/> date: Fri, 15 Aug 2008 18:44:17 -0400 make jobs bigger committer: Elliot Peele <http://issues.rpath.com/> diff --git a/updatebot/build.py b/updatebot/build.py --- a/updatebot/build.py +++ b/updatebot/build.py @@ -94,7 +94,7 @@ jobs[id].append(trv) - if i % 10 == 0: + if i % 20 == 0: id += 1 failed = set() From johnsonm at rpath.com Wed Aug 19 17:39:38 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:39:38 +0000 Subject: mirrorball: fix argument list error Message-ID: <200908192139.n7JLdcM6015772@scc.eng.rpath.com> changeset: d4f518ef20f3 user: Elliot Peele <http://issues.rpath.com/> date: Tue, 24 Jun 2008 14:57:10 -0400 fix argument list error committer: Elliot Peele <http://issues.rpath.com/> diff --git a/updatebot/errors.py b/updatebot/errors.py --- a/updatebot/errors.py +++ b/updatebot/errors.py @@ -141,6 +141,7 @@ in the config. """ + _params = [] _template = 'Product name not defined' class NoSenderFoundError(AdvisoryError): From johnsonm at rpath.com Wed Aug 19 17:42:08 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:42:08 +0000 Subject: mirrorball: skip buildinfio Message-ID: <200908192142.n7JLg8Op021775@scc.eng.rpath.com> changeset: c10c1188b206 user: Elliot Peele <https://issues.rpath.com/> date: Tue, 20 Jan 2009 02:35:04 +0000 skip buildinfio committer: Elliot Peele <https://issues.rpath.com/> diff --git a/scripts/pcheck.py b/scripts/pcheck.py --- a/scripts/pcheck.py +++ b/scripts/pcheck.py @@ -34,7 +34,8 @@ labelMap, trvs, cloneSources=True, - callback=cb) + callback=cb, + updateBuildInfo=False) import epdb epdb.st() From johnsonm at rpath.com Wed Aug 19 17:40:30 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:40:30 +0000 Subject: mirrorball: add stub Message-ID: <200908192140.n7JLeUXl017852@scc.eng.rpath.com> changeset: 1066f3576041 user: Elliot Peele <http://issues.rpath.com/> date: Thu, 28 Aug 2008 10:27:53 -0400 add stub committer: Elliot Peele <http://issues.rpath.com/> diff --git a/updatebot/cmdline/command.py b/updatebot/cmdline/command.py --- a/updatebot/cmdline/command.py +++ b/updatebot/cmdline/command.py @@ -56,4 +56,5 @@ d["verbose"] = NO_PARAM argDef[self.defaultGroup] = d - + def processConfigOptions(self, cfg, cfgMap, argSet): + pass From johnsonm at rpath.com Wed Aug 19 17:40:35 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:40:35 +0000 Subject: mirrorball: make sure _commands is a global Message-ID: <200908192140.n7JLeZsx018073@scc.eng.rpath.com> changeset: f6494e95171a user: Elliot Peele <https://issues.rpath.com/> date: Wed, 03 Sep 2008 00:58:19 -0400 make sure _commands is a global committer: Elliot Peele <https://issues.rpath.com/> diff --git a/updatebot/cmdline/command.py b/updatebot/cmdline/command.py --- a/updatebot/cmdline/command.py +++ b/updatebot/cmdline/command.py @@ -23,6 +23,7 @@ _commands = [] def register(cmd): + global _commands _commands.append(cmd) From johnsonm at rpath.com Wed Aug 19 17:39:14 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:39:14 +0000 Subject: mirrorball: add option for telling the bot what packages to ignore Message-ID: <200908192139.n7JLdEnr014785@scc.eng.rpath.com> changeset: 810b5bcb0546 user: Elliot Peele <http://issues.rpath.com/> date: Tue, 10 Jun 2008 16:29:39 -0400 add option for telling the bot what packages to ignore committer: Elliot Peele <http://issues.rpath.com/> diff --git a/updatebot/config.py b/updatebot/config.py --- a/updatebot/config.py +++ b/updatebot/config.py @@ -37,3 +37,4 @@ topGroup = CfgTroveSpec + excludePackages = CfgList(CfgString) From johnsonm at rpath.com Wed Aug 19 17:42:54 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:42:54 +0000 Subject: mirrorball: recreate all packages if none are specified Message-ID: <200908192142.n7JLgsEE023630@scc.eng.rpath.com> changeset: 1b181d72903f user: Elliot Peele <https://issues.rpath.com/> date: Tue, 05 May 2009 14:00:59 -0400 recreate all packages if none are specified committer: Elliot Peele <https://issues.rpath.com/> diff --git a/scripts/recreate b/scripts/recreate --- a/scripts/recreate +++ b/scripts/recreate @@ -31,4 +31,8 @@ pkgs = sys.argv[2:] +# Recreate everything if no packages are specified. +if not pkgs: + pkgs = True + obj.create(recreate=pkgs) From johnsonm at rpath.com Wed Aug 19 17:38:49 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:38:49 +0000 Subject: mirrorball: add common variable exceptions Message-ID: <200908192138.n7JLcoUf013746@scc.eng.rpath.com> changeset: 312766965c76 user: Elliot Peele <http://issues.rpath.com/> date: Wed, 28 May 2008 00:35:12 -0400 add common variable exceptions committer: Elliot Peele <http://issues.rpath.com/> diff --git a/pylint/pylintrc b/pylint/pylintrc --- a/pylint/pylintrc +++ b/pylint/pylintrc @@ -21,7 +21,7 @@ attr-rgx=[a-z_][a-zA-Z0-9_]{2,30}$ # Good variable names which should always be accepted, separated by a comma -good-names=XmlObj,Elem +good-names=log,_ [REPORTS] From johnsonm at rpath.com Wed Aug 19 17:42:58 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:42:58 +0000 Subject: mirrorball: fix typo Message-ID: <200908192142.n7JLgwEI023800@scc.eng.rpath.com> changeset: 982da6c553d3 user: Elliot Peele <https://issues.rpath.com/> date: Tue, 12 May 2009 10:19:43 -0400 fix typo committer: Elliot Peele <https://issues.rpath.com/> diff --git a/scripts/header.py b/scripts/header.py --- a/scripts/header.py +++ b/scripts/header.py @@ -16,7 +16,7 @@ import sys mirrorballDir = os.environ['HOME'] + '/hg/mirrorball' -sys.path.insert(0, sleestckDir) +sys.path.insert(0, mirrorballDir) from conary.lib import util sys.excepthook = util.genExcepthook() From johnsonm at rpath.com Wed Aug 19 17:39:41 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:39:41 +0000 Subject: mirrorball: fix double slash return Message-ID: <200908192139.n7JLdf8e015875@scc.eng.rpath.com> changeset: 578889cc1637 user: Elliot Peele <http://issues.rpath.com/> date: Tue, 24 Jun 2008 17:29:35 -0400 fix double slash return committer: Elliot Peele <http://issues.rpath.com/> diff --git a/updatebot/util.py b/updatebot/util.py --- a/updatebot/util.py +++ b/updatebot/util.py @@ -34,7 +34,7 @@ root = '' for path in b: root += os.sep + os.path.normpath(path) - return root + return os.path.abspath(root) def srpmToConaryVersion(srcPkg): """ From johnsonm at rpath.com Wed Aug 19 17:40:28 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:40:28 +0000 Subject: mirrorball: return the root logger in case it is needed Message-ID: <200908192140.n7JLeSIQ017784@scc.eng.rpath.com> changeset: 66b71b9e19d3 user: Elliot Peele <http://issues.rpath.com/> date: Mon, 25 Aug 2008 21:44:46 -0400 return the root logger in case it is needed committer: Elliot Peele <http://issues.rpath.com/> diff --git a/updatebot/log.py b/updatebot/log.py --- a/updatebot/log.py +++ b/updatebot/log.py @@ -37,3 +37,5 @@ conaryLog = logging.getLogger('conary') for handler in conaryLog.handlers: conaryLog.removeHandler(handler) + + return rootLog From johnsonm at rpath.com Wed Aug 19 17:39:10 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:39:10 +0000 Subject: mirrorball: remove infomaker import Message-ID: <200908192139.n7JLdAO0014629@scc.eng.rpath.com> changeset: dfa98b1beaae user: Elliot Peele <http://issues.rpath.com/> date: Thu, 05 Jun 2008 16:27:55 -0400 remove infomaker import committer: Elliot Peele <http://issues.rpath.com/> diff --git a/pkgwiz.py b/pkgwiz.py --- a/pkgwiz.py +++ b/pkgwiz.py @@ -18,7 +18,7 @@ import conary.lib.util import os import tempfile -from rpmimport import infomaker, recipemaker, rpmsource, rpmhelper +from rpmimport import recipemaker, rpmsource, rpmhelper import rpmvercmp import shutil import sys From johnsonm at rpath.com Wed Aug 19 17:40:06 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:40:06 +0000 Subject: mirrorball: update sles Message-ID: <200908192140.n7JLe6g5016864@scc.eng.rpath.com> changeset: 2bd3f799e464 user: Elliot Peele <http://bugs.rpath.com/> date: Mon, 11 Aug 2008 16:45:54 -0400 update sles committer: Elliot Peele <http://bugs.rpath.com/> diff --git a/scripts/update.py b/scripts/update.py --- a/scripts/update.py +++ b/scripts/update.py @@ -14,7 +14,7 @@ log.addRootLogger() cfg = config.UpdateBotConfig() -cfg.read(os.environ['HOME'] + '/hg/mirrorball/config/updatebotrc') +cfg.read(os.environ['HOME'] + '/hg/mirrorball/config/sles/updatebotrc') obj = bot.Bot(cfg) obj.update() From johnsonm at rpath.com Wed Aug 19 17:39:17 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:39:17 +0000 Subject: mirrorball: import exceptions Message-ID: <200908192139.n7JLdHL1014927@scc.eng.rpath.com> changeset: a66a552d6f8f user: Elliot Peele <http://issues.rpath.com/> date: Wed, 11 Jun 2008 15:23:48 -0400 import exceptions committer: Elliot Peele <http://issues.rpath.com/> diff --git a/updatebot/conaryhelper.py b/updatebot/conaryhelper.py --- a/updatebot/conaryhelper.py +++ b/updatebot/conaryhelper.py @@ -23,6 +23,7 @@ from conary import conaryclient, conarycfg, trove from updatebot import util +from updatebot.errors import TooManyFlavorsFoundError log = logging.getLogger('updatebot.conaryhelper') From johnsonm at rpath.com Wed Aug 19 17:42:05 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:42:05 +0000 Subject: mirrorball: ids must be strs to join them Message-ID: <200908192142.n7JLg5WA021656@scc.eng.rpath.com> changeset: cc41ae5ea31d user: Elliot Peele <https://issues.rpath.com/> date: Thu, 15 Jan 2009 10:39:03 -0500 ids must be strs to join them committer: Elliot Peele <https://issues.rpath.com/> diff --git a/updatebot/build.py b/updatebot/build.py --- a/updatebot/build.py +++ b/updatebot/build.py @@ -322,7 +322,7 @@ else: jobIds = jobId - jobIdsStr = ','.join(jobIds) + jobIdsStr = ','.join(map(str, jobIds)) # Do the commit startTime = time.time() From johnsonm at rpath.com Wed Aug 19 17:43:02 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:43:02 +0000 Subject: mirrorball: add advisory loading Message-ID: <200908192143.n7JLh2En023953@scc.eng.rpath.com> changeset: 46bf945083b1 user: Elliot Peele <https://issues.rpath.com/> date: Mon, 08 Jun 2009 22:07:35 -0400 add advisory loading committer: Elliot Peele <https://issues.rpath.com/> diff --git a/scripts/pkgsource b/scripts/pkgsource --- a/scripts/pkgsource +++ b/scripts/pkgsource @@ -37,3 +37,10 @@ pkgSource.load() import epdb; epdb.st() + +updates = [] +for path, client in pkgSource._clients.iteritems(): + if 'Updates' in path: + updates.extend(client.getUpdateInfo()) + +import epdb; epdb.st() From johnsonm at rpath.com Wed Aug 19 17:40:16 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:40:16 +0000 Subject: mirrorball: arch is amd64 on ubuntu Message-ID: <200908192140.n7JLeHp9017273@scc.eng.rpath.com> changeset: cf38dcbecbbd user: Elliot Peele <http://issues.rpath.com/> date: Thu, 14 Aug 2008 21:03:26 -0400 arch is amd64 on ubuntu committer: Elliot Peele <http://issues.rpath.com/> diff --git a/aptmd/common.py b/aptmd/common.py --- a/aptmd/common.py +++ b/aptmd/common.py @@ -66,7 +66,7 @@ def _architecture(self): arch = self._getLine() - assert arch in ('all', 'i386', 'x86_64') + assert arch in ('all', 'i386', 'amd64') self._curObj.arch = arch def _version(self): From johnsonm at rpath.com Wed Aug 19 17:42:21 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:42:21 +0000 Subject: mirrorball: fix typo Message-ID: <200908192142.n7JLgLMC022302@scc.eng.rpath.com> changeset: 791f92045b75 user: Elliot Peele <https://issues.rpath.com/> date: Sun, 08 Feb 2009 05:10:55 +0000 fix typo committer: Elliot Peele <https://issues.rpath.com/> diff --git a/updatebot/build.py b/updatebot/build.py --- a/updatebot/build.py +++ b/updatebot/build.py @@ -441,7 +441,7 @@ self.type = type def __str__(self): - msg = '$(name)s: %(trv)s [%(jobId)s] - ' + msg = '%(name)s: %(trv)s [%(jobId)s] - ' if self.type == MESSAGE_TYPES['results']: msg += 'done' else: From johnsonm at rpath.com Wed Aug 19 17:40:17 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:40:17 +0000 Subject: mirrorball: update logger name Message-ID: <200908192140.n7JLeHuP017290@scc.eng.rpath.com> changeset: e0e3e95a9bd2 user: Elliot Peele <http://issues.rpath.com/> date: Thu, 14 Aug 2008 21:04:43 -0400 update logger name committer: Elliot Peele <http://issues.rpath.com/> diff --git a/updatebot/pkgsource/rpmsource.py b/updatebot/pkgsource/rpmsource.py --- a/updatebot/pkgsource/rpmsource.py +++ b/updatebot/pkgsource/rpmsource.py @@ -22,7 +22,7 @@ import repomd from updatebot import util -log = logging.getLogger('rpmimport.rpmsource') +log = logging.getLogger('updatebot.pkgsource') class RpmSource(object): """ From johnsonm at rpath.com Wed Aug 19 17:41:11 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:41:11 +0000 Subject: mirrorball: branch merge Message-ID: <200908192141.n7JLfBr4019505@scc.eng.rpath.com> changeset: 82764c75438c user: Elliot Peele <https://issues.rpath.com/> date: Wed, 29 Oct 2008 16:43:27 -0400 branch merge committer: Elliot Peele <https://issues.rpath.com/> diff --git a/scripts/header.py b/scripts/header.py --- a/scripts/header.py +++ b/scripts/header.py @@ -32,7 +32,7 @@ log.addRootLogger() cfg = config.UpdateBotConfig() -cfg.read(os.environ['HOME'] + '/hg/mirrorball/config/ubuntu/updatebotrc') +cfg.read(os.environ['HOME'] + '/hg/mirrorball/config/%s/updatebotrc' % sys.argv[1]) builder = build.Builder(cfg) From johnsonm at rpath.com Wed Aug 19 17:41:34 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:41:34 +0000 Subject: mirrorball: remove extra print Message-ID: <200908192141.n7JLfYmG020429@scc.eng.rpath.com> changeset: d075ddacc0e0 user: Elliot Peele <https://issues.rpath.com/> date: Tue, 18 Nov 2008 12:18:38 -0500 remove extra print committer: Elliot Peele <https://issues.rpath.com/> diff --git a/aptmd/container.py b/aptmd/container.py --- a/aptmd/container.py +++ b/aptmd/container.py @@ -19,7 +19,6 @@ for cls in self.__class__.__mro__: if hasattr(cls, '__slots__'): for item in cls.__slots__: - print item setattr(self, item, None) self._data = {} From johnsonm at rpath.com Wed Aug 19 17:40:28 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:40:28 +0000 Subject: mirrorball: add compare function for sorting by package name Message-ID: <200908192140.n7JLeSIJ017750@scc.eng.rpath.com> changeset: ff2e9e40bc80 user: Elliot Peele <http://bugs.rpath.com/> date: Mon, 25 Aug 2008 13:02:29 -0400 add compare function for sorting by package name committer: Elliot Peele <http://bugs.rpath.com/> diff --git a/updatebot/util.py b/updatebot/util.py --- a/updatebot/util.py +++ b/updatebot/util.py @@ -82,3 +82,10 @@ return archcmp return 0 + +def packageCompareByName(a, b): + nameCmp = cmp(a.name, b.name) + if nameCmp != 0: + return nameCmp + + return packagevercmp(a, b) From johnsonm at rpath.com Wed Aug 19 17:39:48 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:39:48 +0000 Subject: mirrorball: add new module rpmutils Message-ID: <200908192139.n7JLdmkG016130@scc.eng.rpath.com> changeset: e1d9014cad31 user: Elliot Peele <http://issues.rpath.com/> date: Wed, 09 Jul 2008 12:00:37 -0400 add new module rpmutils committer: Elliot Peele <http://issues.rpath.com/> diff --git a/pylint/run_pylint b/pylint/run_pylint --- a/pylint/run_pylint +++ b/pylint/run_pylint @@ -35,7 +35,7 @@ done if [ -z "$files" ] ; then - files="updatebot repomd rpmimport" + files="updatebot repomd rpmutils" fi pylint --init-hook='import sys; sys.path.append("."); import init_pylint' --rcfile='../pylintrc' $pylintArgs $files From johnsonm at rpath.com Wed Aug 19 17:41:15 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:41:15 +0000 Subject: mirrorball: add pmap module Message-ID: <200908192141.n7JLfFFW019658@scc.eng.rpath.com> changeset: f8d8d8a6bd0a user: Elliot Peele <https://issues.rpath.com/> date: Thu, 06 Nov 2008 20:35:35 -0500 add pmap module committer: Elliot Peele <https://issues.rpath.com/> diff --git a/pylint/run_pylint b/pylint/run_pylint --- a/pylint/run_pylint +++ b/pylint/run_pylint @@ -35,7 +35,7 @@ done if [ -z "$files" ] ; then - files="updatebot repomd rpmutils aptmd" + files="updatebot repomd rpmutils aptmd pmap" fi pylint --init-hook='import sys; sys.path.append("."); import init_pylint' --rcfile='../pylintrc' $pylintArgs $files From johnsonm at rpath.com Wed Aug 19 17:41:06 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:41:06 +0000 Subject: mirrorball: build centos groups Message-ID: <200908192141.n7JLf6Qi019299@scc.eng.rpath.com> changeset: 9db3cbaf5af1 user: Elliot Peele <https://issues.rpath.com/> date: Wed, 29 Oct 2008 16:02:56 -0400 build centos groups committer: Elliot Peele <https://issues.rpath.com/> diff --git a/scripts/buildgroups b/scripts/buildgroups --- a/scripts/buildgroups +++ b/scripts/buildgroups @@ -20,7 +20,7 @@ log.addRootLogger() cfg = config.UpdateBotConfig() -cfg.read(os.environ['HOME'] + '/hg/mirrorball/config/ubuntu/updatebotrc') +cfg.read(os.environ['HOME'] + '/hg/mirrorball/config/centos/updatebotrc') builder = build.Builder(cfg) From johnsonm at rpath.com Wed Aug 19 17:40:14 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:40:14 +0000 Subject: mirrorball: add aptmd to pylint runs Message-ID: <200908192140.n7JLeEqR017188@scc.eng.rpath.com> changeset: f0eb787f03a6 user: Elliot Peele <http://issues.rpath.com/> date: Thu, 14 Aug 2008 18:11:06 -0400 add aptmd to pylint runs committer: Elliot Peele <http://issues.rpath.com/> diff --git a/pylint/run_pylint b/pylint/run_pylint --- a/pylint/run_pylint +++ b/pylint/run_pylint @@ -35,7 +35,7 @@ done if [ -z "$files" ] ; then - files="updatebot repomd rpmutils" + files="updatebot repomd rpmutils aptmd" fi pylint --init-hook='import sys; sys.path.append("."); import init_pylint' --rcfile='../pylintrc' $pylintArgs $files From johnsonm at rpath.com Wed Aug 19 17:41:04 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:41:04 +0000 Subject: mirrorball: remove breakpoint Message-ID: <200908192141.n7JLf4NB019231@scc.eng.rpath.com> changeset: 6d364874c123 user: Elliot Peele <https://issues.rpath.com/> date: Wed, 22 Oct 2008 15:22:01 -0400 remove breakpoint committer: Elliot Peele <https://issues.rpath.com/> diff --git a/updatebot/bot.py b/updatebot/bot.py --- a/updatebot/bot.py +++ b/updatebot/bot.py @@ -119,7 +119,7 @@ # Build all newly imported packages. trvMap, failed = self._builder.buildmany(toBuild) - import epdb; epdb.st() +# import epdb; epdb.st() ## trvMap = self._builder.build(toBuild) #import epdb; epdb.st() From johnsonm at rpath.com Wed Aug 19 17:41:42 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:41:42 +0000 Subject: mirrorball: fix comment Message-ID: <200908192141.n7JLfgB7020719@scc.eng.rpath.com> changeset: 22884f40f0a5 user: Elliot Peele <https://issues.rpath.com/> date: Wed, 19 Nov 2008 13:41:48 -0500 fix comment committer: Elliot Peele <https://issues.rpath.com/> diff --git a/updatebot/bot.py b/updatebot/bot.py --- a/updatebot/bot.py +++ b/updatebot/bot.py @@ -137,7 +137,7 @@ log.info('no updates available') return - # Populate patch source not that we know that there are updates + # Populate patch source now that we know that there are updates # available. self._advisor.load() From johnsonm at rpath.com Wed Aug 19 17:42:53 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:42:53 +0000 Subject: mirrorball: set threads to daemon mode Message-ID: <200908192142.n7JLgr3j023579@scc.eng.rpath.com> changeset: 9a5834eff44c user: Elliot Peele <https://issues.rpath.com/> date: Mon, 04 May 2009 15:56:34 -0400 set threads to daemon mode committer: Elliot Peele <https://issues.rpath.com/> diff --git a/updatebot/build.py b/updatebot/build.py --- a/updatebot/build.py +++ b/updatebot/build.py @@ -518,6 +518,8 @@ def __init__(self, cfg, toBuild, status, name=None, offset=0): Thread.__init__(self, name=name) + self.setDaemon(True) + self.name = name self.offset = offset self.toBuild = toBuild From johnsonm at rpath.com Wed Aug 19 17:38:57 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:38:57 +0000 Subject: mirrorball: add string representation of packages Message-ID: <200908192139.n7JLcvJZ014061@scc.eng.rpath.com> changeset: f3f5a0f64d81 user: Elliot Peele <http://issues.rpath.com/> date: Fri, 30 May 2008 12:57:05 -0400 add string representation of packages committer: Elliot Peele <http://issues.rpath.com/> diff --git a/repomd/packagexml.py b/repomd/packagexml.py --- a/repomd/packagexml.py +++ b/repomd/packagexml.py @@ -126,6 +126,9 @@ else: raise UnknownElementError(child) + def __str__(self): + return '%(name)s-%(version)s-%(release)s' % self.__dict__ + class _RpmRequires(xmllib.BaseNode): ''' From johnsonm at rpath.com Wed Aug 19 17:39:10 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:39:10 +0000 Subject: mirrorball: pass through base path Message-ID: <200908192139.n7JLdAOB014648@scc.eng.rpath.com> changeset: b884cec984c7 user: Elliot Peele <http://issues.rpath.com/> date: Sun, 08 Jun 2008 16:46:07 -0400 pass through base path committer: Elliot Peele <http://issues.rpath.com/> diff --git a/rpmimport/rpmsource.py b/rpmimport/rpmsource.py --- a/rpmimport/rpmsource.py +++ b/rpmimport/rpmsource.py @@ -107,7 +107,7 @@ """ client = repomd.Client(url + '/' + basePath) - self.loadFromClient(client) + self.loadFromClient(client, basePath=basePath) def loadFromClient(self, client, basePath=''): """ From johnsonm at rpath.com Wed Aug 19 17:40:21 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:40:21 +0000 Subject: mirrorball: remove epdb from update code Message-ID: <200908192140.n7JLeLAn017460@scc.eng.rpath.com> changeset: 5657e64f809a user: Elliot Peele <http://bugs.rpath.com/> date: Mon, 18 Aug 2008 22:03:10 -0400 remove epdb from update code committer: Elliot Peele <http://bugs.rpath.com/> diff --git a/updatebot/bot.py b/updatebot/bot.py --- a/updatebot/bot.py +++ b/updatebot/bot.py @@ -214,8 +214,6 @@ newTroves = self._updater.publish(toPublish, expected, self._cfg.targetLabel) - import epdb; epdb.st() - # Send advisories. self._advisor.send(toAdvise, newTroves) From johnsonm at rpath.com Wed Aug 19 17:42:49 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:42:49 +0000 Subject: mirrorball: sort output Message-ID: <200908192142.n7JLgnVH023442@scc.eng.rpath.com> changeset: e49f81ac77ce user: Elliot Peele <https://issues.rpath.com/> date: Mon, 27 Apr 2009 11:00:53 -0400 sort output committer: Elliot Peele <https://issues.rpath.com/> diff --git a/scripts/import b/scripts/import --- a/scripts/import +++ b/scripts/import @@ -32,6 +32,7 @@ import epdb ; epdb.st() for job in trvMap: - for source in job: - for bin in trvMap[source]: - print '%s=%s[%s]' % bin + for source in sorted(job): + for bin in job[source]: + if ':' not in bin[0]: + print '%s=%s[%s]' % bin From johnsonm at rpath.com Wed Aug 19 17:40:12 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:40:12 +0000 Subject: mirrorball: make sure to reset state on every line Message-ID: <200908192140.n7JLeCnm017086@scc.eng.rpath.com> changeset: e7305911f0d4 user: Elliot Peele <http://issues.rpath.com/> date: Thu, 14 Aug 2008 17:11:19 -0400 make sure to reset state on every line committer: Elliot Peele <http://issues.rpath.com/> diff --git a/aptmd/parser.py b/aptmd/parser.py --- a/aptmd/parser.py +++ b/aptmd/parser.py @@ -25,6 +25,9 @@ } def tokenize(self, line): + self._singleQuotedString = False + self._doubleQuotedString = False + self._list = [''] for char in line: self._cur = char From johnsonm at rpath.com Wed Aug 19 17:38:48 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:38:48 +0000 Subject: mirrorball: disable reporting information for tests that have been disabled Message-ID: <200908192138.n7JLcmvM013698@scc.eng.rpath.com> changeset: dc45669e20e4 user: Elliot Peele <http://issues.rpath.com/> date: Tue, 27 May 2008 19:44:59 -0400 disable reporting information for tests that have been disabled committer: Elliot Peele <http://issues.rpath.com/> diff --git a/pylint/pylintrc b/pylint/pylintrc --- a/pylint/pylintrc +++ b/pylint/pylintrc @@ -24,13 +24,10 @@ good-names=XmlObj,Elem - - [REPORTS] files-output=yes reports=yes include-ids=yes [MESSAGES CONTROL] -disable-msg=W0102,W0142 -#,I0011 +disable-msg=W0102,W0142,I0011 From johnsonm at rpath.com Wed Aug 19 17:40:44 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:40:44 +0000 Subject: mirrorball: branch merge Message-ID: <200908192140.n7JLeisU018430@scc.eng.rpath.com> changeset: 95e5e9462fa2 user: Elliot Peele <https://issues.rpath.com/> date: Wed, 10 Sep 2008 18:37:09 -0400 branch merge committer: Elliot Peele <https://issues.rpath.com/> diff --git a/scripts/pkgsource.py b/scripts/pkgsource.py --- a/scripts/pkgsource.py +++ b/scripts/pkgsource.py @@ -21,7 +21,7 @@ from updatebot import pkgsource cfg = config.UpdateBotConfig() -cfg.read(os.environ['HOME'] + '/hg/mirrorball/config/sles/updatebotrc') +cfg.read(os.environ['HOME'] + '/hg/mirrorball/config/ubuntu/updatebotrc') pkgSource = pkgsource.PackageSource(cfg) From johnsonm at rpath.com Wed Aug 19 17:40:19 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:40:19 +0000 Subject: mirrorball: switch to importing ubuntu Message-ID: <200908192140.n7JLeJow017375@scc.eng.rpath.com> changeset: 3cdeeb7d64f1 user: Elliot Peele <http://issues.rpath.com/> date: Fri, 15 Aug 2008 18:43:45 -0400 switch to importing ubuntu committer: Elliot Peele <http://issues.rpath.com/> diff --git a/scripts/import.py b/scripts/import.py --- a/scripts/import.py +++ b/scripts/import.py @@ -27,7 +27,7 @@ log.addRootLogger() cfg = config.UpdateBotConfig() -cfg.read(os.environ['HOME'] + '/hg/mirrorball/config/opensuse/updatebotrc') +cfg.read(os.environ['HOME'] + '/hg/mirrorball/config/ubuntu/updatebotrc') obj = bot.Bot(cfg) trvMap = obj.create() From johnsonm at rpath.com Wed Aug 19 17:41:26 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:41:26 +0000 Subject: mirrorball: correct comment Message-ID: <200908192141.n7JLfQdB020137@scc.eng.rpath.com> changeset: 30fdf9bb80bc user: Elliot Peele <https://issues.rpath.com/> date: Fri, 14 Nov 2008 19:06:17 -0500 correct comment committer: Elliot Peele <https://issues.rpath.com/> diff --git a/updatebot/advisories/common.py b/updatebot/advisories/common.py --- a/updatebot/advisories/common.py +++ b/updatebot/advisories/common.py @@ -198,7 +198,7 @@ self._cfg = cfg self._pkgSource = pkgSource - # { binPkg: patchObj } + # { binPkg: set([patchObj, ...]) } self._pkgMap = dict() # { patchObj: set([srcPkg, ...] } From johnsonm at rpath.com Wed Aug 19 17:38:57 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:38:57 +0000 Subject: mirrorball: run pylint on rpmimport as well Message-ID: <200908192139.n7JLcvZd014042@scc.eng.rpath.com> changeset: 5703c6f3e06a user: Elliot Peele <http://issues.rpath.com/> date: Fri, 30 May 2008 00:58:42 -0400 run pylint on rpmimport as well committer: Elliot Peele <http://issues.rpath.com/> diff --git a/pylint/run_pylint b/pylint/run_pylint --- a/pylint/run_pylint +++ b/pylint/run_pylint @@ -35,7 +35,7 @@ done if [ -z "$files" ] ; then - files="updatebot repomd " # "rpmimport" + files="updatebot repomd rpmimport" fi pylint --init-hook='import sys; sys.path.append("."); import init_pylint' --rcfile='../pylintrc' $pylintArgs $files From johnsonm at rpath.com Wed Aug 19 17:39:36 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:39:36 +0000 Subject: mirrorball: write out an rmakerc Message-ID: <200908192139.n7JLdam7015721@scc.eng.rpath.com> changeset: 36214ef19a55 user: Elliot Peele <http://issues.rpath.com/> date: Mon, 23 Jun 2008 19:38:14 -0400 write out an rmakerc committer: Elliot Peele <http://issues.rpath.com/> diff --git a/test/slehelp.py b/test/slehelp.py --- a/test/slehelp.py +++ b/test/slehelp.py @@ -27,6 +27,7 @@ self.updateBotCfg.configPath = self.cfg.root self.cfg.user = ('test', 'test') self.writeFile(self.cfg.root + '/conaryrc', '') + self.writeFile(self.cfg.root + '/rmakerc', '') os.chdir(self.workDir) def initializeFlavor(self): From johnsonm at rpath.com Wed Aug 19 17:39:52 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:39:52 +0000 Subject: mirrorball: add script to sync a mirror of opensuse Message-ID: <200908192139.n7JLdq9i016317@scc.eng.rpath.com> changeset: 04e8a5ffdc9d user: Elliot Peele <http://issues.rpath.com/> date: Wed, 09 Jul 2008 16:42:52 -0400 add script to sync a mirror of opensuse committer: Elliot Peele <http://issues.rpath.com/> diff --git a/scripts/sync-opensuse.sh b/scripts/sync-opensuse.sh new file mode 100644 --- /dev/null +++ b/scripts/sync-opensuse.sh @@ -0,0 +1,5 @@ +#!/bin/bash -xe + +rsync -arv --progress --exclude iso --exclude 10.* --exclude ppc --exclude ppc64 rsync://rsync.opensuse.org/opensuse-full /mnt/rpath/linux/ + +./hardlink.py /mnt/rpath/linux/opensuse From johnsonm at rpath.com Wed Aug 19 17:40:55 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:40:55 +0000 Subject: mirrorball: branch merge Message-ID: <200908192140.n7JLetRL018890@scc.eng.rpath.com> changeset: 640cac864ee1 user: Elliot Peele <https://issues.rpath.com/> date: Wed, 24 Sep 2008 18:35:45 -0400 branch merge committer: Elliot Peele <https://issues.rpath.com/> diff --git a/scripts/sync-centos.sh b/scripts/sync-centos.sh --- a/scripts/sync-centos.sh +++ b/scripts/sync-centos.sh @@ -16,6 +16,6 @@ SOURCE=rsync://mirrors.us.kernel.org/CentOS-nodvd DEST=/l/CentOS/ -rsync -arv --progress --exclude 2* --exclude 3* --exclude 4* $SOURCE $DEST +rsync -arv --progress --bwlimit=1024 --exclude 2* --exclude 3* --exclude 4* $SOURCE $DEST ./hardlink.py $DEST From johnsonm at rpath.com Wed Aug 19 17:41:08 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:41:08 +0000 Subject: mirrorball: getState is no longer a staticmethod Message-ID: <200908192141.n7JLf8a3019385@scc.eng.rpath.com> changeset: 3f2ef85d8caa user: Elliot Peele <https://issues.rpath.com/> date: Tue, 28 Oct 2008 15:27:56 -0400 getState is no longer a staticmethod committer: Elliot Peele <https://issues.rpath.com/> diff --git a/aptmd/parser.py b/aptmd/parser.py --- a/aptmd/parser.py +++ b/aptmd/parser.py @@ -118,7 +118,7 @@ def _filter(self, filter, state): self._stateFilters[re.compile(filter)] = state - def _getState(key): + def _getState(self, key): key = key.strip() key = key.lower() if key.endswith(':'): From johnsonm at rpath.com Wed Aug 19 17:39:58 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:39:58 +0000 Subject: mirrorball: more verbose Message-ID: <200908192140.n7JLdwFO016555@scc.eng.rpath.com> changeset: bae199eed1c3 user: Elliot Peele <http://issues.rpath.com/> date: Fri, 25 Jul 2008 15:34:39 -0400 more verbose committer: Elliot Peele <http://issues.rpath.com/> diff --git a/scripts/import.py b/scripts/import.py --- a/scripts/import.py +++ b/scripts/import.py @@ -29,6 +29,10 @@ cfg = config.UpdateBotConfig() cfg.read(os.environ['HOME'] + '/hg/mirrorball/config/opensuse/updatebotrc') obj = bot.Bot(cfg) -obj.create() +trvMap = obj.create() import epdb ; epdb.st() + +for source in trvMap: + for bin in trvMap[source]: + print '%s=%s[%s]' % bin From johnsonm at rpath.com Wed Aug 19 17:42:40 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:42:40 +0000 Subject: mirrorball: turn down bw limit for centos Message-ID: <200908192142.n7JLge10023085@scc.eng.rpath.com> changeset: 903ffa7e8b3d user: Elliot Peele <https://issues.rpath.com/> date: Thu, 02 Apr 2009 16:15:57 -0400 turn down bw limit for centos committer: Elliot Peele <https://issues.rpath.com/> diff --git a/scripts/sync-centos.sh b/scripts/sync-centos.sh --- a/scripts/sync-centos.sh +++ b/scripts/sync-centos.sh @@ -17,6 +17,6 @@ DEST=/l/CentOS/ date -rsync -arv --progress --bwlimit=1024 --exclude 2* --exclude 3* --exclude 4* $SOURCE $DEST +rsync -arv --progress --bwlimit=700 --exclude 2* --exclude 3* --exclude 4* $SOURCE $DEST ./hardlink.py $DEST From johnsonm at rpath.com Wed Aug 19 17:42:45 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:42:45 +0000 Subject: mirrorball: set default value of trvmap Message-ID: <200908192142.n7JLgjXZ023272@scc.eng.rpath.com> changeset: e599da24addf user: Elliot Peele <https://issues.rpath.com/> date: Tue, 21 Apr 2009 13:45:58 -0400 set default value of trvmap committer: Elliot Peele <https://issues.rpath.com/> diff --git a/updatebot/bot.py b/updatebot/bot.py --- a/updatebot/bot.py +++ b/updatebot/bot.py @@ -101,6 +101,7 @@ log.info('failed to create %s packages' % len(fail)) log.info('found %s packages to build' % len(toBuild)) + trvMap = [] if len(toBuild): if not rebuild: # Build all newly imported packages. From johnsonm at rpath.com Wed Aug 19 17:40:54 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:40:54 +0000 Subject: mirrorball: throttle centos Message-ID: <200908192140.n7JLesvm018872@scc.eng.rpath.com> changeset: 343f814cff4d user: Elliot Peele <https://issues.rpath.com/> date: Wed, 24 Sep 2008 17:03:46 -0400 throttle centos committer: Elliot Peele <https://issues.rpath.com/> diff --git a/scripts/sync-centos.sh b/scripts/sync-centos.sh --- a/scripts/sync-centos.sh +++ b/scripts/sync-centos.sh @@ -16,6 +16,6 @@ SOURCE=rsync://mirrors.us.kernel.org/CentOS-nodvd DEST=/l/CentOS/ -rsync -arv --progress --exclude 2* --exclude 3* --exclude 4* $SOURCE $DEST +rsync -arv --progress --bwlimit=1024 --exclude 2* --exclude 3* --exclude 4* $SOURCE $DEST ./hardlink.py $DEST From johnsonm at rpath.com Wed Aug 19 17:41:46 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:41:46 +0000 Subject: mirrorball: fix typo Message-ID: <200908192141.n7JLfk09020906@scc.eng.rpath.com> changeset: b3ed6fd752b6 user: Elliot Peele <https://issues.rpath.com/> date: Thu, 20 Nov 2008 10:20:32 -0500 fix typo committer: Elliot Peele <https://issues.rpath.com/> diff --git a/updatebot/config.py b/updatebot/config.py --- a/updatebot/config.py +++ b/updatebot/config.py @@ -22,7 +22,7 @@ from conary import versions from conary.conarycfg import CfgFlavor, CfgLabel from conary.lib.cfgtypes import CfgString, CfgList, CfgRegExp, CfgBool -from coanry.lib.cfgtypes import ParseError +from conary.lib.cfgtypes import ParseError from rmake.build.buildcfg import CfgTroveSpec From johnsonm at rpath.com Wed Aug 19 17:41:51 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:41:51 +0000 Subject: mirrorball: add back in new lines Message-ID: <200908192141.n7JLfp1o021127@scc.eng.rpath.com> changeset: d519f85eed47 user: Elliot Peele <https://issues.rpath.com/> date: Wed, 17 Dec 2008 18:28:18 -0500 add back in new lines committer: Elliot Peele <https://issues.rpath.com/> diff --git a/aptmd/parser.py b/aptmd/parser.py --- a/aptmd/parser.py +++ b/aptmd/parser.py @@ -135,7 +135,9 @@ func() self._text = '' else: - self._text += ' '.join(self._line) + self._text += '\n' + ' '.join(self._line) + else: + self._text += '\n' def _getState(self, key): """ From johnsonm at rpath.com Wed Aug 19 17:39:11 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:39:11 +0000 Subject: mirrorball: default to debug level Message-ID: <200908192139.n7JLdB9r014682@scc.eng.rpath.com> changeset: 57f0f278303a user: Elliot Peele <http://issues.rpath.com/> date: Tue, 10 Jun 2008 00:29:14 -0400 default to debug level committer: Elliot Peele <http://issues.rpath.com/> diff --git a/updatebot/log.py b/updatebot/log.py --- a/updatebot/log.py +++ b/updatebot/log.py @@ -30,7 +30,7 @@ '%(name)s %(message)s') handler.setFormatter(formatter) rootLog.addHandler(handler) - rootLog.setLevel(logging.INFO) + rootLog.setLevel(logging.DEBUG) # Delete conary's log handler since it puts things on stderr and without # any timestamps. From johnsonm at rpath.com Wed Aug 19 17:39:51 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:39:51 +0000 Subject: mirrorball: add rpmutils, drop rpmimport Message-ID: <200908192139.n7JLdpmD016266@scc.eng.rpath.com> changeset: b035cbde06c7 user: Elliot Peele <http://issues.rpath.com/> date: Wed, 09 Jul 2008 14:29:02 -0400 add rpmutils, drop rpmimport committer: Elliot Peele <http://issues.rpath.com/> diff --git a/test/testsuite.py b/test/testsuite.py --- a/test/testsuite.py +++ b/test/testsuite.py @@ -139,7 +139,7 @@ def getCoverageDirs(handler, environ): basePath = os.environ['SLEESTACK_PATH'] - coverageDirs = [ 'updatebot', 'rpmimport', 'repomd', ] + coverageDirs = [ 'updatebot', 'rpmutils', 'repomd', ] coveragePath = [] for path in coverageDirs: From johnsonm at rpath.com Wed Aug 19 17:39:17 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:39:17 +0000 Subject: mirrorball: add a prefix to tmp dirs Message-ID: <200908192139.n7JLdHsV014910@scc.eng.rpath.com> changeset: e9e864723394 user: Elliot Peele <http://issues.rpath.com/> date: Wed, 11 Jun 2008 11:10:33 -0400 add a prefix to tmp dirs committer: Elliot Peele <http://issues.rpath.com/> diff --git a/rpmimport/recipemaker.py b/rpmimport/recipemaker.py --- a/rpmimport/recipemaker.py +++ b/rpmimport/recipemaker.py @@ -149,7 +149,7 @@ """ cwd = os.getcwd() - tmpdir = tempfile.mkdtemp() + tmpdir = tempfile.mkdtemp(prefix='rpmimport-') os.chdir(tmpdir) self._checkout(pkgname) manifest = open('manifest').readlines() From johnsonm at rpath.com Wed Aug 19 17:41:12 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:41:12 +0000 Subject: mirrorball: branch merge Message-ID: <200908192141.n7JLfC3V019539@scc.eng.rpath.com> changeset: 0cbfa9fc85bd user: Elliot Peele <https://issues.rpath.com/> date: Thu, 30 Oct 2008 12:56:14 -0400 branch merge committer: Elliot Peele <https://issues.rpath.com/> diff --git a/scripts/header.py b/scripts/header.py --- a/scripts/header.py +++ b/scripts/header.py @@ -24,7 +24,7 @@ from updatebot import config def usage(): - print 'usage: buildpackages <platform> [pkg1, pkg2, ...]' + print 'usage: %s <platform> [pkg1, pkg2, ...]' % sys.argv[0] sys.exit(1) if len(sys.argv) < 2 or sys.argv[1] not in os.listdir(os.environ['HOME'] + '/hg/mirrorball/config'): From johnsonm at rpath.com Wed Aug 19 17:42:20 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:42:20 +0000 Subject: mirrorball: except all exceptions Message-ID: <200908192142.n7JLgKtv022251@scc.eng.rpath.com> changeset: 372e0070e71b user: Elliot Peele <https://issues.rpath.com/> date: Sun, 08 Feb 2009 04:14:59 +0000 except all exceptions committer: Elliot Peele <https://issues.rpath.com/> diff --git a/updatebot/build.py b/updatebot/build.py --- a/updatebot/build.py +++ b/updatebot/build.py @@ -503,6 +503,8 @@ job = self.builder._helper.getJob(self.jobId) except xml.parsers.expat.ExpatError, e: return False, None + except Exception, e: + return False, None return True, job def _status(self, msg, type=0): From johnsonm at rpath.com Wed Aug 19 17:40:33 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:40:33 +0000 Subject: mirrorball: build ubuntu groups & build from group Message-ID: <200908192140.n7JLeX8B017954@scc.eng.rpath.com> changeset: b84c8510f2a2 user: Elliot Peele <https://issues.rpath.com/> date: Thu, 28 Aug 2008 19:07:55 -0400 build ubuntu groups & build from group committer: Elliot Peele <https://issues.rpath.com/> diff --git a/scripts/buildgroups b/scripts/buildgroups --- a/scripts/buildgroups +++ b/scripts/buildgroups @@ -20,7 +20,7 @@ log.addRootLogger() cfg = config.UpdateBotConfig() -cfg.read(os.environ['HOME'] + '/hg/mirrorball/config/sles/updatebotrc') +cfg.read(os.environ['HOME'] + '/hg/mirrorball/config/ubuntu/updatebotrc') builder = build.Builder(cfg) From johnsonm at rpath.com Wed Aug 19 17:41:19 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:41:19 +0000 Subject: mirrorball: don't try to build factories Message-ID: <200908192141.n7JLfJuM019863@scc.eng.rpath.com> changeset: 5a943dd7ecac user: Elliot Peele <https://issues.rpath.com/> date: Thu, 13 Nov 2008 18:17:22 -0500 don't try to build factories committer: Elliot Peele <https://issues.rpath.com/> diff --git a/updatebot/update.py b/updatebot/update.py --- a/updatebot/update.py +++ b/updatebot/update.py @@ -88,6 +88,7 @@ # skip special packages if (name.startswith('info-') or name.startswith('group-') or + name.startswith('factory-') or name in self._cfg.excludePackages): continue From johnsonm at rpath.com Wed Aug 19 17:41:37 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:41:37 +0000 Subject: mirrorball: add extraPromoteTroves Message-ID: <200908192141.n7JLfbPA020515@scc.eng.rpath.com> changeset: a16b8a1d5892 user: Elliot Peele <https://issues.rpath.com/> date: Tue, 18 Nov 2008 12:31:00 -0500 add extraPromoteTroves committer: Elliot Peele <https://issues.rpath.com/> diff --git a/updatebot/config.py b/updatebot/config.py --- a/updatebot/config.py +++ b/updatebot/config.py @@ -99,6 +99,9 @@ # Label to promote to targetLabel = CfgBranch + # Packages other than the topGroup that need to be promoted. + extraPromoteTroves = (CfgList(CfgTroveSpec), []) + # Packages to import package = (CfgList(CfgString), []) From johnsonm at rpath.com Wed Aug 19 17:42:53 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:42:53 +0000 Subject: mirrorball: recreate all packages if recreate is not a list Message-ID: <200908192142.n7JLgrlU023613@scc.eng.rpath.com> changeset: a74be4aaa114 user: Elliot Peele <https://issues.rpath.com/> date: Tue, 05 May 2009 13:42:35 -0400 recreate all packages if recreate is not a list committer: Elliot Peele <https://issues.rpath.com/> diff --git a/updatebot/bot.py b/updatebot/bot.py --- a/updatebot/bot.py +++ b/updatebot/bot.py @@ -69,7 +69,7 @@ self._pkgSource.load() # Build list of packages - if recreate: + if type(recreate) == list: toPackage = set(recreate) elif self._cfg.packageAll: toPackage = set() From johnsonm at rpath.com Wed Aug 19 17:42:45 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:42:45 +0000 Subject: mirrorball: update import script output for new buildmany output Message-ID: <200908192142.n7JLgjSV023289@scc.eng.rpath.com> changeset: 71b01e559abe user: Elliot Peele <https://issues.rpath.com/> date: Tue, 21 Apr 2009 14:18:07 -0400 update import script output for new buildmany output committer: Elliot Peele <https://issues.rpath.com/> diff --git a/scripts/import b/scripts/import --- a/scripts/import +++ b/scripts/import @@ -31,6 +31,7 @@ import epdb ; epdb.st() -for source in trvMap: - for bin in trvMap[source]: - print '%s=%s[%s]' % bin +for job in trvMap: + for source in job: + for bin in trvMap[source]: + print '%s=%s[%s]' % bin From johnsonm at rpath.com Wed Aug 19 17:39:11 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:39:11 +0000 Subject: mirrorball: make packages hashable Message-ID: <200908192139.n7JLdBcd014665@scc.eng.rpath.com> changeset: cfbc0fc83648 user: Elliot Peele <http://issues.rpath.com/> date: Mon, 09 Jun 2008 11:44:17 -0400 make packages hashable committer: Elliot Peele <http://issues.rpath.com/> diff --git a/repomd/packagexml.py b/repomd/packagexml.py --- a/repomd/packagexml.py +++ b/repomd/packagexml.py @@ -122,6 +122,10 @@ def __str__(self): return '%(name)s-%(version)s-%(release)s' % self.__dict__ + def __hash__(self): + return hash((self.name, self.epoch, self.version, self.release, + self.arch)) + class _RpmRequires(xmllib.BaseNode): """ From johnsonm at rpath.com Wed Aug 19 17:41:21 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:41:21 +0000 Subject: mirrorball: add platformName option Message-ID: <200908192141.n7JLfLYQ019933@scc.eng.rpath.com> changeset: 1660b2c40ef7 user: Elliot Peele <https://issues.rpath.com/> date: Fri, 14 Nov 2008 16:38:15 -0500 add platformName option committer: Elliot Peele <https://issues.rpath.com/> diff --git a/updatebot/config.py b/updatebot/config.py --- a/updatebot/config.py +++ b/updatebot/config.py @@ -52,6 +52,9 @@ # name of the product to use in advisories productName = CfgString + # platform short name + platformName = CfgString + # path to configuration files relative to updatebotrc (conaryrc, rmakerc) configPath = (CfgString, './') From johnsonm at rpath.com Wed Aug 19 17:43:03 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:43:03 +0000 Subject: mirrorball: compare packages by name as well Message-ID: <200908192143.n7JLh3is024021@scc.eng.rpath.com> changeset: e524fa76c6dd user: Elliot Peele <https://issues.rpath.com/> date: Tue, 09 Jun 2009 15:25:21 -0400 compare packages by name as well committer: Elliot Peele <https://issues.rpath.com/> diff --git a/repomd/packagexml.py b/repomd/packagexml.py --- a/repomd/packagexml.py +++ b/repomd/packagexml.py @@ -39,7 +39,7 @@ return hash((self.name, self.version, self.release, self.arch)) def __cmp__(self, other): - return util.packageCompare(self, other) + return util.packageCompareByName(self, other) class _Package(SlotNode, PackageCompare): From johnsonm at rpath.com Wed Aug 19 17:42:32 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:42:32 +0000 Subject: mirrorball: jobId can be a list of ints, just cast them to a string Message-ID: <200908192142.n7JLgWX6022762@scc.eng.rpath.com> changeset: 747b9efb9bb2 user: Elliot Peele <https://issues.rpath.com/> date: Mon, 16 Mar 2009 11:24:31 -0400 jobId can be a list of ints, just cast them to a string committer: Elliot Peele <https://issues.rpath.com/> diff --git a/updatebot/errors.py b/updatebot/errors.py --- a/updatebot/errors.py +++ b/updatebot/errors.py @@ -47,7 +47,7 @@ """ _params = ['jobId', 'why'] - _template = 'rMake job %(jobId)d failed to commit: %(why)s' + _template = 'rMake job %(jobId)s failed to commit: %(why)s' class JobFailedError(UpdateBotError): From johnsonm at rpath.com Wed Aug 19 17:41:39 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:41:39 +0000 Subject: mirrorball: always set strictMode true Message-ID: <200908192141.n7JLfddg020600@scc.eng.rpath.com> changeset: 610ea5281da4 user: Elliot Peele <https://issues.rpath.com/> date: Wed, 19 Nov 2008 11:27:38 -0500 always set strictMode true committer: Elliot Peele <https://issues.rpath.com/> diff --git a/updatebot/build.py b/updatebot/build.py --- a/updatebot/build.py +++ b/updatebot/build.py @@ -62,6 +62,7 @@ self._rmakeCfg.read(util.join(self._cfg.configPath, 'rmakerc')) self._rmakeCfg.useConaryConfig(self._ccfg) self._rmakeCfg.copyInConfig = False + self._rmakeCfg.strictMode = True self._helper = helper.rMakeHelper(buildConfig=self._rmakeCfg) From johnsonm at rpath.com Wed Aug 19 17:40:26 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:40:26 +0000 Subject: mirrorball: sort troves to be built Message-ID: <200908192140.n7JLeROP017699@scc.eng.rpath.com> changeset: 30359bc1cf6b user: Elliot Peele <http://bugs.rpath.com/> date: Mon, 25 Aug 2008 12:58:07 -0400 sort troves to be built committer: Elliot Peele <http://bugs.rpath.com/> diff --git a/updatebot/build.py b/updatebot/build.py --- a/updatebot/build.py +++ b/updatebot/build.py @@ -88,6 +88,11 @@ @return troveMap: dictionary of troveSpecs to built troves """ + troveSpecs = list(troveSpecs) + def trvSort(a, b): + return cmp(a[0], b[0]) + troveSpecs.sort(trvSort) + id = 0 jobs = {} for i, trv in enumerate(troveSpecs): From johnsonm at rpath.com Wed Aug 19 17:41:11 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:41:11 +0000 Subject: mirrorball: slightly better usage info Message-ID: <200908192141.n7JLfCnE019522@scc.eng.rpath.com> changeset: 0c9ca6b7f009 user: Elliot Peele <https://issues.rpath.com/> date: Wed, 29 Oct 2008 16:45:15 -0400 slightly better usage info committer: Elliot Peele <https://issues.rpath.com/> diff --git a/scripts/header.py b/scripts/header.py --- a/scripts/header.py +++ b/scripts/header.py @@ -24,7 +24,7 @@ from updatebot import config def usage(): - print 'usage: buildpackages <platform> [pkg1, pkg2, ...]' + print 'usage: %s <platform> [pkg1, pkg2, ...]' % sys.argv[0] sys.exit(1) if len(sys.argv) < 2 or sys.argv[1] not in os.listdir(os.environ['HOME'] + '/hg/mirrorball/config'): From johnsonm at rpath.com Wed Aug 19 17:42:55 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:42:55 +0000 Subject: mirrorball: always try to create the package if recreate is set Message-ID: <200908192142.n7JLgtP3023647@scc.eng.rpath.com> changeset: 6e45347f4978 user: Elliot Peele <https://issues.rpath.com/> date: Tue, 05 May 2009 14:34:11 -0400 always try to create the package if recreate is set committer: Elliot Peele <https://issues.rpath.com/> diff --git a/updatebot/update.py b/updatebot/update.py --- a/updatebot/update.py +++ b/updatebot/update.py @@ -241,7 +241,7 @@ srcPkg = self._getPackagesToImport(pkg) - if srcPkg.name not in pkgs: + if srcPkg.name not in pkgs or recreate: toUpdate.add(srcPkg) # Update all of the unique sources. From johnsonm at rpath.com Wed Aug 19 17:41:47 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:41:47 +0000 Subject: mirrorball: remove inacurate comment Message-ID: <200908192141.n7JLflqs020957@scc.eng.rpath.com> changeset: de1b89b2e30f user: Elliot Peele <https://issues.rpath.com/> date: Tue, 09 Dec 2008 14:07:46 -0500 remove inacurate comment committer: Elliot Peele <https://issues.rpath.com/> diff --git a/updatebot/conaryhelper.py b/updatebot/conaryhelper.py --- a/updatebot/conaryhelper.py +++ b/updatebot/conaryhelper.py @@ -93,8 +93,6 @@ latest = self._findLatest(trvlst) - # Magic number should probably be a config option. - # 2 here is the number of flavors expected. if len(latest) != self._groupFlavorCount: raise TooManyFlavorsFoundError(why=latest) From johnsonm at rpath.com Wed Aug 19 17:42:57 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:42:57 +0000 Subject: mirrorball: don't assume that patches has length Message-ID: <200908192142.n7JLgvO3023749@scc.eng.rpath.com> changeset: 2f799dee47ae user: Elliot Peele <https://issues.rpath.com/> date: Mon, 11 May 2009 16:04:22 -0400 don't assume that patches has length committer: Elliot Peele <https://issues.rpath.com/> diff --git a/updatebot/advisories/centos.py b/updatebot/advisories/centos.py --- a/updatebot/advisories/centos.py +++ b/updatebot/advisories/centos.py @@ -137,6 +137,9 @@ return True, otherwise return False. """ + if not len(patchSet): + return False + primary = list(patchSet)[0] for patch in patchSet: if patch is primary: From johnsonm at rpath.com Wed Aug 19 17:41:34 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:41:34 +0000 Subject: mirrorball: make sure to include the last message in the mailbox Message-ID: <200908192141.n7JLfYnc020395@scc.eng.rpath.com> changeset: 9646e4c1837f user: Elliot Peele <https://issues.rpath.com/> date: Tue, 18 Nov 2008 11:19:12 -0500 make sure to include the last message in the mailbox committer: Elliot Peele <https://issues.rpath.com/> diff --git a/pmap/common.py b/pmap/common.py --- a/pmap/common.py +++ b/pmap/common.py @@ -65,3 +65,7 @@ for line in msg.get_payload().split('\n'): self._parseLine(line) + + # Make sure last object gets added to self._objects while allowing + # subclasses to have special handling in newContainer. + self._newContainer() From johnsonm at rpath.com Wed Aug 19 17:39:20 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:39:20 +0000 Subject: mirrorball: if there isn't a patches.xml in the repo return empty list Message-ID: <200908192139.n7JLdKhh015052@scc.eng.rpath.com> changeset: 80098ca3eaf1 user: Elliot Peele <http://issues.rpath.com/> date: Thu, 12 Jun 2008 22:20:45 -0400 if there isn't a patches.xml in the repo return empty list committer: Elliot Peele <http://issues.rpath.com/> diff --git a/repomd/__init__.py b/repomd/__init__.py --- a/repomd/__init__.py +++ b/repomd/__init__.py @@ -65,6 +65,10 @@ """ node = self._repomd.getRepoData('patches') + + if node is None: + return [] + return [ x.parseChildren() for x in node.parseChildren().getPatches() ] def getPackageDetail(self): From johnsonm at rpath.com Wed Aug 19 17:42:46 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:42:46 +0000 Subject: mirrorball: use system libs Message-ID: <200908192142.n7JLgkJZ023306@scc.eng.rpath.com> changeset: 422df087eded user: Elliot Peele <https://issues.rpath.com/> date: Tue, 21 Apr 2009 14:20:18 -0400 use system libs committer: Elliot Peele <https://issues.rpath.com/> diff --git a/scripts/pkgsource b/scripts/pkgsource --- a/scripts/pkgsource +++ b/scripts/pkgsource @@ -16,10 +16,7 @@ import os import sys -sys.path.insert(0, os.environ['HOME'] + '/hg/rpath-xmllib') -sys.path.insert(0, os.environ['HOME'] + '/hg/conary') sys.path.insert(0, os.environ['HOME'] + '/hg/mirrorball') -sys.path.insert(0, os.environ['HOME'] + '/hg/rmake') from conary.lib import util sys.excepthook = util.genExcepthook() From johnsonm at rpath.com Wed Aug 19 17:39:18 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:39:18 +0000 Subject: mirrorball: remove unused variable Message-ID: <200908192139.n7JLdICh014983@scc.eng.rpath.com> changeset: f58309f4b253 user: Elliot Peele <http://issues.rpath.com/> date: Wed, 11 Jun 2008 20:50:38 -0400 remove unused variable committer: Elliot Peele <http://issues.rpath.com/> diff --git a/updatebot/update.py b/updatebot/update.py --- a/updatebot/update.py +++ b/updatebot/update.py @@ -130,7 +130,6 @@ """ needsUpdate = False - newLocations = [ x.location for x in self._rpmSource.srcPkgMap[srpm] ] newNames = [ x.name for x in self._rpmSource.srcPkgMap[srpm] ] manifest = [ x.strip() for x in self._recipeMaker.getManifest(nvf[0]) ] for line in manifest: From johnsonm at rpath.com Wed Aug 19 17:42:52 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:42:52 +0000 Subject: mirrorball: branch merge Message-ID: <200908192142.n7JLgqRP023528@scc.eng.rpath.com> changeset: 3c91f54d7b2c user: Elliot Peele <https://issues.rpath.com/> date: Fri, 01 May 2009 13:17:16 -0400 branch merge committer: Elliot Peele <https://issues.rpath.com/> diff --git a/updatebot/update.py b/updatebot/update.py --- a/updatebot/update.py +++ b/updatebot/update.py @@ -246,7 +246,7 @@ fail = set() toBuild = set() verCache = self._conaryhelper.getLatestVersions() - for pkg in toUpdate: + for pkg in sorted(toUpdate): try: # Only import packages that haven't been imported before version = verCache.get('%s:source' % pkg.name) From johnsonm at rpath.com Wed Aug 19 17:40:32 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:40:32 +0000 Subject: mirrorball: disable copyInConfig Message-ID: <200908192140.n7JLeWcH017937@scc.eng.rpath.com> changeset: 0035d7ddfad5 user: Elliot Peele <https://issues.rpath.com/> date: Thu, 28 Aug 2008 15:37:42 -0400 disable copyInConfig committer: Elliot Peele <https://issues.rpath.com/> diff --git a/updatebot/build.py b/updatebot/build.py --- a/updatebot/build.py +++ b/updatebot/build.py @@ -61,6 +61,7 @@ self._rmakeCfg = buildcfg.BuildConfiguration(readConfigFiles=False) self._rmakeCfg.read(util.join(self._cfg.configPath, 'rmakerc')) self._rmakeCfg.useConaryConfig(self._ccfg) + self._rmakeCfg.copyInConfig = False self._helper = helper.rMakeHelper(buildConfig=self._rmakeCfg) From johnsonm at rpath.com Wed Aug 19 17:42:19 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:42:19 +0000 Subject: mirrorball: in the False case job may not be defined Message-ID: <200908192142.n7JLgJaJ022217@scc.eng.rpath.com> changeset: ac38362179d8 user: Elliot Peele <https://issues.rpath.com/> date: Sat, 07 Feb 2009 22:39:08 +0000 in the False case job may not be defined committer: Elliot Peele <https://issues.rpath.com/> diff --git a/updatebot/build.py b/updatebot/build.py --- a/updatebot/build.py +++ b/updatebot/build.py @@ -486,7 +486,7 @@ time.sleep(5) job = self.builder._helper.getJob(self.jobId) except xml.parsers.expat.ExpatError, e: - return False, job + return False, None return True, job def _status(self, msg, type=0): From johnsonm at rpath.com Wed Aug 19 17:41:09 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:41:09 +0000 Subject: mirrorball: actualy honor the platform name when loading configeration Message-ID: <200908192141.n7JLf9oA019436@scc.eng.rpath.com> changeset: 9b251d0ae680 user: Elliot Peele <https://issues.rpath.com/> date: Tue, 28 Oct 2008 16:29:57 -0400 actualy honor the platform name when loading configeration committer: Elliot Peele <https://issues.rpath.com/> diff --git a/scripts/header.py b/scripts/header.py --- a/scripts/header.py +++ b/scripts/header.py @@ -32,7 +32,7 @@ log.addRootLogger() cfg = config.UpdateBotConfig() -cfg.read(os.environ['HOME'] + '/hg/mirrorball/config/ubuntu/updatebotrc') +cfg.read(os.environ['HOME'] + '/hg/mirrorball/config/%s/updatebotrc' % sys.argv[1]) builder = build.Builder(cfg) From johnsonm at rpath.com Wed Aug 19 17:42:04 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:42:04 +0000 Subject: mirrorball: reformat text Message-ID: <200908192142.n7JLg4fm021621@scc.eng.rpath.com> changeset: d11aa39cfb51 user: Elliot Peele <https://issues.rpath.com/> date: Wed, 14 Jan 2009 12:57:22 -0500 reformat text committer: Elliot Peele <https://issues.rpath.com/> diff --git a/updatebot/build.py b/updatebot/build.py --- a/updatebot/build.py +++ b/updatebot/build.py @@ -182,7 +182,7 @@ for ctx, job in jobs.iteritems(): jobIds[ctx] = self._startJob(job) - fmtstr = ','.join([ '%s:%s' % (x, y) for x, y in jobIds.iteritems()]) + fmtstr = ', '.join([ '%s:%s' % (x, y) for x, y in jobIds.iteritems()]) log.info('Started %s' % fmtstr) # Wait for the jobs to finish. From johnsonm at rpath.com Wed Aug 19 17:40:42 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:40:42 +0000 Subject: mirrorball: ubuntu/updatebotrc: add python-setuptools & swig Message-ID: <200908192140.n7JLegom018362@scc.eng.rpath.com> changeset: b219c10773e7 user: Jeff Uphoff <https://issues.rpath.com/> date: Tue, 09 Sep 2008 23:43:27 -0400 ubuntu/updatebotrc: add python-setuptools & swig committer: Jeff Uphoff <https://issues.rpath.com/> diff --git a/scripts/pkgsource.py b/scripts/pkgsource.py --- a/scripts/pkgsource.py +++ b/scripts/pkgsource.py @@ -21,7 +21,7 @@ from updatebot import pkgsource cfg = config.UpdateBotConfig() -cfg.read(os.environ['HOME'] + '/hg/mirrorball/config/sles/updatebotrc') +cfg.read(os.environ['HOME'] + '/hg/mirrorball/config/ubuntu/updatebotrc') pkgSource = pkgsource.PackageSource(cfg) From johnsonm at rpath.com Wed Aug 19 17:39:02 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:39:02 +0000 Subject: mirrorball: print usage message if the url does not start with http Message-ID: <200908192139.n7JLd2LA014286@scc.eng.rpath.com> changeset: 7e4e7eb4dd30 user: Matt Wilson <https://issues.rpath.com/> date: Mon, 02 Jun 2008 15:46:27 -0400 print usage message if the url does not start with http committer: Matt Wilson <https://issues.rpath.com/> diff --git a/pkgwiz.py b/pkgwiz.py --- a/pkgwiz.py +++ b/pkgwiz.py @@ -291,6 +291,11 @@ url = DEFAULT_URL if not basePaths: basePaths = DEFAULT_BASE_PATHS + + if not url.startswith('http'): + self.help() + return 1 + cwd = os.getcwd() tmpdir = tempfile.mkdtemp() print 'workdir is', tmpdir From johnsonm at rpath.com Wed Aug 19 17:41:53 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:41:53 +0000 Subject: mirrorball: configure ubuntu for updates Message-ID: <200908192141.n7JLfr03021196@scc.eng.rpath.com> changeset: d6ce71862c60 user: Elliot Peele <https://issues.rpath.com/> date: Fri, 19 Dec 2008 14:32:30 -0500 configure ubuntu for updates committer: Elliot Peele <https://issues.rpath.com/> diff --git a/updatebot/config.py b/updatebot/config.py --- a/updatebot/config.py +++ b/updatebot/config.py @@ -74,6 +74,9 @@ # platform short name platformName = CfgString + # upstream product version + upstreamProductVersion = CfgString + # disables checks for update completeness, this should only be enabled if # you know what you are doing and have a good reason. disableUpdateSanity = CfgBool From johnsonm at rpath.com Wed Aug 19 17:40:58 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:40:58 +0000 Subject: mirrorball: centos tweaks Message-ID: <200908192140.n7JLewdh019026@scc.eng.rpath.com> changeset: 45c10b72b93d user: Jeff Uphoff <https://issues.rpath.com/> date: Fri, 03 Oct 2008 16:45:34 -0400 centos tweaks committer: Jeff Uphoff <https://issues.rpath.com/> diff --git a/scripts/findbinaries.py b/scripts/findbinaries.py --- a/scripts/findbinaries.py +++ b/scripts/findbinaries.py @@ -40,7 +40,7 @@ updater = bot._updater helper = updater._conaryhelper -sources, failed = updater.create(cfg.package, buildAll=True) +sources, failed = updater.create(cfg.package, buildAll=False) pkgs = helper._repos.getTroveLeavesByLabel({None: {helper._ccfg.buildLabel: None}}) pkgSet = set([ x.split(':')[0] for x in pkgs ]) From johnsonm at rpath.com Wed Aug 19 17:39:24 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:39:24 +0000 Subject: mirrorball: add config option for repositories that should be ignored during advisory Message-ID: <200908192139.n7JLdOBb015225@scc.eng.rpath.com> changeset: ca4c49360b4d user: Elliot Peele <http://issues.rpath.com/> date: Mon, 16 Jun 2008 17:30:35 -0400 add config option for repositories that should be ignored during advisory check, ie. advisories that we generated metadata for that don't have advisories committer: Elliot Peele <http://issues.rpath.com/> diff --git a/updatebot/config.py b/updatebot/config.py --- a/updatebot/config.py +++ b/updatebot/config.py @@ -38,3 +38,4 @@ topGroup = CfgTroveSpec excludePackages = CfgList(CfgString) + advisoryException = CfgList(CfgList(CfgString)) From johnsonm at rpath.com Wed Aug 19 17:39:31 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:39:31 +0000 Subject: mirrorball: fix keyerror Message-ID: <200908192139.n7JLdVw9015533@scc.eng.rpath.com> changeset: 02fbc06fa060 user: Elliot Peele <http://issues.rpath.com/> date: Thu, 19 Jun 2008 21:46:20 -0400 fix keyerror committer: Elliot Peele <http://issues.rpath.com/> diff --git a/updatebot/build.py b/updatebot/build.py --- a/updatebot/build.py +++ b/updatebot/build.py @@ -90,7 +90,7 @@ n = sn.split(':')[0] if (n, sv, None) not in ret: ret[(n, sv, None)] = set() - for name, version, flavor in trvMap[(sn, sv, sf)]: + for name, version, flavor in trvMap[(sn, sv, sf, c)]: if name == n: ret[(n, v, None)].add((name, version, flavor)) From johnsonm at rpath.com Wed Aug 19 17:42:04 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:42:04 +0000 Subject: mirrorball: fix typo Message-ID: <200908192142.n7JLg49s021639@scc.eng.rpath.com> changeset: a993bb41585d user: Elliot Peele <https://issues.rpath.com/> date: Wed, 14 Jan 2009 15:59:34 -0500 fix typo committer: Elliot Peele <https://issues.rpath.com/> diff --git a/updatebot/update.py b/updatebot/update.py --- a/updatebot/update.py +++ b/updatebot/update.py @@ -353,7 +353,7 @@ metadata = self._getMetadataFromPkgSource(srcPkg) self._conaryhelper.setMetadata(nvf[0], metadata) - newVersion = self._conarhelper.commit(nvf[0], + newVersion = self._conaryhelper.commit(nvf[0], commitMessage=self._cfg.commitMessage) return newVersion From johnsonm at rpath.com Wed Aug 19 17:41:28 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:41:28 +0000 Subject: mirrorball: replace newlines with spaces Message-ID: <200908192141.n7JLfSHb020223@scc.eng.rpath.com> changeset: 357f9537862d user: Elliot Peele <https://issues.rpath.com/> date: Mon, 17 Nov 2008 16:43:38 -0500 replace newlines with spaces committer: Elliot Peele <https://issues.rpath.com/> diff --git a/pmap/common.py b/pmap/common.py --- a/pmap/common.py +++ b/pmap/common.py @@ -62,6 +62,7 @@ self._curObj.fromName = fromLine[fromLine.find('('):].strip('()') self._curObj.timestamp = ' '.join(msg.get_from().split()[4:]) self._curObj.subject = msg['Subject'] + self._curObj.subject.replace('\n\t', ' ') for line in msg.get_payload().split('\n'): self._parseLine(line) From johnsonm at rpath.com Wed Aug 19 17:40:24 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:40:24 +0000 Subject: mirrorball: try to silence more build output Message-ID: <200908192140.n7JLeOd9017580@scc.eng.rpath.com> changeset: 996f48e145b9 user: Elliot Peele <http://issues.rpath.com/> date: Tue, 19 Aug 2008 11:43:06 -0400 try to silence more build output committer: Elliot Peele <http://issues.rpath.com/> diff --git a/updatebot/build.py b/updatebot/build.py --- a/updatebot/build.py +++ b/updatebot/build.py @@ -323,3 +323,13 @@ """ Don't care about resolving/installing chroot """ + + def _tailBuildLog(self, jobId, troveTuple): + """ + Don't care about the build log + """ + + def _stopTailing(self, jobId, troveTuple): + """ + Don't care about the build log + """ From johnsonm at rpath.com Wed Aug 19 17:40:04 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:40:04 +0000 Subject: mirrorball: set for building one a time Message-ID: <200908192140.n7JLe42B016796@scc.eng.rpath.com> changeset: 4e31ddf84e93 user: Elliot Peele <http://issues.rpath.com/> date: Tue, 29 Jul 2008 22:08:32 -0400 set for building one a time committer: Elliot Peele <http://issues.rpath.com/> diff --git a/updatebot/bot.py b/updatebot/bot.py --- a/updatebot/bot.py +++ b/updatebot/bot.py @@ -114,8 +114,8 @@ # import epdb; epdb.st() # Build all newly imported packages. - #trvMap, failed = self._builder.buildmany(toBuild) - trvMap = self._builder.build(toBuild) + trvMap, failed = self._builder.buildmany(toBuild) + #trvMap = self._builder.build(toBuild) import epdb; epdb.st() From johnsonm at rpath.com Wed Aug 19 17:39:38 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:39:38 +0000 Subject: mirrorball: return all built groups Message-ID: <200908192139.n7JLdcpN015789@scc.eng.rpath.com> changeset: 751aae8b1e3c user: Elliot Peele <http://issues.rpath.com/> date: Tue, 24 Jun 2008 15:57:15 -0400 return all built groups committer: Elliot Peele <http://issues.rpath.com/> diff --git a/updatebot/build.py b/updatebot/build.py --- a/updatebot/build.py +++ b/updatebot/build.py @@ -97,7 +97,7 @@ if (n, sv, None) not in ret: ret[(n, sv, None)] = set() for name, version, flavor in trvMap[(sn, sv, sf, c)]: - if name == n: + if name == n or name.startswith('group-'): ret[(n, sv, None)].add((name, version, flavor)) return ret From johnsonm at rpath.com Wed Aug 19 17:38:56 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:38:56 +0000 Subject: mirrorball: insert default coverage path into environment Message-ID: <200908192139.n7JLcubl014025@scc.eng.rpath.com> changeset: 0998ac3c5db6 user: Elliot Peele <http://issues.rpath.com/> date: Wed, 28 May 2008 21:13:19 -0400 insert default coverage path into environment committer: Elliot Peele <http://issues.rpath.com/> diff --git a/test/testsuite.py b/test/testsuite.py --- a/test/testsuite.py +++ b/test/testsuite.py @@ -56,6 +56,9 @@ sys.path.insert(0, thisPath) return thisPath + # set default COVERAGE_PATH, if it was not set. + coveragePath = setPathFromEnv('COVERAGE_PATH', 'utils') + # set default CONARY_PATH, if it was not set. conaryPath = setPathFromEnv('CONARY_PATH', 'conary') From johnsonm at rpath.com Wed Aug 19 17:39:19 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:39:19 +0000 Subject: mirrorball: actually unlink the file Message-ID: <200908192139.n7JLdJhY015018@scc.eng.rpath.com> changeset: 89807b6657bd user: Elliot Peele <http://issues.rpath.com/> date: Thu, 12 Jun 2008 22:19:42 -0400 actually unlink the file committer: Elliot Peele <http://issues.rpath.com/> diff --git a/repomd/repository.py b/repomd/repository.py --- a/repomd/repository.py +++ b/repomd/repository.py @@ -51,10 +51,11 @@ shutil.copyfileobj(inf, outf) if os.path.basename(fileName).endswith('.gz'): - return gzip.open(fn) + fh = gzip.open(fn) else: - return open(fn) + fh = open(fn) os.unlink(fn) + return fh @classmethod def _getTempFile(cls): From johnsonm at rpath.com Wed Aug 19 17:40:27 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:40:27 +0000 Subject: mirrorball: don't build everything Message-ID: <200908192140.n7JLeRNw017716@scc.eng.rpath.com> changeset: cdc5ea7ed11a user: Elliot Peele <http://bugs.rpath.com/> date: Mon, 25 Aug 2008 12:59:29 -0400 don't build everything committer: Elliot Peele <http://bugs.rpath.com/> diff --git a/updatebot/bot.py b/updatebot/bot.py --- a/updatebot/bot.py +++ b/updatebot/bot.py @@ -114,7 +114,7 @@ self._populatePkgSource() # Import sources into repository. - toBuild, fail = self._updater.create(self._cfg.package, buildAll=True) + toBuild, fail = self._updater.create(self._cfg.package, buildAll=False) # Build all newly imported packages. trvMap, failed = self._builder.buildmany(toBuild) From johnsonm at rpath.com Wed Aug 19 17:42:51 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:42:51 +0000 Subject: mirrorball: sort packages to update Message-ID: <200908192142.n7JLgpPe023511@scc.eng.rpath.com> changeset: 9f9ecc75ce91 user: Elliot Peele <https://issues.rpath.com/> date: Wed, 29 Apr 2009 18:10:57 -0400 sort packages to update committer: Elliot Peele <https://issues.rpath.com/> diff --git a/updatebot/update.py b/updatebot/update.py --- a/updatebot/update.py +++ b/updatebot/update.py @@ -246,7 +246,7 @@ fail = set() toBuild = set() verCache = self._conaryhelper.getLatestVersions() - for pkg in toUpdate: + for pkg in sorted(toUpdate): try: # Only import packages that haven't been imported before version = verCache.get('%s:source' % pkg.name) From johnsonm at rpath.com Wed Aug 19 17:41:41 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:41:41 +0000 Subject: mirrorball: mirror changes once the update is complete Message-ID: <200908192141.n7JLffT3020702@scc.eng.rpath.com> changeset: 19530f6fc6c3 user: Elliot Peele <https://issues.rpath.com/> date: Wed, 19 Nov 2008 13:29:48 -0500 mirror changes once the update is complete committer: Elliot Peele <https://issues.rpath.com/> diff --git a/updatebot/bot.py b/updatebot/bot.py --- a/updatebot/bot.py +++ b/updatebot/bot.py @@ -173,6 +173,9 @@ # Send advisories. self._advisor.send(toAdvise, newTroves) + # Mirror out content + self._update.mirror() + log.info('update completed successfully') log.info('updated %s packages and sent %s advisories' % (len(toUpdate), len(toAdvise))) From johnsonm at rpath.com Wed Aug 19 17:42:15 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:42:15 +0000 Subject: mirrorball: add support for handling new repomd Message-ID: <200908192142.n7JLgF9h022064@scc.eng.rpath.com> changeset: 8f55af3fac01 user: Elliot Peele <https://issues.rpath.com/> date: Thu, 05 Feb 2009 23:53:45 -0500 add support for handling new repomd committer: Elliot Peele <https://issues.rpath.com/> diff --git a/updatebot/pkgsource/rpmsource.py b/updatebot/pkgsource/rpmsource.py --- a/updatebot/pkgsource/rpmsource.py +++ b/updatebot/pkgsource/rpmsource.py @@ -98,7 +98,7 @@ 'x86_64' in pkg.location): continue - if pkg.sourcerpm == '': + if pkg.sourcerpm == '' or pkg.sourcerpm is None: self._procSrc(pkg) else: self._procBin(pkg) From johnsonm at rpath.com Wed Aug 19 17:42:37 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:42:37 +0000 Subject: mirrorball: reference correct class Message-ID: <200908192142.n7JLgbCh022949@scc.eng.rpath.com> changeset: 65f6933a300d user: Elliot Peele <https://issues.rpath.com/> date: Thu, 19 Mar 2009 17:07:24 -0400 reference correct class committer: Elliot Peele <https://issues.rpath.com/> diff --git a/updatebot/build.py b/updatebot/build.py --- a/updatebot/build.py +++ b/updatebot/build.py @@ -99,7 +99,7 @@ # Use default tmpDir when building with rMake since the specified # tmpDir may not exist in the build root. - self._rmakeCfg.tmpDir = conarycfg.ConaryConfiguration.tmpDir[1] + self._rmakeCfg.tmpDir = conarycfg.ConaryContext.tmpDir[1] self._helper = helper.rMakeHelper(buildConfig=self._rmakeCfg) From johnsonm at rpath.com Wed Aug 19 17:42:48 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:42:48 +0000 Subject: mirrorball: don't fail if there are no groups Message-ID: <200908192142.n7JLgmeu023425@scc.eng.rpath.com> changeset: c90bc48ac71e user: Elliot Peele <https://issues.rpath.com/> date: Wed, 22 Apr 2009 17:35:21 -0400 don't fail if there are no groups committer: Elliot Peele <https://issues.rpath.com/> diff --git a/scripts/findbinaries b/scripts/findbinaries --- a/scripts/findbinaries +++ b/scripts/findbinaries @@ -66,7 +66,11 @@ pkgs = helper._repos.getTroveLeavesByLabel({None: {helper._ccfg.buildLabel: None}}) -groupPkgs = helper.getSourceTroves(cfg.topGroup) + +try: + groupPkgs = helper.getSourceTroves(cfg.topGroup) +except errors.GroupNotFound: + groupPkgs = {} pkgs = filterPkgs(pkgs) groupPkgs = filterPkgs(groupPkgs) From johnsonm at rpath.com Wed Aug 19 17:41:49 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:41:49 +0000 Subject: mirrorball: remove break point Message-ID: <200908192141.n7JLfnth021059@scc.eng.rpath.com> changeset: 494635f071ec user: Elliot Peele <https://issues.rpath.com/> date: Tue, 16 Dec 2008 18:45:06 -0500 remove break point committer: Elliot Peele <https://issues.rpath.com/> diff --git a/updatebot/advisories/centos.py b/updatebot/advisories/centos.py --- a/updatebot/advisories/centos.py +++ b/updatebot/advisories/centos.py @@ -98,8 +98,6 @@ msg.summary = msg.summary.replace('[CentOS-announce]', '') msg.summary = msg.summary.strip() - import epdb; epdb.st() - for pkgName in msg.pkgs: # Toss out any arches that we don't know how to handle. if not self._supportedArch(pkgName): From johnsonm at rpath.com Wed Aug 19 17:40:39 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:40:39 +0000 Subject: mirrorball: fix typo Message-ID: <200908192140.n7JLeduE018243@scc.eng.rpath.com> changeset: b2272a33add4 user: Elliot Peele <https://issues.rpath.com/> date: Mon, 08 Sep 2008 02:34:10 -0400 fix typo committer: Elliot Peele <https://issues.rpath.com/> diff --git a/extra/logparse.py b/extra/logparse.py --- a/extra/logparse.py +++ b/extra/logparse.py @@ -479,7 +479,7 @@ recipeDir = self._helper._checkout(pkgName) control = self.getControl() if not control and os.path.exists(os.path.join(recipeDir, 'control')): - self._helper.removeFile('control') + self._helper._removeFile('control') if control: fd = open(os.path.join(recipeDir, 'control'), 'w') fd.write(control) From johnsonm at rpath.com Wed Aug 19 17:40:36 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:40:36 +0000 Subject: mirrorball: don't built everything Message-ID: <200908192140.n7JLea56018090@scc.eng.rpath.com> changeset: e6dc310024bb user: Elliot Peele <https://issues.rpath.com/> date: Wed, 03 Sep 2008 11:27:56 -0400 don't built everything committer: Elliot Peele <https://issues.rpath.com/> diff --git a/updatebot/bot.py b/updatebot/bot.py --- a/updatebot/bot.py +++ b/updatebot/bot.py @@ -114,7 +114,7 @@ self._populatePkgSource() # Import sources into repository. - toBuild, fail = self._updater.create(self._cfg.package, buildAll=True) + toBuild, fail = self._updater.create(self._cfg.package, buildAll=False) # Build all newly imported packages. # trvMap, failed = self._builder.buildmany(toBuild) From johnsonm at rpath.com Wed Aug 19 17:42:47 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:42:47 +0000 Subject: mirrorball: use system libs Message-ID: <200908192142.n7JLglsH023374@scc.eng.rpath.com> changeset: aa4c0d17c1c7 user: Elliot Peele <https://issues.rpath.com/> date: Wed, 22 Apr 2009 14:35:22 -0400 use system libs committer: Elliot Peele <https://issues.rpath.com/> diff --git a/scripts/rebuild b/scripts/rebuild --- a/scripts/rebuild +++ b/scripts/rebuild @@ -16,11 +16,7 @@ import os import sys -sys.path.insert(0, os.environ['HOME'] + '/hg/rpath-xmllib') -sys.path.insert(0, os.environ['HOME'] + '/hg/conary') sys.path.insert(0, os.environ['HOME'] + '/hg/mirrorball') -sys.path.insert(0, os.environ['HOME'] + '/hg/rmake') -sys.path.insert(0, os.environ['HOME'] + '/hg/xobj/py') from conary.lib import util sys.excepthook = util.genExcepthook() From johnsonm at rpath.com Wed Aug 19 17:39:01 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:39:01 +0000 Subject: mirrorball: remove "return" short circuit preventing commits (it was for testing) Message-ID: <200908192139.n7JLd1JZ014266@scc.eng.rpath.com> changeset: f3cfdc89d014 user: Matt Wilson <https://issues.rpath.com/> date: Mon, 02 Jun 2008 15:44:55 -0400 remove "return" short circuit preventing commits (it was for testing) committer: Matt Wilson <https://issues.rpath.com/> diff --git a/rpmimport/recipemaker.py b/rpmimport/recipemaker.py --- a/rpmimport/recipemaker.py +++ b/rpmimport/recipemaker.py @@ -47,7 +47,6 @@ except Exception, e: print '++++++ error building', pkgname, str(e) return - return cvc.sourceCommand(self.cfg, [ 'commit' ], { 'message': From johnsonm at rpath.com Wed Aug 19 17:42:28 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:42:28 +0000 Subject: mirrorball: add new config options Message-ID: <200908192142.n7JLgTds022592@scc.eng.rpath.com> changeset: 554d833b72e1 user: Elliot Peele <https://issues.rpath.com/> date: Tue, 24 Feb 2009 16:42:35 -0500 add new config options committer: Elliot Peele <https://issues.rpath.com/> diff --git a/updatebot/config.py b/updatebot/config.py --- a/updatebot/config.py +++ b/updatebot/config.py @@ -180,7 +180,11 @@ satisUrl = CfgString # Try to build from source rpms - buildFromSource = (CfgBool, False) + buildFromSource = (CfgBool, False) + + # Write package metadata to the source trove no matter the source + # package format. + writePackageMetadata = (CfgBool, False) class UpdateBotConfig(cfg.SectionedConfigFile): From johnsonm at rpath.com Wed Aug 19 17:41:25 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:41:25 +0000 Subject: mirrorball: don't load advisories in __init__ Message-ID: <200908192141.n7JLfPq8020103@scc.eng.rpath.com> changeset: 59149e560484 user: Elliot Peele <https://issues.rpath.com/> date: Fri, 14 Nov 2008 18:46:50 -0500 don't load advisories in __init__ committer: Elliot Peele <https://issues.rpath.com/> diff --git a/updatebot/advisories/__init__.py b/updatebot/advisories/__init__.py --- a/updatebot/advisories/__init__.py +++ b/updatebot/advisories/__init__.py @@ -42,6 +42,6 @@ % (backend, e)) def Advisor(cfg, pkgSource, backend): - klass = __getBackend(backend) - obj = klass.Advisor(cfg, pkgSource) - return obj.load() + module = __getBackend(backend) + obj = module.Advisor(cfg, pkgSource) + return obj From johnsonm at rpath.com Wed Aug 19 17:40:40 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:40:40 +0000 Subject: mirrorball: fix syntax Message-ID: <200908192140.n7JLeeZu018260@scc.eng.rpath.com> changeset: c17f8015ac03 user: Elliot Peele <https://issues.rpath.com/> date: Mon, 08 Sep 2008 02:36:39 -0400 fix syntax committer: Elliot Peele <https://issues.rpath.com/> diff --git a/extra/logparse.py b/extra/logparse.py --- a/extra/logparse.py +++ b/extra/logparse.py @@ -479,7 +479,7 @@ recipeDir = self._helper._checkout(pkgName) control = self.getControl() if not control and os.path.exists(os.path.join(recipeDir, 'control')): - self._helper._removeFile('control') + self._helper._removeFile(recipeDir, 'control') if control: fd = open(os.path.join(recipeDir, 'control'), 'w') fd.write(control) From johnsonm at rpath.com Wed Aug 19 17:42:46 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:42:46 +0000 Subject: mirrorball: use local libs Message-ID: <200908192142.n7JLgkZ6023323@scc.eng.rpath.com> changeset: 0a55801f03cb user: Elliot Peele <https://issues.rpath.com/> date: Tue, 21 Apr 2009 14:23:49 -0400 use local libs committer: Elliot Peele <https://issues.rpath.com/> diff --git a/scripts/header.py b/scripts/header.py --- a/scripts/header.py +++ b/scripts/header.py @@ -15,11 +15,7 @@ import os import sys -sys.path.insert(0, os.environ['HOME'] + '/hg/rpath-xmllib') -sys.path.insert(0, os.environ['HOME'] + '/hg/conary') sys.path.insert(0, os.environ['HOME'] + '/hg/mirrorball') -sys.path.insert(0, os.environ['HOME'] + '/hg/rmake') -sys.path.insert(0, os.environ['HOME'] + '/hg/xobj/py') from conary.lib import util sys.excepthook = util.genExcepthook() From johnsonm at rpath.com Wed Aug 19 17:40:34 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:40:34 +0000 Subject: mirrorball: updatebot/bot.py: restore buildAll=True Message-ID: <200908192140.n7JLeYtp018022@scc.eng.rpath.com> changeset: 1a740c0850b0 user: Jeff Uphoff <https://issues.rpath.com/> date: Tue, 02 Sep 2008 22:13:52 -0400 updatebot/bot.py: restore buildAll=True committer: Jeff Uphoff <https://issues.rpath.com/> diff --git a/updatebot/bot.py b/updatebot/bot.py --- a/updatebot/bot.py +++ b/updatebot/bot.py @@ -114,7 +114,7 @@ self._populatePkgSource() # Import sources into repository. - toBuild, fail = self._updater.create(self._cfg.package, buildAll=False) + toBuild, fail = self._updater.create(self._cfg.package, buildAll=True) # Build all newly imported packages. # trvMap, failed = self._builder.buildmany(toBuild) From johnsonm at rpath.com Wed Aug 19 17:41:50 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:41:50 +0000 Subject: mirrorball: fix typo Message-ID: <200908192141.n7JLfo8G021076@scc.eng.rpath.com> changeset: cdd5cee4ec38 user: Elliot Peele <https://issues.rpath.com/> date: Wed, 17 Dec 2008 12:47:47 -0500 fix typo committer: Elliot Peele <https://issues.rpath.com/> diff --git a/updatebot/pkgsource/debsource.py b/updatebot/pkgsource/debsource.py --- a/updatebot/pkgsource/debsource.py +++ b/updatebot/pkgsource/debsource.py @@ -49,7 +49,7 @@ Load repository metadata from a config object. """ - client = repomd.Client(self._cfg.repositoryUrl) + client = aptmd.Client(self._cfg.repositoryUrl) for repo in self._cfg.repositoryPaths: log.info('loading repository data %s' % repo) self.loadFromClient(client, repo) From johnsonm at rpath.com Wed Aug 19 17:41:01 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:41:01 +0000 Subject: mirrorball: add newContainer method Message-ID: <200908192141.n7JLf1h2019128@scc.eng.rpath.com> changeset: 6753f26869df user: Elliot Peele <https://issues.rpath.com/> date: Wed, 22 Oct 2008 15:53:48 -0400 add newContainer method committer: Elliot Peele <https://issues.rpath.com/> diff --git a/aptmd/parser.py b/aptmd/parser.py --- a/aptmd/parser.py +++ b/aptmd/parser.py @@ -133,6 +133,13 @@ return key + def _newContainer(self): + if self._curObj is not None: + if hasattr(self._curObj, 'finalize'): + self._curObj.finalize() + self._objects.append(self._curObj) + self._curObj = self._containerClass() + def parse(self, fileObj): self._objects = [] Parser.parse(self, fileObj) From johnsonm at rpath.com Wed Aug 19 17:41:23 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:41:23 +0000 Subject: mirrorball: backend should not be optional Message-ID: <200908192141.n7JLfN1i020001@scc.eng.rpath.com> changeset: 3cb15bb55727 user: Elliot Peele <https://issues.rpath.com/> date: Fri, 14 Nov 2008 17:47:22 -0500 backend should not be optional committer: Elliot Peele <https://issues.rpath.com/> diff --git a/updatebot/advisories/__init__.py b/updatebot/advisories/__init__.py --- a/updatebot/advisories/__init__.py +++ b/updatebot/advisories/__init__.py @@ -34,7 +34,7 @@ raise InvalidBackendError('Could not load %s backend: %s' % (backend, e)) -def Advisor(cfg, pkgSource, backend='centos'): +def Advisor(cfg, pkgSource, backend): klass = __getBackend(backend) obj = klass(cfg, pkgSource) return obj.load() From johnsonm at rpath.com Wed Aug 19 17:39:28 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:39:28 +0000 Subject: mirrorball: change to workDir before running test Message-ID: <200908192139.n7JLdSwR015379@scc.eng.rpath.com> changeset: d9d97d2d8618 user: Elliot Peele <http://issues.rpath.com/> date: Tue, 17 Jun 2008 15:18:35 -0400 change to workDir before running test committer: Elliot Peele <http://issues.rpath.com/> diff --git a/test/slehelp.py b/test/slehelp.py --- a/test/slehelp.py +++ b/test/slehelp.py @@ -15,6 +15,7 @@ import testsuite testsuite.setup() +import os import rmakehelp from updatebot import config @@ -26,6 +27,7 @@ self.updateBotCfg.configPath = self.cfg.root self.cfg.user = ('test', 'test') self.writeFile(self.cfg.root + '/conaryrc', '') + os.chdir(self.workDir) def initializeFlavor(self): pass From johnsonm at rpath.com Wed Aug 19 17:40:07 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:40:07 +0000 Subject: mirrorball: make it so that the first package found is used Message-ID: <200908192140.n7JLe77C016915@scc.eng.rpath.com> changeset: e9f455a92079 user: Elliot Peele <http://bugs.rpath.com/> date: Mon, 11 Aug 2008 18:59:02 -0400 make it so that the first package found is used committer: Elliot Peele <http://bugs.rpath.com/> diff --git a/updatebot/update.py b/updatebot/update.py --- a/updatebot/update.py +++ b/updatebot/update.py @@ -186,8 +186,8 @@ pkgMap[key] = pkg continue - # check if newer, last wins - if util.packagevercmp(pkg, pkgMap[key]) in (0, 1): + # check if newer, first wins + if util.packagevercmp(pkg, pkgMap[key]) in (1, ): pkgMap[key] = pkg ret = pkgMap.values() From johnsonm at rpath.com Wed Aug 19 17:41:36 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:41:36 +0000 Subject: mirrorball: make sure msg.packages is set Message-ID: <200908192141.n7JLfapP020480@scc.eng.rpath.com> changeset: 4827e2e7c4f2 user: Elliot Peele <https://issues.rpath.com/> date: Tue, 18 Nov 2008 12:29:52 -0500 make sure msg.packages is set committer: Elliot Peele <https://issues.rpath.com/> diff --git a/updatebot/advisories/centos.py b/updatebot/advisories/centos.py --- a/updatebot/advisories/centos.py +++ b/updatebot/advisories/centos.py @@ -102,6 +102,9 @@ if binPkg not in self._pkgMap: self._pkgMap[binPkg] = set() self._pkgMap[binPkg].add(msg) + + if msg.packages is None: + msg.packages = set() msg.packages.add(binPkg) # Strip arch out of the subject From johnsonm at rpath.com Wed Aug 19 17:42:13 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:42:13 +0000 Subject: mirrorball: allow packages to be included in an update that are not mentioned in the Message-ID: <200908192142.n7JLgDjI021979@scc.eng.rpath.com> changeset: b16b668a3ba8 user: Elliot Peele <https://issues.rpath.com/> date: Thu, 05 Feb 2009 18:15:18 -0500 allow packages to be included in an update that are not mentioned in the advisory for ubuntu committer: Elliot Peele <https://issues.rpath.com/> diff --git a/updatebot/advisories/ubuntu.py b/updatebot/advisories/ubuntu.py --- a/updatebot/advisories/ubuntu.py +++ b/updatebot/advisories/ubuntu.py @@ -29,6 +29,8 @@ Class for processing Ubuntu advisory information. """ + allowExtraPackages = True + def load(self): """ Parse the required data to generate a mapping of binary package From johnsonm at rpath.com Wed Aug 19 17:41:59 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:41:59 +0000 Subject: mirrorball: add more paths Message-ID: <200908192141.n7JLfxpn021417@scc.eng.rpath.com> changeset: 6030b4bd0798 user: Elliot Peele <https://issues.rpath.com/> date: Mon, 12 Jan 2009 16:56:10 -0500 add more paths committer: Elliot Peele <https://issues.rpath.com/> diff --git a/scripts/validatemanifests.py b/scripts/validatemanifests.py --- a/scripts/validatemanifests.py +++ b/scripts/validatemanifests.py @@ -19,6 +19,9 @@ sys.path.insert(0, os.environ['HOME'] + '/hg/26/xobj/py') sys.path.insert(0, os.environ['HOME'] + '/hg/26/mirrorball') +sys.path.insert(0, os.environ['HOME'] + '/hg/26/conary') +sys.path.insert(0, os.environ['HOME'] + '/hg/26/rmake') +sys.path.insert(0, os.environ['HOME'] + '/hg/26/epdb') from conary.lib import util sys.excepthook = util.genExcepthook() From johnsonm at rpath.com Wed Aug 19 17:42:36 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:42:36 +0000 Subject: mirrorball: add scientific linux to the list of available modules Message-ID: <200908192142.n7JLgagm022915@scc.eng.rpath.com> changeset: 2f5af5a15cd0 user: Elliot Peele <https://issues.rpath.com/> date: Mon, 16 Mar 2009 17:33:06 -0400 add scientific linux to the list of available modules committer: Elliot Peele <https://issues.rpath.com/> diff --git a/updatebot/advisories/__init__.py b/updatebot/advisories/__init__.py --- a/updatebot/advisories/__init__.py +++ b/updatebot/advisories/__init__.py @@ -26,7 +26,7 @@ Raised when an unsupported backend is used. """ -__supportedBackends = ('sles', 'centos', 'ubuntu', 'fedora') +__supportedBackends = ('sles', 'centos', 'ubuntu', 'fedora', 'scientific') def __getBackend(backend): if backend not in __supportedBackends: From johnsonm at rpath.com Wed Aug 19 17:41:35 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:41:35 +0000 Subject: mirrorball: walk entire set of slots Message-ID: <200908192141.n7JLfZnp020446@scc.eng.rpath.com> changeset: c0f39dec0abc user: Elliot Peele <https://issues.rpath.com/> date: Tue, 18 Nov 2008 12:20:21 -0500 walk entire set of slots committer: Elliot Peele <https://issues.rpath.com/> diff --git a/repomd/xmlcommon.py b/repomd/xmlcommon.py --- a/repomd/xmlcommon.py +++ b/repomd/xmlcommon.py @@ -66,6 +66,8 @@ __slots__ = () def __init__(self, *args, **kw): - for attr in self.__slots__: - setattr(self, attr, None) + for cls in self.__class__.__mro__: + if hasattr(cls, '__slots__'): + for attr in self.__slots__: + setattr(self, attr, None) xmllib.BaseNode.__init__(self, *args, **kw) From johnsonm at rpath.com Wed Aug 19 17:40:22 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:40:22 +0000 Subject: mirrorball: add an excepthook Message-ID: <200908192140.n7JLeMXt017494@scc.eng.rpath.com> changeset: cc35d8efa5f3 user: Elliot Peele <http://bugs.rpath.com/> date: Mon, 18 Aug 2008 22:04:06 -0400 add an excepthook committer: Elliot Peele <http://bugs.rpath.com/> diff --git a/scripts/buildpackages b/scripts/buildpackages --- a/scripts/buildpackages +++ b/scripts/buildpackages @@ -14,6 +14,9 @@ sys.path.insert(0, os.environ['HOME'] + '/hg/conary') sys.path.insert(0, os.environ['HOME'] + '/hg/mirrorball') +from conary.lib import util +sys.excepthook = util.genExcepthook() + from updatebot import log from updatebot import build from updatebot import config @@ -24,7 +27,6 @@ builder = build.Builder(cfg) - trvs = set() label = cfg.topSourceGroup[1] for pkg in sys.argv[1:]: From johnsonm at rpath.com Wed Aug 19 17:42:22 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:42:22 +0000 Subject: mirrorball: don't poke at the rmake server so much Message-ID: <200908192142.n7JLgMsr022336@scc.eng.rpath.com> changeset: d8b23c6a3398 user: Elliot Peele <https://issues.rpath.com/> date: Sun, 08 Feb 2009 06:35:19 +0000 don't poke at the rmake server so much committer: Elliot Peele <https://issues.rpath.com/> diff --git a/updatebot/build.py b/updatebot/build.py --- a/updatebot/build.py +++ b/updatebot/build.py @@ -506,7 +506,7 @@ try: job = self.builder._helper.getJob(self.jobId) while not job.isFinished() and not job.isFailed(): - time.sleep(5) + time.sleep(20 + self.offset) job = self.builder._helper.getJob(self.jobId) except xml.parsers.expat.ExpatError, e: return False, None From johnsonm at rpath.com Wed Aug 19 17:40:07 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:40:07 +0000 Subject: mirrorball: don't print progress Message-ID: <200908192140.n7JLe7f4016898@scc.eng.rpath.com> changeset: 7a0ac308929e user: Elliot Peele <http://bugs.rpath.com/> date: Mon, 11 Aug 2008 18:18:39 -0400 don't print progress committer: Elliot Peele <http://bugs.rpath.com/> diff --git a/scripts/sync-opensuse.sh b/scripts/sync-opensuse.sh old mode 100644 new mode 100755 --- a/scripts/sync-opensuse.sh +++ b/scripts/sync-opensuse.sh @@ -1,5 +1,5 @@ #!/bin/bash -xe -rsync -arv --progress --exclude iso --exclude 10.* --exclude ppc --exclude ppc64 rsync://rsync.opensuse.org/opensuse-full /mnt/rpath/linux/ +rsync -arv --exclude iso --exclude 10.* --exclude ppc --exclude ppc64 rsync://rsync.opensuse.org/opensuse-full/opensuse/ /mnt/rpath/linux/opensuse ./hardlink.py /mnt/rpath/linux/opensuse From johnsonm at rpath.com Wed Aug 19 17:40:34 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:40:34 +0000 Subject: mirrorball: updatebotrc: add apache2.2-common and introduced deps Message-ID: <200908192140.n7JLeY5O018005@scc.eng.rpath.com> changeset: d49589fa55b2 user: Jeff Uphoff <https://issues.rpath.com/> date: Tue, 02 Sep 2008 22:12:29 -0400 updatebotrc: add apache2.2-common and introduced deps committer: Jeff Uphoff <https://issues.rpath.com/> diff --git a/updatebot/bot.py b/updatebot/bot.py --- a/updatebot/bot.py +++ b/updatebot/bot.py @@ -114,7 +114,7 @@ self._populatePkgSource() # Import sources into repository. - toBuild, fail = self._updater.create(self._cfg.package, buildAll=True) + toBuild, fail = self._updater.create(self._cfg.package, buildAll=False) # Build all newly imported packages. # trvMap, failed = self._builder.buildmany(toBuild) From johnsonm at rpath.com Wed Aug 19 17:41:15 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:41:15 +0000 Subject: mirrorball: use conary-test-2.0 Message-ID: <200908192141.n7JLfFbY019675@scc.eng.rpath.com> changeset: 55308947a311 user: Elliot Peele <https://issues.rpath.com/> date: Thu, 06 Nov 2008 20:35:56 -0500 use conary-test-2.0 committer: Elliot Peele <https://issues.rpath.com/> diff --git a/test/testsuite.py b/test/testsuite.py --- a/test/testsuite.py +++ b/test/testsuite.py @@ -56,7 +56,7 @@ testutilsPath = setPathFromEnv('TESTUTILS_PATH', '../testutils') conaryDir = setPathFromEnv('CONARY_PATH', '../conary') - conaryTestPath = setPathFromEnv('CONARY_TEST_PATH', '../conary-test') + conaryTestPath = setPathFromEnv('CONARY_TEST_PATH', '../conary-test-2.0') setPathFromEnv('CONARY_POLICY_PATH', '/usr/lib/conary/policy') mirrorballPath = setPathFromEnv('SLEESTACK_PATH', '') From johnsonm at rpath.com Wed Aug 19 17:41:37 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:41:37 +0000 Subject: mirrorball: remove breakpoint Message-ID: <200908192141.n7JLfbFW020532@scc.eng.rpath.com> changeset: f4f34f7b9393 user: Elliot Peele <https://issues.rpath.com/> date: Tue, 18 Nov 2008 12:34:34 -0500 remove breakpoint committer: Elliot Peele <https://issues.rpath.com/> diff --git a/updatebot/advisories/common.py b/updatebot/advisories/common.py --- a/updatebot/advisories/common.py +++ b/updatebot/advisories/common.py @@ -241,7 +241,6 @@ log.info('package not in updates repository %s' % binPkg) log.debug(binPkg.location) elif len(patches) > 0: - import epdb; epdb.st() log.info('found package not mentioned in advisory %s' % binPkg) log.debug(binPkg.location) From johnsonm at rpath.com Wed Aug 19 17:41:49 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:41:49 +0000 Subject: mirrorball: filter out older versions Message-ID: <200908192141.n7JLfnaQ021025@scc.eng.rpath.com> changeset: 94abdd2827c3 user: Elliot Peele <https://issues.rpath.com/> date: Thu, 11 Dec 2008 17:47:14 -0500 filter out older versions committer: Elliot Peele <https://issues.rpath.com/> diff --git a/scripts/promote.py b/scripts/promote.py --- a/scripts/promote.py +++ b/scripts/promote.py @@ -25,6 +25,7 @@ slog = logging.getLogger('script') trvLst = helper._repos.findTrove(helper._ccfg.buildLabel, cfg.topGroup) +trvLst = helper._findLatest(trvLst) cs, packages = helper.promote( trvLst, @@ -48,6 +49,8 @@ pkgs.sort() for pkg in pkgs: + if pkg[0].startswith('group-'): + continue for flv in pkgMap[pkg]: if len(pkgMap[pkg]) > 1 and flv == deps.Flavor(): continue From johnsonm at rpath.com Wed Aug 19 17:41:34 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:41:34 +0000 Subject: mirrorball: walk all of the __slots__ to set them to None Message-ID: <200908192141.n7JLfYmI020412@scc.eng.rpath.com> changeset: bbc7efab7b1c user: Elliot Peele <https://issues.rpath.com/> date: Tue, 18 Nov 2008 12:16:32 -0500 walk all of the __slots__ to set them to None committer: Elliot Peele <https://issues.rpath.com/> diff --git a/aptmd/container.py b/aptmd/container.py --- a/aptmd/container.py +++ b/aptmd/container.py @@ -16,8 +16,11 @@ __slots__ = ('_data', ) def __init__(self): - for item in self.__slots__: - setattr(self, item, None) + for cls in self.__class__.__mro__: + if hasattr(cls, '__slots__'): + for item in cls.__slots__: + print item + setattr(self, item, None) self._data = {} From johnsonm at rpath.com Wed Aug 19 17:40:37 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:40:37 +0000 Subject: mirrorball: honor excluded packages when rebuilding Message-ID: <200908192140.n7JLecna018158@scc.eng.rpath.com> changeset: c10c5fa755a4 user: Elliot Peele <https://issues.rpath.com/> date: Fri, 05 Sep 2008 18:43:54 -0400 honor excluded packages when rebuilding committer: Elliot Peele <https://issues.rpath.com/> diff --git a/updatebot/update.py b/updatebot/update.py --- a/updatebot/update.py +++ b/updatebot/update.py @@ -250,7 +250,8 @@ if buildAll and pkgs: toBuild.update( [ (x, self._conaryhelper.getLatestSourceVersion(x), None) - for x in pkgs if not x.startswith('info-') ] + for x in pkgs if not x.startswith('info-') + and x not in self._cfg.excludePackages ] ) return toBuild, fail From johnsonm at rpath.com Wed Aug 19 17:40:50 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:40:50 +0000 Subject: mirrorball: actually return the parsed value Message-ID: <200908192140.n7JLeor3018685@scc.eng.rpath.com> changeset: fec9ecdac05d user: Elliot Peele <https://issues.rpath.com/> date: Tue, 23 Sep 2008 13:36:42 -0400 actually return the parsed value committer: Elliot Peele <https://issues.rpath.com/> diff --git a/aptmd/packages.py b/aptmd/packages.py --- a/aptmd/packages.py +++ b/aptmd/packages.py @@ -44,12 +44,14 @@ }) def parse(self, fn): - BaseParser.parse(self, fn) + ret = BaseParser.parse(self, fn) # If there is any text left, collect it in the description if self._text: self._curObj.description = self._text self._text = '' + return ret + def _source(self): source = self._getLine() assert source != '' From johnsonm at rpath.com Wed Aug 19 17:41:23 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:41:23 +0000 Subject: mirrorball: add doc strings Message-ID: <200908192141.n7JLfNYI020035@scc.eng.rpath.com> changeset: fb7c45856491 user: Elliot Peele <https://issues.rpath.com/> date: Fri, 14 Nov 2008 18:13:24 -0500 add doc strings committer: Elliot Peele <https://issues.rpath.com/> diff --git a/updatebot/advisories/__init__.py b/updatebot/advisories/__init__.py --- a/updatebot/advisories/__init__.py +++ b/updatebot/advisories/__init__.py @@ -12,6 +12,10 @@ # full details. # +""" +Top level advisories module. +""" + import logging from imputil import imp diff --git a/updatebot/advisories/common.py b/updatebot/advisories/common.py --- a/updatebot/advisories/common.py +++ b/updatebot/advisories/common.py @@ -12,6 +12,10 @@ # full details. # +""" +Base modules for dealing +""" + import time import smtplib import logging From johnsonm at rpath.com Wed Aug 19 17:42:57 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:42:57 +0000 Subject: mirrorball: don't raise an exception, let empty patch list to pass through Message-ID: <200908192142.n7JLgvm8023766@scc.eng.rpath.com> changeset: 4541979bc80a user: Elliot Peele <https://issues.rpath.com/> date: Mon, 11 May 2009 16:04:43 -0400 don't raise an exception, let empty patch list to pass through committer: Elliot Peele <https://issues.rpath.com/> diff --git a/updatebot/advisories/common.py b/updatebot/advisories/common.py --- a/updatebot/advisories/common.py +++ b/updatebot/advisories/common.py @@ -225,9 +225,6 @@ if binPkg in self._pkgMap: patches.update(self._pkgMap[binPkg]) - if len(patches) == 0: - raise NoAdvisoryFoundError(why=srcPkg) - if self._checkForDuplicates(patches): patches = set([patches.pop()]) From johnsonm at rpath.com Wed Aug 19 17:40:14 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:40:14 +0000 Subject: mirrorball: add config option for selectiong repository backend Message-ID: <200908192140.n7JLeEK5017205@scc.eng.rpath.com> changeset: 2951fd6cf9b0 user: Elliot Peele <http://issues.rpath.com/> date: Thu, 14 Aug 2008 18:12:34 -0400 add config option for selectiong repository backend committer: Elliot Peele <http://issues.rpath.com/> diff --git a/updatebot/config.py b/updatebot/config.py --- a/updatebot/config.py +++ b/updatebot/config.py @@ -53,6 +53,9 @@ # path to configuration files (conaryrc, rmakerc) configPath = CfgString + # type of upstream repostory to pull packages from, supported are apt and yum. + repositoryFormat = (CfgString, 'yum') + # Default commit message to use when committing to the repository. commitMessage = (CfgString, 'Automated commit by updateBot') From johnsonm at rpath.com Wed Aug 19 17:40:41 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:40:41 +0000 Subject: mirrorball: don't default to distructive means Message-ID: <200908192140.n7JLef8v018311@scc.eng.rpath.com> changeset: a20c9e46a2e2 user: Elliot Peele <https://issues.rpath.com/> date: Mon, 08 Sep 2008 22:06:16 -0400 don't default to distructive means committer: Elliot Peele <https://issues.rpath.com/> diff --git a/extra/logparse.py b/extra/logparse.py --- a/extra/logparse.py +++ b/extra/logparse.py @@ -579,10 +579,10 @@ obj.makeBuckets() # obj.findPathConflicts() - for logObj in obj._logFiles: - print 80 * '=' - print 'Package:', logObj - print logObj.getControl() - logObj.writeControl() + #for logObj in obj._logFiles: + # print 80 * '=' + # print 'Package:', logObj + # print logObj.getControl() + # logObj.writeControl() import epdb; epdb.st() From johnsonm at rpath.com Wed Aug 19 17:39:47 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:39:47 +0000 Subject: mirrorball: pull rpmvercmp from rpmutils module Message-ID: <200908192139.n7JLdlWK016113@scc.eng.rpath.com> changeset: 2e03b4c26118 user: Elliot Peele <http://issues.rpath.com/> date: Wed, 09 Jul 2008 11:58:53 -0400 pull rpmvercmp from rpmutils module committer: Elliot Peele <http://issues.rpath.com/> diff --git a/updatebot/update.py b/updatebot/update.py --- a/updatebot/update.py +++ b/updatebot/update.py @@ -18,7 +18,7 @@ import logging -from rpmvercmp import rpmvercmp +from rpmutils import rpmvercmp from updatebot import util from updatebot import conaryhelper diff --git a/updatebot/util.py b/updatebot/util.py --- a/updatebot/util.py +++ b/updatebot/util.py @@ -22,7 +22,7 @@ import os from conary.lib.util import rmtree -from rpmvercmp import rpmvercmp +from rpmutils import rpmvercmp def join(a, *b): """ From johnsonm at rpath.com Wed Aug 19 17:41:23 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:41:23 +0000 Subject: mirrorball: backend is no longer a keyword arg Message-ID: <200908192141.n7JLfN5X020018@scc.eng.rpath.com> changeset: be68160e3258 user: Elliot Peele <https://issues.rpath.com/> date: Fri, 14 Nov 2008 17:47:54 -0500 backend is no longer a keyword arg committer: Elliot Peele <https://issues.rpath.com/> diff --git a/updatebot/bot.py b/updatebot/bot.py --- a/updatebot/bot.py +++ b/updatebot/bot.py @@ -43,7 +43,7 @@ self._pkgSource = pkgsource.PackageSource(self._cfg) self._updater = update.Updater(self._cfg, self._pkgSource) self._advisor = advisories.Advisor(self._cfg, self._pkgSource, - backend=self._cfg.platformName) + self._cfg.platformName) self._builder = build.Builder(self._cfg) @staticmethod From johnsonm at rpath.com Wed Aug 19 17:40:54 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:40:54 +0000 Subject: mirrorball: make init compatable to conarycfg object Message-ID: <200908192140.n7JLesfj018838@scc.eng.rpath.com> changeset: f7249ff57755 user: Elliot Peele <https://issues.rpath.com/> date: Wed, 24 Sep 2008 22:53:22 -0400 make init compatable to conarycfg object committer: Elliot Peele <https://issues.rpath.com/> diff --git a/updatebot/config.py b/updatebot/config.py --- a/updatebot/config.py +++ b/updatebot/config.py @@ -111,3 +111,6 @@ emailTo = (CfgList(CfgString), []) emailBcc = (CfgList(CfgString), []) smtpServer = CfgString + + def __init__(self, *args, **kwargs): + cfg.SectionedConfigFile.__init__(self) From johnsonm at rpath.com Wed Aug 19 17:40:45 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:40:45 +0000 Subject: mirrorball: avoid the universe Message-ID: <200908192140.n7JLej50018464@scc.eng.rpath.com> changeset: 9d31a44dd8ab user: Elliot Peele <https://issues.rpath.com/> date: Wed, 10 Sep 2008 23:53:13 -0400 avoid the universe committer: Elliot Peele <https://issues.rpath.com/> diff --git a/scripts/sync-ubuntu.sh b/scripts/sync-ubuntu.sh --- a/scripts/sync-ubuntu.sh +++ b/scripts/sync-ubuntu.sh @@ -16,6 +16,6 @@ SOURCE=rsync://mirrors.us.kernel.org/ubuntu/ DEST=/l/ubuntu/ -rsync -arv --progress --exclude dapper* --exclude feisty* --exclude gustsy* --delete --exclude pool/restricted/* --exclude pool/multiverse/* $SOURCE $DEST +rsync -arv --progress --exclude dapper* --exclude feisty* --exclude gustsy* --delete --exclude pool/universe/* --exclude pool/restricted/* --exclude pool/multiverse/* $SOURCE $DEST ./hardlink.py $DEST From johnsonm at rpath.com Wed Aug 19 17:39:06 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:39:06 +0000 Subject: mirrorball: cleanup from quote replacement Message-ID: <200908192139.n7JLd6Jf014486@scc.eng.rpath.com> changeset: 0c0e5c3cb2e2 user: Elliot Peele <http://issues.rpath.com/> date: Mon, 02 Jun 2008 22:29:00 -0400 cleanup from quote replacement committer: Elliot Peele <http://issues.rpath.com/> diff --git a/updatebot/build.py b/updatebot/build.py --- a/updatebot/build.py +++ b/updatebot/build.py @@ -147,7 +147,11 @@ # pylint: disable-msg=R0901 def _troveLogUpdated(self, (jobId, troveTuple), state, status): - """Don't care about trove logs''' + """ + Don't care about trove logs + """ def _trovePreparingChroot(self, (jobId, troveTuple), host, path): - """Don't care about resolving/installing chroot''' + """ + Don't care about resolving/installing chroot + """ From johnsonm at rpath.com Wed Aug 19 17:40:43 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:40:43 +0000 Subject: mirrorball: pull in the universe Message-ID: <200908192140.n7JLehX8018396@scc.eng.rpath.com> changeset: 56fad72b777a user: Elliot Peele <https://issues.rpath.com/> date: Wed, 10 Sep 2008 18:35:00 -0400 pull in the universe committer: Elliot Peele <https://issues.rpath.com/> diff --git a/scripts/sync-ubuntu.sh b/scripts/sync-ubuntu.sh --- a/scripts/sync-ubuntu.sh +++ b/scripts/sync-ubuntu.sh @@ -16,6 +16,6 @@ SOURCE=rsync://mirrors.us.kernel.org/ubuntu/ DEST=/l/ubuntu/ -rsync -arv --progress --exclude dapper* --exclude feisty* --exclude gustsy* --delete --exclude pool/universe/* --exclude pool/restricted/* --exclude pool/multiverse/* $SOURCE $DEST +rsync -arv --progress --exclude dapper* --exclude feisty* --exclude gustsy* --delete --exclude pool/restricted/* --exclude pool/multiverse/* $SOURCE $DEST ./hardlink.py $DEST From johnsonm at rpath.com Wed Aug 19 17:40:24 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:40:24 +0000 Subject: mirrorball: build all troves Message-ID: <200908192140.n7JLeOZF017597@scc.eng.rpath.com> changeset: 45380a37b97e user: Elliot Peele <http://issues.rpath.com/> date: Tue, 19 Aug 2008 11:43:27 -0400 build all troves committer: Elliot Peele <http://issues.rpath.com/> diff --git a/updatebot/bot.py b/updatebot/bot.py --- a/updatebot/bot.py +++ b/updatebot/bot.py @@ -113,12 +113,8 @@ # Populate rpm source object from yum metadata. self._populatePkgSource() - #import epdb; epdb.st() - # Import sources into repository. - toBuild, fail = self._updater.create(self._cfg.package) - -# import epdb; epdb.st() + toBuild, fail = self._updater.create(self._cfg.package, buildAll=True) # Build all newly imported packages. trvMap, failed = self._builder.buildmany(toBuild) From johnsonm at rpath.com Wed Aug 19 17:41:41 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:41:41 +0000 Subject: mirrorball: add some log messages Message-ID: <200908192141.n7JLff00020685@scc.eng.rpath.com> changeset: 84a8bead41c4 user: Elliot Peele <https://issues.rpath.com/> date: Wed, 19 Nov 2008 13:29:36 -0500 add some log messages committer: Elliot Peele <https://issues.rpath.com/> diff --git a/updatebot/conaryhelper.py b/updatebot/conaryhelper.py --- a/updatebot/conaryhelper.py +++ b/updatebot/conaryhelper.py @@ -480,6 +480,8 @@ log.info('mirroring disabled, no mirror.conf found for this platform') return + log.info('starting mirror') + # Always use DEBUG logging when mirroring curLevel = clog.fmtLogger.level clog.setVerbosity(clog.DEBUG) @@ -493,4 +495,6 @@ # Reset loglevel clog.setVerbosity(curLevel) + log.info('mirror complete') + return rc From johnsonm at rpath.com Wed Aug 19 17:42:21 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:42:21 +0000 Subject: mirrorball: better build loop Message-ID: <200908192142.n7JLgLnW022285@scc.eng.rpath.com> changeset: ce811d60ff71 user: Elliot Peele <https://issues.rpath.com/> date: Sun, 08 Feb 2009 04:18:33 +0000 better build loop committer: Elliot Peele <https://issues.rpath.com/> diff --git a/updatebot/build.py b/updatebot/build.py --- a/updatebot/build.py +++ b/updatebot/build.py @@ -470,12 +470,16 @@ retries = 10 built = False while not built and retries: + retries -= 1 try: self._doBuild() except Exception, e: built = False + continue built = True - retries -= 1 + + if not built: + self.error('job failed') self.toBuild.task_done() From johnsonm at rpath.com Wed Aug 19 17:41:59 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:41:59 +0000 Subject: mirrorball: add support for local mirror configs Message-ID: <200908192141.n7JLfxx4021400@scc.eng.rpath.com> changeset: 027a6e3a9869 user: Elliot Peele <https://issues.rpath.com/> date: Mon, 12 Jan 2009 16:28:13 -0500 add support for local mirror configs committer: Elliot Peele <https://issues.rpath.com/> diff --git a/updatebot/conaryhelper.py b/updatebot/conaryhelper.py --- a/updatebot/conaryhelper.py +++ b/updatebot/conaryhelper.py @@ -57,6 +57,10 @@ # Have to initialize flavors to commit to the repository. self._ccfg.initializeFlavors() + mirrorDir = util.join(cfg.configPath, 'mirrors') + if os.path.exists(mirrorDir): + self._ccfg.mirrorDirs.insert(0, mirrorDir) + self._mcfg = None mcfgfn = util.join(cfg.configPath, 'mirror.conf') if os.path.exists(mcfgfn): From johnsonm at rpath.com Wed Aug 19 17:43:05 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:43:05 +0000 Subject: mirrorball: add script for building a list of packages split arch Message-ID: <200908192143.n7JLh5IU024107@scc.eng.rpath.com> changeset: 280bab8feb96 user: Elliot Peele <https://issues.rpath.com/> date: Mon, 22 Jun 2009 10:24:45 -0400 add script for building a list of packages split arch committer: Elliot Peele <https://issues.rpath.com/> diff --git a/scripts/buildsplit b/scripts/buildsplit new file mode 100755 --- /dev/null +++ b/scripts/buildsplit @@ -0,0 +1,23 @@ +#!/usr/bin/python +# +# Copryright (c) 2008-2009 rPath, Inc. +# + +""" +Script for cooking groups defined in the updatebot config. +""" + +from header import * + +if len(sys.argv) < 3: + usage() + +trvs = set() +label = cfg.topSourceGroup[1] +for pkg in sys.argv[2:]: + trvs.add((pkg, label, None)) +trvMap = builder.buildsplitarch(trvs) + +print "built:\n" + +display(trvMap) From johnsonm at rpath.com Wed Aug 19 17:43:04 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:43:04 +0000 Subject: mirrorball: fix typo Message-ID: <200908192143.n7JLh4XG024073@scc.eng.rpath.com> changeset: f64a387fb3c2 user: Elliot Peele <https://issues.rpath.com/> date: Mon, 15 Jun 2009 22:16:49 -0400 fix typo committer: Elliot Peele <https://issues.rpath.com/> diff --git a/updatebot/build.py b/updatebot/build.py --- a/updatebot/build.py +++ b/updatebot/build.py @@ -311,7 +311,7 @@ # is only for kernels. if flavor.stronglySatisfies(deps.parseFlavor('kernel.debug')): continue - flavor = deps.parseFlavor(str(flavor).replace('kernel', name) + flavor = deps.parseFlavor(str(flavor).replace('kernel', name)) troves.append((name, version, flavor, context)) # Handle special package flavors when specified. From johnsonm at rpath.com Wed Aug 19 17:39:21 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:39:21 +0000 Subject: mirrorball: fix typo Message-ID: <200908192139.n7JLdL0I015106@scc.eng.rpath.com> changeset: cfb56767487c user: Elliot Peele <http://issues.rpath.com/> date: Mon, 16 Jun 2008 11:37:56 -0400 fix typo committer: Elliot Peele <http://issues.rpath.com/> diff --git a/updatebot/update.py b/updatebot/update.py --- a/updatebot/update.py +++ b/updatebot/update.py @@ -154,7 +154,7 @@ pkgs.sort(util.packagevercmp) # Raise an exception if the versions of the packages aren't equal. - if rpmvercmp(pkg[-1].version, binPkg.version) != 0: + if rpmvercmp(pkgs[-1].version, binPkg.version) != 0: raise UpdateRemovesPackageError(why='all rpms in the ' 'manifest should have the same version, trying ' 'to add %s' % (pkgs[-1], )) From johnsonm at rpath.com Wed Aug 19 17:41:42 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:41:42 +0000 Subject: mirrorball: fix typo Message-ID: <200908192141.n7JLfgpw020736@scc.eng.rpath.com> changeset: 73fd1898d188 user: Elliot Peele <https://issues.rpath.com/> date: Wed, 19 Nov 2008 21:11:16 -0500 fix typo committer: Elliot Peele <https://issues.rpath.com/> diff --git a/updatebot/advisories/common.py b/updatebot/advisories/common.py --- a/updatebot/advisories/common.py +++ b/updatebot/advisories/common.py @@ -243,7 +243,7 @@ log.info('found package not mentioned in advisory %s' % binPkg) log.debug(binPkg.location) - if not self.allowExtraPacakges: + if not self.allowExtraPackages: raise ExtraPackagesFoundInUpdateError(pkg=binPkg, src=srcPkg, advisory=list(patches)[0]) else: From johnsonm at rpath.com Wed Aug 19 17:39:39 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:39:39 +0000 Subject: mirrorball: include all components and subpackages in return set Message-ID: <200908192139.n7JLdda2015807@scc.eng.rpath.com> changeset: be12e5e181e2 user: Elliot Peele <http://issues.rpath.com/> date: Tue, 24 Jun 2008 16:03:50 -0400 include all components and subpackages in return set committer: Elliot Peele <http://issues.rpath.com/> diff --git a/updatebot/build.py b/updatebot/build.py --- a/updatebot/build.py +++ b/updatebot/build.py @@ -96,9 +96,7 @@ n = sn.split(':')[0] if (n, sv, None) not in ret: ret[(n, sv, None)] = set() - for name, version, flavor in trvMap[(sn, sv, sf, c)]: - if name == n or name.startswith('group-'): - ret[(n, sv, None)].add((name, version, flavor)) + ret[(n, sv, None)].update(set(trvMap[(sn, sv, sf, c)])) return ret From johnsonm at rpath.com Wed Aug 19 17:38:51 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:38:51 +0000 Subject: mirrorball: fix some typos in coverage setup Message-ID: <200908192139.n7JLcpZu013827@scc.eng.rpath.com> changeset: 030e63ddc449 user: Elliot Peele <http://issues.rpath.com/> date: Wed, 28 May 2008 21:00:03 -0400 fix some typos in coverage setup committer: Elliot Peele <http://issues.rpath.com/> diff --git a/test/testsuite.py b/test/testsuite.py --- a/test/testsuite.py +++ b/test/testsuite.py @@ -136,11 +136,11 @@ def getCoverageDirs(handler, environ): basePath = os.environ['SLEESTACK_PATH'] - coverageDirs = [ 'updateBot', 'rpmimport', 'repomd', ] + coverageDirs = [ 'updatebot', 'rpmimport', 'repomd', ] coveragePath = [] for path in coverageDirs: - covaregePath.append(os.path.normpath(os.path.join(basePath, path))) + coveragePath.append(os.path.normpath(os.path.join(basePath, path))) return coveragePath From johnsonm at rpath.com Wed Aug 19 17:42:53 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:42:53 +0000 Subject: mirrorball: add buildmany script that builds each package in separate jobs Message-ID: <200908192142.n7JLgrmI023596@scc.eng.rpath.com> changeset: 3e6c1c482d28 user: Elliot Peele <https://issues.rpath.com/> date: Tue, 05 May 2009 13:13:59 -0400 add buildmany script that builds each package in separate jobs committer: Elliot Peele <https://issues.rpath.com/> diff --git a/scripts/buildmany b/scripts/buildmany new file mode 100755 --- /dev/null +++ b/scripts/buildmany @@ -0,0 +1,23 @@ +#!/usr/bin/python +# +# Copryright (c) 2008-2009 rPath, Inc. +# + +""" +Script for cooking groups defined in the updatebot config. +""" + +from header import * + +if len(sys.argv) < 3: + usage() + +trvs = set() +label = cfg.topSourceGroup[1] +for pkg in sys.argv[2:]: + trvs.add((pkg, label, None)) +trvMap = builder.buildmany2(trvs) + +print "built:\n" + +display(trvMap) From johnsonm at rpath.com Wed Aug 19 17:39:20 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:39:20 +0000 Subject: mirrorball: add handling for obsoletes Message-ID: <200908192139.n7JLdKe6015035@scc.eng.rpath.com> changeset: fb0b9ecb8c50 user: Elliot Peele <http://issues.rpath.com/> date: Thu, 12 Jun 2008 22:20:14 -0400 add handling for obsoletes committer: Elliot Peele <http://issues.rpath.com/> diff --git a/repomd/patchxml.py b/repomd/patchxml.py --- a/repomd/patchxml.py +++ b/repomd/patchxml.py @@ -56,6 +56,8 @@ self.requires = child.getChildren('entry', namespace='rpm') elif child.getName() == 'rpm:recommends': self.recommends = child.getChildren('entry', namespace='rpm') + elif child.getName() == 'rpm:obsoletes': + self.obsoletes = child.getChildren('entry', namespace='rpm') elif child.getName() == 'reboot-needed': self.rebootNeeded = True elif child.getName() == 'license-to-confirm': From johnsonm at rpath.com Wed Aug 19 17:38:52 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:38:52 +0000 Subject: mirrorball: fix syntax error and set configPath Message-ID: <200908192139.n7JLcqHd013848@scc.eng.rpath.com> changeset: 2a72fe4855f1 user: Elliot Peele <http://issues.rpath.com/> date: Wed, 28 May 2008 21:00:33 -0400 fix syntax error and set configPath committer: Elliot Peele <http://issues.rpath.com/> diff --git a/test/slehelp.py b/test/slehelp.py --- a/test/slehelp.py +++ b/test/slehelp.py @@ -17,12 +17,13 @@ import rmakehelp -from updateBot import config +from updatebot import config class Helper(rmakehelp.RmakeHelper): def setUp(self): rmakehelp.RmakeHelper.setUp(self) - self.updateBotCfg = config.UpdateBotConfig(False) + self.updateBotCfg = config.UpdateBotConfig() + self.updateBotCfg.configPath = self.cfg.root self.cfg.user = ('test', 'test') self.writeFile(self.cfg.root + '/conaryrc', '') From johnsonm at rpath.com Wed Aug 19 17:42:46 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:42:46 +0000 Subject: mirrorball: log build failures Message-ID: <200908192142.n7JLgke4023340@scc.eng.rpath.com> changeset: 8a1d2dab59ff user: Elliot Peele <https://issues.rpath.com/> date: Tue, 21 Apr 2009 16:52:21 -0400 log build failures committer: Elliot Peele <https://issues.rpath.com/> diff --git a/updatebot/bot.py b/updatebot/bot.py --- a/updatebot/bot.py +++ b/updatebot/bot.py @@ -106,6 +106,10 @@ if not rebuild: # Build all newly imported packages. trvMap, failed = self._builder.buildmany2(sorted(toBuild)) + log.info('failed to import %s packages' % len(failed)) + if len(failed): + for pkg in failed: + log.warn('%s' % (pkg, )) else: # ReBuild all packages. trvMap = self._builder.buildsplitarch(sorted(toBuild)) From johnsonm at rpath.com Wed Aug 19 17:40:15 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:40:15 +0000 Subject: mirrorball: fix typo Message-ID: <200908192140.n7JLeFcL017222@scc.eng.rpath.com> changeset: bf42e5b3fef9 user: Elliot Peele <http://issues.rpath.com/> date: Thu, 14 Aug 2008 18:13:01 -0400 fix typo committer: Elliot Peele <http://issues.rpath.com/> diff --git a/updatebot/pkgsource/debsource.py b/updatebot/pkgsource/debsource.py --- a/updatebot/pkgsource/debsource.py +++ b/updatebot/pkgsource/debsource.py @@ -67,7 +67,7 @@ self.srcPkgMap[srcPkg] = set() for binPkgName in srcPkg.binaries: for binPkg in self.binNameMap[binPkgName]: - if binPkg.version == srcPkg.version and binPkg.release = srcPkg.release: + if binPkg.version == srcPkg.version and binPkg.release == srcPkg.release: self.srcPkgMap[srcPkg].add(binPkg) self.srcPkgMap[srcPkg].add(srcPkg) From johnsonm at rpath.com Wed Aug 19 17:41:29 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:41:29 +0000 Subject: mirrorball: strip newlines from subjects Message-ID: <200908192141.n7JLfTAn020274@scc.eng.rpath.com> changeset: dca481c459e4 user: Elliot Peele <https://issues.rpath.com/> date: Mon, 17 Nov 2008 18:35:48 -0500 strip newlines from subjects committer: Elliot Peele <https://issues.rpath.com/> diff --git a/pmap/common.py b/pmap/common.py --- a/pmap/common.py +++ b/pmap/common.py @@ -61,8 +61,7 @@ self._curObj.fromAddr = fromLine[:fromLine.find('(')].replace(' at ', '@') self._curObj.fromName = fromLine[fromLine.find('('):].strip('()') self._curObj.timestamp = ' '.join(msg.get_from().split()[4:]) - self._curObj.subject = msg['Subject'] - self._curObj.subject.replace('\n\t', ' ') + self._curObj.subject = msg['Subject'].replace('\n\t', ' ') for line in msg.get_payload().split('\n'): self._parseLine(line) From johnsonm at rpath.com Wed Aug 19 17:43:06 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:43:06 +0000 Subject: mirrorball: fix typo Message-ID: <200908192143.n7JLh6Dd024141@scc.eng.rpath.com> changeset: 150d7b6d1ea5 user: Elliot Peele <https://issues.rpath.com/> date: Mon, 06 Jul 2009 14:41:21 -0400 fix typo committer: Elliot Peele <https://issues.rpath.com/> diff --git a/updatebot/update.py b/updatebot/update.py --- a/updatebot/update.py +++ b/updatebot/update.py @@ -77,10 +77,10 @@ Return True if this is a package that should be filtered out. """ - if (name.startswith('info-') or - name.startswith('group-') or - name.startswith('factory-') or - name in self._cfg.excludePackages): + if (pkgname.startswith('info-') or + pkgname.startswith('group-') or + pkgname.startswith('factory-') or + pkgname in self._cfg.excludePackages): return True return False From johnsonm at rpath.com Wed Aug 19 17:41:16 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:41:16 +0000 Subject: mirrorball: add missing import and add new method getFullLine Message-ID: <200908192141.n7JLfGB1019727@scc.eng.rpath.com> changeset: 7d4b83b6429d user: Elliot Peele <https://issues.rpath.com/> date: Fri, 07 Nov 2008 16:17:22 -0500 add missing import and add new method getFullLine committer: Elliot Peele <https://issues.rpath.com/> diff --git a/aptmd/parser.py b/aptmd/parser.py --- a/aptmd/parser.py +++ b/aptmd/parser.py @@ -12,6 +12,8 @@ # full details. # +import re + class _QuotedLineTokenizer(object): def __init__(self): self._cur = None @@ -96,6 +98,9 @@ def _getLine(self): return ' '.join(self._line[1:]).strip() + def _getFullLine(self): + return ' '.join(self._line).strip() + def _checkLength(self, length, gt=False): if gt: assert(len(self._line) > length) else: assert(len(self._line) == length) From johnsonm at rpath.com Wed Aug 19 17:42:37 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:42:37 +0000 Subject: mirrorball: can't hash dicts, use a list Message-ID: <200908192142.n7JLgbZC022983@scc.eng.rpath.com> changeset: 7575cd602ea6 user: Elliot Peele <https://issues.rpath.com/> date: Tue, 24 Mar 2009 13:11:57 -0400 can't hash dicts, use a list committer: Elliot Peele <https://issues.rpath.com/> diff --git a/updatebot/build.py b/updatebot/build.py --- a/updatebot/build.py +++ b/updatebot/build.py @@ -648,13 +648,13 @@ def _getResultsAndErrors(self): errors = set() - results = set() + results = [] for trv, msgs in self._trvs.iteritems(): msg = msgs[-1] if msg.type == MESSAGE_TYPES['error']: errors.add((trv, msg.jobId)) elif msg.type == MESSAGE_TYPES['results']: - results.add(msg.message) + results.append(msg.message) return results, errors From johnsonm at rpath.com Wed Aug 19 17:39:43 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:39:43 +0000 Subject: mirrorball: don't override built-in filter function Message-ID: <200908192139.n7JLdh8d015943@scc.eng.rpath.com> changeset: 8f46201a9c95 user: Elliot Peele <http://issues.rpath.com/> date: Thu, 26 Jun 2008 10:49:53 -0400 don't override built-in filter function committer: Elliot Peele <http://issues.rpath.com/> diff --git a/updatebot/patchsource.py b/updatebot/patchsource.py --- a/updatebot/patchsource.py +++ b/updatebot/patchsource.py @@ -71,10 +71,10 @@ @type patch: repomd.patchxml._Patch """ - for _, filter in self._cfg.patchFilter: - if filter.match(patch.summary): + for _, regex in self._cfg.patchFilter: + if regex.match(patch.summary): return True - if filter.match(patch.description): + if regex.match(patch.description): return True return False From johnsonm at rpath.com Wed Aug 19 17:41:09 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:41:09 +0000 Subject: mirrorball: more work on pmap Message-ID: <200908192141.n7JLf9fY019402@scc.eng.rpath.com> changeset: aa3e01dbf7c2 user: Elliot Peele <https://issues.rpath.com/> date: Tue, 28 Oct 2008 15:28:21 -0400 more work on pmap committer: Elliot Peele <https://issues.rpath.com/> diff --git a/pmap/common.py b/pmap/common.py --- a/pmap/common.py +++ b/pmap/common.py @@ -14,7 +14,7 @@ import tempfile -from upstream import mailbox +from vendor import mailbox from aptmd.container import Container from aptmd.parser import ContainerizedParser as Parser @@ -48,4 +48,4 @@ def _parseMsg(self, msg): self._newContainer() - + diff --git a/pmap/ubuntu.py b/pmap/ubuntu.py --- a/pmap/ubuntu.py +++ b/pmap/ubuntu.py @@ -23,3 +23,9 @@ BaseParser.__init__(self) self._containerClass = Container + + self._states.update({ + + }) + + def From johnsonm at rpath.com Wed Aug 19 17:40:18 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:40:18 +0000 Subject: mirrorball: populate binPkgMap in the correct scope Message-ID: <200908192140.n7JLeIOI017341@scc.eng.rpath.com> changeset: 20bf809fda2c user: Elliot Peele <http://issues.rpath.com/> date: Fri, 15 Aug 2008 11:24:47 -0400 populate binPkgMap in the correct scope committer: Elliot Peele <http://issues.rpath.com/> diff --git a/updatebot/pkgsource/debsource.py b/updatebot/pkgsource/debsource.py --- a/updatebot/pkgsource/debsource.py +++ b/updatebot/pkgsource/debsource.py @@ -77,7 +77,8 @@ if (binPkg.version == srcPkg.version and binPkg.release == srcPkg.release): self.srcPkgMap[srcPkg].add(binPkg) + self.srcPkgMap[srcPkg].add(srcPkg) - for pkg in self.srcPkgMap[srcPkg]: - self.binPkgMap[pkg] = srcPkg + for pkg in self.srcPkgMap[srcPkg]: + self.binPkgMap[pkg] = srcPkg From johnsonm at rpath.com Wed Aug 19 17:40:09 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:40:09 +0000 Subject: mirrorball: fix CfgBranch so that it works Message-ID: <200908192140.n7JLe9hs017000@scc.eng.rpath.com> changeset: 606183c5a1d9 user: Elliot Peele <http://bugs.rpath.com/> date: Wed, 13 Aug 2008 15:54:12 -0400 fix CfgBranch so that it works targetLabel should be a branch committer: Elliot Peele <http://bugs.rpath.com/> diff --git a/updatebot/config.py b/updatebot/config.py --- a/updatebot/config.py +++ b/updatebot/config.py @@ -34,7 +34,7 @@ """ try: - versions.Branch(val) + return versions.VersionFromString(val) except versions.ParseError, e: raise ParseError, e @@ -73,7 +73,7 @@ sourceLabel = (CfgList(CfgBranch), []) # Label to promote to - targetLabel = CfgLabel + targetLabel = CfgBranch # Packages to import package = (CfgList(CfgString), []) From johnsonm at rpath.com Wed Aug 19 17:42:48 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:42:48 +0000 Subject: mirrorball: don't look at the group, just checkout everything on the label Message-ID: <200908192142.n7JLgmeW023408@scc.eng.rpath.com> changeset: 49760ddea3a2 user: Elliot Peele <https://issues.rpath.com/> date: Wed, 22 Apr 2009 17:35:03 -0400 don't look at the group, just checkout everything on the label committer: Elliot Peele <https://issues.rpath.com/> diff --git a/scripts/checkoutall b/scripts/checkoutall --- a/scripts/checkoutall +++ b/scripts/checkoutall @@ -23,6 +23,6 @@ cfg.read(os.environ['HOME'] + '/hg/mirrorball/config/%s/updatebotrc' % sys.argv[1]) helper = conaryhelper.ConaryHelper(cfg) -pkgs = [ name for name, version, flavor in helper.getSourceTroves(cfg.topGroup) - if version.trailingLabel().asString() == cfg.topGroup[1] ] +pkgs = [ x.split(':')[0] for x in helper.getLatestVersions() if x.endswith(':source') ] + checkin.checkout(helper._repos, helper._ccfg, None, pkgs) From johnsonm at rpath.com Wed Aug 19 17:39:24 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:39:24 +0000 Subject: mirrorball: add advisory related errors Message-ID: <200908192139.n7JLdOqB015242@scc.eng.rpath.com> changeset: 3f3d8eedfb67 user: Elliot Peele <http://issues.rpath.com/> date: Mon, 16 Jun 2008 17:30:49 -0400 add advisory related errors committer: Elliot Peele <http://issues.rpath.com/> diff --git a/updatebot/errors.py b/updatebot/errors.py --- a/updatebot/errors.py +++ b/updatebot/errors.py @@ -92,3 +92,16 @@ TooManFlavorsFoundError, raised when the bot finds more flavors of the top level group trove than expected. """ + +class AdvisoryError(UnhandledUpdateError): + """ + Base error for other advisory errors to inherit from. + """ + + _template = 'An advisory error has occured: %(why)s' + +class NoAdvisoryFoundError(AdvisoryError): + """ + NoAdvisoryFoundError, raised when the bot can not find an advisory for an + updated package. + """ From johnsonm at rpath.com Wed Aug 19 17:39:12 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:39:12 +0000 Subject: mirrorball: fix up manifest get Message-ID: <200908192139.n7JLdCQk014734@scc.eng.rpath.com> changeset: f1d8e741a313 user: Elliot Peele <http://issues.rpath.com/> date: Tue, 10 Jun 2008 16:26:03 -0400 fix up manifest get committer: Elliot Peele <http://issues.rpath.com/> diff --git a/rpmimport/recipemaker.py b/rpmimport/recipemaker.py --- a/rpmimport/recipemaker.py +++ b/rpmimport/recipemaker.py @@ -20,6 +20,7 @@ import os import shutil +import tempfile from conary import cvc class RecipeMaker(object): @@ -148,8 +149,15 @@ """ cwd = os.getcwd() - os.chdir(tempfile.mkdtemp()) + tmpdir = tempfile.mkdtemp() + os.chdir(tmpdir) self._checkout(pkgname) manifest = open('manifest').readlines() os.chdir(cwd) + + try: + shutil.rmtree(tmpdir) + except OSError: + pass + return manifest From johnsonm at rpath.com Wed Aug 19 17:42:41 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:42:41 +0000 Subject: mirrorball: use multi stage sync for rawhide Message-ID: <200908192142.n7JLgfCa023102@scc.eng.rpath.com> changeset: ed4836bd0c4c user: Elliot Peele <https://issues.rpath.com/> date: Thu, 02 Apr 2009 16:16:28 -0400 use multi stage sync for rawhide committer: Elliot Peele <https://issues.rpath.com/> diff --git a/scripts/sync-fedora-devel.sh b/scripts/sync-fedora-devel.sh --- a/scripts/sync-fedora-devel.sh +++ b/scripts/sync-fedora-devel.sh @@ -13,10 +13,14 @@ # full details. # -SOURCE=rsync://mirrors.kernel.org/fedora/development -DEST=/l/fedora/ +SOURCE=rsync://mirror.linux.ncsu.edu/fedora-linux-development +DEST=/l/fedora/development/ date -rsync -arv --progress --bwlimit=800 --exclude ppc --export ppc64 $SOURCE $DEST +CMD="rsync -arv --progress --bwlimit=800 --exclude ppc --exclude ppc64" + +$CMD --exclude repodata $SOURCE $DEST +$CMD $SOURCE $DEST +$CMD --delete $SOURCE $DEST ./hardlink.py $DEST From johnsonm at rpath.com Wed Aug 19 17:41:32 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:41:32 +0000 Subject: mirrorball: reverse test to be correct Message-ID: <200908192141.n7JLfWtX020344@scc.eng.rpath.com> changeset: 0d9c62db1ee0 user: Elliot Peele <https://issues.rpath.com/> date: Wed, 12 Nov 2008 17:02:56 -0500 reverse test to be correct committer: Elliot Peele <https://issues.rpath.com/> diff --git a/updatebot/conaryhelper.py b/updatebot/conaryhelper.py --- a/updatebot/conaryhelper.py +++ b/updatebot/conaryhelper.py @@ -438,7 +438,7 @@ grpTrvs = set([ (x[0], x[2]) for x in trvLst if not x[0].endswith(':source') ]) grpDiff = set([ x[0] for x in trvDiff.difference(grpTrvs) ]) extraTroves = set([ x[0] for x in extraPromoteTroves ]) - if checkPackageList and not grpDiff.difference(extraTroves): + if checkPackageList and grpDiff.difference(extraTroves): raise PromoteMismatchError(expected=oldPkgs, actual=newPkgs) log.info('committing changeset') From johnsonm at rpath.com Wed Aug 19 17:42:38 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:42:38 +0000 Subject: mirrorball: remove commented out code Message-ID: <200908192142.n7JLgcat023000@scc.eng.rpath.com> changeset: cf85ca85df79 user: Elliot Peele <https://issues.rpath.com/> date: Tue, 24 Mar 2009 13:12:18 -0400 remove commented out code committer: Elliot Peele <https://issues.rpath.com/> diff --git a/updatebot/update.py b/updatebot/update.py --- a/updatebot/update.py +++ b/updatebot/update.py @@ -247,11 +247,8 @@ toBuild = set() verCache = self._conaryhelper.getLatestVersions() for pkg in toUpdate: - #log.info('attempting to import %s' % pkg) - try: # Only import packages that haven't been imported before - #version = self._conaryhelper.getLatestSourceVersion(pkg.name) version = verCache.get('%s:source' % pkg.name) if not version: log.info('attempting to import %s' % pkg) From johnsonm at rpath.com Wed Aug 19 17:38:50 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:38:50 +0000 Subject: mirrorball: add xmllib path to pythonpath Message-ID: <200908192138.n7JLcoWP013765@scc.eng.rpath.com> changeset: 908f77a32608 user: Elliot Peele <http://issues.rpath.com/> date: Wed, 28 May 2008 00:35:25 -0400 add xmllib path to pythonpath committer: Elliot Peele <http://issues.rpath.com/> diff --git a/pylint/init_pylint.py b/pylint/init_pylint.py --- a/pylint/init_pylint.py +++ b/pylint/init_pylint.py @@ -36,7 +36,11 @@ # set default RMAKE_PATH, if it was not set. rmakePath = setPathFromEnv('RMAKE_PATH', 'rmake') -for path in rmakePath, conaryPath, mirrorballPath: +# set default XMLLIB_PATH, if it was not set. +xmllibPath = setPathFromEnv('XMLLIB_PATH', 'rpath-xmllib') + +# paths end up in the opposite order than they are listed. +for path in xmllibPath, rmakePath, conaryPath, mirrorballPath: if path in sys.path: sys.path.remove(path) sys.path.insert(0, path) From johnsonm at rpath.com Wed Aug 19 17:40:33 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:40:33 +0000 Subject: mirrorball: handle building all troves if configured to do so, even if the sources are Message-ID: <200908192140.n7JLeXFH017988@scc.eng.rpath.com> changeset: 8d59fb1719ca user: Elliot Peele <https://issues.rpath.com/> date: Fri, 29 Aug 2008 13:31:14 -0400 handle building all troves if configured to do so, even if the sources are from a binary group committer: Elliot Peele <https://issues.rpath.com/> diff --git a/updatebot/update.py b/updatebot/update.py --- a/updatebot/update.py +++ b/updatebot/update.py @@ -247,6 +247,12 @@ log.error('failed to import %s: %s' % (pkg, e)) fail.add((pkg, e)) + if buildAll and pkgs: + toBuild.update( + [ (x, self._conaryhelper.getLatestSourceVersion(x), None) + for x in pkgs if not x.startswith('info-') ] + ) + return toBuild, fail def _getExistingPackageNames(self): From johnsonm at rpath.com Wed Aug 19 17:39:27 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:39:27 +0000 Subject: mirrorball: add exception for no manifest in source component Message-ID: <200908192139.n7JLdSDH015362@scc.eng.rpath.com> changeset: 4b50bc0b76c1 user: Elliot Peele <http://issues.rpath.com/> date: Tue, 17 Jun 2008 11:30:01 -0400 add exception for no manifest in source component committer: Elliot Peele <http://issues.rpath.com/> diff --git a/updatebot/errors.py b/updatebot/errors.py --- a/updatebot/errors.py +++ b/updatebot/errors.py @@ -93,6 +93,15 @@ level group trove than expected. """ +class NoManifestFoundError(UnhandledUpdateError): + """ + NoManifestFoundError, raised when the bot checks out a source component + and doesn't find a manifest file. + """ + + _params = ['pkgname', 'dir'] + _template = 'No manifest was found for %(pkgname)s in directory %(dir)s' + class AdvisoryError(UnhandledUpdateError): """ Base error for other advisory errors to inherit from. From johnsonm at rpath.com Wed Aug 19 17:39:49 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:39:49 +0000 Subject: mirrorball: add script for running bot Message-ID: <200908192139.n7JLdnNh016198@scc.eng.rpath.com> changeset: 045b571d2dd0 user: Elliot Peele <http://issues.rpath.com/> date: Wed, 09 Jul 2008 14:25:04 -0400 add script for running bot committer: Elliot Peele <http://issues.rpath.com/> diff --git a/scripts/test.py b/scripts/test.py new file mode 100755 --- /dev/null +++ b/scripts/test.py @@ -0,0 +1,21 @@ +#!/usr/bin/python + +import os +import sys + +sys.path.insert(0, os.environ['HOME'] + '/hg/rpath-xmllib') +sys.path.insert(0, os.environ['HOME'] + '/hg/conary') +sys.path.insert(0, os.environ['HOME'] + '/hg/mirrorball') + +from conary.lib import util +sys.excepthook = util.genExcepthook() + +from updatebot import bot, config, log + +log.addRootLogger() +cfg = config.UpdateBotConfig() +cfg.read(os.environ['HOME'] + '/hg/mirrorball/config/updatebotrc') +obj = bot.Bot(cfg) +obj.run() + +import epdb ; epdb.st() From johnsonm at rpath.com Wed Aug 19 17:40:44 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:40:44 +0000 Subject: mirrorball: add jobid to failed jobs Message-ID: <200908192140.n7JLeiVu018413@scc.eng.rpath.com> changeset: 1781fcc510d1 user: Elliot Peele <https://issues.rpath.com/> date: Wed, 10 Sep 2008 18:35:29 -0400 add jobid to failed jobs committer: Elliot Peele <https://issues.rpath.com/> diff --git a/updatebot/build.py b/updatebot/build.py --- a/updatebot/build.py +++ b/updatebot/build.py @@ -141,13 +141,13 @@ for trv, jobId in jobs.iteritems(): job = self._helper.getJob(jobId) if job.isFailed(): - failed.add(trv) + failed.add((trv, jobId)) elif job.isFinished(): try: res = self.commit(jobId) results.update(res) except JobFailedError: - failed.add(trv) + failed.add((trv, jobId)) return results, failed From johnsonm at rpath.com Wed Aug 19 17:42:37 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:42:37 +0000 Subject: mirrorball: use buildmany2 and sort packages Message-ID: <200908192142.n7JLgbta022966@scc.eng.rpath.com> changeset: c1b463b280dc user: Elliot Peele <https://issues.rpath.com/> date: Tue, 24 Mar 2009 13:11:27 -0400 use buildmany2 and sort packages committer: Elliot Peele <https://issues.rpath.com/> diff --git a/updatebot/bot.py b/updatebot/bot.py --- a/updatebot/bot.py +++ b/updatebot/bot.py @@ -102,10 +102,10 @@ if not rebuild: # Build all newly imported packages. - trvMap, failed = self._builder.buildmany(toBuild) + trvMap, failed = self._builder.buildmany2(sorted(toBuild)) else: # ReBuild all packages. - trvMap = self._builder.buildsplitarch(toBuild) + trvMap = self._builder.buildsplitarch(sorted(toBuild)) log.info('import completed successfully') log.info('imported %s source packages' % (len(toBuild), )) From johnsonm at rpath.com Wed Aug 19 17:41:14 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:41:14 +0000 Subject: mirrorball: add anaconda-templates Message-ID: <200908192141.n7JLfE8S019641@scc.eng.rpath.com> changeset: 3ac8b222bdd1 user: Elliot Peele <https://issues.rpath.com/> date: Tue, 04 Nov 2008 15:59:52 -0500 add anaconda-templates committer: Elliot Peele <https://issues.rpath.com/> diff --git a/scripts/promote-centos.sh b/scripts/promote-centos.sh --- a/scripts/promote-centos.sh +++ b/scripts/promote-centos.sh @@ -20,6 +20,7 @@ group-appliance:source=centos.rpath.com at rpath:centos-5-devel \ platform-definition:source=centos.rpath.com at rpath:centos-5-devel \ kernel=centos.rpath.com at rpath:centos-5-devel \ + anaconda-templates=centos.rpath.com at rpath:centos-5-devel \ group-os=centos.rpath.com at rpath:centos-5-devel \ /centos.rpath.com at rpath:centos-5-devel--/centos.rpath.com at rpath:centos-5 \ /centos.rpath.com at rpath:centos-devel//centos-5-devel--/centos.rpath.com at rpath:centos-devel//centos-5 \ From johnsonm at rpath.com Wed Aug 19 17:42:59 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:42:59 +0000 Subject: mirrorball: add filters for advisory exceptions Message-ID: <200908192142.n7JLgxYd023834@scc.eng.rpath.com> changeset: b41a266d2878 user: Elliot Peele <https://issues.rpath.com/> date: Wed, 13 May 2009 11:44:45 -0400 add filters for advisory exceptions committer: Elliot Peele <https://issues.rpath.com/> diff --git a/updatebot/advisories/centos.py b/updatebot/advisories/centos.py --- a/updatebot/advisories/centos.py +++ b/updatebot/advisories/centos.py @@ -16,6 +16,7 @@ Advisory module for CentOS. """ +import re import os import pmap import logging @@ -115,6 +116,11 @@ # W0613 - Unused argument binPkg # pylint: disable-msg=W0613 + for fltr in self._cfg.advisoryException: + path, exp = fltr[0].split() + if path in binPkg.location and re.match(exp, binPkg.name): + return True + return False def _isUpdatesRepo(self, binPkg): From johnsonm at rpath.com Wed Aug 19 17:39:04 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:39:04 +0000 Subject: mirrorball: switch to the correct list type Message-ID: <200908192139.n7JLd4LY014395@scc.eng.rpath.com> changeset: 453b44dc6166 user: Elliot Peele <http://issues.rpath.com/> date: Mon, 02 Jun 2008 21:28:20 -0400 switch to the correct list type add config option for topGroup committer: Elliot Peele <http://issues.rpath.com/> diff --git a/updatebot/config.py b/updatebot/config.py --- a/updatebot/config.py +++ b/updatebot/config.py @@ -18,6 +18,7 @@ from conary.lib import cfg from conary.lib.cfgtypes import CfgString, CfgList +from rmake.build.buildcfg import CfgTroveSpec class UpdateBotConfig(cfg.SectionedConfigFile): ''' @@ -32,4 +33,7 @@ commitMessage = (CfgString, 'Automated commit by updateBot') repositoryUrl = CfgString - repositoryPaths = CfgList(CfgLineList(CfgString)) + repositoryPaths = (CfgList(CfgString), ['/']) + + topGroup = CfgTroveSpec + From johnsonm at rpath.com Wed Aug 19 17:41:58 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:41:58 +0000 Subject: mirrorball: fix bugs in checkout cache handling Message-ID: <200908192141.n7JLfwq0021383@scc.eng.rpath.com> changeset: e3ee4305ca90 user: Elliot Peele <https://issues.rpath.com/> date: Sun, 11 Jan 2009 23:15:16 -0500 fix bugs in checkout cache handling committer: Elliot Peele <https://issues.rpath.com/> diff --git a/updatebot/conaryhelper.py b/updatebot/conaryhelper.py --- a/updatebot/conaryhelper.py +++ b/updatebot/conaryhelper.py @@ -302,6 +302,7 @@ error=False) # Commit to repository. + recipeDir = self._checkoutCache[pkgname] self._commit(recipeDir, commitMessage) # Get new version of the source trove. @@ -389,7 +390,7 @@ del self._checkoutCache[pkgDir] finally: os.chdir(cwd) - util.rmtree(recipeDir) + util.rmtree(pkgDir) @staticmethod def _addFile(pkgDir, fileName): From johnsonm at rpath.com Wed Aug 19 17:43:01 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:43:01 +0000 Subject: mirrorball: ignore epoch if it is not set Message-ID: <200908192143.n7JLh1VY023936@scc.eng.rpath.com> changeset: 58582e8d86df user: Elliot Peele <https://issues.rpath.com/> date: Mon, 08 Jun 2009 22:06:48 -0400 ignore epoch if it is not set committer: Elliot Peele <https://issues.rpath.com/> diff --git a/updatebot/lib/util.py b/updatebot/lib/util.py --- a/updatebot/lib/util.py +++ b/updatebot/lib/util.py @@ -58,9 +58,12 @@ @type b: repomd.packagexml._Package """ - epochcmp = rpmvercmp(a.epoch, b.epoch) - if epochcmp != 0: - return epochcmp + # Not all "packages" have epoch set. If comparing between two packages, at + # least one without an epoch specified, ignore epoch. + if a.epoch is not None and b.epoch is not None: + epochcmp = rpmvercmp(a.epoch, b.epoch) + if epochcmp != 0: + return epochcmp vercmp = rpmvercmp(a.version, b.version) if vercmp != 0: From johnsonm at rpath.com Wed Aug 19 17:40:45 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:40:45 +0000 Subject: mirrorball: hack to workaround ubuntu branching structure Message-ID: <200908192140.n7JLekX3018498@scc.eng.rpath.com> changeset: e06c847b4557 user: Elliot Peele <https://issues.rpath.com/> date: Thu, 11 Sep 2008 15:52:11 -0400 hack to workaround ubuntu branching structure committer: Elliot Peele <https://issues.rpath.com/> diff --git a/updatebot/conaryhelper.py b/updatebot/conaryhelper.py --- a/updatebot/conaryhelper.py +++ b/updatebot/conaryhelper.py @@ -356,6 +356,13 @@ """ versions = self._getVersionsByName('%s:source' % pkgname) + + # FIXME: This is a hack to work around the fact that ubuntu has some + # shadows and packages that overlap on the label, + # _getVersionsByName needs to be smarter. + if len(versions) > 1: + versions = [ x for x in versions if not x.isShadow() ] + assert len(versions) in (0, 1) if len(versions) == 1: From johnsonm at rpath.com Wed Aug 19 17:39:55 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:39:55 +0000 Subject: mirrorball: switch to four spaces instead of tabs Message-ID: <200908192140.n7JLduqE016453@scc.eng.rpath.com> changeset: 7e329008315b user: Elliot Peele <http://issues.rpath.com/> date: Mon, 14 Jul 2008 12:56:10 -0400 switch to four spaces instead of tabs committer: Elliot Peele <http://issues.rpath.com/> diff --git a/scripts/buildgroups b/scripts/buildgroups --- a/scripts/buildgroups +++ b/scripts/buildgroups @@ -41,4 +41,4 @@ for srcTrv in grpTrvMap.iterkeys(): print displayTrove(srcTrv) for binTrv in grpTrvMap[srcTrv]: - print "\t", displayTrove(binTrv) + print " " * 4, displayTrove(binTrv) diff --git a/scripts/buildpackages b/scripts/buildpackages --- a/scripts/buildpackages +++ b/scripts/buildpackages @@ -43,4 +43,4 @@ for srcTrv in trvMap.iterkeys(): print displayTrove(srcTrv) for binTrv in trvMap[srcTrv]: - print "\t", displayTrove(binTrv) + print " " * 4, displayTrove(binTrv) From johnsonm at rpath.com Wed Aug 19 17:42:56 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:42:56 +0000 Subject: mirrorball: do not load default config Message-ID: <200908192142.n7JLgu69023698@scc.eng.rpath.com> changeset: 1df5091769b8 user: Elliot Peele <https://issues.rpath.com/> date: Wed, 06 May 2009 16:38:19 -0400 do not load default config committer: Elliot Peele <https://issues.rpath.com/> diff --git a/scripts/buildautoloadrecipes b/scripts/buildautoloadrecipes --- a/scripts/buildautoloadrecipes +++ b/scripts/buildautoloadrecipes @@ -30,7 +30,10 @@ exit 1 fi -buildLabel=$(conary config --config-file $platformConfig | grep buildLabel | awk '{print $2}') +buildLabel=$(conary config \ + --skip-default-config \ + --config-file $platformConfig \ + | grep buildLabel | awk '{print $2}') autoLoad="" autoLoadPackages=" @@ -49,6 +52,7 @@ for pkg in $autoLoadPackages ; do eval cvc cook \ + --skip-default-config \ --config-file $platformConfig \ --context $context \ --no-deps \ From johnsonm at rpath.com Wed Aug 19 17:39:26 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:39:26 +0000 Subject: mirrorball: add arch when sorting Message-ID: <200908192139.n7JLdQ5H015310@scc.eng.rpath.com> changeset: 401a56da1b15 user: Elliot Peele <http://issues.rpath.com/> date: Mon, 16 Jun 2008 20:22:06 -0400 add arch when sorting committer: Elliot Peele <http://issues.rpath.com/> diff --git a/repomd/packagexml.py b/repomd/packagexml.py --- a/repomd/packagexml.py +++ b/repomd/packagexml.py @@ -124,7 +124,7 @@ raise UnknownElementError(child) def __str__(self): - return '-'.join([self.name, self.epoch, self.version, self.release]) + return '-'.join([self.name, self.epoch, self.version, self.release, self.arch]) def __repr__(self): return os.path.basename(self.location) @@ -138,6 +138,10 @@ if pkgvercmp != 0: return pkgvercmp + archcmp = cmp(self.arch, other.arch) + if archcmp != 0: + return archcmp + return cmp(self.location, other.location) From johnsonm at rpath.com Wed Aug 19 17:42:29 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:42:29 +0000 Subject: mirrorball: write buildrequires correctly Message-ID: <200908192142.n7JLgTD7022626@scc.eng.rpath.com> changeset: 04006dc80950 user: Elliot Peele <https://issues.rpath.com/> date: Tue, 24 Feb 2009 16:43:10 -0500 write buildrequires correctly add initial support for generating metadata for packages other than apt committer: Elliot Peele <https://issues.rpath.com/> diff --git a/updatebot/update.py b/updatebot/update.py --- a/updatebot/update.py +++ b/updatebot/update.py @@ -350,7 +350,8 @@ # FIXME: This is apt specific for now. Once repomd has been rewritten # to use something other than rpath-xmllib we should be able to # convert this to xobj. - if self._cfg.repositoryFormat == 'apt': + if (self._cfg.repositoryFormat == 'apt' or + self._cfg.writePackageMetadata): metadata = self._getMetadataFromPkgSource(srcPkg) self._conaryhelper.setMetadata(nvf[0], metadata) From johnsonm at rpath.com Wed Aug 19 17:39:54 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:39:54 +0000 Subject: mirrorball: use sles Message-ID: <200908192139.n7JLds1O016385@scc.eng.rpath.com> changeset: 0d3cb91a4e12 user: Elliot Peele <http://issues.rpath.com/> date: Thu, 10 Jul 2008 18:09:13 -0400 use sles committer: Elliot Peele <http://issues.rpath.com/> diff --git a/scripts/buildgroups b/scripts/buildgroups --- a/scripts/buildgroups +++ b/scripts/buildgroups @@ -20,7 +20,7 @@ log.addRootLogger() cfg = config.UpdateBotConfig() -cfg.read(os.environ['HOME'] + '/hg/mirrorball/config/updatebotrc') +cfg.read(os.environ['HOME'] + '/hg/mirrorball/config/sles/updatebotrc') builder = build.Builder(cfg) diff --git a/scripts/buildpackages b/scripts/buildpackages --- a/scripts/buildpackages +++ b/scripts/buildpackages @@ -20,7 +20,7 @@ log.addRootLogger() cfg = config.UpdateBotConfig() -cfg.read(os.environ['HOME'] + '/hg/mirrorball/config/updatebotrc') +cfg.read(os.environ['HOME'] + '/hg/mirrorball/config/sles/updatebotrc') builder = build.Builder(cfg) From johnsonm at rpath.com Wed Aug 19 17:41:14 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:41:14 +0000 Subject: mirrorball: now that kernels are flavored, make sure to promote all flavors Message-ID: <200908192141.n7JLfEvd019624@scc.eng.rpath.com> changeset: 8e9e2ebc39b0 user: Elliot Peele <https://issues.rpath.com/> date: Mon, 03 Nov 2008 16:02:20 -0500 now that kernels are flavored, make sure to promote all flavors committer: Elliot Peele <https://issues.rpath.com/> diff --git a/scripts/promote-centos.sh b/scripts/promote-centos.sh --- a/scripts/promote-centos.sh +++ b/scripts/promote-centos.sh @@ -19,6 +19,7 @@ time cvc promote $cfg \ group-appliance:source=centos.rpath.com at rpath:centos-5-devel \ platform-definition:source=centos.rpath.com at rpath:centos-5-devel \ + kernel=centos.rpath.com at rpath:centos-5-devel \ group-os=centos.rpath.com at rpath:centos-5-devel \ /centos.rpath.com at rpath:centos-5-devel--/centos.rpath.com at rpath:centos-5 \ /centos.rpath.com at rpath:centos-devel//centos-5-devel--/centos.rpath.com at rpath:centos-devel//centos-5 \ From johnsonm at rpath.com Wed Aug 19 17:39:33 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:39:33 +0000 Subject: mirrorball: fixup version query Message-ID: <200908192139.n7JLdXEe015601@scc.eng.rpath.com> changeset: ea5bd172ca76 user: Elliot Peele <http://issues.rpath.com/> date: Mon, 23 Jun 2008 17:07:09 -0400 fixup version query committer: Elliot Peele <http://issues.rpath.com/> diff --git a/updatebot/conaryhelper.py b/updatebot/conaryhelper.py --- a/updatebot/conaryhelper.py +++ b/updatebot/conaryhelper.py @@ -286,7 +286,8 @@ @type targetLabel: conary Label object """ - fromLabel = trvLst[0][1].getTrailingRevision().label + # Assume that all troves are on the same label. + fromLabel = trvLst[0][1].trailingLabel() success, cs = client.createSiblingCloneChangeSet({fromLabel:targetLabel}, trvLst, cloneSources=True) @@ -306,4 +307,3 @@ self._repos.commitChangeSet(cs) return packageList - From johnsonm at rpath.com Wed Aug 19 17:39:22 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:39:22 +0000 Subject: mirrorball: clean up bin import method Message-ID: <200908192139.n7JLdMLN015140@scc.eng.rpath.com> changeset: ba1614e19f0c user: Elliot Peele <http://issues.rpath.com/> date: Mon, 16 Jun 2008 13:35:45 -0400 clean up bin import method committer: Elliot Peele <http://issues.rpath.com/> diff --git a/rpmimport/rpmsource.py b/rpmimport/rpmsource.py --- a/rpmimport/rpmsource.py +++ b/rpmimport/rpmsource.py @@ -88,11 +88,10 @@ srpm = package.sourcerpm longLoc = basePath + '/' + package.location package.location = longLoc - if self.rpmMap.has_key(srpm): - shortLoc = os.path.basename(package.location) - self.rpmMap[srpm][longLoc] = package - else: - self.rpmMap[srpm] = {longLoc: package} + + if srpm not in self.rpmMap: + self.rpmMap[srpm] = {} + self.rpmMap[srpm][longLoc] = package self.revMap[package.name] = srpm if package.name not in self.binNameMap: From johnsonm at rpath.com Wed Aug 19 17:42:20 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:42:20 +0000 Subject: mirrorball: only try to build 10 times before giving up Message-ID: <200908192142.n7JLgKNS022268@scc.eng.rpath.com> changeset: db76baee0719 user: Elliot Peele <https://issues.rpath.com/> date: Sun, 08 Feb 2009 04:16:21 +0000 only try to build 10 times before giving up committer: Elliot Peele <https://issues.rpath.com/> diff --git a/updatebot/build.py b/updatebot/build.py --- a/updatebot/build.py +++ b/updatebot/build.py @@ -463,18 +463,19 @@ self.jobId = None def run(self): - done = False while True: self.trv = self.toBuild.get() self.log('received trv') + retries = 10 built = False - while not built: + while not built and retries: try: self._doBuild() except Exception, e: built = False built = True + retries -= 1 self.toBuild.task_done() From johnsonm at rpath.com Wed Aug 19 17:42:55 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:42:55 +0000 Subject: mirrorball: make script work Message-ID: <200908192142.n7JLgtJU023681@scc.eng.rpath.com> changeset: c20abca30465 user: Elliot Peele <https://issues.rpath.com/> date: Wed, 06 May 2009 16:07:35 -0400 make script work committer: Elliot Peele <https://issues.rpath.com/> diff --git a/scripts/buildautoloadrecipes b/scripts/buildautoloadrecipes --- a/scripts/buildautoloadrecipes +++ b/scripts/buildautoloadrecipes @@ -30,6 +30,8 @@ exit 1 fi +buildLabel=$(conary config --config-file $platformConfig | grep buildLabel | awk '{print $2}') + autoLoad="" autoLoadPackages=" baserequires @@ -46,11 +48,11 @@ " for pkg in $autoLoadPackages ; do - cvc cook \ + eval cvc cook \ --config-file $platformConfig \ --context $context \ --no-deps \ $autoLoad \ - $pkg - autoLoad="$autoLoad --config 'autoLoadRecipes $pkg'" + $pkg --debug-all + autoLoad="$autoLoad --config \"autoLoadRecipes $pkg=$buildLabel\"" done From johnsonm at rpath.com Wed Aug 19 17:39:43 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:39:43 +0000 Subject: mirrorball: add some white space for readability Message-ID: <200908192139.n7JLdhSH015960@scc.eng.rpath.com> changeset: c11ebcea631f user: Elliot Peele <http://issues.rpath.com/> date: Fri, 27 Jun 2008 13:39:35 -0400 add some white space for readability committer: Elliot Peele <http://issues.rpath.com/> diff --git a/updatebot/build.py b/updatebot/build.py --- a/updatebot/build.py +++ b/updatebot/build.py @@ -158,11 +158,13 @@ startTime = time.time() job = self._helper.getJob(jobId) log.info('Starting commit of job %d', jobId) + self._helper.client.startCommit([jobId, ]) succeeded, data = commit.commitJobs(self._client, [job, ], self._rmakeCfg.reposName, self._cfg.commitMessage) + if not succeeded: self._helper.client.commitFailed([jobId, ], data) raise CommitFailedError(jobId=job.jobId, why=data) From johnsonm at rpath.com Wed Aug 19 17:40:29 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:40:29 +0000 Subject: mirrorball: add a version constant Message-ID: <200908192140.n7JLeTAq017801@scc.eng.rpath.com> changeset: 50f998c974f1 user: Elliot Peele <http://issues.rpath.com/> date: Mon, 25 Aug 2008 21:45:10 -0400 add a version constant committer: Elliot Peele <http://issues.rpath.com/> diff --git a/updatebot/constants.py b/updatebot/constants.py new file mode 100644 --- /dev/null +++ b/updatebot/constants.py @@ -0,0 +1,15 @@ +# +# Copyright (c) 2008 rPath, Inc. +# +# This program is distributed under the terms of the Common Public License, +# version 1.0. A copy of this license should have been distributed with this +# source file in a file called LICENSE. If it is not present, the license +# is always available at http://www.rpath.com/permanent/licenses/CPL-1.0. +# +# This program is distributed in the hope that it will be useful, but +# without any warranty; without even the implied warranty of merchantability +# or fitness for a particular purpose. See the Common Public License for +# full details. +# + +version = '0.1' From johnsonm at rpath.com Wed Aug 19 17:40:31 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:40:31 +0000 Subject: mirrorball: load from $HOME Message-ID: <200908192140.n7JLeVF2017903@scc.eng.rpath.com> changeset: 0d791ec05254 user: Elliot Peele <http://bugs.rpath.com/> date: Thu, 28 Aug 2008 10:30:22 -0400 load from $HOME committer: Elliot Peele <http://bugs.rpath.com/> diff --git a/scripts/pkgsource.py b/scripts/pkgsource.py --- a/scripts/pkgsource.py +++ b/scripts/pkgsource.py @@ -1,9 +1,14 @@ #!/usr/bin/python +import os import sys from conary.lib import util sys.excepthook = util.genExcepthook() +sys.path.insert(0, os.environ['HOME'] + '/hg/rpath-xmllib') +sys.path.insert(0, os.environ['HOME'] + '/hg/conary') +sys.path.insert(0, os.environ['HOME'] + '/hg/mirrorball') + import logging import updatebot.log @@ -15,7 +20,7 @@ from updatebot import pkgsource cfg = config.UpdateBotConfig() -cfg.read('/data/hg/mirrorball/config/ubuntu/updatebotrc') +cfg.read(os.environ['HOME'] + '/hg/mirrorball/config/ubuntu/updatebotrc') client = Client('http://i.rdu.rpath.com/ubuntu') pkgSource = pkgsource.PackageSource(cfg) From johnsonm at rpath.com Wed Aug 19 17:42:09 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:42:09 +0000 Subject: mirrorball: add an extra check Message-ID: <200908192142.n7JLgAVD021826@scc.eng.rpath.com> changeset: c1d226bef623 user: Elliot Peele <https://issues.rpath.com/> date: Fri, 23 Jan 2009 14:06:04 -0500 add an extra check committer: Elliot Peele <https://issues.rpath.com/> diff --git a/scripts/pcheck.py b/scripts/pcheck.py --- a/scripts/pcheck.py +++ b/scripts/pcheck.py @@ -67,6 +67,11 @@ log.warning('not promoting %s=%s[%s]' % (name, version, flavor)) +# Ask before moving on. +okay = conaryclient.cmdline.askYn('continue with clone? [y/N]', default=False) +if not okay: + sys.exit(0) + # Make the promote changeset. log.info('Creating promote changeset') cb = conaryclient.callbacks.CloneCallback(cfg, 'automated commit') @@ -83,7 +88,7 @@ sys.exit(1) # Ask before committing. -okay = conaryclient.cmdline.askYn('continue with clone? [y/N]', default=False) +okay = conaryclient.cmdline.askYn('commit changset? [y/N]', default=False) # Commit changeset. if okay: From johnsonm at rpath.com Wed Aug 19 17:39:12 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:39:12 +0000 Subject: mirrorball: add a repr method Message-ID: <200908192139.n7JLdCsm014717@scc.eng.rpath.com> changeset: 5f8919137eb0 user: Elliot Peele <http://issues.rpath.com/> date: Tue, 10 Jun 2008 16:25:29 -0400 add a repr method addapt str method to slots committer: Elliot Peele <http://issues.rpath.com/> diff --git a/repomd/packagexml.py b/repomd/packagexml.py --- a/repomd/packagexml.py +++ b/repomd/packagexml.py @@ -18,6 +18,8 @@ __all__ = ('PackageXmlMixIn', ) +import os + from rpath_common.xmllib import api1 as xmllib from repomd.errors import UnknownElementError, UnknownAttributeError @@ -120,7 +122,10 @@ raise UnknownElementError(child) def __str__(self): - return '%(name)s-%(version)s-%(release)s' % self.__dict__ + return '-'.join([self.name, self.epoch, self.version, self.release]) + + def __repr__(self): + return os.path.basename(self.location) def __hash__(self): return hash((self.name, self.epoch, self.version, self.release, From johnsonm at rpath.com Wed Aug 19 17:42:24 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:42:24 +0000 Subject: mirrorball: log number of failed packages Message-ID: <200908192142.n7JLgOu0022438@scc.eng.rpath.com> changeset: faf3f391ee49 user: Elliot Peele <https://issues.rpath.com/> date: Thu, 12 Feb 2009 16:03:40 -0500 log number of failed packages switch back to non threaded builds committer: Elliot Peele <https://issues.rpath.com/> diff --git a/updatebot/bot.py b/updatebot/bot.py --- a/updatebot/bot.py +++ b/updatebot/bot.py @@ -80,9 +80,12 @@ toBuild, fail = self._updater.create(toPackage, buildAll=rebuild) + log.info('failed to create %s packages' % len(fail)) + log.info('found %s packages to build' % len(toBuild)) + if not rebuild: # Build all newly imported packages. - trvMap, failed = self._builder.buildmany2(toBuild) + trvMap, failed = self._builder.buildmany(toBuild) else: # ReBuild all packages. trvMap = self._builder.buildsplitarch(toBuild) From johnsonm at rpath.com Wed Aug 19 17:40:54 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:40:54 +0000 Subject: mirrorball: the beginings of a buildpackages command Message-ID: <200908192140.n7JLesMZ018855@scc.eng.rpath.com> changeset: b78ad20a402f user: Elliot Peele <https://issues.rpath.com/> date: Fri, 26 Sep 2008 14:44:32 -0400 the beginings of a buildpackages command committer: Elliot Peele <https://issues.rpath.com/> diff --git a/updatebot/cmdline/command.py b/updatebot/cmdline/command.py --- a/updatebot/cmdline/command.py +++ b/updatebot/cmdline/command.py @@ -27,7 +27,7 @@ _commands.append(cmd) -class BotCommand(options.AbstractCommand): +class _BotCommand(options.AbstractCommand): defaultGroup = 'Common Options' docs = {'config' : (VERBOSE_HELP, @@ -58,3 +58,15 @@ def processConfigOptions(self, cfg, cfgMap, argSet): pass + + +class BuildPackageCommand(_BotCommand): + """ + Build a list of packages. + """ + + commands = [ 'buildpackages', ] + help = 'build packages' + + def runCommand(self, client, cfg, argSet, args): + pass From johnsonm at rpath.com Wed Aug 19 17:39:33 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:39:33 +0000 Subject: mirrorball: add config options for advisories Message-ID: <200908192139.n7JLdXeH015618@scc.eng.rpath.com> changeset: f3a48d45e3c0 user: Elliot Peele <http://issues.rpath.com/> date: Mon, 23 Jun 2008 17:07:22 -0400 add config options for advisories committer: Elliot Peele <http://issues.rpath.com/> diff --git a/updatebot/config.py b/updatebot/config.py --- a/updatebot/config.py +++ b/updatebot/config.py @@ -30,6 +30,9 @@ # R0904 - to many public methods # pylint: disable-msg=R0904 + # name of the product to use in advisories + productName = CfgString + # path to configuration files (conaryrc, rmakerc) configPath = CfgString @@ -65,3 +68,9 @@ # flavors to build the source group. groupFlavors = (CfgList(CfgFlavor), []) + + # email information for sending advisories + emailFrom = CfgString + emailTo = (CfgList(CfgString), []) + emailBcc = (CfgList(CfgString), []) + smtpServer = CfgString From johnsonm at rpath.com Wed Aug 19 17:40:53 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:40:53 +0000 Subject: mirrorball: Backed out changeset d8a628d10787 Message-ID: <200908192140.n7JLerUm018787@scc.eng.rpath.com> changeset: 8c5e586df087 user: Jeff Uphoff <https://issues.rpath.com/> date: Wed, 24 Sep 2008 10:39:47 -0400 Backed out changeset d8a628d10787 committer: Jeff Uphoff <https://issues.rpath.com/> diff --git a/scripts/buildgroups b/scripts/buildgroups --- a/scripts/buildgroups +++ b/scripts/buildgroups @@ -20,7 +20,7 @@ log.addRootLogger() cfg = config.UpdateBotConfig() -cfg.read(os.environ['HOME'] + '/hg/mirrorball/config/sles/updatebotrc') +cfg.read(os.environ['HOME'] + '/hg/mirrorball/config/ubuntu/updatebotrc') builder = build.Builder(cfg) diff --git a/scripts/buildpackages b/scripts/buildpackages --- a/scripts/buildpackages +++ b/scripts/buildpackages @@ -23,7 +23,7 @@ log.addRootLogger() cfg = config.UpdateBotConfig() -cfg.read(os.environ['HOME'] + '/hg/mirrorball/config/sles/updatebotrc') +cfg.read(os.environ['HOME'] + '/hg/mirrorball/config/ubuntu/updatebotrc') builder = build.Builder(cfg) From johnsonm at rpath.com Wed Aug 19 17:39:38 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:39:38 +0000 Subject: mirrorball: add config option for patch filters Message-ID: <200908192139.n7JLdcDD015755@scc.eng.rpath.com> changeset: f95e88803de5 user: Elliot Peele <http://issues.rpath.com/> date: Tue, 24 Jun 2008 14:51:24 -0400 add config option for patch filters committer: Elliot Peele <http://issues.rpath.com/> diff --git a/updatebot/config.py b/updatebot/config.py --- a/updatebot/config.py +++ b/updatebot/config.py @@ -18,7 +18,7 @@ from conary.lib import cfg from conary.conarycfg import CfgFlavor, CfgLabel -from conary.lib.cfgtypes import CfgString, CfgList +from conary.lib.cfgtypes import CfgString, CfgList, CfgRegExp from rmake.build.buildcfg import CfgTroveSpec @@ -63,6 +63,9 @@ # default advisory message to send with these packages. advisoryException = (CfgList(CfgList(CfgString)), []) + # Filter out patches with matching descriptions or summaries. + patchFilter = (CfgList(CfgRegExp), []) + # list of contexts that all packages are built in. archContexts = CfgList(CfgString) From johnsonm at rpath.com Wed Aug 19 17:39:09 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:39:09 +0000 Subject: mirrorball: remove main, it was only for initial testing Message-ID: <200908192139.n7JLd9HS014590@scc.eng.rpath.com> changeset: fbac457976dd user: Elliot Peele <http://issues.rpath.com/> date: Tue, 03 Jun 2008 11:33:49 -0400 remove main, it was only for initial testing committer: Elliot Peele <http://issues.rpath.com/> diff --git a/updatebot/conaryhelper.py b/updatebot/conaryhelper.py --- a/updatebot/conaryhelper.py +++ b/updatebot/conaryhelper.py @@ -127,20 +127,3 @@ troveCs = cs.getNewTroveVersion(name, version, flavor) trv = trove.Trove(troveCs, skipIntegrityChecks=True) return trv - - -if __name__ == '__main__': - import sys - from conary.lib import util as cnyutil - sys.excepthook = cnyutil.genExcepthook() - - from updatebot import config - Cfg = config.UpdateBotConfig() - Cfg.topGroup = ('group-dist', 'sle.rpath.com at rpath:sle-devel', None) - Cfg.configPath = '../' - - Obj = ConaryHelper(Cfg) - SrcTrvs = Obj.getSourceTroves() - - import epdb - epdb.st() From johnsonm at rpath.com Wed Aug 19 17:38:47 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:38:47 +0000 Subject: mirrorball: switch to using pylintrc from product-definition Message-ID: <200908192138.n7JLclM8013628@scc.eng.rpath.com> changeset: 1e4a7fb34503 user: Elliot Peele <http://issues.rpath.com/> date: Tue, 27 May 2008 18:16:36 -0400 switch to using pylintrc from product-definition committer: Elliot Peele <http://issues.rpath.com/> diff --git a/pylint/pylintrc b/pylint/pylintrc --- a/pylint/pylintrc +++ b/pylint/pylintrc @@ -1,6 +1,6 @@ [BASIC] # Regular expression which should only match correct class names -class-rgx=[A-Z][a-zA-Z0-9]+$ +class-rgx=[A-Z_][a-zA-Z0-9]+$ # Regular expression which should only match correct function names function-rgx=[a-z_][a-zA-Z0-9]*$ @@ -21,7 +21,10 @@ attr-rgx=[a-z_][a-zA-Z0-9_]{2,30}$ # Good variable names which should always be accepted, separated by a comma -good-names=i,j,k,n,v,f,ex,Run,_,db,cu,__ +good-names=XmlObj,Elem + + + [REPORTS] files-output=yes @@ -29,4 +32,5 @@ include-ids=yes [MESSAGES CONTROL] -disable-msg=W0142,I0011 +disable-msg=W0102,W0142 +#,I0011 From johnsonm at rpath.com Wed Aug 19 17:40:04 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:40:04 +0000 Subject: mirrorball: only build new packages Message-ID: <200908192140.n7JLe5mJ016813@scc.eng.rpath.com> changeset: 5fe3951c9ee2 user: Elliot Peele <http://issues.rpath.com/> date: Tue, 29 Jul 2008 22:08:45 -0400 only build new packages committer: Elliot Peele <http://issues.rpath.com/> diff --git a/updatebot/update.py b/updatebot/update.py --- a/updatebot/update.py +++ b/updatebot/update.py @@ -233,10 +233,10 @@ else: version = version[0] -# if not self._conaryhelper._getVersionsByName(pkg.name): - toBuild.add((pkg.name, version, None)) -# else: -# log.info('not building %s' % pkg.name) + if not self._conaryhelper._getVersionsByName(pkg.name): + toBuild.add((pkg.name, version, None)) + else: + log.info('not building %s' % pkg.name) except Exception, e: log.error('failed to import %s: %s' % (pkg, e)) fail.add((pkg, e)) From johnsonm at rpath.com Wed Aug 19 17:39:52 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:39:52 +0000 Subject: mirrorball: remove breakpoint, update comment Message-ID: <200908192139.n7JLdqOm016283@scc.eng.rpath.com> changeset: a84c3cb85c08 user: Elliot Peele <http://issues.rpath.com/> date: Wed, 09 Jul 2008 15:03:19 -0400 remove breakpoint, update comment committer: Elliot Peele <http://issues.rpath.com/> diff --git a/updatebot/bot.py b/updatebot/bot.py --- a/updatebot/bot.py +++ b/updatebot/bot.py @@ -100,8 +100,8 @@ log.info('no updates available') return - # Don't populate the patch source until we know that there are - # updates. + # Populate patch source not that we know that there are updates + # available. self._populatePatchSource() # Check to see if advisories exist for all required packages. @@ -133,7 +133,6 @@ newTroves = self._updater.publish(toPublish, expected, self._cfg.targetLabel) - import epdb; epdb.st() # Send advisories. self._advisor.send(toAdvise, newTroves) From johnsonm at rpath.com Wed Aug 19 17:42:56 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:42:56 +0000 Subject: mirrorball: use system libs, take platform as arg Message-ID: <200908192142.n7JLguXc023715@scc.eng.rpath.com> changeset: ae0e3e50c4b2 user: Elliot Peele <https://issues.rpath.com/> date: Mon, 11 May 2009 15:09:16 -0400 use system libs, take platform as arg committer: Elliot Peele <https://issues.rpath.com/> diff --git a/scripts/update b/scripts/update --- a/scripts/update +++ b/scripts/update @@ -3,11 +3,7 @@ import os import sys -sys.path.insert(0, os.environ['HOME'] + '/hg/rpath-xmllib') -sys.path.insert(0, os.environ['HOME'] + '/hg/conary') sys.path.insert(0, os.environ['HOME'] + '/hg/mirrorball') -sys.path.insert(0, os.environ['HOME'] + '/hg/rmake') -sys.path.insert(0, os.environ['HOME'] + '/hg/xobj/py') from updatebot import log log.addRootLogger() @@ -18,6 +14,6 @@ from updatebot import bot, config cfg = config.UpdateBotConfig() -cfg.read(os.environ['HOME'] + '/hg/mirrorball/config/ubuntu/updatebotrc') +cfg.read(os.environ['HOME'] + '/hg/mirrorball/config/%s/updatebotrc' % sys.argv[1]) obj = bot.Bot(cfg) obj.update() From johnsonm at rpath.com Wed Aug 19 17:42:23 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:42:23 +0000 Subject: mirrorball: add some logging Message-ID: <200908192142.n7JLgNJZ022370@scc.eng.rpath.com> changeset: 96ef86599c98 user: Elliot Peele <https://issues.rpath.com/> date: Mon, 09 Feb 2009 10:51:30 -0500 add some logging committer: Elliot Peele <https://issues.rpath.com/> diff --git a/updatebot/build.py b/updatebot/build.py --- a/updatebot/build.py +++ b/updatebot/build.py @@ -477,6 +477,7 @@ self._doBuild() except Exception, e: built = False + self.log('traceback while building %s, retrying' % e) continue built = True @@ -509,8 +510,10 @@ time.sleep(20 + self.offset) job = self.builder._helper.getJob(self.jobId) except xml.parsers.expat.ExpatError, e: + self.log('bad xml from server, retrying') return False, None except Exception, e: + self.log('unknown error querying server: %s' % e) return False, None return True, job From johnsonm at rpath.com Wed Aug 19 17:43:05 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:43:05 +0000 Subject: mirrorball: add handling for new key Message-ID: <200908192143.n7JLh5qV024090@scc.eng.rpath.com> changeset: 4efcab30b645 user: Elliot Peele <https://issues.rpath.com/> date: Mon, 15 Jun 2009 22:26:23 -0400 add handling for new key committer: Elliot Peele <https://issues.rpath.com/> diff --git a/repomd/updateinfoxml.py b/repomd/updateinfoxml.py --- a/repomd/updateinfoxml.py +++ b/repomd/updateinfoxml.py @@ -174,7 +174,7 @@ __slots__ = ('filename', 'name', 'arch', 'version', 'release', 'reboot_suggested', 'restart_suggested', 'epoch', 'location', - 'summary') + 'summary', 'relogin_suggested') # All attributes are defined in __init__ by iterating over __slots__, # this confuses pylint. @@ -193,6 +193,8 @@ self.reboot_suggested = child.finalize() elif n == 'restart_suggested': self.restart_suggested = child.finalize() + elif n == 'relogin_suggested': + self.relogin_suggested = child.finalize() else: raise UnknownElementError(child) From johnsonm at rpath.com Wed Aug 19 17:41:03 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:41:03 +0000 Subject: mirrorball: add platform argument Message-ID: <200908192141.n7JLf3pb019213@scc.eng.rpath.com> changeset: d7c18cc86825 user: Elliot Peele <https://issues.rpath.com/> date: Wed, 22 Oct 2008 15:21:23 -0400 add platform argument committer: Elliot Peele <https://issues.rpath.com/> diff --git a/scripts/buildpackages b/scripts/buildpackages --- a/scripts/buildpackages +++ b/scripts/buildpackages @@ -21,15 +21,19 @@ from updatebot import build from updatebot import config +if len(sys.argv) < 3 or sys.argv[1] not in os.listdir(os.environ['HOME'] + '/hg/mirrorball/config'): + print 'usage: buildpackages <platform> [pkg1, pkg2, ...]' + sys.exit(1) + log.addRootLogger() cfg = config.UpdateBotConfig() -cfg.read(os.environ['HOME'] + '/hg/mirrorball/config/ubuntu/updatebotrc') +cfg.read(os.environ['HOME'] + '/hg/mirrorball/config/%s/updatebotrc' % sys.argv[1]) builder = build.Builder(cfg) trvs = set() label = cfg.topSourceGroup[1] -for pkg in sys.argv[1:]: +for pkg in sys.argv[2:]: trvs.add((pkg, label, None)) trvMap = builder.build(trvs) From johnsonm at rpath.com Wed Aug 19 17:38:49 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:38:49 +0000 Subject: mirrorball: add xmllib path to environment Message-ID: <200908192138.n7JLcnxe013721@scc.eng.rpath.com> changeset: 295192696335 user: Elliot Peele <http://issues.rpath.com/> date: Tue, 27 May 2008 22:09:37 -0400 add xmllib path to environment committer: Elliot Peele <http://issues.rpath.com/> diff --git a/test/testsuite.py b/test/testsuite.py --- a/test/testsuite.py +++ b/test/testsuite.py @@ -68,12 +68,15 @@ # set default RMAKE_TEST_PATH, if it was not set. rmakeTestPath = setPathFromEnv('RMAKE_TEST_PATH', 'rmake-private/test') + # set default XMLLIB_PATH, if it was not set. + xmllibPath = setPathFromEnv('XMLLIB_PATH', 'rpath-xmllib') + testDir = os.path.dirname(os.path.realpath(__file__)) # Insert the following paths into the python path and sys path in # listed order. paths = (mirrorballPath, rmakePath, testDir, conaryPath, conaryTestPath, - rmakeTestPath) + rmakeTestPath, xmllibPath) pythonPath = os.environ.get('PYTHONPATH', "") for p in reversed(paths): if p in sys.path: From johnsonm at rpath.com Wed Aug 19 17:42:29 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:42:29 +0000 Subject: mirrorball: handle errors a bit better Message-ID: <200908192142.n7JLgT3X022609@scc.eng.rpath.com> changeset: e6201fcbdf08 user: Elliot Peele <https://issues.rpath.com/> date: Tue, 24 Feb 2009 16:42:46 -0500 handle errors a bit better committer: Elliot Peele <https://issues.rpath.com/> diff --git a/updatebot/build.py b/updatebot/build.py --- a/updatebot/build.py +++ b/updatebot/build.py @@ -44,8 +44,10 @@ ret = func(self, *args, **kwargs) return ret except xml.parsers.expat.ExpatError, e: - exception = e + exception = None except Exception, e: + if retry is True: + raise exception = e if type(retry) == int: @@ -107,7 +109,7 @@ troves = self._formatInput(troveSpecs) jobId = self._startJob(troves) - self._monitorJob(jobId) + self._monitorJob(jobId, retry=2) self._sanityCheckJob(jobId) trvMap = self._commitJob(jobId) ret = self._formatOutput(trvMap) From johnsonm at rpath.com Wed Aug 19 17:39:03 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:39:03 +0000 Subject: mirrorball: add config options for yum repository url and base paths Message-ID: <200908192139.n7JLd3Zx014322@scc.eng.rpath.com> changeset: d4dcb86fa16c user: Elliot Peele <http://issues.rpath.com/> date: Mon, 02 Jun 2008 13:14:39 -0400 add config options for yum repository url and base paths committer: Elliot Peele <http://issues.rpath.com/> diff --git a/updatebot/config.py b/updatebot/config.py --- a/updatebot/config.py +++ b/updatebot/config.py @@ -17,7 +17,7 @@ ''' from conary.lib import cfg -from conary.lib.cfgtypes import CfgString +from conary.lib.cfgtypes import CfgString, CfgList class UpdateBotConfig(cfg.SectionedConfigFile): ''' @@ -28,5 +28,8 @@ # pylint: disable-msg=R0904 # path to configuration files (conaryrc, rmakerc) - configPath = CfgString - commitMessage = (CfgString, 'Automated commit by updateBot') + configPath = CfgString + commitMessage = (CfgString, 'Automated commit by updateBot') + + repositoryUrl = CfgString + repositoryPaths = CfgList(CfgLineList(CfgString)) From johnsonm at rpath.com Wed Aug 19 17:39:23 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:39:23 +0000 Subject: mirrorball: add __cmp__ method to packagexml so that it can properly be used in Message-ID: <200908192139.n7JLdNsH015191@scc.eng.rpath.com> changeset: cdfcdc422fce user: Elliot Peele <http://issues.rpath.com/> date: Mon, 16 Jun 2008 17:28:28 -0400 add __cmp__ method to packagexml so that it can properly be used in dictionaries committer: Elliot Peele <http://issues.rpath.com/> diff --git a/repomd/packagexml.py b/repomd/packagexml.py --- a/repomd/packagexml.py +++ b/repomd/packagexml.py @@ -22,6 +22,8 @@ from rpath_common.xmllib import api1 as xmllib +from updatebot import util + from repomd.errors import UnknownElementError, UnknownAttributeError from repomd.xmlcommon import SlotNode @@ -131,6 +133,13 @@ return hash((self.name, self.epoch, self.version, self.release, self.arch)) + def __cmp__(self, other): + pkgvercmp = util.packagevercmp(self, other) + if pkgvercmp != 0: + return pkgvercmp + + return cmp(self.location, other.location) + class _RpmRequires(xmllib.BaseNode): """ From johnsonm at rpath.com Wed Aug 19 17:40:31 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:40:31 +0000 Subject: mirrorball: update for import Message-ID: <200908192140.n7JLeVGr017886@scc.eng.rpath.com> changeset: fc7dd54834ca user: Elliot Peele <http://bugs.rpath.com/> date: Thu, 28 Aug 2008 10:29:59 -0400 update for import committer: Elliot Peele <http://bugs.rpath.com/> diff --git a/updatebot/bot.py b/updatebot/bot.py --- a/updatebot/bot.py +++ b/updatebot/bot.py @@ -114,14 +114,15 @@ self._populatePkgSource() # Import sources into repository. - toBuild, fail = self._updater.create(self._cfg.package, buildAll=False) + toBuild, fail = self._updater.create(self._cfg.package, buildAll=True) # Build all newly imported packages. - trvMap, failed = self._builder.buildmany(toBuild) +# trvMap, failed = self._builder.buildmany(toBuild) - import epdb; epdb.st() +# import epdb; epdb.st() - #trvMap = self._builder.build(toBuild) +# trvMap = self._builder.build(toBuild) +# import epdb; epdb.st() trvs = self._builder._formatInput(toBuild) jobs = {} for trv in trvs: From johnsonm at rpath.com Wed Aug 19 17:41:40 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:41:40 +0000 Subject: mirrorball: fix up tu Message-ID: <200908192141.n7JLfe5i020634@scc.eng.rpath.com> changeset: 3df221af132f user: Elliot Peele <https://issues.rpath.com/> date: Wed, 19 Nov 2008 13:24:27 -0500 fix up tu committer: Elliot Peele <https://issues.rpath.com/> diff --git a/scripts/promote-centos.sh b/scripts/promote-centos.sh --- a/scripts/promote-centos.sh +++ b/scripts/promote-centos.sh @@ -23,7 +23,7 @@ anaconda-templates=centos.rpath.com at rpath:centos-5-devel \ group-os=centos.rpath.com at rpath:centos-5-devel \ /centos.rpath.com at rpath:centos-5-devel--/centos.rpath.com at rpath:centos-5 \ - /centos.rpath.com at rpath:centos-devel//centos-5-devel--/centos.rpath.com at rpath:centos-devel//centos-5 \ + /centos.rpath.com at rpath:centos-devel//centos-5-devel--/centos.rpath.com at rpath:centos-5 \ /conary.rpath.com at rpl:devel//2--/centos.rpath.com at rpath:centos-5 \ /conary.rpath.com at rpl:devel//2//centos.rpath.com at rpath:centos-5-devel--/centos.rpath.com at rpath:centos-5 \ /conary.rpath.com at rpl:devel//centos.rpath.com at rpath:centos-5-devel--/centos.rpath.com at rpath:centos-5 From johnsonm at rpath.com Wed Aug 19 17:38:51 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:38:51 +0000 Subject: mirrorball: remove some unneeded pylint exceptions Message-ID: <200908192138.n7JLcpXx013806@scc.eng.rpath.com> changeset: f75207e86dcd user: Elliot Peele <http://issues.rpath.com/> date: Wed, 28 May 2008 11:43:33 -0400 remove some unneeded pylint exceptions committer: Elliot Peele <http://issues.rpath.com/> diff --git a/repomd/patchxml.py b/repomd/patchxml.py --- a/repomd/patchxml.py +++ b/repomd/patchxml.py @@ -22,12 +22,6 @@ # R0902 - Too many instance attributes # pylint: disable-msg=R0902 - # W0232 - Class has no __init__ method (Yes, really it does) - # pylint: disable-msg=W0232 - - # R0903 - Too few public methods - # pylint: disable-msg=R0903 - name = None summary = None description = None @@ -96,12 +90,6 @@ Parser for the atoms element of a path-*.xml file. ''' - # W0232 - Class has no __init__ method (Yes, really it does) - # pylint: disable-msg=W0232 - - # R0903 - Too few public methods - # pylint: disable-msg=R0903 - def addChild(self, child): ''' Parse children of atoms element. From johnsonm at rpath.com Wed Aug 19 17:40:51 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:40:51 +0000 Subject: mirrorball: branch merge Message-ID: <200908192140.n7JLepuC018736@scc.eng.rpath.com> changeset: 99034c9e7cf7 user: Elliot Peele <https://issues.rpath.com/> date: Tue, 23 Sep 2008 16:32:49 -0400 branch merge committer: Elliot Peele <https://issues.rpath.com/> diff --git a/scripts/promote-ubuntu.sh b/scripts/promote-ubuntu.sh --- a/scripts/promote-ubuntu.sh +++ b/scripts/promote-ubuntu.sh @@ -30,6 +30,7 @@ platform-definition:source=ubuntu.rpath.org at rpath:ubuntu-hardy-devel \ group-os=ubuntu.rpath.org at rpath:ubuntu-hardy-devel \ /ubuntu.rpath.org at rpath:ubuntu-hardy-devel--/ubuntu.rpath.org at rpath:ubuntu-hardy \ + /ubuntu.rpath.org at rpath:ubuntu-devel//ubuntu-hardy-devel--/ubuntu.rpath.org at rpath:ubuntu-devel//ubuntu-hardy \ /conary.rpath.com at rpl:devel//conary.rpath.com at rpl:2--/ubuntu.rpath.org at rpath:ubuntu-hardy \ /conary.rpath.com at rpl:devel//conary.rpath.com at rpl:2//ubuntu.rpath.org at rpath:ubuntu-hardy-devel--/ubuntu.rpath.org at rpath:ubuntu-hardy \ /conary.rpath.com at rpl:devel//ubuntu.rpath.org at rpath:ubuntu-hardy-devel--/ubuntu.rpath.org at rpath:ubuntu-hardy From johnsonm at rpath.com Wed Aug 19 17:40:25 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:40:25 +0000 Subject: mirrorball: add script for just opening a package source Message-ID: <200908192140.n7JLePPX017631@scc.eng.rpath.com> changeset: 77ba6ded5920 user: Elliot Peele <http://issues.rpath.com/> date: Mon, 25 Aug 2008 14:47:58 -0400 add script for just opening a package source committer: Elliot Peele <http://issues.rpath.com/> diff --git a/scripts/pkgsource.py b/scripts/pkgsource.py new file mode 100755 --- /dev/null +++ b/scripts/pkgsource.py @@ -0,0 +1,29 @@ +#!/usr/bin/python + +import sys +from conary.lib import util +sys.excepthook = util.genExcepthook() + +import logging +import updatebot.log + +updatebot.log.addRootLogger() +log = logging.getLogger('test') + +from aptmd import Client +from updatebot import config +from updatebot import pkgsource + +cfg = config.UpdateBotConfig() +cfg.read('/data/hg/mirrorball/config/ubuntu/updatebotrc') + +client = Client('http://i.rdu.rpath.com/ubuntu') +pkgSource = pkgsource.PackageSource(cfg) + +for path in cfg.repositoryPaths: + log.info('loading %s' % path) + pkgSource.loadFromClient(client, path) + +pkgSource.finalize() + +import epdb; epdb.st() From johnsonm at rpath.com Wed Aug 19 17:40:52 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:40:52 +0000 Subject: mirrorball: sles/updatebotrc add emailBcc to juphoff, stub use of juphoff config dir Message-ID: <200908192140.n7JLeqEd018770@scc.eng.rpath.com> changeset: e44604b5fd2b user: Jeff Uphoff <https://issues.rpath.com/> date: Wed, 24 Sep 2008 10:36:31 -0400 sles/updatebotrc add emailBcc to juphoff, stub use of juphoff config dir committer: Jeff Uphoff <https://issues.rpath.com/> diff --git a/scripts/buildgroups b/scripts/buildgroups --- a/scripts/buildgroups +++ b/scripts/buildgroups @@ -20,7 +20,7 @@ log.addRootLogger() cfg = config.UpdateBotConfig() -cfg.read(os.environ['HOME'] + '/hg/mirrorball/config/ubuntu/updatebotrc') +cfg.read(os.environ['HOME'] + '/hg/mirrorball/config/sles/updatebotrc') builder = build.Builder(cfg) diff --git a/scripts/buildpackages b/scripts/buildpackages --- a/scripts/buildpackages +++ b/scripts/buildpackages @@ -23,7 +23,7 @@ log.addRootLogger() cfg = config.UpdateBotConfig() -cfg.read(os.environ['HOME'] + '/hg/mirrorball/config/ubuntu/updatebotrc') +cfg.read(os.environ['HOME'] + '/hg/mirrorball/config/sles/updatebotrc') builder = build.Builder(cfg) From johnsonm at rpath.com Wed Aug 19 17:39:20 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:39:20 +0000 Subject: mirrorball: add a map for binary package names -> package objects Message-ID: <200908192139.n7JLdKBe015071@scc.eng.rpath.com> changeset: ea667f99fd02 user: Elliot Peele <http://issues.rpath.com/> date: Thu, 12 Jun 2008 22:21:38 -0400 add a map for binary package names -> package objects committer: Elliot Peele <http://issues.rpath.com/> diff --git a/rpmimport/rpmsource.py b/rpmimport/rpmsource.py --- a/rpmimport/rpmsource.py +++ b/rpmimport/rpmsource.py @@ -54,6 +54,9 @@ # {srcName: [srcPkg, ... ] } self.srcNameMap = dict() + # {binName: [binPkg, ... ] } + self.binNameMap = dict() + def _procSrc(self, basePath, package): """ Process source rpms. @@ -92,6 +95,10 @@ self.rpmMap[srpm] = {longLoc: package} self.revMap[package.name] = srpm + if package.name not in self.binNameMap: + self.binNameMap[package.name] = [] + self.binNameMap[package.name].append(package) + def load(self, url, basePath=''): """ Walk the yum repository rooted at url/basePath and collect information From johnsonm at rpath.com Wed Aug 19 17:40:48 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:40:48 +0000 Subject: mirrorball: add missing frumptuu Message-ID: <200908192140.n7JLemrQ018583@scc.eng.rpath.com> changeset: b05b3c37a916 user: Elliot Peele <https://issues.rpath.com/> date: Sat, 13 Sep 2008 20:06:43 -0400 add missing frumptuu committer: Elliot Peele <https://issues.rpath.com/> diff --git a/scripts/promote-ubuntu.sh b/scripts/promote-ubuntu.sh --- a/scripts/promote-ubuntu.sh +++ b/scripts/promote-ubuntu.sh @@ -30,6 +30,7 @@ platform-definition:source=ubuntu.rpath.org at rpath:ubuntu-hardy-devel \ group-os=ubuntu.rpath.org at rpath:ubuntu-hardy-devel \ /ubuntu.rpath.org at rpath:ubuntu-hardy-devel--/ubuntu.rpath.org at rpath:ubuntu-hardy \ + /ubuntu.rpath.org at rpath:ubuntu-devel//ubuntu-hardy-devel--/ubuntu.rpath.org at rpath:ubuntu-devel//ubuntu-hardy \ /conary.rpath.com at rpl:devel//conary.rpath.com at rpl:2--/ubuntu.rpath.org at rpath:ubuntu-hardy \ /conary.rpath.com at rpl:devel//conary.rpath.com at rpl:2//ubuntu.rpath.org at rpath:ubuntu-hardy-devel--/ubuntu.rpath.org at rpath:ubuntu-hardy \ /conary.rpath.com at rpl:devel//ubuntu.rpath.org at rpath:ubuntu-hardy-devel--/ubuntu.rpath.org at rpath:ubuntu-hardy From johnsonm at rpath.com Wed Aug 19 17:40:14 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:40:14 +0000 Subject: mirrorball: generically look for locations, deb sources don't have a .location Message-ID: <200908192140.n7JLeEtj017171@scc.eng.rpath.com> changeset: f1cbe66149dc user: Elliot Peele <http://issues.rpath.com/> date: Thu, 14 Aug 2008 18:05:21 -0400 generically look for locations, deb sources don't have a .location committer: Elliot Peele <http://issues.rpath.com/> diff --git a/updatebot/update.py b/updatebot/update.py --- a/updatebot/update.py +++ b/updatebot/update.py @@ -317,9 +317,14 @@ @type srcPkg: repomd.packagexml._Package """ + manifest = [] manifestPkgs = list(self._pkgSource.srcPkgMap[srcPkg]) - pkgs = self._getLatestOfAvailableArches(manifestPkgs) - return [ x.location for x in pkgs ] + for pkg in self._getLatestOfAvailableArches(manifestPkgs): + if hasattr(pkg, 'location'): + manifest.append(pkg.location) + elif hasattr(pkg, 'files'): + manifest.extend(pkg.files) + return manifest def publish(self, trvLst, expected, targetLabel, checkPackageList=True): """ From johnsonm at rpath.com Wed Aug 19 17:39:57 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:39:57 +0000 Subject: mirrorball: update docs Message-ID: <200908192140.n7JLdvUe016521@scc.eng.rpath.com> changeset: bb8370fd985c user: Elliot Peele <http://issues.rpath.com/> date: Fri, 25 Jul 2008 15:28:54 -0400 update docs committer: Elliot Peele <http://issues.rpath.com/> diff --git a/updatebot/conaryhelper.py b/updatebot/conaryhelper.py --- a/updatebot/conaryhelper.py +++ b/updatebot/conaryhelper.py @@ -66,7 +66,7 @@ the top level group config option. @param group: group to query @type group: None or troveTuple (name, versionStr, flavorStr) - @return set of source trove specs + @return dict of source trove specs to list of binary trove specs """ # E1101 - Instance of 'ConaryConfiguration' has no 'buildLabel' member @@ -124,7 +124,7 @@ refrenced by that trove. @param troveSpec: trove to walk. @type troveSpec: (name, versionObj, flavorObj) - @return set([(trvSpec, trvSpec, ...]) + @return {srcTrvSpec: [binTrvSpec, binTrvSpec, ...]} """ # W0212 - Access to a protected member _TROVEINFO_TAG_SOURCENAME of a From johnsonm at rpath.com Wed Aug 19 17:40:39 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:40:39 +0000 Subject: mirrorball: add method for removing files Message-ID: <200908192140.n7JLedxl018209@scc.eng.rpath.com> changeset: bde617631917 user: Elliot Peele <https://issues.rpath.com/> date: Mon, 08 Sep 2008 02:24:38 -0400 add method for removing files committer: Elliot Peele <https://issues.rpath.com/> diff --git a/updatebot/conaryhelper.py b/updatebot/conaryhelper.py --- a/updatebot/conaryhelper.py +++ b/updatebot/conaryhelper.py @@ -312,6 +312,25 @@ finally: os.chdir(cwd) + @staticmethod + def _removeFile(pkgDir, fileName): + """ + Remove a file from a source component. + @param pkgDir: directory where package is checked out to. + @type pkgDir: string + @param fileName: file name to add. + @type fileName: string + """ + + log.info('removing file: %s' % fileName) + + cwd = os.getcwd() + try: + os.chdir(pkgDir) + checkin.removeFile(fileName) + finally: + os.chdir(cwd) + def _getVersionsByName(self, pkgname): """ Figure out if a trove exists in the repository. From johnsonm at rpath.com Wed Aug 19 17:42:47 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:42:47 +0000 Subject: mirrorball: use system libs, take platform name as arg Message-ID: <200908192142.n7JLglDa023357@scc.eng.rpath.com> changeset: 8ad0c3b96471 user: Elliot Peele <https://issues.rpath.com/> date: Wed, 22 Apr 2009 13:32:23 -0400 use system libs, take platform name as arg committer: Elliot Peele <https://issues.rpath.com/> diff --git a/scripts/findbinaries b/scripts/findbinaries --- a/scripts/findbinaries +++ b/scripts/findbinaries @@ -16,11 +16,7 @@ import os import sys -sys.path.insert(0, os.environ['HOME'] + '/hg/rpath-xmllib') -sys.path.insert(0, os.environ['HOME'] + '/hg/conary') sys.path.insert(0, os.environ['HOME'] + '/hg/mirrorball') -sys.path.insert(0, os.environ['HOME'] + '/hg/xobj/py') -sys.path.insert(0, os.environ['HOME'] + '/hg/rmake') from conary.lib import util sys.excepthook = util.genExcepthook() @@ -39,7 +35,7 @@ slog = logging.getLogger('findbinaries') cfg = config.UpdateBotConfig() -cfg.read(os.environ['HOME'] + '/hg/mirrorball/config/sles/updatebotrc') +cfg.read(os.environ['HOME'] + '/hg/mirrorball/config/%s/updatebotrc' % sys.argv[1]) bot = bot.Bot(cfg) updater = bot._updater From johnsonm at rpath.com Wed Aug 19 17:39:31 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:39:31 +0000 Subject: mirrorball: setup flavor objects on a per package basis Message-ID: <200908192139.n7JLdV63015516@scc.eng.rpath.com> changeset: edd334f487bd user: Elliot Peele <http://issues.rpath.com/> date: Thu, 19 Jun 2008 21:45:54 -0400 setup flavor objects on a per package basis committer: Elliot Peele <http://issues.rpath.com/> diff --git a/updatebot/conaryhelper.py b/updatebot/conaryhelper.py --- a/updatebot/conaryhelper.py +++ b/updatebot/conaryhelper.py @@ -43,9 +43,6 @@ # Have to initialize flavors to commit to the repository. self._ccfg.initializeFlavors() - # Setup flavor objects - use.setBuildFlagsFromFlavor(pkgname, self._ccfg.buildFlavor, error=False) - self._client = conaryclient.ConaryClient(self._ccfg) self._repos = self._client.getRepos() @@ -200,6 +197,9 @@ manifestfh.write('\n') manifestfh.close() + # Setup flavor objects + use.setBuildFlagsFromFlavor(pkgname, self._ccfg.buildFlavor, error=False) + # Commit to repository. self._commit(recipeDir, commitMessage) util.rmtree(recipeDir) From johnsonm at rpath.com Wed Aug 19 17:41:39 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:41:39 +0000 Subject: mirrorball: add mirror script Message-ID: <200908192141.n7JLfdx0020617@scc.eng.rpath.com> changeset: f0f5f8f2d7d1 user: Elliot Peele <https://issues.rpath.com/> date: Wed, 19 Nov 2008 13:23:59 -0500 add mirror script committer: Elliot Peele <https://issues.rpath.com/> diff --git a/scripts/mirror.py b/scripts/mirror.py new file mode 100755 --- /dev/null +++ b/scripts/mirror.py @@ -0,0 +1,20 @@ +#!/usr/bin/python2.6 +# +# Copyright (c) 2008 rPath, Inc. +# +# This program is distributed under the terms of the Common Public License, +# version 1.0. A copy of this license should have been distributed with this +# source file in a file called LICENSE. If it is not present, the license +# is always available at http://www.rpath.com/permanent/licenses/CPL-1.0. +# +# This program is distributed in the hope that it will be useful, but +# without any warranty; without even the implied warranty of merchantability +# or fitness for a particular purpose. See the Common Public License for +# full details. +# + +from header import * + +from updatebot import conaryhelper +helper = conaryhelper.ConaryHelper(cfg) +helper.mirror() From johnsonm at rpath.com Wed Aug 19 17:41:13 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:41:13 +0000 Subject: mirrorball: add mirroring for centos Message-ID: <200908192141.n7JLfDcP019607@scc.eng.rpath.com> changeset: b0e3adc9c65b user: Elliot Peele <https://issues.rpath.com/> date: Fri, 31 Oct 2008 15:15:15 -0400 add mirroring for centos committer: Elliot Peele <https://issues.rpath.com/> diff --git a/scripts/mirror-centos.sh b/scripts/mirror-centos.sh new file mode 100755 --- /dev/null +++ b/scripts/mirror-centos.sh @@ -0,0 +1,16 @@ +#!/bin/sh +# +# Copyright (c) 2008 rPath, Inc. +# +# This program is distributed under the terms of the Common Public License, +# version 1.0. A copy of this license should have been distributed with this +# source file in a file called LICENSE. If it is not present, the license +# is always available at http://www.rpath.com/permanent/licenses/CPL-1.0. +# +# This program is distributed in the hope that it will be useful, but +# without any warranty; without even the implied warranty of merchantability +# or fitness for a particular purpose. See the Common Public License for +# full details. +# + +~/hg/conary/scripts/mirror --config-file=/home/elliot/hg/mirrorball/config/centos/mirror.conf -v From johnsonm at rpath.com Wed Aug 19 17:43:07 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:43:07 +0000 Subject: mirrorball: normalize paths in manifest files, some manifests were created with double Message-ID: <200908192143.n7JLh7PD024175@scc.eng.rpath.com> changeset: 74ceac37b464 user: Elliot Peele <https://issues.rpath.com/> date: Mon, 27 Jul 2009 15:35:33 -0400 normalize paths in manifest files, some manifests were created with double slashes committer: Elliot Peele <https://issues.rpath.com/> diff --git a/updatebot/update.py b/updatebot/update.py --- a/updatebot/update.py +++ b/updatebot/update.py @@ -16,6 +16,7 @@ Module for finding packages to update and updating them. """ +import os import logging from rpmutils import rpmvercmp @@ -153,6 +154,9 @@ metadata = None manifest = self._conaryhelper.getManifest(nvf[0]) for line in manifest: + # Some manifests were created with double slashes, need to + # normalize the path to work around this problem. + line = os.path.normpath(line) if line in self._pkgSource.locationMap: binPkg = self._pkgSource.locationMap[line] srcPkg = self._pkgSource.binPkgMap[binPkg] From johnsonm at rpath.com Wed Aug 19 17:40:58 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:40:58 +0000 Subject: mirrorball: add script to mirror ubuntu Message-ID: <200908192140.n7JLewnF019009@scc.eng.rpath.com> changeset: 3aba0c4852ea user: Elliot Peele <https://issues.rpath.com/> date: Mon, 29 Sep 2008 15:36:21 -0400 add script to mirror ubuntu committer: Elliot Peele <https://issues.rpath.com/> diff --git a/scripts/mirror-ubuntu.sh b/scripts/mirror-ubuntu.sh new file mode 100755 --- /dev/null +++ b/scripts/mirror-ubuntu.sh @@ -0,0 +1,16 @@ +#!/bin/sh +# +# Copyright (c) 2008 rPath, Inc. +# +# This program is distributed under the terms of the Common Public License, +# version 1.0. A copy of this license should have been distributed with this +# source file in a file called LICENSE. If it is not present, the license +# is always available at http://www.rpath.com/permanent/licenses/CPL-1.0. +# +# This program is distributed in the hope that it will be useful, but +# without any warranty; without even the implied warranty of merchantability +# or fitness for a particular purpose. See the Common Public License for +# full details. +# + +~/hg/conary/scripts/mirror --config-file=/home/elliot/hg/mirrorball/config/ubuntu/mirror.conf -v From johnsonm at rpath.com Wed Aug 19 17:38:56 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:38:56 +0000 Subject: mirrorball: remove cruft Message-ID: <200908192139.n7JLcu9B014004@scc.eng.rpath.com> changeset: afc81c56d690 user: Elliot Peele <http://issues.rpath.com/> date: Wed, 28 May 2008 21:05:10 -0400 remove cruft committer: Elliot Peele <http://issues.rpath.com/> diff --git a/test/unit_test/updatebottest/buildtest.py b/test/unit_test/updatebottest/buildtest.py --- a/test/unit_test/updatebottest/buildtest.py +++ b/test/unit_test/updatebottest/buildtest.py @@ -24,18 +24,6 @@ from updatebot import errors class BuilderTest(slehelp.Helper): - def _getBuilder(self): - mockJob = mock.MockObject(stableReturnValues=True) - mockJob.isFailed._mock.setReturn(False) - mockJob.isFinished._mock.setReturn(False) - mockJob.iterBuiltTroves._mock.setReturn([]) - mockHelper = mock.MockObject(stableReturnValues=True) - mockHelper.createBuildJob._mock.setReturn(mockJob) - mockHelper.buildJob._mock.setReturn(1) - mockHelper.getJob._mock.setReturn(mockJob) - builder = build.Builder(self.updateBotCfg) - - def testStartJob(self): trvSpecs = (('foo', '', ''), ) From johnsonm at rpath.com Wed Aug 19 17:41:40 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:41:40 +0000 Subject: mirrorball: return the return code Message-ID: <200908192141.n7JLfeN1020668@scc.eng.rpath.com> changeset: 89a7f82f4b98 user: Elliot Peele <https://issues.rpath.com/> date: Wed, 19 Nov 2008 13:28:06 -0500 return the return code committer: Elliot Peele <https://issues.rpath.com/> diff --git a/updatebot/conaryhelper.py b/updatebot/conaryhelper.py --- a/updatebot/conaryhelper.py +++ b/updatebot/conaryhelper.py @@ -28,8 +28,10 @@ from conary.build import use from conary import conarycfg from conary import conaryclient +from conary.lib import log as clog from conary.conaryclient import mirror + from updatebot import util from updatebot.errors import GroupNotFound from updatebot.errors import TooManyFlavorsFoundError @@ -478,8 +480,6 @@ log.info('mirroring disabled, no mirror.conf found for this platform') return - from conary.lib import log as clog - # Always use DEBUG logging when mirroring curLevel = clog.fmtLogger.level clog.setVerbosity(clog.DEBUG) @@ -492,3 +492,5 @@ # Reset loglevel clog.setVerbosity(curLevel) + + return rc From johnsonm at rpath.com Wed Aug 19 17:42:44 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:42:44 +0000 Subject: mirrorball: branch merge Message-ID: <200908192142.n7JLgipr023238@scc.eng.rpath.com> changeset: 096b908da97a user: Elliot Peele <https://issues.rpath.com/> date: Mon, 20 Apr 2009 15:49:50 -0400 branch merge committer: Elliot Peele <https://issues.rpath.com/> diff --git a/scripts/import b/scripts/import --- a/scripts/import +++ b/scripts/import @@ -16,11 +16,7 @@ import os import sys -sys.path.insert(0, os.environ['HOME'] + '/hg/xobj/py') -sys.path.insert(0, os.environ['HOME'] + '/hg/rpath-xmllib') -sys.path.insert(0, os.environ['HOME'] + '/hg/conary') sys.path.insert(0, os.environ['HOME'] + '/hg/mirrorball') -sys.path.insert(0, os.environ['HOME'] + '/hg/rmake') from conary.lib import util sys.excepthook = util.genExcepthook() diff --git a/scripts/sync-scientific.sh b/scripts/sync-scientific.sh --- a/scripts/sync-scientific.sh +++ b/scripts/sync-scientific.sh @@ -1,6 +1,6 @@ #!/bin/bash # -# Copyright (c) 2008 rPath, Inc. +# Copyright (c) 2008-2009 rPath, Inc. # # This program is distributed under the terms of the Common Public License, # version 1.0. A copy of this license should have been distributed with this From johnsonm at rpath.com Wed Aug 19 17:40:47 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:40:47 +0000 Subject: mirrorball: remove two flavor contraint now that we have a platform with more than two Message-ID: <200908192140.n7JLelmF018566@scc.eng.rpath.com> changeset: 5ca09ae41a36 user: Elliot Peele <https://issues.rpath.com/> date: Sat, 13 Sep 2008 19:41:12 -0400 remove two flavor contraint now that we have a platform with more than two flavors, instead use current flavor count committer: Elliot Peele <https://issues.rpath.com/> diff --git a/updatebot/conaryhelper.py b/updatebot/conaryhelper.py --- a/updatebot/conaryhelper.py +++ b/updatebot/conaryhelper.py @@ -41,6 +41,8 @@ """ def __init__(self, cfg): + self._groupFlavorCount = len(cfg.groupFlavors) + self._ccfg = conarycfg.ConaryConfiguration(readConfigFiles=False) self._ccfg.read(util.join(cfg.configPath, 'conaryrc')) self._ccfg.dbPath = ':memory:' @@ -81,7 +83,7 @@ # Magic number should probably be a config option. # 2 here is the number of flavors expected. - if len(latest) != 2: + if len(latest) != self._groupFlavorCount: raise TooManyFlavorsFoundError(why=latest) d = {} From johnsonm at rpath.com Wed Aug 19 17:41:15 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:41:15 +0000 Subject: mirrorball: branch merge Message-ID: <200908192141.n7JLfFC3019692@scc.eng.rpath.com> changeset: 6bb6d8f7532e user: Elliot Peele <https://issues.rpath.com/> date: Thu, 06 Nov 2008 20:36:25 -0500 branch merge committer: Elliot Peele <https://issues.rpath.com/> diff --git a/pylint/run_pylint b/pylint/run_pylint --- a/pylint/run_pylint +++ b/pylint/run_pylint @@ -35,7 +35,7 @@ done if [ -z "$files" ] ; then - files="updatebot repomd rpmutils aptmd" + files="updatebot repomd rpmutils aptmd pmap" fi pylint --init-hook='import sys; sys.path.append("."); import init_pylint' --rcfile='../pylintrc' $pylintArgs $files diff --git a/test/testsuite.py b/test/testsuite.py --- a/test/testsuite.py +++ b/test/testsuite.py @@ -56,7 +56,7 @@ testutilsPath = setPathFromEnv('TESTUTILS_PATH', '../testutils') conaryDir = setPathFromEnv('CONARY_PATH', '../conary') - conaryTestPath = setPathFromEnv('CONARY_TEST_PATH', '../conary-test') + conaryTestPath = setPathFromEnv('CONARY_TEST_PATH', '../conary-test-2.0') setPathFromEnv('CONARY_POLICY_PATH', '/usr/lib/conary/policy') mirrorballPath = setPathFromEnv('SLEESTACK_PATH', '') From johnsonm at rpath.com Wed Aug 19 17:42:41 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:42:41 +0000 Subject: mirrorball: sync entire 5.x tree for scientific linux Message-ID: <200908192142.n7JLgfD7023119@scc.eng.rpath.com> changeset: 1dbf5d2e3083 user: Elliot Peele <https://issues.rpath.com/> date: Thu, 02 Apr 2009 16:17:31 -0400 sync entire 5.x tree for scientific linux committer: Elliot Peele <https://issues.rpath.com/> diff --git a/scripts/sync-scientific.sh b/scripts/sync-scientific.sh --- a/scripts/sync-scientific.sh +++ b/scripts/sync-scientific.sh @@ -1,6 +1,6 @@ #!/bin/bash # -# Copyright (c) 2009 rPath, Inc. +# Copyright (c) 2008 rPath, Inc. # # This program is distributed under the terms of the Common Public License, # version 1.0. A copy of this license should have been distributed with this @@ -13,10 +13,10 @@ # full details. # -SOURCE=rsync://rsync.scientificlinux.org/scientific/53 +SOURCE=rsync://rsync.scientificlinux.org/scientific/ DEST=/l/scientific/ date -rsync -arv --progress --bwlimit=1024 --exclude iso $SOURCE $DEST +rsync -arv --progress --bwlimit=600 --exclude iso --exclude 3* --exclude 4* --exclude RHAPS* --exclude livecd --exclude mirrorlist --exclude obsolete --exclude virtual-images $SOURCE $DEST ./hardlink.py $DEST From johnsonm at rpath.com Wed Aug 19 17:41:52 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:41:52 +0000 Subject: mirrorball: add line filters Message-ID: <200908192141.n7JLfquS021161@scc.eng.rpath.com> changeset: b1f4b8ca17d8 user: Elliot Peele <https://issues.rpath.com/> date: Fri, 19 Dec 2008 14:31:41 -0500 add line filters committer: Elliot Peele <https://issues.rpath.com/> diff --git a/aptmd/parser.py b/aptmd/parser.py --- a/aptmd/parser.py +++ b/aptmd/parser.py @@ -197,6 +197,8 @@ self._containerClass = None self._stateFilters = { } + self._stateLineFilters = { + } def _filter(self, fltr, state): """ @@ -205,6 +207,13 @@ self._stateFilters[re.compile(fltr)] = state + def _filterLine(self, fltr, state): + """ + Build a state based on line filter. + """ + + self._stateLineFilters[re.compile(fltr)] = state + def _getState(self, key): """ Filter states based on filter map. @@ -222,6 +231,10 @@ if fltr.match(key): return state + for fltr, state in self._stateLineFilters.iteritems(): + if fltr.match(self._getFullLine()): + return state + return key def _newContainer(self): From johnsonm at rpath.com Wed Aug 19 17:41:19 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:41:19 +0000 Subject: mirrorball: use repository short name Message-ID: <200908192141.n7JLfJl5019846@scc.eng.rpath.com> changeset: f90cc5b5d134 user: Elliot Peele <https://issues.rpath.com/> date: Thu, 13 Nov 2008 10:53:19 -0500 use repository short name committer: Elliot Peele <https://issues.rpath.com/> diff --git a/updatebot/bot.py b/updatebot/bot.py --- a/updatebot/bot.py +++ b/updatebot/bot.py @@ -61,8 +61,7 @@ client = aptmd.Client(self._cfg.repositoryUrl) for repo in self._cfg.repositoryPaths: - log.info('loading repository data %s/%s' - % (self._cfg.repositoryUrl, repo)) + log.info('loading repository data %s' % repo) if self._cfg.repositoryFormat == 'yum': client = repomd.Client(self._cfg.repositoryUrl + '/' + repo) @@ -82,8 +81,7 @@ return for path, client in self._clients.iteritems(): - log.info('loading patch information %s/%s' - % (self._cfg.repositoryUrl, path)) + log.info('loading patch information %s' % path) self._patchSource.loadFromClient(client, path) self._patchSourcePopulated = True From johnsonm at rpath.com Wed Aug 19 17:41:07 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:41:07 +0000 Subject: mirrorball: vendorize email Message-ID: <200908192141.n7JLf7cB019334@scc.eng.rpath.com> changeset: 6d61c398c89a user: Elliot Peele <https://issues.rpath.com/> date: Wed, 22 Oct 2008 18:38:38 -0400 vendorize email committer: Elliot Peele <https://issues.rpath.com/> diff --git a/vendor/email/__init__.py b/vendor/email/__init__.py --- a/vendor/email/__init__.py +++ b/vendor/email/__init__.py @@ -121,13 +121,13 @@ for _name in _LOWERNAMES: importer = LazyImporter(_name.lower()) - sys.modules['email.' + _name] = importer - setattr(sys.modules['email'], _name, importer) + sys.modules['vendor.email.' + _name] = importer + setattr(sys.modules['vendor.email'], _name, importer) -import email.mime +import vendor.email.mime for _name in _MIMENAMES: importer = LazyImporter('mime.' + _name.lower()) - sys.modules['email.MIME' + _name] = importer - setattr(sys.modules['email'], 'MIME' + _name, importer) - setattr(sys.modules['email.mime'], _name, importer) + sys.modules['vendor.email.MIME' + _name] = importer + setattr(sys.modules['vendor.email'], 'MIME' + _name, importer) + setattr(sys.modules['vendor.email.mime'], _name, importer) From johnsonm at rpath.com Wed Aug 19 17:41:38 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:41:38 +0000 Subject: mirrorball: actually send email to the bcc addresses Message-ID: <200908192141.n7JLfcKM020549@scc.eng.rpath.com> changeset: d7563d8f1476 user: Elliot Peele <https://issues.rpath.com/> date: Tue, 18 Nov 2008 13:17:20 -0500 actually send email to the bcc addresses committer: Elliot Peele <https://issues.rpath.com/> diff --git a/updatebot/advisories/common.py b/updatebot/advisories/common.py --- a/updatebot/advisories/common.py +++ b/updatebot/advisories/common.py @@ -89,7 +89,7 @@ smtp = self._smtpConnect() try: - results = smtp.sendmail(self._from, self._to, message.as_string()) + results = smtp.sendmail(self._from, self._to + self._bcc, message.as_string()) except (SMTPRecipientsRefused, SMTPHeloError, SMTPSenderRefused, SMTPDataError), e: raise FailedToSendAdvisoryError(error=e) @@ -111,7 +111,6 @@ email['Subject'] = self._subject email['From'] = '%s <%s>' % (self._fromName, self._from) email['To'] = self._formatList(self._to) - email['Bcc'] = self._formatList(self._bcc) return email @staticmethod From johnsonm at rpath.com Wed Aug 19 17:42:28 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:42:28 +0000 Subject: mirrorball: fix logic error in requires parsing Message-ID: <200908192142.n7JLgS6c022558@scc.eng.rpath.com> changeset: d5ed050b22c1 user: Elliot Peele <https://issues.rpath.com/> date: Mon, 16 Feb 2009 02:35:27 -0500 fix logic error in requires parsing committer: Elliot Peele <https://issues.rpath.com/> diff --git a/repomd/packagexml.py b/repomd/packagexml.py --- a/repomd/packagexml.py +++ b/repomd/packagexml.py @@ -166,15 +166,15 @@ """ if child.getName() in ('rpm:entry', 'suse:entry'): + child.kind = None + child.name = None + child.epoch = None + child.version = None + child.release = None + child.flags = None + child.pre = None + for attr, value in child.iterAttributes(): - child.kind = None - child.name = None - child.epoch = None - child.version = None - child.release = None - child.flags = None - child.pre = None - if attr == 'kind': child.kind = value elif attr == 'name': From johnsonm at rpath.com Wed Aug 19 17:40:39 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:40:39 +0000 Subject: mirrorball: remove empty control files Message-ID: <200908192140.n7JLed0T018226@scc.eng.rpath.com> changeset: 9c335a49a752 user: Elliot Peele <https://issues.rpath.com/> date: Mon, 08 Sep 2008 02:27:15 -0400 remove empty control files committer: Elliot Peele <https://issues.rpath.com/> diff --git a/extra/logparse.py b/extra/logparse.py --- a/extra/logparse.py +++ b/extra/logparse.py @@ -477,10 +477,14 @@ log.info('writing control file for %s' % pkgName) recipeDir = self._helper._checkout(pkgName) - fd = open(os.path.join(recipeDir, 'control'), 'w') - fd.write(self.getControl()) - fd.close() - self._helper._addFile(recipeDir, 'control') + control = self.getControl() + if not control and os.path.exists(os.path.join(recipeDir, 'control')): + self._helper.removeFile('control') + if control: + fd = open(os.path.join(recipeDir, 'control'), 'w') + fd.write(control) + fd.close() + self._helper._addFile(recipeDir, 'control') use.setBuildFlagsFromFlavor(pkgName, self._helper._ccfg.buildFlavor, error=False) From johnsonm at rpath.com Wed Aug 19 17:41:25 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:41:25 +0000 Subject: mirrorball: fix typos Message-ID: <200908192141.n7JLfP0x020086@scc.eng.rpath.com> changeset: 1be908848b11 user: Elliot Peele <https://issues.rpath.com/> date: Fri, 14 Nov 2008 18:46:38 -0500 fix typos committer: Elliot Peele <https://issues.rpath.com/> diff --git a/updatebot/advisories/sles.py b/updatebot/advisories/sles.py --- a/updatebot/advisories/sles.py +++ b/updatebot/advisories/sles.py @@ -24,7 +24,7 @@ for path, client in self._pkgSource.getClients().iteritems(): log.info('loading patch information %s' % path) for patch in client.getPatchDetail(): - self._lineOne(patch, path) + self._loadOne(patch, path) def _loadOne(self, patch, path): """ @@ -40,9 +40,9 @@ for package in patch.packages: package.location = path + '/' + package.location - if package not in self.pkgMap: - self.pkgMap[package] = set() - self.pkgMap[package].add(patch) + if package not in self._pkgMap: + self._pkgMap[package] = set() + self._pkgMap[package].add(patch) def _filterPatch(self, patch): """ From johnsonm at rpath.com Wed Aug 19 17:42:58 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:42:58 +0000 Subject: mirrorball: make it easier to run in a different directory Message-ID: <200908192142.n7JLgw5E023783@scc.eng.rpath.com> changeset: 28b3fbaeac55 user: Elliot Peele <https://issues.rpath.com/> date: Tue, 12 May 2009 10:18:42 -0400 make it easier to run in a different directory committer: Elliot Peele <https://issues.rpath.com/> diff --git a/scripts/header.py b/scripts/header.py --- a/scripts/header.py +++ b/scripts/header.py @@ -15,7 +15,8 @@ import os import sys -sys.path.insert(0, os.environ['HOME'] + '/hg/mirrorball') +mirrorballDir = os.environ['HOME'] + '/hg/mirrorball' +sys.path.insert(0, sleestckDir) from conary.lib import util sys.excepthook = util.genExcepthook() @@ -28,12 +29,12 @@ print 'usage: %s <platform> [pkg1, pkg2, ...]' % sys.argv[0] sys.exit(1) -if len(sys.argv) < 2 or sys.argv[1] not in os.listdir(os.environ['HOME'] + '/hg/mirrorball/config'): +if len(sys.argv) < 2 or sys.argv[1] not in os.listdir(mirrorballDir + '/config'): usage() log.addRootLogger() cfg = config.UpdateBotConfig() -cfg.read(os.environ['HOME'] + '/hg/mirrorball/config/%s/updatebotrc' % sys.argv[1]) +cfg.read(mirrorballDir + '/config/%s/updatebotrc' % sys.argv[1]) builder = build.Builder(cfg) From johnsonm at rpath.com Wed Aug 19 17:42:42 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:42:42 +0000 Subject: mirrorball: add code for printing out missing srpms Message-ID: <200908192142.n7JLggWE023170@scc.eng.rpath.com> changeset: 8a7fca76a5ac user: Elliot Peele <https://issues.rpath.com/> date: Thu, 09 Apr 2009 15:47:51 -0400 add code for printing out missing srpms committer: Elliot Peele <https://issues.rpath.com/> diff --git a/updatebot/pkgsource/rpmsource.py b/updatebot/pkgsource/rpmsource.py --- a/updatebot/pkgsource/rpmsource.py +++ b/updatebot/pkgsource/rpmsource.py @@ -206,6 +206,20 @@ count = sum([ len(x) for x in self._rpmMap.itervalues() ]) log.warn('found %s binary rpms without matching srpms' % count) + #srcs = {} + #for x in self._rpmMap.itervalues(): + # for y in x: + # if y.sourcerpm not in srcs: + # srcs[y.sourcerpm] = set() + # srcs[y.sourcerpm].add(y.location) + + #for src, locs in srcs.iteritems(): + # log.warn('missing srpm: %s' % src) + # log.warn('for rpm(s):') + # for loc in sorted(locs): + # log.warn('\t%s' % loc) + + def loadFileLists(self, client, basePath): """ Parse file information. From johnsonm at rpath.com Wed Aug 19 17:41:02 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:41:02 +0000 Subject: mirrorball: stub for ubuntu parser Message-ID: <200908192141.n7JLf22I019162@scc.eng.rpath.com> changeset: 3e9bbee0b322 user: Elliot Peele <https://issues.rpath.com/> date: Wed, 22 Oct 2008 15:54:44 -0400 stub for ubuntu parser committer: Elliot Peele <https://issues.rpath.com/> diff --git a/pmap/ubuntu.py b/pmap/ubuntu.py new file mode 100644 --- /dev/null +++ b/pmap/ubuntu.py @@ -0,0 +1,25 @@ +# +# Copyright (c) 2008 rPath, Inc. +# +# This program is distributed under the terms of the Common Public License, +# version 1.0. A copy of this license should have been distributed with this +# source file in a file called LICENSE. If it is not present, the license +# is always available at http://www.rpath.com/permanent/licenses/CPL-1.0. +# +# This program is distributed in the hope that it will be useful, but +# without any warranty; without even the implied warranty of merchantability +# or fitness for a particular purpose. See the Common Public License for +# full details. +# + +from pmap.common import BaseParser +from pmap.common import BaseContainer + +class Container(BaseContainer): + pass + +class Parser(BaseParser): + def __init__(self): + BaseParser.__init__(self) + + self._containerClass = Container From johnsonm at rpath.com Wed Aug 19 17:40:01 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:40:01 +0000 Subject: mirrorball: add the ability to parse filelists.xml; optimize packagexml parser Message-ID: <200908192140.n7JLe13o016657@scc.eng.rpath.com> changeset: 97dc5eb2ba02 user: Matt Wilson <https://issues.rpath.com/> date: Sat, 21 Jun 2008 15:29:13 -0400 add the ability to parse filelists.xml; optimize packagexml parser committer: Matt Wilson <https://issues.rpath.com/> diff --git a/repomd/repomdxml.py b/repomd/repomdxml.py --- a/repomd/repomdxml.py +++ b/repomd/repomdxml.py @@ -23,6 +23,7 @@ from repomd.primaryxml import PrimaryXml from repomd.patchesxml import PatchesXml +from repomd.filelistsxml import FileListsXml from repomd.xmlcommon import XmlFileParser, SlotNode from repomd.errors import UnknownElementError @@ -47,6 +48,9 @@ elif child.type == 'primary': child._parser = PrimaryXml(None, child.location) child.parseChildren = child._parser.parse + elif child.type == 'filelists': + child._parser = FileListsXml(None, child.location) + child.parseChildren = child._parser.parse xmllib.BaseNode.addChild(self, child) else: raise UnknownElementError(child) From johnsonm at rpath.com Wed Aug 19 17:38:59 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:38:59 +0000 Subject: mirrorball: use standard python modules to retrieve the metadata; remove the tmp file Message-ID: <200908192139.n7JLd0ew014176@scc.eng.rpath.com> changeset: 0520c9ce415f user: Matt Wilson <https://issues.rpath.com/> date: Mon, 02 Jun 2008 15:31:16 -0400 use standard python modules to retrieve the metadata; remove the tmp file committer: Matt Wilson <https://issues.rpath.com/> diff --git a/repomd/repository.py b/repomd/repository.py --- a/repomd/repository.py +++ b/repomd/repository.py @@ -20,8 +20,9 @@ import os import gzip +import shutil import tempfile -import urlgrabber +import urllib2 class Repository(object): ''' @@ -44,12 +45,16 @@ fn = self._getTempFile() realUrl = self._getRealUrl(fileName) - dlFile = urlgrabber.urlgrab(realUrl, filename=fn) + + inf = urllib2.urlopen(realUrl) + outf = open(fn, 'w') + shutil.copyfileobj(inf, outf) if os.path.basename(fileName).endswith('.gz'): - return gzip.open(dlFile) + return gzip.open(fn) else: - return open(dlFile) + return open(fn) + os.unlink(fn) @classmethod def _getTempFile(cls): From johnsonm at rpath.com Wed Aug 19 17:41:25 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:41:25 +0000 Subject: mirrorball: fix typos Message-ID: <200908192141.n7JLfPix020120@scc.eng.rpath.com> changeset: 036c2667c3dc user: Elliot Peele <https://issues.rpath.com/> date: Fri, 14 Nov 2008 18:48:38 -0500 fix typos committer: Elliot Peele <https://issues.rpath.com/> diff --git a/updatebot/bot.py b/updatebot/bot.py --- a/updatebot/bot.py +++ b/updatebot/bot.py @@ -69,7 +69,7 @@ log.info('starting import') # Populate rpm source object from yum metadata. - self.pkgSource.load() + self._pkgSource.load() # Import sources into repository. toBuild, fail = self._updater.create(self._cfg.package, buildAll=False) @@ -128,7 +128,7 @@ log.info('starting update') # Populate rpm source object from yum metadata. - self.pkgSource.load() + self._pkgSource.load() # Get troves to update and send advisories. toAdvise, toUpdate = self._updater.getUpdates() @@ -144,8 +144,6 @@ # Check to see if advisories exist for all required packages. self._advisor.check(toAdvise) - import epdb; epdb.st() - # Update source for nvf, srcPkg in toUpdate: toAdvise.remove((nvf, srcPkg)) From johnsonm at rpath.com Wed Aug 19 17:40:42 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:40:42 +0000 Subject: mirrorball: branch merge Message-ID: <200908192140.n7JLeg9H018345@scc.eng.rpath.com> changeset: 926f54c73add user: Elliot Peele <https://issues.rpath.com/> date: Mon, 08 Sep 2008 22:08:18 -0400 branch merge committer: Elliot Peele <https://issues.rpath.com/> diff --git a/updatebot/build.py b/updatebot/build.py --- a/updatebot/build.py +++ b/updatebot/build.py @@ -102,7 +102,7 @@ jobs[id].append(trv) - if i % 20 == 0: + if i % 40 == 0: id += 1 failed = set() @@ -123,13 +123,18 @@ """ jobs = {} + jobkeys = [] for trv in troveSpecs: + jobkeys.append(trv) jobs[trv] = self.start([trv, ]) - for trv, jobId in jobs.iteritems(): + for trv in jobkeys: + jobId = jobs[trv] job = self._helper.getJob(jobId) - if not job.isFinished() and not job.isFailed(): - self.watch(jobId) + while not job.isFinished() and not job.isFailed(): + log.info('waiting for %s' % jobId) + time.sleep(1) + job = self._helper.getJob(jobId) failed = set() results = {} From johnsonm at rpath.com Wed Aug 19 17:40:50 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:40:50 +0000 Subject: mirrorball: add centos mirror script Message-ID: <200908192140.n7JLeo1L018702@scc.eng.rpath.com> changeset: 329f5c2146b6 user: Elliot Peele <https://issues.rpath.com/> date: Tue, 23 Sep 2008 16:31:00 -0400 add centos mirror script committer: Elliot Peele <https://issues.rpath.com/> diff --git a/scripts/sync-centos.sh b/scripts/sync-centos.sh new file mode 100755 --- /dev/null +++ b/scripts/sync-centos.sh @@ -0,0 +1,21 @@ +#!/bin/bash +# +# Copyright (c) 2008 rPath, Inc. +# +# This program is distributed under the terms of the Common Public License, +# version 1.0. A copy of this license should have been distributed with this +# source file in a file called LICENSE. If it is not present, the license +# is always available at http://www.rpath.com/permanent/licenses/CPL-1.0. +# +# This program is distributed in the hope that it will be useful, but +# without any warranty; without even the implied warranty of merchantability +# or fitness for a particular purpose. See the Common Public License for +# full details. +# + +SOURCE=rsync://mirrors.us.kernel.org/CentOS-nodvd +DEST=/l/CentOS/ + +rsync -arv --progress --exclude 2* --exclude 3* --exclude 4* $SOURCE $DEST + +./hardlink.py $DEST From johnsonm at rpath.com Wed Aug 19 17:40:40 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:40:40 +0000 Subject: mirrorball: make jobs bigger Message-ID: <200908192140.n7JLeeGa018277@scc.eng.rpath.com> changeset: ab742ee08edb user: Elliot Peele <https://issues.rpath.com/> date: Mon, 08 Sep 2008 22:07:32 -0400 make jobs bigger committer: Elliot Peele <https://issues.rpath.com/> diff --git a/updatebot/build.py b/updatebot/build.py --- a/updatebot/build.py +++ b/updatebot/build.py @@ -102,7 +102,7 @@ jobs[id].append(trv) - if i % 20 == 0: + if i % 40 == 0: id += 1 failed = set() @@ -123,13 +123,18 @@ """ jobs = {} + jobkeys = [] for trv in troveSpecs: + jobkeys.append(trv) jobs[trv] = self.start([trv, ]) - for trv, jobId in jobs.iteritems(): + for trv in jobkeys: + jobId = jobs[trv] job = self._helper.getJob(jobId) - if not job.isFinished() and not job.isFailed(): - self.watch(jobId) + while not job.isFinished() and not job.isFailed(): + log.info('waiting for %s' % jobId) + time.sleep(1) + job = self._helper.getJob(jobId) failed = set() results = {} From johnsonm at rpath.com Wed Aug 19 17:42:36 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:42:36 +0000 Subject: mirrorball: use the default tmpdir when building with rmake since the rmake chroot will Message-ID: <200908192142.n7JLgasK022932@scc.eng.rpath.com> changeset: 38187a56217e user: Elliot Peele <https://issues.rpath.com/> date: Thu, 19 Mar 2009 17:05:20 -0400 use the default tmpdir when building with rmake since the rmake chroot will most likely not contain the custom tmpdir committer: Elliot Peele <https://issues.rpath.com/> diff --git a/updatebot/build.py b/updatebot/build.py --- a/updatebot/build.py +++ b/updatebot/build.py @@ -97,6 +97,10 @@ self._rmakeCfg.copyInConfig = False self._rmakeCfg.strictMode = True + # Use default tmpDir when building with rMake since the specified + # tmpDir may not exist in the build root. + self._rmakeCfg.tmpDir = conarycfg.ConaryConfiguration.tmpDir[1] + self._helper = helper.rMakeHelper(buildConfig=self._rmakeCfg) def build(self, troveSpecs): @@ -187,7 +191,7 @@ return results, failed def buildmany2(self, troveSpecs): - dispatcher = Dispatcher(self._cfg, 100) + dispatcher = Dispatcher(self._cfg, 30) return dispatcher.buildmany(troveSpecs) def buildmany3(self, troveSpecs): From johnsonm at rpath.com Wed Aug 19 17:40:26 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:40:26 +0000 Subject: mirrorball: add sourceVersion for packages with odd source packages Message-ID: <200908192140.n7JLeQFg017665@scc.eng.rpath.com> changeset: ead2ecf4d00a user: Elliot Peele <http://bugs.rpath.com/> date: Sat, 23 Aug 2008 16:35:39 -0400 add sourceVersion for packages with odd source packages committer: Elliot Peele <http://bugs.rpath.com/> diff --git a/aptmd/packages.py b/aptmd/packages.py --- a/aptmd/packages.py +++ b/aptmd/packages.py @@ -15,7 +15,8 @@ from aptmd.common import BaseContainer, BaseParser class _Package(BaseContainer): - __slots__ = ('source', 'location', 'summary', 'description') + __slots__ = ('source', 'sourceVersion', 'location', 'summary', + 'description') class PackagesParser(BaseParser): @@ -45,6 +46,17 @@ def _source(self): source = self._getLine() assert source != '' + + # source in the form "Source: srcName (srcVer)" + if len(self._line) == 3: + source = self._line[1] + + srcVer = self._line[2].strip() + srcVer = srcVer.strip('(') + srcVer = srcVer.strip(')') + + self._curObj.sourceVersion = srcVer + self._curObj.source = source def _filename(self): From johnsonm at rpath.com Wed Aug 19 17:39:58 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:39:58 +0000 Subject: mirrorball: add script for checking out all sources Message-ID: <200908192140.n7JLdx1m016572@scc.eng.rpath.com> changeset: 0e2713e83731 user: Elliot Peele <http://issues.rpath.com/> date: Fri, 25 Jul 2008 15:34:57 -0400 add script for checking out all sources committer: Elliot Peele <http://issues.rpath.com/> diff --git a/scripts/checkoutall.py b/scripts/checkoutall.py new file mode 100755 --- /dev/null +++ b/scripts/checkoutall.py @@ -0,0 +1,25 @@ +#!/usr/bin/python +# +# Copryright (c) 2008 rPath, Inc. +# + +import os +import sys + +sys.path.insert(0, os.environ['HOME'] + '/hg/rpath-xmllib') +sys.path.insert(0, os.environ['HOME'] + '/hg/conary') +sys.path.insert(0, os.environ['HOME'] + '/hg/mirrorball') + +from conary.lib import util +sys.excepthook = util.genExcepthook() + +from conary import checkin +from updatebot import conaryhelper, config, log + +log.addRootLogger() +cfg = config.UpdateBotConfig() +cfg.read(os.environ['HOME'] + '/hg/mirrorball/config/sles/updatebotrc') +helper = conaryhelper.ConaryHelper(cfg) + +pkgs = [ name for name, version, flavor in helper.getSourceTroves(cfg.topGroup) if version.trailingLabel().asString() == cfg.topGroup[1] ] +checkin.checkout(helper._repos, helper._ccfg, None, pkgs) From johnsonm at rpath.com Wed Aug 19 17:41:03 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:41:03 +0000 Subject: mirrorball: stub for centos parser Message-ID: <200908192141.n7JLf32f019179@scc.eng.rpath.com> changeset: 4d9775618b16 user: Elliot Peele <https://issues.rpath.com/> date: Wed, 22 Oct 2008 15:55:05 -0400 stub for centos parser committer: Elliot Peele <https://issues.rpath.com/> diff --git a/pmap/centos.py b/pmap/centos.py new file mode 100644 --- /dev/null +++ b/pmap/centos.py @@ -0,0 +1,25 @@ +# +# Copyright (c) 2008 rPath, Inc. +# +# This program is distributed under the terms of the Common Public License, +# version 1.0. A copy of this license should have been distributed with this +# source file in a file called LICENSE. If it is not present, the license +# is always available at http://www.rpath.com/permanent/licenses/CPL-1.0. +# +# This program is distributed in the hope that it will be useful, but +# without any warranty; without even the implied warranty of merchantability +# or fitness for a particular purpose. See the Common Public License for +# full details. +# + +from pmap.common import BaseParser +from pmap.common import BaseContainer + +class Container(BaseContainer): + pass + +class Parser(BaseParser): + def __init__(self): + BaseParser.__init__(self) + + self._containerClass = Container From johnsonm at rpath.com Wed Aug 19 17:42:52 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:42:52 +0000 Subject: mirrorball: take platform as an argument Message-ID: <200908192142.n7JLgqxo023545@scc.eng.rpath.com> changeset: 9a4c4830c5e9 user: Elliot Peele <https://issues.rpath.com/> date: Mon, 04 May 2009 11:10:42 -0400 take platform as an argument use system libs committer: Elliot Peele <https://issues.rpath.com/> diff --git a/scripts/genmanifest b/scripts/genmanifest --- a/scripts/genmanifest +++ b/scripts/genmanifest @@ -16,11 +16,7 @@ import os import sys -sys.path.insert(0, os.environ['HOME'] + '/hg/rpath-xmllib') -sys.path.insert(0, os.environ['HOME'] + '/hg/conary') sys.path.insert(0, os.environ['HOME'] + '/hg/mirrorball') -sys.path.insert(0, os.environ['HOME'] + '/hg/rmake') -sys.path.insert(0, os.environ['HOME'] + '/hg/epdb') from conary.lib import util sys.excepthook = util.genExcepthook() @@ -29,12 +25,12 @@ log.addRootLogger() cfg = config.UpdateBotConfig() -cfg.read(os.environ['HOME'] + '/hg/mirrorball/config/ubuntu/updatebotrc') +cfg.read(os.environ['HOME'] + '/hg/mirrorball/config/%s/updatebotrc' % sys.argv[1]) obj = bot.Bot(cfg) obj._pkgSource.load() -pkgName = sys.argv[1] +pkgName = sys.argv[2] srcPkg = obj._updater._getPackagesToImport(pkgName) manifest = obj._updater._getManifestFromPkgSource(srcPkg) From johnsonm at rpath.com Wed Aug 19 17:43:08 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:43:08 +0000 Subject: mirrorball: pass in productVersion for centos as well Message-ID: <200908192143.n7JLh89b024243@scc.eng.rpath.com> changeset: f94b61a845a1 user: Elliot Peele <https://issues.rpath.com/> date: Thu, 30 Jul 2009 15:28:30 -0400 pass in productVersion for centos as well committer: Elliot Peele <https://issues.rpath.com/> diff --git a/pmap/__init__.py b/pmap/__init__.py --- a/pmap/__init__.py +++ b/pmap/__init__.py @@ -83,12 +83,14 @@ raise InvalidBackendError('Could not load %s backend: %s' % (backend, e)) -def parse(url, backend='centos', productVersion=None): +def parse(url, **kwargs): """ Parse a mbox archive pointed to by url. """ + backend = kwargs.pop('backend') + fh = _getFileObjFromUrl(url) backend = _getBackend(backend) - parser = backend.Parser(productVersion=productVersion) + parser = backend.Parser(**kwargs) return parser.parse(fh) diff --git a/pmap/centos.py b/pmap/centos.py --- a/pmap/centos.py +++ b/pmap/centos.py @@ -54,7 +54,7 @@ Parse for CentOS mail archives. """ - def __init__(self): + def __init__(self, productVersion=None): BaseParser.__init__(self) self._containerClass = CentOSAdvisory From johnsonm at rpath.com Wed Aug 19 17:41:49 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:41:49 +0000 Subject: mirrorball: manipulate the summary, which gets used as the subject of the advisory email Message-ID: <200908192141.n7JLfn5H021042@scc.eng.rpath.com> changeset: 2a852aa82825 user: Elliot Peele <https://issues.rpath.com/> date: Tue, 16 Dec 2008 18:42:28 -0500 manipulate the summary, which gets used as the subject of the advisory email committer: Elliot Peele <https://issues.rpath.com/> diff --git a/updatebot/advisories/centos.py b/updatebot/advisories/centos.py --- a/updatebot/advisories/centos.py +++ b/updatebot/advisories/centos.py @@ -91,12 +91,14 @@ # Strip arch out of the subject for arch in self.supportedArches: - if arch in msg.subject: - msg.subject = msg.subject.replace('%s ' % arch, '') + if arch in msg.summary: + msg.summary = msg.summary.replace('%s ' % arch, '') # Strip subject. - msg.subject = msg.subject.replace('[CentOS-announce]', '') - msg.subject = msg.subject.strip() + msg.summary = msg.summary.replace('[CentOS-announce]', '') + msg.summary = msg.summary.strip() + + import epdb; epdb.st() for pkgName in msg.pkgs: # Toss out any arches that we don't know how to handle. From johnsonm at rpath.com Wed Aug 19 17:42:10 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:42:10 +0000 Subject: mirrorball: add more checks Message-ID: <200908192142.n7JLgAJP021843@scc.eng.rpath.com> changeset: 31733af14a5f user: Elliot Peele <https://issues.rpath.com/> date: Fri, 23 Jan 2009 14:09:42 -0500 add more checks committer: Elliot Peele <https://issues.rpath.com/> diff --git a/scripts/pcheck.py b/scripts/pcheck.py --- a/scripts/pcheck.py +++ b/scripts/pcheck.py @@ -11,6 +11,8 @@ from conary import conarycfg from conary import conaryclient +log.setVerbosity(log.INFO) + cfg = conarycfg.ConaryConfiguration(True) cfg.setContext('1-binary') @@ -57,6 +59,7 @@ # Find troves that have labels that are not in the label map. log.info('Searching for troves that will not be promoted') branches = labelMap.keys() +found = False for name, version, flavor in sorted(oldTrvSpecs): if version.branch() not in branches: match = False @@ -64,8 +67,11 @@ if label in version.versions: match = True if not match: + found = True log.warning('not promoting %s=%s[%s]' % (name, version, flavor)) +if not found: + log.info('No troves found that will not be promoted') # Ask before moving on. okay = conaryclient.cmdline.askYn('continue with clone? [y/N]', default=False) From johnsonm at rpath.com Wed Aug 19 17:42:12 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:42:12 +0000 Subject: mirrorball: add exception for a 404 Message-ID: <200908192142.n7JLgCZc021928@scc.eng.rpath.com> changeset: 4234be298757 user: Elliot Peele <https://issues.rpath.com/> date: Thu, 05 Feb 2009 16:37:44 -0500 add exception for a 404 committer: Elliot Peele <https://issues.rpath.com/> diff --git a/pmap/__init__.py b/pmap/__init__.py --- a/pmap/__init__.py +++ b/pmap/__init__.py @@ -26,7 +26,7 @@ from imputil import imp -__all__ = ('InvalidBackendError', 'parse') +__all__ = ('InvalidBackendError', 'ArchiveNotFoundError', 'parse') __supportedBackends = ('ubuntu', 'centos') class InvalidBackendError(Exception): @@ -34,6 +34,11 @@ Raised when requested backend is not available. """ +class ArchiveNotFoundError(Exception): + """ + Raised when an archive could not be retrieved. + """ + def _getFileObjFromUrl(url): """ Given a URL download the file, gunzip if needed, and return an open @@ -43,7 +48,11 @@ fn = tempfile.mktemp(prefix='pmap') # download file - inf = urllib2.urlopen(url) + try: + inf = urllib2.urlopen(url) + except urllib2.HTTPError, e: + if e.getcode() == 404: + raise ArchiveNotFoundError, e outf = open(fn, 'w') shutil.copyfileobj(inf, outf) From johnsonm at rpath.com Wed Aug 19 17:39:23 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:39:23 +0000 Subject: mirrorball: don't cache parser objects, this just uses more memory Message-ID: <200908192139.n7JLdN3S015174@scc.eng.rpath.com> changeset: 6ada75b670b1 user: Elliot Peele <http://issues.rpath.com/> date: Mon, 16 Jun 2008 17:27:51 -0400 don't cache parser objects, this just uses more memory committer: Elliot Peele <http://issues.rpath.com/> diff --git a/repomd/xmlcommon.py b/repomd/xmlcommon.py --- a/repomd/xmlcommon.py +++ b/repomd/xmlcommon.py @@ -53,15 +53,15 @@ # W0212 - Access to a protected member _parser of a client class # pylint: disable-msg=W0212 - if not self._data or refresh: - fn = self._repository.get(self._path) - self._data = self._databinder.parseFile(fn) + #if not self._data or refresh: + fn = self._repository.get(self._path) + data = self._databinder.parseFile(fn) - for child in self._data.iterChildren(): - if hasattr(child, '_parser'): - child._parser._repository = self._repository + for child in data.iterChildren(): + if hasattr(child, '_parser'): + child._parser._repository = self._repository - return self._data + return data class SlotNode(xmllib.BaseNode): From johnsonm at rpath.com Wed Aug 19 17:39:19 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:39:19 +0000 Subject: mirrorball: iterate over both strong and weak refs Message-ID: <200908192139.n7JLdJdH015000@scc.eng.rpath.com> changeset: 43def59b07fa user: Elliot Peele <http://issues.rpath.com/> date: Wed, 11 Jun 2008 20:51:31 -0400 iterate over both strong and weak refs committer: Elliot Peele <http://issues.rpath.com/> diff --git a/updatebot/conaryhelper.py b/updatebot/conaryhelper.py --- a/updatebot/conaryhelper.py +++ b/updatebot/conaryhelper.py @@ -116,10 +116,14 @@ topTrove = self._getTrove(cs, name, version, flavor) + # Iterate over both strong and weak refs because msw said it was a + # good idea. srcTrvs = set() sources = self._repos.getTroveInfo(trove._TROVEINFO_TAG_SOURCENAME, - list(topTrove.iterTroveList(weakRefs=True))) - for i, (n, v, f) in enumerate(topTrove.iterTroveList(weakRefs=True)): + list(topTrove.iterTroveList(weakRefs=True, + strongRefs=True))) + for i, (n, v, f) in enumerate(topTrove.iterTroveList(weakRefs=True, + strongRefs=True)): srcTrvs.add((sources[i](), v.getSourceVersion(), None)) return srcTrvs From johnsonm at rpath.com Wed Aug 19 17:42:31 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:42:31 +0000 Subject: mirrorball: cleanups Message-ID: <200908192142.n7JLgVGX022694@scc.eng.rpath.com> changeset: 0aeffea0a607 user: Elliot Peele <https://issues.rpath.com/> date: Mon, 16 Mar 2009 11:21:26 -0400 cleanups committer: Elliot Peele <https://issues.rpath.com/> diff --git a/scripts/import.py b/scripts/import.py --- a/scripts/import.py +++ b/scripts/import.py @@ -1,4 +1,4 @@ -#!/usr/bin/python2.6 +#!/usr/bin/python # # Copyright (c) 2008 rPath, Inc. # @@ -16,12 +16,14 @@ import os import sys -sys.path.insert(0, os.environ['HOME'] + '/hg/26/rpath-xmllib') -sys.path.insert(0, os.environ['HOME'] + '/hg/26/conary') +sys.path.insert(0, os.environ['HOME'] + '/hg/xobj/py') + +#sys.path.insert(0, os.environ['HOME'] + '/hg/26/rpath-xmllib') +#sys.path.insert(0, os.environ['HOME'] + '/hg/26/conary') sys.path.insert(0, os.environ['HOME'] + '/hg/26/mirrorball') -sys.path.insert(0, os.environ['HOME'] + '/hg/26/rmake') -sys.path.insert(0, os.environ['HOME'] + '/hg/26/epdb') -sys.path.insert(0, os.environ['HOME'] + '/hg/26/xobj/py') +#sys.path.insert(0, os.environ['HOME'] + '/hg/26/rmake') +#sys.path.insert(0, os.environ['HOME'] + '/hg/26/epdb') +#sys.path.insert(0, os.environ['HOME'] + '/hg/26/xobj/py') from conary.lib import util sys.excepthook = util.genExcepthook() From johnsonm at rpath.com Wed Aug 19 17:39:03 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:39:03 +0000 Subject: mirrorball: add util module Message-ID: <200908192139.n7JLd3ep014360@scc.eng.rpath.com> changeset: 7c12cbcc9694 user: Elliot Peele <http://issues.rpath.com/> date: Mon, 02 Jun 2008 21:26:58 -0400 add util module committer: Elliot Peele <http://issues.rpath.com/> diff --git a/updatebot/util.py b/updatebot/util.py new file mode 100644 --- /dev/null +++ b/updatebot/util.py @@ -0,0 +1,29 @@ +# +# Copyright (c) 2008 rPath, Inc. +# +# This program is distributed under the terms of the Common Public License, +# version 1.0. A copy of this license should have been distributed with this +# source file in a file called LICENSE. If it is not present, the license +# is always available at http://www.rpath.com/permanent/licenses/CPL-1.0. +# +# This program is distributed in the hope that it will be useful, but +# without any warranty; without even the implied warranty of merchantability +# or fitness for a particular purpose. See the Common Public License for +# full details. +# + +""" +Module for common utility functions. +""" + +import os + +def join(a, *b): + """ + Version of os.path.join that doesn't reroot when it finds a leading /. + """ + + root = os.path.normpath(a) + for path in b: + root += os.sep + os.path.normpath(path) + return root From johnsonm at rpath.com Wed Aug 19 17:40:11 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:40:11 +0000 Subject: mirrorball: use common repr Message-ID: <200908192140.n7JLeBHJ017069@scc.eng.rpath.com> changeset: d22dac0e529d user: Elliot Peele <http://issues.rpath.com/> date: Thu, 14 Aug 2008 16:52:29 -0400 use common repr committer: Elliot Peele <http://issues.rpath.com/> diff --git a/aptmd/common.py b/aptmd/common.py --- a/aptmd/common.py +++ b/aptmd/common.py @@ -18,6 +18,11 @@ class BaseContainer(Container): __slots__ = ('name', 'arch', 'version', 'release') + def __repr__(self): + klass = self.__class__.__name__.strip('_') + return '<%s(%s, %s, %s, %s)>' % (klass, self.name, self.version, + self.release, self.arch) + def __hash__(self): return hash((self.name, self.arch, self.version, self.release)) @@ -46,6 +51,7 @@ @staticmethod def _getState(key): + key = key.strip() if key.endswith(':'): key = key[:-1] return key.lower() diff --git a/aptmd/packages.py b/aptmd/packages.py --- a/aptmd/packages.py +++ b/aptmd/packages.py @@ -17,9 +17,6 @@ class _Package(BaseContainer): __slots__ = ('source', 'location', 'summary', 'description') - def __repr__(self): - return '<Package(%s)>' % self.location - class PackagesParser(BaseParser): From johnsonm at rpath.com Wed Aug 19 17:39:12 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:39:12 +0000 Subject: mirrorball: add get manifest method Message-ID: <200908192139.n7JLdC2r014699@scc.eng.rpath.com> changeset: a4cd5cc9bc33 user: Elliot Peele <http://issues.rpath.com/> date: Tue, 10 Jun 2008 00:29:47 -0400 add get manifest method committer: Elliot Peele <http://issues.rpath.com/> diff --git a/rpmimport/recipemaker.py b/rpmimport/recipemaker.py --- a/rpmimport/recipemaker.py +++ b/rpmimport/recipemaker.py @@ -27,9 +27,8 @@ Class for creating and managing rpm factory based source components. """ - def __init__(self, cfg, repos, rpmSource): + def __init__(self, cfg, rpmSource): self.cfg = cfg - self.repos = repos self.rpmSource = rpmSource def _updateSourceComponent(self, pkgname, manifestContents, @@ -139,3 +138,18 @@ """ self._createOrUpdate(pkgname, srpm, update=True) + + def getManifest(self, pkgname): + """ + Get the contents of the manifest file. + @param pkgname: name of the package + @type pkgname: string + @return manifest contents + """ + + cwd = os.getcwd() + os.chdir(tempfile.mkdtemp()) + self._checkout(pkgname) + manifest = open('manifest').readlines() + os.chdir(cwd) + return manifest From johnsonm at rpath.com Wed Aug 19 17:39:29 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:39:29 +0000 Subject: mirrorball: add some needed errors Message-ID: <200908192139.n7JLdTrM015447@scc.eng.rpath.com> changeset: 6ea7498f4d15 user: Elliot Peele <http://issues.rpath.com/> date: Thu, 19 Jun 2008 21:26:58 -0400 add some needed errors committer: Elliot Peele <http://issues.rpath.com/> diff --git a/updatebot/errors.py b/updatebot/errors.py --- a/updatebot/errors.py +++ b/updatebot/errors.py @@ -102,6 +102,26 @@ _params = ['pkgname', 'dir'] _template = 'No manifest was found for %(pkgname)s in directory %(dir)s' +class PromoteFailedError(UnhandledUpdateError): + """ + PromoteFailedError, raised when the bot fails to promote the binary group + to the target release label. + """ + + _params = ['what'] + _template = 'Failed to promote %(what)s' + +class PromoteMismatchError(PromoteFailedError): + """ + PromoteMismatchError, raised when the promote to the production label + either tries to promote packages that are unexpected or does not + promote all expected pacakges. + """ + + _parms = ['expected', 'actual'] + _template = ('Expected to promote %(expected)s, actually tried to promote' + ' %(actual)s.') + class AdvisoryError(UnhandledUpdateError): """ Base error for other advisory errors to inherit from. From johnsonm at rpath.com Wed Aug 19 17:39:56 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:39:56 +0000 Subject: mirrorball: switch to using an in memory sqlite db so that no longer have contention for Message-ID: <200908192140.n7JLduXY016470@scc.eng.rpath.com> changeset: b4429cd6df98 user: Elliot Peele <http://issues.rpath.com/> date: Mon, 14 Jul 2008 20:32:49 -0400 switch to using an in memory sqlite db so that no longer have contention for the system db that we don't use committer: Elliot Peele <http://issues.rpath.com/> diff --git a/updatebot/build.py b/updatebot/build.py --- a/updatebot/build.py +++ b/updatebot/build.py @@ -46,6 +46,7 @@ self._ccfg = conarycfg.ConaryConfiguration(readConfigFiles=False) self._ccfg.read(util.join(self._cfg.configPath, 'conaryrc')) + self._ccfg.dbPath = ':memory:' self._ccfg.initializeFlavors() self._client = conaryclient.ConaryClient(self._ccfg) diff --git a/updatebot/conaryhelper.py b/updatebot/conaryhelper.py --- a/updatebot/conaryhelper.py +++ b/updatebot/conaryhelper.py @@ -43,6 +43,7 @@ def __init__(self, cfg): self._ccfg = conarycfg.ConaryConfiguration(readConfigFiles=False) self._ccfg.read(util.join(cfg.configPath, 'conaryrc')) + self._ccfg.dbPath = ':memory:' # Have to initialize flavors to commit to the repository. self._ccfg.initializeFlavors() From johnsonm at rpath.com Wed Aug 19 17:39:47 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:39:47 +0000 Subject: mirrorball: the rpmsource now provides sets, cast them to lists for sorting Message-ID: <200908192139.n7JLdlIx016079@scc.eng.rpath.com> changeset: c83d196b70d3 user: Elliot Peele <http://issues.rpath.com/> date: Wed, 09 Jul 2008 11:55:15 -0400 the rpmsource now provides sets, cast them to lists for sorting committer: Elliot Peele <http://issues.rpath.com/> diff --git a/updatebot/update.py b/updatebot/update.py --- a/updatebot/update.py +++ b/updatebot/update.py @@ -146,7 +146,7 @@ # Novell releases updates to only the binary rpms of a package # that have chnaged. We have to use binaries from the old srpm. # Get the last version of the pkg and add it to the srcPkgMap. - pkgs = self._rpmSource.binNameMap[binPkg.name] + pkgs = list(self._rpmSource.binNameMap[binPkg.name]) # get the correct arch pkg = [ x for x in self._getLatestOfAvailableArches(pkgs) @@ -214,7 +214,7 @@ @type srcPkg: repomd.packagexml._Package """ - manifestPkgs = self._rpmSource.srcPkgMap[srcPkg] + manifestPkgs = list(self._rpmSource.srcPkgMap[srcPkg]) pkgs = self._getLatestOfAvailableArches(manifestPkgs) return [ x.location for x in pkgs ] From johnsonm at rpath.com Wed Aug 19 17:40:20 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:40:20 +0000 Subject: mirrorball: switch to buildmany for initial import Message-ID: <200908192140.n7JLeKGL017409@scc.eng.rpath.com> changeset: 2c2b77bd5a49 user: Elliot Peele <http://issues.rpath.com/> date: Fri, 15 Aug 2008 18:44:42 -0400 switch to buildmany for initial import committer: Elliot Peele <http://issues.rpath.com/> diff --git a/scripts/buildpackages b/scripts/buildpackages --- a/scripts/buildpackages +++ b/scripts/buildpackages @@ -20,7 +20,7 @@ log.addRootLogger() cfg = config.UpdateBotConfig() -cfg.read(os.environ['HOME'] + '/hg/mirrorball/config/sles/updatebotrc') +cfg.read(os.environ['HOME'] + '/hg/mirrorball/config/ubuntu/updatebotrc') builder = build.Builder(cfg) diff --git a/updatebot/bot.py b/updatebot/bot.py --- a/updatebot/bot.py +++ b/updatebot/bot.py @@ -118,10 +118,13 @@ # Import sources into repository. toBuild, fail = self._updater.create(self._cfg.package) +# import epdb; epdb.st() + + # Build all newly imported packages. + trvMap, failed = self._builder.buildmany(toBuild) + import epdb; epdb.st() - # Build all newly imported packages. - #trvMap, failed = self._builder.buildmany(toBuild) #trvMap = self._builder.build(toBuild) trvs = self._builder._formatInput(toBuild) jobs = {} From johnsonm at rpath.com Wed Aug 19 17:42:17 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:42:17 +0000 Subject: mirrorball: add a simple script to walk a set of packages through the release process Message-ID: <200908192142.n7JLgHg6022132@scc.eng.rpath.com> changeset: 52fd014484b6 user: Elliot Peele <https://issues.rpath.com/> date: Fri, 06 Feb 2009 15:47:54 -0500 add a simple script to walk a set of packages through the release process committer: Elliot Peele <https://issues.rpath.com/> diff --git a/scripts/build b/scripts/build new file mode 100755 --- /dev/null +++ b/scripts/build @@ -0,0 +1,25 @@ +#!/bin/bash +# +# Copyright (c) 2009 rPath, Inc. +# +# Script for moving a package through the release cycle. +# +# This program is distributed under the terms of the Common Public License, +# version 1.0. A copy of this license should have been distributed with this +# source file in a file called LICENSE. If it is not present, the license +# is always available at http://www.rpath.com/permanent/licenses/CPL-1.0. +# +# This program is distributed in the hope that it will be useful, but +# without any warranty; without even the implied warranty of merchantability +# or fitness for a particular purpose. See the Common Public License for +# full details. +# + +platform=$1 +shift +pkgs=$@ + +./buildpackages $platform $pkgs +./buildgroups $platform +./promote.py $platform +./mirror.py $platform From johnsonm at rpath.com Wed Aug 19 17:40:08 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:40:08 +0000 Subject: mirrorball: add script for mirroring ubuntu Message-ID: <200908192140.n7JLe8WQ016932@scc.eng.rpath.com> changeset: 935cf43bb458 user: Elliot Peele <http://bugs.rpath.com/> date: Wed, 13 Aug 2008 14:20:44 -0400 add script for mirroring ubuntu committer: Elliot Peele <http://bugs.rpath.com/> diff --git a/scripts/sync-ubunutu.sh b/scripts/sync-ubunutu.sh new file mode 100644 --- /dev/null +++ b/scripts/sync-ubunutu.sh @@ -0,0 +1,21 @@ +#!/bin/bash +# +# Copyright (c) 2008 rPath, Inc. +# +# This program is distributed under the terms of the Common Public License, +# version 1.0. A copy of this license should have been distributed with this +# source file in a file called LICENSE. If it is not present, the license +# is always available at http://www.rpath.com/permanent/licenses/CPL-1.0. +# +# This program is distributed in the hope that it will be useful, but +# without any warranty; without even the implied warranty of merchantability +# or fitness for a particular purpose. See the Common Public License for +# full details. +# + +SOURCE=rsync://mirrors.us.kernel.org/ubuntu/ +DEST=/l/ubuntu/ + +rsync -arv --progress --exclude dapper* --exclude feisty* --exclude gustsy* --delete --exclude pool/universe/* --exclude pool/restricted/* --exclude pool/multiverse/* $SOURCE $DEST + +./hardlink.py $DEST From johnsonm at rpath.com Wed Aug 19 17:40:55 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:40:55 +0000 Subject: mirrorball: branch merge Message-ID: <200908192140.n7JLet1t018907@scc.eng.rpath.com> changeset: ffa17e083b3b user: Elliot Peele <https://issues.rpath.com/> date: Fri, 26 Sep 2008 14:45:12 -0400 branch merge committer: Elliot Peele <https://issues.rpath.com/> diff --git a/updatebot/cmdline/command.py b/updatebot/cmdline/command.py --- a/updatebot/cmdline/command.py +++ b/updatebot/cmdline/command.py @@ -27,7 +27,7 @@ _commands.append(cmd) -class BotCommand(options.AbstractCommand): +class _BotCommand(options.AbstractCommand): defaultGroup = 'Common Options' docs = {'config' : (VERBOSE_HELP, @@ -58,3 +58,15 @@ def processConfigOptions(self, cfg, cfgMap, argSet): pass + + +class BuildPackageCommand(_BotCommand): + """ + Build a list of packages. + """ + + commands = [ 'buildpackages', ] + help = 'build packages' + + def runCommand(self, client, cfg, argSet, args): + pass diff --git a/updatebot/config.py b/updatebot/config.py --- a/updatebot/config.py +++ b/updatebot/config.py @@ -111,3 +111,6 @@ emailTo = (CfgList(CfgString), []) emailBcc = (CfgList(CfgString), []) smtpServer = CfgString + + def __init__(self, *args, **kwargs): + cfg.SectionedConfigFile.__init__(self) From johnsonm at rpath.com Wed Aug 19 17:42:30 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:42:30 +0000 Subject: mirrorball: write buildrequires corretly Message-ID: <200908192142.n7JLgU6H022643@scc.eng.rpath.com> changeset: cee94eb247c7 user: Elliot Peele <https://issues.rpath.com/> date: Tue, 24 Feb 2009 16:44:03 -0500 write buildrequires corretly committer: Elliot Peele <https://issues.rpath.com/> diff --git a/updatebot/conaryhelper.py b/updatebot/conaryhelper.py --- a/updatebot/conaryhelper.py +++ b/updatebot/conaryhelper.py @@ -309,8 +309,8 @@ Set the contents of the build requires file in the repository. @param pkgname: name of hte package to edit @type pkgname: string - @param buildrequires: list of build requires - @type buildrequires: list + @param buildrequires: list of build requires, source names tuples + @type buildrequires: list of two tuples """ log.info('setting buildrequires for %s' % pkgname) @@ -320,8 +320,9 @@ # generate buildrequires file buildRequiresfh = open(buildRequiresFileName, 'w') - buildRequiresfh.write('\n'.join(buildrequires)) - buildRequiresfh.write('\n') + for buildreq in buildrequires: + buildRequiresfh.write(' '.join(buildreq)) + buildRequiresfh.write('\n') buildRequiresfh.close() # add file to the source compoent From johnsonm at rpath.com Wed Aug 19 17:42:18 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:42:18 +0000 Subject: mirrorball: increase number of workers Message-ID: <200908192142.n7JLgI5w022200@scc.eng.rpath.com> changeset: a81d5608239e user: Elliot Peele <https://issues.rpath.com/> date: Sat, 07 Feb 2009 22:15:57 +0000 increase number of workers fix typo committer: Elliot Peele <https://issues.rpath.com/> diff --git a/updatebot/build.py b/updatebot/build.py --- a/updatebot/build.py +++ b/updatebot/build.py @@ -162,7 +162,7 @@ return results, failed def buildmany2(self, troveSpecs): - dispatcher = Dispatcher(self._cfg, 10) + dispatcher = Dispatcher(self._cfg, 20) return dispatcher.buildmany(troveSpecs) def buildsplitarch(self, troveSpecs): @@ -500,7 +500,7 @@ self._status(msg, type=MESSAGE_TYPES['log']) def results(self, res): - self._status(res, type=MESSAGE_TYPES['result']) + self._status(res, type=MESSAGE_TYPES['results']) class Dispatcher(object): workerClass = BuildWorker @@ -519,7 +519,7 @@ def provisionWorkers(self): for i in range(self._workerCount): - worker = BuildWorker(self._cfg, self._toBuild, self._status, + worker = self.workerClass(self._cfg, self._toBuild, self._status, name='Build Worker %s' % i) self._workers.append(worker) From johnsonm at rpath.com Wed Aug 19 17:40:25 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:40:25 +0000 Subject: mirrorball: add a new case to callpse foo1[a-z]* -> foo Message-ID: <200908192140.n7JLePAp017648@scc.eng.rpath.com> changeset: e206ba29f4f7 user: Elliot Peele <http://bugs.rpath.com/> date: Sat, 23 Aug 2008 16:35:05 -0400 add a new case to callpse foo1[a-z]* -> foo committer: Elliot Peele <http://bugs.rpath.com/> diff --git a/extra/pkgrename.py b/extra/pkgrename.py --- a/extra/pkgrename.py +++ b/extra/pkgrename.py @@ -58,6 +58,14 @@ n = r.name + # collapse foo1[a-z]* when foo is the base package + elif (n.startswith(r.name) + and len(n) > len(r.name) + and n[len(r.name):len(r.name)+1].isdigit() + and n[len(r.name)+1:].isalpha()): + + n = r.name + # collapse foo1 -> foo if foo exists elif n[-1].isdigit(): # find all package names with suffixes trimmed @@ -194,3 +202,8 @@ 'lib32bz2-dev', 'libbz2-dev', 'libbz2-1.0', 'bzip2', 'lib64bz2-1.0', 'lib64bz2-dev'], set(['bzip2', 'libbz2', 'lib32bz2', 'lib64bz2'])) + + test('zlib', + ['zlib1g-dev', 'lib32z1-dev', 'lib32z1', 'zlib1g-dbg', 'zlib1g', + 'lib64z1-dev', 'zlib1g-dbg', 'zlib1g', 'zlib1g-dev', 'lib64z1'], + set(['zlib', 'lib32z1', 'lib64z1'])) From johnsonm at rpath.com Wed Aug 19 17:39:16 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:39:16 +0000 Subject: mirrorball: add start of advisory module Message-ID: <200908192139.n7JLdGiA014876@scc.eng.rpath.com> changeset: 92271d6bdb1b user: Elliot Peele <http://issues.rpath.com/> date: Tue, 10 Jun 2008 16:34:36 -0400 add start of advisory module committer: Elliot Peele <http://issues.rpath.com/> diff --git a/updatebot/advise.py b/updatebot/advise.py new file mode 100644 --- /dev/null +++ b/updatebot/advise.py @@ -0,0 +1,28 @@ +# +# Copyright (c) 2008 rPath, Inc. +# +# This program is distributed under the terms of the Common Public License, +# version 1.0. A copy of this license should have been distributed with this +# source file in a file called LICENSE. If it is not present, the license +# is always available at http://www.rpath.com/permanent/licenses/CPL-1.0. +# +# This program is distributed in the hope that it will be useful, but +# without any warranty; without even the implied warranty of merchantability +# or fitness for a particular purpose. See the Common Public License for +# full details. +# + +""" +Module for managing/manipulating advisories. +""" + +from updatebot.errors import * + +class Advisor(object): + """ + Class for managing, manipulating, and distributing advisories. + """ + + def __init__(self, cfg, rpmSource): + self._cfg = cfg + self._rpmSource = rpmSource From johnsonm at rpath.com Wed Aug 19 17:42:32 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:42:32 +0000 Subject: mirrorball: add getLatestVersions method that finds all of the latest versions on the Message-ID: <200908192142.n7JLgW9f022745@scc.eng.rpath.com> changeset: 292c83254165 user: Elliot Peele <https://issues.rpath.com/> date: Mon, 16 Mar 2009 11:24:05 -0400 add getLatestVersions method that finds all of the latest versions on the build label committer: Elliot Peele <https://issues.rpath.com/> diff --git a/updatebot/conaryhelper.py b/updatebot/conaryhelper.py --- a/updatebot/conaryhelper.py +++ b/updatebot/conaryhelper.py @@ -514,6 +514,28 @@ return None + def getLatestVersions(self): + """ + Find all of the versions on the buildLabel. + @return {trvName: trvVersion} + """ + + label = self._ccfg.buildLabel + + trvMap = self._repos.getTroveLeavesByLabel({None: {label: None}}) + + verMap = {} + for name, verDict in trvMap.iteritems(): + if len(verDict) > 1: + vers = verDict.keys() + vers.sort() + ver = vers[-1] + else: + ver = verDict.keys()[0] + verMap[name] = ver + + return verMap + def promote(self, trvLst, expected, sourceLabels, targetLabel, checkPackageList=True, extraPromoteTroves=None, commit=True): From johnsonm at rpath.com Wed Aug 19 17:39:40 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:39:40 +0000 Subject: mirrorball: add errors test Message-ID: <200908192139.n7JLdehE015841@scc.eng.rpath.com> changeset: b13fc8eb10d6 user: Elliot Peele <http://issues.rpath.com/> date: Tue, 24 Jun 2008 16:15:00 -0400 add errors test committer: Elliot Peele <http://issues.rpath.com/> diff --git a/test/unit_test/updatebottest/errortest.py b/test/unit_test/updatebottest/errortest.py new file mode 100644 --- /dev/null +++ b/test/unit_test/updatebottest/errortest.py @@ -0,0 +1,28 @@ +# +# Copyright (c) 2008 rPath, Inc. +# +# This program is distributed under the terms of the Common Public License, +# version 1.0. A copy of this license should have been distributed with this +# source file in a file called LICENSE. If it is not present, the license +# is always available at http://www.rpath.com/permanent/licenses/CPL-1.0. +# +# This program is distributed in the hope that it will be useful, but +# without any warranty; without even the implied warranty of merchantability +# or fitness for a particular purpose. See the Common Public License for +# full details. +# + +import testsetup + +import mock +import slehelp + +from updatebot import errors + +class ErrorTest(slehelp.Helper): + def testRepr(self): + error = errors.UpdateBotError() + self.failUnlessEqual(repr(error), 'updatebot.errors.UpdateBotError()') + + +testsetup.main() From johnsonm at rpath.com Wed Aug 19 17:40:23 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:40:23 +0000 Subject: mirrorball: add keyword arg for building all troves found rather than just new troves Message-ID: <200908192140.n7JLeNXQ017546@scc.eng.rpath.com> changeset: 42b7e370133c user: Elliot Peele <http://issues.rpath.com/> date: Mon, 18 Aug 2008 22:07:13 -0400 add keyword arg for building all troves found rather than just new troves committer: Elliot Peele <http://issues.rpath.com/> diff --git a/updatebot/update.py b/updatebot/update.py --- a/updatebot/update.py +++ b/updatebot/update.py @@ -195,11 +195,13 @@ return ret - def create(self, pkgNames): + def create(self, pkgNames, buildAll=False): """ Import a new package into the repository. @param pkgNames: list of packages to import @type pkgNames: list + @param buildAll: return a list of all troves found rather than just the new ones. + @type buildAll: boolean @return new source [(name, version, flavor), ... ] """ @@ -233,7 +235,7 @@ else: version = version[0] - if not self._conaryhelper._getVersionsByName(pkg.name): + if not self._conaryhelper._getVersionsByName(pkg.name) or buildAll: toBuild.add((pkg.name, version, None)) else: log.info('not building %s' % pkg.name) From johnsonm at rpath.com Wed Aug 19 17:39:24 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:39:24 +0000 Subject: mirrorball: move generation of locationmap so that all paths are cached in the map Message-ID: <200908192139.n7JLdO7h015208@scc.eng.rpath.com> changeset: 20051c2d5394 user: Elliot Peele <http://issues.rpath.com/> date: Mon, 16 Jun 2008 17:29:29 -0400 move generation of locationmap so that all paths are cached in the map committer: Elliot Peele <http://issues.rpath.com/> diff --git a/rpmimport/rpmsource.py b/rpmimport/rpmsource.py --- a/rpmimport/rpmsource.py +++ b/rpmimport/rpmsource.py @@ -76,6 +76,7 @@ if package.name not in self.srcNameMap: self.srcNameMap[package.name] = [] self.srcNameMap[package.name].append(package) + self.locationMap[package.location] = package def _procBin(self, basePath, package): """ @@ -97,6 +98,7 @@ if package.name not in self.binNameMap: self.binNameMap[package.name] = [] self.binNameMap[package.name].append(package) + self.locationMap[package.location] = package def load(self, url, basePath=''): """ @@ -151,7 +153,6 @@ self.srcPkgMap[pkg].append(pkg) for binPkg in self.srcPkgMap[pkg]: - self.locationMap[binPkg.location] = binPkg self.binPkgMap[binPkg] = pkg log.warn('found %s source rpms without matching binary rpms' % count) From johnsonm at rpath.com Wed Aug 19 17:42:56 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:42:56 +0000 Subject: mirrorball: fix some typoes Message-ID: <200908192142.n7JLgu4D023732@scc.eng.rpath.com> changeset: eee147a145b3 user: Elliot Peele <https://issues.rpath.com/> date: Mon, 11 May 2009 16:01:19 -0400 fix some typoes raise an exception if no advisories were found committer: Elliot Peele <https://issues.rpath.com/> diff --git a/updatebot/advisories/common.py b/updatebot/advisories/common.py --- a/updatebot/advisories/common.py +++ b/updatebot/advisories/common.py @@ -225,6 +225,9 @@ if binPkg in self._pkgMap: patches.update(self._pkgMap[binPkg]) + if len(patches) == 0: + raise NoAdvisoryFoundError(why=srcPkg) + if self._checkForDuplicates(patches): patches = set([patches.pop()]) @@ -307,7 +310,7 @@ """ log.error('The %s backend does not implement %s' - % (self.__class__.__name__, '_isUpdateRepo')) + % (self.__class__.__name__, '_isUpdatesRepo')) raise NotImplementedError def _checkForDuplicates(self, patchSet): @@ -321,7 +324,7 @@ """ log.error('The %s backend does not implement %s' - % (self.__class__.__name__, '_isUpdateRepo')) + % (self.__class__.__name__, '_checkForDuplicates')) raise NotImplementedError def _filterPatch(self, patch): From johnsonm at rpath.com Wed Aug 19 17:41:43 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:41:43 +0000 Subject: mirrorball: add script for doing a forced update Message-ID: <200908192141.n7JLfh3O020787@scc.eng.rpath.com> changeset: 7bce3883eba7 user: Elliot Peele <https://issues.rpath.com/> date: Wed, 19 Nov 2008 22:46:53 -0500 add script for doing a forced update committer: Elliot Peele <https://issues.rpath.com/> diff --git a/scripts/forceupdate.py b/scripts/forceupdate.py new file mode 100755 --- /dev/null +++ b/scripts/forceupdate.py @@ -0,0 +1,29 @@ +#!/usr/bin/python2.6 +# +# Copyright (c) 2008 rPath, Inc. +# +# This program is distributed under the terms of the Common Public License, +# version 1.0. A copy of this license should have been distributed with this +# source file in a file called LICENSE. If it is not present, the license +# is always available at http://www.rpath.com/permanent/licenses/CPL-1.0. +# +# This program is distributed in the hope that it will be useful, but +# without any warranty; without even the implied warranty of merchantability +# or fitness for a particular purpose. See the Common Public License for +# full details. +# + +""" +This script is for pushing updates with advisories even if some packages have +gone away. Mostly useful when something has been obsoleted upstream. +""" + +from header import * + +assert len(sys.argv) > 2 +pkgs = sys.argv[2:] + +from updatebot import bot + +b = bot.Bot(cfg) +b.update(force=pkgs) From johnsonm at rpath.com Wed Aug 19 17:39:59 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:39:59 +0000 Subject: mirrorball: yes, i'm committing break points Message-ID: <200908192140.n7JLdxWA016606@scc.eng.rpath.com> changeset: 5024da8cb101 user: Elliot Peele <http://issues.rpath.com/> date: Fri, 25 Jul 2008 16:02:57 -0400 yes, i'm committing break points committer: Elliot Peele <http://issues.rpath.com/> diff --git a/updatebot/bot.py b/updatebot/bot.py --- a/updatebot/bot.py +++ b/updatebot/bot.py @@ -106,23 +106,30 @@ # Populate rpm source object from yum metadata. self._populateRpmSource() - import epdb; epdb.st() +# import epdb; epdb.st() # Import sources into repository. toBuild, fail = self._updater.create(self._cfg.package) - import epdb; epdb.st() +# import epdb; epdb.st() # Build all newly imported packages. + #trvMap, failed = self._builder.buildmany(toBuild) trvMap = self._builder.build(toBuild) + import epdb; epdb.st() + for trv in self._flattenSetDict(trvMap): log.info('built: %s' % trv) + import epdb; epdb.st() + log.info('import completed successfully') log.info('imported %s source packages' % (len(toBuild), )) log.info('elapsed time %s' % (time.time() - start, )) + return trvMap + def update(self): """ Update the conary repository from the yum repositories. From johnsonm at rpath.com Wed Aug 19 17:42:36 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:42:36 +0000 Subject: mirrorball: add adivsory stub for scientific linux Message-ID: <200908192142.n7JLgat8022898@scc.eng.rpath.com> changeset: 98d584b98d27 user: Elliot Peele <https://issues.rpath.com/> date: Mon, 16 Mar 2009 17:31:25 -0400 add adivsory stub for scientific linux committer: Elliot Peele <https://issues.rpath.com/> diff --git a/updatebot/advisories/scientific.py b/updatebot/advisories/scientific.py new file mode 100644 --- /dev/null +++ b/updatebot/advisories/scientific.py @@ -0,0 +1,30 @@ +# +# Copyright (c) 2008-2009 rPath, Inc. +# +# This program is distributed under the terms of the Common Public License, +# version 1.0. A copy of this license should have been distributed with this +# source file in a file called LICENSE. If it is not present, the license +# is always available at http://www.rpath.com/permanent/licenses/CPL-1.0. +# +# This program is distributed in the hope that it will be useful, but +# without any warranty; without even the implied warranty of merchantability +# or fitness for a particular purpose. See the Common Public License for +# full details. +# + +""" +Advisory module for Scientific Linux. +""" + +import os +import pmap +import logging + +from updatebot.advisories.common import BaseAdvisor + +log = logging.getLogger('updatebot.advisories') + +class Advisor(BaseAdvisor): + """ + Class for processing Scientific Linux advisory information. + """ From johnsonm at rpath.com Wed Aug 19 17:39:57 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:39:57 +0000 Subject: mirrorball: add branch config type Message-ID: <200908192140.n7JLdvsP016504@scc.eng.rpath.com> changeset: 0bb61d842cc0 user: Elliot Peele <http://issues.rpath.com/> date: Fri, 25 Jul 2008 15:28:18 -0400 add branch config type committer: Elliot Peele <http://issues.rpath.com/> diff --git a/updatebot/config.py b/updatebot/config.py --- a/updatebot/config.py +++ b/updatebot/config.py @@ -17,11 +17,28 @@ """ from conary.lib import cfg +from conary import versions from conary.conarycfg import CfgFlavor, CfgLabel -from conary.lib.cfgtypes import CfgString, CfgList, CfgRegExp +from conary.lib.cfgtypes import CfgString, CfgList, CfgRegExp, ParseError from rmake.build.buildcfg import CfgTroveSpec +class CfgBranch(CfgLabel): + """ + Class for representing conary branches. + """ + + def parseString(self, val): + """ + Parse config string. + """ + + try: + versions.Branch(val) + except versions.ParseError, e: + raise ParseError, e + + class UpdateBotConfig(cfg.SectionedConfigFile): """ Config class for updatebot. @@ -53,7 +70,7 @@ # Other labels that are referenced in the group that need to be flattend # onto the targetLabel. - sourceLabel = (CfgList(CfgLabel), []) + sourceLabel = (CfgList(CfgBranch), []) # Label to promote to targetLabel = CfgLabel From johnsonm at rpath.com Wed Aug 19 17:40:38 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:40:38 +0000 Subject: mirrorball: load required use flags Message-ID: <200908192140.n7JLec34018192@scc.eng.rpath.com> changeset: fc998b1e521d user: Elliot Peele <https://issues.rpath.com/> date: Mon, 08 Sep 2008 01:37:30 -0400 load required use flags committer: Elliot Peele <https://issues.rpath.com/> diff --git a/extra/logparse.py b/extra/logparse.py --- a/extra/logparse.py +++ b/extra/logparse.py @@ -22,6 +22,7 @@ sys.path.insert(0, os.environ['HOME'] + '/hg/conary') sys.path.insert(0, os.environ['HOME'] + '/hg/mirrorball') +from conary.build import use from conary.conaryclient.cmdline import askYn from rpath_common.xmllib import api1 as xmllib @@ -475,12 +476,16 @@ pkgName = self._getPkgName() log.info('writing control file for %s' % pkgName) - recipeDir = self.helper._checkout(pkgName) + recipeDir = self._helper._checkout(pkgName) fd = open(os.path.join(recipeDir, 'control'), 'w') fd.write(self.getControl()) fd.close() - self.helper._addFile(recipeDir, 'control') - self.helper._commit(recipeDir, 'add/update control file') + self._helper._addFile(recipeDir, 'control') + + use.setBuildFlagsFromFlavor(pkgName, self._helper._ccfg.buildFlavor, + error=False) + + self._helper._commit(recipeDir, 'add/update control file') def __repr__(self): return self._name From johnsonm at rpath.com Wed Aug 19 17:41:38 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:41:38 +0000 Subject: mirrorball: branch merge Message-ID: <200908192141.n7JLfcRl020566@scc.eng.rpath.com> changeset: cbe9d120c5cc user: Elliot Peele <https://issues.rpath.com/> date: Tue, 18 Nov 2008 13:33:31 -0500 branch merge committer: Elliot Peele <https://issues.rpath.com/> diff --git a/updatebot/pkgsource/rpmsource.py b/updatebot/pkgsource/rpmsource.py --- a/updatebot/pkgsource/rpmsource.py +++ b/updatebot/pkgsource/rpmsource.py @@ -181,6 +181,7 @@ # Now that we have processed all of the rpms, build some more data # structures. count = 0 + toDelete = set() for pkg in self._srcPkgs: key = (pkg.name, pkg.epoch, pkg.version, pkg.release, pkg.arch) if pkg in self.srcPkgMap: @@ -198,7 +199,7 @@ self.srcPkgMap[pkg] = self._rpmMap[key] self.srcPkgMap[pkg].add(pkg) - del self._rpmMap[key] + toDelete.add(key) for binPkg in self.srcPkgMap[pkg]: self.binPkgMap[binPkg] = pkg @@ -206,6 +207,10 @@ if count > 0: log.warn('found %s source rpms without matching binary rpms' % count) + # Defer deletes, contents of rpmMap are used more than once. + for key in toDelete: + del self._rpmMap[key] + # Attempt to match up remaining binaries with srpms. for srcTup in self._rpmMap.keys(): srcKey = list(srcTup) From johnsonm at rpath.com Wed Aug 19 17:39:32 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:39:32 +0000 Subject: mirrorball: special case groups Message-ID: <200908192139.n7JLdWwL015550@scc.eng.rpath.com> changeset: 775700482814 user: Elliot Peele <http://issues.rpath.com/> date: Fri, 20 Jun 2008 00:45:53 -0400 special case groups committer: Elliot Peele <http://issues.rpath.com/> diff --git a/updatebot/build.py b/updatebot/build.py --- a/updatebot/build.py +++ b/updatebot/build.py @@ -75,8 +75,14 @@ # Build all troves in defined contexts. troves = [] for name, version, flavor in troveSpecs: - for context in self._cfg.archContexts: - troves.append((name, version, flavor, context)) + # Don't set context for groups, they will already have the + # correct flavors. + if name.startswith('group-'): + troves.append((name, version, flavor)) + else: + # Build all packages as x86 and x86_64. + for context in self._cfg.archContexts: + troves.append((name, version, flavor, context)) jobId = self._startJob(troves) self._monitorJob(jobId) @@ -92,7 +98,7 @@ ret[(n, sv, None)] = set() for name, version, flavor in trvMap[(sn, sv, sf, c)]: if name == n: - ret[(n, v, None)].add((name, version, flavor)) + ret[(n, sv, None)].add((name, version, flavor)) return ret From johnsonm at rpath.com Wed Aug 19 17:39:14 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:39:14 +0000 Subject: mirrorball: add more errors Message-ID: <200908192139.n7JLdEcu014803@scc.eng.rpath.com> changeset: fd8f03677073 user: Elliot Peele <http://issues.rpath.com/> date: Tue, 10 Jun 2008 16:30:02 -0400 add more errors committer: Elliot Peele <http://issues.rpath.com/> diff --git a/updatebot/errors.py b/updatebot/errors.py --- a/updatebot/errors.py +++ b/updatebot/errors.py @@ -57,3 +57,38 @@ _params = ['jobId', 'why'] _templates = 'rMake job %(jobId)s failed: %(why)s' + + +class UnhandledUpdateError(UpdateBotError): + """ + UnhandledUpdateError, raised when the bot finds a state that it does not + know how to handle. + """ + + _params = ['why'] + _template = 'An unhandled update case has occured: %(why)s' + + +class TooManySrpmsError(UnhandledUpdateError): + """ + TooManySrpmsError, raised when the bot finds multiple srpms of the same + version. + """ + +class UpdateGoesBackwardsError(UnhandledUpdateError): + """ + UpdateGoesBackwardsError, raised when the bot tries to update to an older + version. + """ + +class UpdateRemovesPackageError(UnhandledUpdateError): + """ + UpdateRemovesPackageError, raised when the bot tries to remove an rpm from + the manifest. + """ + +class TooManyFlavorsFoundError(UnhandledUpdateError): + """ + TooManFlavorsFoundError, raised when the bot finds more flavors of the top + level group trove than expected. + """ From johnsonm at rpath.com Wed Aug 19 17:41:46 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:41:46 +0000 Subject: mirrorball: print the date to the log before starting a mirror Message-ID: <200908192141.n7JLfk9r020923@scc.eng.rpath.com> changeset: 61aa41c769f5 user: Elliot Peele <https://issues.rpath.com/> date: Mon, 01 Dec 2008 11:13:29 -0500 print the date to the log before starting a mirror committer: Elliot Peele <https://issues.rpath.com/> diff --git a/scripts/sync-centos.sh b/scripts/sync-centos.sh --- a/scripts/sync-centos.sh +++ b/scripts/sync-centos.sh @@ -16,6 +16,7 @@ SOURCE=rsync://mirrors.us.kernel.org/CentOS-nodvd DEST=/l/CentOS/ +date rsync -arv --progress --bwlimit=1024 --exclude 2* --exclude 3* --exclude 4* $SOURCE $DEST ./hardlink.py $DEST diff --git a/scripts/sync-opensuse.sh b/scripts/sync-opensuse.sh --- a/scripts/sync-opensuse.sh +++ b/scripts/sync-opensuse.sh @@ -1,5 +1,6 @@ #!/bin/bash -xe +date rsync -arv --exclude iso --exclude 10.* --exclude ppc --exclude ppc64 rsync://rsync.opensuse.org/opensuse-full/opensuse/ /mnt/rpath/linux/opensuse ./hardlink.py /mnt/rpath/linux/opensuse diff --git a/scripts/sync-ubuntu.sh b/scripts/sync-ubuntu.sh --- a/scripts/sync-ubuntu.sh +++ b/scripts/sync-ubuntu.sh @@ -16,6 +16,8 @@ SOURCE=rsync://mirrors.us.kernel.org/ubuntu/ DEST=/l/ubuntu/ +date + rsync -arv --progress --exclude dapper* --exclude feisty* --exclude gustsy* --delete --exclude pool/universe/* --exclude pool/restricted/* --exclude pool/multiverse/* $SOURCE $DEST ./hardlink.py $DEST From johnsonm at rpath.com Wed Aug 19 17:39:05 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:39:05 +0000 Subject: mirrorball: add loadFromClient so that we can cache client objects in updatebot Message-ID: <200908192139.n7JLd5SM014414@scc.eng.rpath.com> changeset: 08d1241ff7ef user: Elliot Peele <http://issues.rpath.com/> date: Mon, 02 Jun 2008 21:29:17 -0400 add loadFromClient so that we can cache client objects in updatebot committer: Elliot Peele <http://issues.rpath.com/> diff --git a/rpmimport/rpmsource.py b/rpmimport/rpmsource.py --- a/rpmimport/rpmsource.py +++ b/rpmimport/rpmsource.py @@ -106,7 +106,7 @@ self.rpmMap[srpm] = {longLoc: package} self.revMap[package.name] = srpm - def load(self, url, basePath): + def load(self, url, basePath=''): """ Walk the yum repository rooted at url/basePath and collect information about rpms found. @@ -117,6 +117,17 @@ """ client = repomd.Client(url + '/' + basePath) + self.loadFromClient(client) + + def loadFromClient(self, client, basePath=''): + ''' + Walk the yum repository rooted at url/basePath and collect information + about rpms found. + @param client: client object for extracting data from the repo metadata + @type client: repomd.Client + @param basePath: path to prefix location metadata with + @type basePath: string + ''' for pkg in client.getPackageDetail(): # ignore the 32-bit compatibility libs - we will From johnsonm at rpath.com Wed Aug 19 17:40:22 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:40:22 +0000 Subject: mirrorball: disable all plugins Message-ID: <200908192140.n7JLeMGZ017529@scc.eng.rpath.com> changeset: 7104dee06c2a user: Elliot Peele <http://issues.rpath.com/> date: Mon, 18 Aug 2008 22:05:00 -0400 disable all plugins committer: Elliot Peele <http://issues.rpath.com/> diff --git a/updatebot/build.py b/updatebot/build.py --- a/updatebot/build.py +++ b/updatebot/build.py @@ -52,7 +52,9 @@ # manager, then create a new rmake config object so that rmakeUser # will be parsed correctly. rmakeCfg = buildcfg.BuildConfiguration(readConfigFiles=False) - pluginMgr = plugins.PluginManager(rmakeCfg.pluginDirs) + disabledPlugins = [ x[0] for x in rmakeCfg.usePlugin.items() if not x[1] ] + disabledPlugins.append('monitor') + pluginMgr = plugins.PluginManager(rmakeCfg.pluginDirs, disabledPlugins) pluginMgr.loadPlugins() pluginMgr.callClientHook('client_preInit', self, []) @@ -255,7 +257,7 @@ log.info('Starting commit of job %d', jobId) self._helper.client.startCommit([jobId, ]) - succeeded, data = commit.commitJobs(self._helper.getConaryClient(), #self._client, + succeeded, data = commit.commitJobs(self._helper.getConaryClient(), [job, ], self._rmakeCfg.reposName, self._cfg.commitMessage) From johnsonm at rpath.com Wed Aug 19 17:39:44 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:39:44 +0000 Subject: mirrorball: filter out sources Message-ID: <200908192139.n7JLdiMN015977@scc.eng.rpath.com> changeset: 592a351ee1cf user: Elliot Peele <http://issues.rpath.com/> date: Fri, 27 Jun 2008 13:40:02 -0400 filter out sources committer: Elliot Peele <http://issues.rpath.com/> diff --git a/updatebot/conaryhelper.py b/updatebot/conaryhelper.py --- a/updatebot/conaryhelper.py +++ b/updatebot/conaryhelper.py @@ -328,15 +328,16 @@ packageList = [ x.getNewNameVersionFlavor() for x in cs.iterNewTroveList() ] - oldPkgs = set([ (x[0], x[2]) for x in expected ]) - newPkgs = set([ (x[0], x[2]) for x in packageList ]) + oldPkgs = set([ (x[0], x[2]) for x in expected if not x[0].endswith(':source') ]) + newPkgs = set([ (x[0], x[2]) for x in packageList if not x[0].endswith(':source') ]) # Make sure that all packages being promoted are in the set of packages # that we think should be available to promote. Note that all packages # in expected will not be promoted because not all packages are # included in the groups. difference = newPkgs.difference(oldPkgs) - if difference != set(): + grpTrvs = set([ (x[0], x[2]) for x in trvLst if not x[0].endswith(':source') ]) + if difference != grpTrvs: raise PromoteMismatchError(expected=oldPkgs, actual=newPkgs) log.info('committing changeset') From johnsonm at rpath.com Wed Aug 19 17:42:48 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:42:48 +0000 Subject: mirrorball: use system libs Message-ID: <200908192142.n7JLgmoo023391@scc.eng.rpath.com> changeset: 1587fdfce598 user: Elliot Peele <https://issues.rpath.com/> date: Wed, 22 Apr 2009 16:59:58 -0400 use system libs allow for specifying platform committer: Elliot Peele <https://issues.rpath.com/> diff --git a/scripts/checkoutall b/scripts/checkoutall --- a/scripts/checkoutall +++ b/scripts/checkoutall @@ -1,13 +1,15 @@ #!/usr/bin/python # -# Copryright (c) 2008 rPath, Inc. +# Copryright (c) 2008-2009 rPath, Inc. # +""" +Checkout all sources for a given platform to the current working directory. +""" + import os import sys -sys.path.insert(0, os.environ['HOME'] + '/hg/rpath-xmllib') -sys.path.insert(0, os.environ['HOME'] + '/hg/conary') sys.path.insert(0, os.environ['HOME'] + '/hg/mirrorball') from conary.lib import util @@ -18,8 +20,9 @@ log.addRootLogger() cfg = config.UpdateBotConfig() -cfg.read(os.environ['HOME'] + '/hg/mirrorball/config/sles/updatebotrc') +cfg.read(os.environ['HOME'] + '/hg/mirrorball/config/%s/updatebotrc' % sys.argv[1]) helper = conaryhelper.ConaryHelper(cfg) -pkgs = [ name for name, version, flavor in helper.getSourceTroves(cfg.topGroup) if version.trailingLabel().asString() == cfg.topGroup[1] ] +pkgs = [ name for name, version, flavor in helper.getSourceTroves(cfg.topGroup) + if version.trailingLabel().asString() == cfg.topGroup[1] ] checkin.checkout(helper._repos, helper._ccfg, None, pkgs) From johnsonm at rpath.com Wed Aug 19 17:39:33 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:39:33 +0000 Subject: mirrorball: convert classmethods to staticmethods Message-ID: <200908192139.n7JLdXEh015584@scc.eng.rpath.com> changeset: 7f9da7edb365 user: Elliot Peele <http://issues.rpath.com/> date: Fri, 20 Jun 2008 11:01:33 -0400 convert classmethods to staticmethods committer: Elliot Peele <http://issues.rpath.com/> diff --git a/updatebot/advise.py b/updatebot/advise.py --- a/updatebot/advise.py +++ b/updatebot/advise.py @@ -85,8 +85,8 @@ return True return False - @classmethod - def _isSecurity(self, binPkg): + @staticmethod + def _isSecurity(binPkg): """ Check the repository name. If this package didn't come from a updates repository it is probably not security related. diff --git a/updatebot/conaryhelper.py b/updatebot/conaryhelper.py --- a/updatebot/conaryhelper.py +++ b/updatebot/conaryhelper.py @@ -79,8 +79,8 @@ return srcTrvs - @classmethod - def _findLatest(cls, trvlst): + @staticmethod + def _findLatest(trvlst): """ Given a list of trove specs, find the most recent versions. @param trvlst: list of trove specs @@ -130,8 +130,8 @@ return srcTrvs - @classmethod - def _getTrove(cls, cs, name, version, flavor): + @staticmethod + def _getTrove(cs, name, version, flavor): """ Get a trove object for a given name, version, flavor from a changeset. @param cs: conary changeset object From johnsonm at rpath.com Wed Aug 19 17:41:33 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:41:33 +0000 Subject: mirrorball: defer deleting rpmMap contents Message-ID: <200908192141.n7JLfX2u020361@scc.eng.rpath.com> changeset: f014d25c47ff user: Elliot Peele <https://issues.rpath.com/> date: Tue, 18 Nov 2008 11:10:58 -0500 defer deleting rpmMap contents committer: Elliot Peele <https://issues.rpath.com/> diff --git a/updatebot/pkgsource/rpmsource.py b/updatebot/pkgsource/rpmsource.py --- a/updatebot/pkgsource/rpmsource.py +++ b/updatebot/pkgsource/rpmsource.py @@ -150,6 +150,7 @@ # Now that we have processed all of the rpms, build some more data # structures. count = 0 + toDelete = set() for pkg in self._srcPkgs: key = (pkg.name, pkg.epoch, pkg.version, pkg.release, pkg.arch) if pkg in self.srcPkgMap: @@ -167,7 +168,7 @@ self.srcPkgMap[pkg] = self._rpmMap[key] self.srcPkgMap[pkg].add(pkg) - del self._rpmMap[key] + toDelete.add(key) for binPkg in self.srcPkgMap[pkg]: self.binPkgMap[binPkg] = pkg @@ -175,6 +176,10 @@ if count > 0: log.warn('found %s source rpms without matching binary rpms' % count) + # Defer deletes, contents of rpmMap are used more than once. + for key in toDelete: + del self._rpmMap[key] + # Attempt to match up remaining binaries with srpms. for srcTup in self._rpmMap.keys(): srcKey = list(srcTup) From johnsonm at rpath.com Wed Aug 19 17:43:01 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:43:01 +0000 Subject: mirrorball: don't synthesize sources for packages that should be matched up to another Message-ID: <200908192143.n7JLh1s4023919@scc.eng.rpath.com> changeset: 4997dc8d72b7 user: Elliot Peele <https://issues.rpath.com/> date: Mon, 08 Jun 2009 14:06:50 -0400 don't synthesize sources for packages that should be matched up to another source through epoch fuzzing committer: Elliot Peele <https://issues.rpath.com/> diff --git a/updatebot/pkgsource/rpmsource.py b/updatebot/pkgsource/rpmsource.py --- a/updatebot/pkgsource/rpmsource.py +++ b/updatebot/pkgsource/rpmsource.py @@ -270,6 +270,17 @@ # of the file. The factory will take care of the rest. srcPkg.location = pkg.sourcerpm + # Handle sub packages with different epochs that should be taken + # care of with the epoch fuzzing that happens in finalize. This + # should only happen with differently named packages. + if name not in [ x.name for x in bins ]: + # Find sources that match on all cases except epoch. + sources = [ x for x in self._srcMap.iterkeys() + if (name, version, release, arch) == (x[0], x[2], x[3], x[4]) ] + + # leave it up to fuzzing + if sources: continue + # add source to structures if (name, epoch, version, release, arch) not in self._srcMap: log.warn('synthesizing source package %s' % srcPkg) From johnsonm at rpath.com Wed Aug 19 17:39:16 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:39:16 +0000 Subject: mirrorball: list all instances of an rpm in the rpmMap dict Message-ID: <200908192139.n7JLdGLE014893@scc.eng.rpath.com> changeset: 30607f8ff1cd user: Elliot Peele <http://issues.rpath.com/> date: Tue, 10 Jun 2008 16:39:34 -0400 list all instances of an rpm in the rpmMap dict update createManifest to handle this committer: Elliot Peele <http://issues.rpath.com/> diff --git a/rpmimport/rpmsource.py b/rpmimport/rpmsource.py --- a/rpmimport/rpmsource.py +++ b/rpmimport/rpmsource.py @@ -87,11 +87,6 @@ package.location = longLoc if self.rpmMap.has_key(srpm): shortLoc = os.path.basename(package.location) - # remove duplicates of this binary RPM - last one wins - #existing = [ x for x in self.rpmMap[srpm].iterkeys() - # if os.path.basename(x) == shortLoc ] - #for key in existing: - # del self.rpmMap[srpm][key] self.rpmMap[srpm][longLoc] = package else: self.rpmMap[srpm] = {longLoc: package} @@ -216,6 +211,13 @@ """ l = [] l.append(self.srcPath[srpm].location) - l.extend([x.location for x in self.getRPMS(srpm)]) + + locs = [] + for loc in self.getRPMS(srpm): + baseLoc = os.path.basename(loc) + if baseLoc not in locs: + locs.append(baseLoc) + l.append(loc) + # add a trailing newline return '\n'.join(sorted(l) + ['']) From johnsonm at rpath.com Wed Aug 19 17:41:48 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:41:48 +0000 Subject: mirrorball: make commit optional Message-ID: <200908192141.n7JLfmmX020991@scc.eng.rpath.com> changeset: e948616cfd8c user: Elliot Peele <https://issues.rpath.com/> date: Wed, 10 Dec 2008 18:17:18 -0500 make commit optional committer: Elliot Peele <https://issues.rpath.com/> diff --git a/updatebot/conaryhelper.py b/updatebot/conaryhelper.py --- a/updatebot/conaryhelper.py +++ b/updatebot/conaryhelper.py @@ -383,7 +383,8 @@ return None def promote(self, trvLst, expected, sourceLabels, targetLabel, - checkPackageList=True, extraPromoteTroves=None): + checkPackageList=True, extraPromoteTroves=None, + commit=True): """ Promote a group and its contents to a target label. @param trvLst: list of troves to publish @@ -401,6 +402,8 @@ @param extraPromoteTroves: troves to promote in addition to the troves that have been built. @type extraPromoteTroves: list of trove specs. + @param commit: commit the promote changeset or just return it. + @type commit: boolean """ start = time.time() @@ -461,6 +464,9 @@ if checkPackageList and grpDiff.difference(extraTroves): raise PromoteMismatchError(expected=oldPkgs, actual=newPkgs) + if not commit: + return cs, packageList + log.info('committing changeset') self._repos.commitChangeSet(cs) From johnsonm at rpath.com Wed Aug 19 17:40:36 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:40:36 +0000 Subject: mirrorball: update to handle different pacakge stores Message-ID: <200908192140.n7JLeaRO018107@scc.eng.rpath.com> changeset: f9280085991d user: Elliot Peele <https://issues.rpath.com/> date: Fri, 05 Sep 2008 11:39:49 -0400 update to handle different pacakge stores committer: Elliot Peele <https://issues.rpath.com/> diff --git a/scripts/pkgsource.py b/scripts/pkgsource.py --- a/scripts/pkgsource.py +++ b/scripts/pkgsource.py @@ -15,20 +15,27 @@ updatebot.log.addRootLogger() log = logging.getLogger('test') -from aptmd import Client +import aptmd +import repomd from updatebot import config from updatebot import pkgsource cfg = config.UpdateBotConfig() -cfg.read(os.environ['HOME'] + '/hg/mirrorball/config/ubuntu/updatebotrc') +cfg.read(os.environ['HOME'] + '/hg/mirrorball/config/sles/updatebotrc') -client = Client('http://i.rdu.rpath.com/ubuntu') pkgSource = pkgsource.PackageSource(cfg) -for path in cfg.repositoryPaths: - log.info('loading %s' % path) - pkgSource.loadFromClient(client, path) - +if cfg.repositoryFormat == 'apt': + client = aptmd.Client(cfg.repositoryUrl) + for path in cfg.repositoryPaths: + log.info('loading %s' % path) + pkgSource.loadFromClient(client, path) +else: + for path in cfg.repositoryPaths: + client = repomd.Client(cfg.repositoryUrl + '/' + path) + log.info('loading %s' % path) + pkgSource.loadFromClient(client, path) + pkgSource.finalize() import epdb; epdb.st() From johnsonm at rpath.com Wed Aug 19 17:39:07 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:39:07 +0000 Subject: mirrorball: cleanup more pylint errors Message-ID: <200908192139.n7JLd7RF014539@scc.eng.rpath.com> changeset: 337ac74ee244 user: Elliot Peele <http://issues.rpath.com/> date: Mon, 02 Jun 2008 23:08:19 -0400 cleanup more pylint errors committer: Elliot Peele <http://issues.rpath.com/> diff --git a/repomd/packagexml.py b/repomd/packagexml.py --- a/repomd/packagexml.py +++ b/repomd/packagexml.py @@ -28,6 +28,10 @@ Python representation of package section of xml files from the repository metadata. """ + + # R0902 - Too many instance attributes + # pylint: disable-msg=R0902 + __slots__ = ('name', 'arch', 'epoch', 'version', 'release', 'checksum', 'checksumType', 'summary', 'description', 'fileTimestamp', 'buildTimestamp', 'packageSize', @@ -41,6 +45,7 @@ # W0201 - Attribute $foo defined outside __init__ # pylint: disable-msg=W0201 + def addChild(self, child): """ Parse children of package element. diff --git a/repomd/patchxml.py b/repomd/patchxml.py --- a/repomd/patchxml.py +++ b/repomd/patchxml.py @@ -18,6 +18,10 @@ """ Python representation of patch-*.xml from the repository metadata. """ + + # R0902 - Too many instance attributes + # pylint: disable-msg=R0902 + __slots__ = ('name', 'summary', 'description', 'version', 'release', 'requires', 'recommends', 'rebootNeeded', 'licenseToConfirm', 'packageManager', 'category', From johnsonm at rpath.com Wed Aug 19 17:39:55 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:39:55 +0000 Subject: mirrorball: add import script Message-ID: <200908192139.n7JLdt9E016436@scc.eng.rpath.com> changeset: 14f484dee1f5 user: Elliot Peele <http://issues.rpath.com/> date: Fri, 11 Jul 2008 23:40:50 -0400 add import script committer: Elliot Peele <http://issues.rpath.com/> diff --git a/scripts/import.py b/scripts/import.py new file mode 100755 --- /dev/null +++ b/scripts/import.py @@ -0,0 +1,34 @@ +#!/usr/bin/python +# +# Copyright (c) 2008 rPath, Inc. +# +# This program is distributed under the terms of the Common Public License, +# version 1.0. A copy of this license should have been distributed with this +# source file in a file called LICENSE. If it is not present, the license +# is always available at http://www.rpath.com/permanent/licenses/CPL-1.0. +# +# This program is distributed in the hope that it will be useful, but +# without any warranty; without even the implied warranty of merchantability +# or fitness for a particular purpose. See the Common Public License for +# full details. +# + +import os +import sys + +sys.path.insert(0, os.environ['HOME'] + '/hg/rpath-xmllib') +sys.path.insert(0, os.environ['HOME'] + '/hg/conary') +sys.path.insert(0, os.environ['HOME'] + '/hg/mirrorball') + +from conary.lib import util +sys.excepthook = util.genExcepthook() + +from updatebot import bot, config, log + +log.addRootLogger() +cfg = config.UpdateBotConfig() +cfg.read(os.environ['HOME'] + '/hg/mirrorball/config/opensuse/updatebotrc') +obj = bot.Bot(cfg) +obj.create() + +import epdb ; epdb.st() From johnsonm at rpath.com Wed Aug 19 17:42:17 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:42:17 +0000 Subject: mirrorball: add support for running a full trove sync Message-ID: <200908192142.n7JLgHBN022149@scc.eng.rpath.com> changeset: 8c0b3121cb6a user: Elliot Peele <https://issues.rpath.com/> date: Sat, 07 Feb 2009 12:33:29 -0500 add support for running a full trove sync committer: Elliot Peele <https://issues.rpath.com/> diff --git a/updatebot/conaryhelper.py b/updatebot/conaryhelper.py --- a/updatebot/conaryhelper.py +++ b/updatebot/conaryhelper.py @@ -567,7 +567,7 @@ return packageList - def mirror(self): + def mirror(self, fullTroveSync=False): """ Mirror the current platform to the external repository if a mirror.conf exists. @@ -585,7 +585,7 @@ clog.setVerbosity(clog.DEBUG) callback = mirror.ChangesetCallback() - rc = mirror.mainWorkflow(cfg=self._mcfg, callback=callback) + rc = mirror.mainWorkflow(cfg=self._mcfg, callback=callback, sync=fullTroveSync) if rc is not None and rc != 0: raise MirrorFailedError(rc=rc) diff --git a/updatebot/update.py b/updatebot/update.py --- a/updatebot/update.py +++ b/updatebot/update.py @@ -415,9 +415,9 @@ extraPromoteTroves=self._cfg.extraPromoteTroves ) - def mirror(self): + def mirror(self, fullTroveSync=False): """ If a mirror is configured, mirror out any changes. """ - return self._conaryhelper.mirror() + return self._conaryhelper.mirror(fullTroveSync=fullTroveSync) From johnsonm at rpath.com Wed Aug 19 17:40:20 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:40:20 +0000 Subject: mirrorball: get new versions for sources that are only going to be built, otherwise we end Message-ID: <200908192140.n7JLeK6F017443@scc.eng.rpath.com> changeset: 9b7844030761 user: Elliot Peele <http://bugs.rpath.com/> date: Mon, 18 Aug 2008 22:02:29 -0400 get new versions for sources that are only going to be built, otherwise we end up rebuilding the old version. committer: Elliot Peele <http://bugs.rpath.com/> diff --git a/updatebot/update.py b/updatebot/update.py --- a/updatebot/update.py +++ b/updatebot/update.py @@ -55,10 +55,17 @@ # Will raise exception if any errors are found, halting execution. if self._sanitizeTrove(nvf, srpm): toUpdate.append((nvf, srpm)) + toAdvise.append((nvf, srpm)) - # Make sure to send advisories for any packages that didn't get - # sent out last time. - toAdvise.append((nvf, srpm)) + + # Update versions for things that are already in the repository. + # The binary version from the group will not be the latest. + else: + # Make sure to send advisories for any packages that didn't get + # sent out last time. + version = self._conaryhelper.getLatestSourceVersion(nvf[0]) + toAdvise.append(((nvf[0], version, nvf[2]), srpm)) + log.info('found %s troves to update, and %s troves to send advisories' % (len(toUpdate), len(toAdvise))) From johnsonm at rpath.com Wed Aug 19 17:42:31 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:42:31 +0000 Subject: mirrorball: better sanity checking for all imports Message-ID: <200908192142.n7JLgVCP022711@scc.eng.rpath.com> changeset: 71269f9cbc3d user: Elliot Peele <https://issues.rpath.com/> date: Mon, 16 Mar 2009 11:22:53 -0400 better sanity checking for all imports committer: Elliot Peele <https://issues.rpath.com/> diff --git a/updatebot/bot.py b/updatebot/bot.py --- a/updatebot/bot.py +++ b/updatebot/bot.py @@ -70,9 +70,26 @@ # Build list of packages if self._cfg.packageAll: - toPackage = set([ x.name for x in - self._pkgSource.binPkgMap.iterkeys() if x.arch != 'src' ]) - toPackage = toPackage.difference(set(self._cfg.package)) + toPackage = set() + for srcName, srcSet in self._pkgSource.srcNameMap.iteritems(): + if len(srcSet) == 0: + continue + + srcList = list(srcSet) + srcList.sort() + latestSrc = srcList[-1] + + if latestSrc not in self._pkgSource.srcPkgMap: + log.warn('not packaging %s, not found in srcPkgMap' % latestSrc.name) + continue + + if latestSrc.name in self._cfg.package: + log.warn('ignoring %s due to exclude rule' % latestSrc.name) + continue + + for binPkg in self._pkgSource.srcPkgMap[latestSrc]: + toPackage.add(binPkg.name) + else: toPackage = set(self._cfg.package) From johnsonm at rpath.com Wed Aug 19 17:41:05 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:41:05 +0000 Subject: mirrorball: branch merge Message-ID: <200908192141.n7JLf5CC019265@scc.eng.rpath.com> changeset: 620111dd2fbb user: Elliot Peele <https://issues.rpath.com/> date: Wed, 22 Oct 2008 15:55:36 -0400 branch merge committer: Elliot Peele <https://issues.rpath.com/> diff --git a/scripts/buildpackages b/scripts/buildpackages --- a/scripts/buildpackages +++ b/scripts/buildpackages @@ -21,15 +21,19 @@ from updatebot import build from updatebot import config +if len(sys.argv) < 3 or sys.argv[1] not in os.listdir(os.environ['HOME'] + '/hg/mirrorball/config'): + print 'usage: buildpackages <platform> [pkg1, pkg2, ...]' + sys.exit(1) + log.addRootLogger() cfg = config.UpdateBotConfig() -cfg.read(os.environ['HOME'] + '/hg/mirrorball/config/ubuntu/updatebotrc') +cfg.read(os.environ['HOME'] + '/hg/mirrorball/config/%s/updatebotrc' % sys.argv[1]) builder = build.Builder(cfg) trvs = set() label = cfg.topSourceGroup[1] -for pkg in sys.argv[1:]: +for pkg in sys.argv[2:]: trvs.add((pkg, label, None)) trvMap = builder.build(trvs) diff --git a/scripts/findbinaries.py b/scripts/findbinaries.py old mode 100755 new mode 100644 diff --git a/updatebot/bot.py b/updatebot/bot.py --- a/updatebot/bot.py +++ b/updatebot/bot.py @@ -119,7 +119,7 @@ # Build all newly imported packages. trvMap, failed = self._builder.buildmany(toBuild) - import epdb; epdb.st() +# import epdb; epdb.st() ## trvMap = self._builder.build(toBuild) #import epdb; epdb.st() From johnsonm at rpath.com Wed Aug 19 17:39:42 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:39:42 +0000 Subject: mirrorball: add better comparisons for patches Message-ID: <200908192139.n7JLdgix015926@scc.eng.rpath.com> changeset: f8f7080a67b8 user: Elliot Peele <http://issues.rpath.com/> date: Wed, 25 Jun 2008 19:24:49 -0400 add better comparisons for patches committer: Elliot Peele <http://issues.rpath.com/> diff --git a/repomd/patchxml.py b/repomd/patchxml.py --- a/repomd/patchxml.py +++ b/repomd/patchxml.py @@ -71,16 +71,31 @@ raise UnknownElementError(child) def __cmp__(self, other): - if self.version > other.version: - return 1 - elif self.version < other.version: - return -1 - elif self.release > other.release: - return 1 - elif self.release < other.release: - return -1 - else: - return 0 + vercmp = cmp(self.version, other.version) + if vercmp != 0: + return vercmp + + relcmp = cmp(self.release, other.release) + if relcmp != 0: + return relcmp + + sumcmp = cmp(self.summary, other.summary) + if sumcmp != 0: + return sumcmp + + desccmp = cmp(self.description, other.description) + if desccmp != 0: + return desccmp + + for pkg in other.packages: + if pkg not in self.packages: + self.packages.append(pkg) + + return 0 + + def __hash__(self): + return hash((self.name, self.version, self.release, self.summary, + self.description)) class _Atoms(xmllib.BaseNode): From johnsonm at rpath.com Wed Aug 19 17:38:55 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:38:55 +0000 Subject: mirrorball: branch merge Message-ID: <200908192139.n7JLctMc013987@scc.eng.rpath.com> changeset: 9d4bf0ccc012 user: Elliot Peele <http://issues.rpath.com/> date: Wed, 28 May 2008 21:01:35 -0400 branch merge committer: Elliot Peele <http://issues.rpath.com/> diff --git a/pkgwiz.py b/pkgwiz.py --- a/pkgwiz.py +++ b/pkgwiz.py @@ -38,6 +38,7 @@ 'aaa_base', 'acl', 'apache2', + 'apache2-mod_python', 'ash', 'attr', 'audit', @@ -52,6 +53,7 @@ 'cpp', 'cracklib', 'cron', + 'curl', 'cyrus-sasl', 'device-mapper', 'db', @@ -75,6 +77,7 @@ 'glib2', 'glibc', 'gmp', + 'gnutls', 'gpm', 'grep', 'grub', @@ -101,10 +104,14 @@ 'libelf', 'libevent', 'libgcc', + 'libgpg-error', + 'libgcrypt', 'libgssapi', + 'libidn', 'libiniparser', 'libjpeg', 'libnscd', + 'libopencdk', 'libpcap', 'libpng', 'librpcsecgss', @@ -114,8 +121,11 @@ 'libxcrypt', 'libxml2', 'libxml2-python', + 'libxslt', 'logrotate', 'lvm2', + 'lzo', + 'm4', 'make', 'mdadm', 'mingetty', @@ -163,12 +173,14 @@ 'resmgr', 'samba', 'sed', + 'sendmail', 'slang', 'sles-release', 'strace', 'sysconfig', 'sysfsutils', 'syslog-ng', + 'sysstat', 'sysvinit', 'tar', 'tcl', @@ -184,6 +196,7 @@ 'vim', 'wget', 'wireless-tools', + 'xntp', #'xorg-x11', 'zlib', 'zip', From johnsonm at rpath.com Wed Aug 19 17:39:50 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:39:50 +0000 Subject: mirrorball: remove cook.py since it isn't used by anything Message-ID: <200908192139.n7JLdo2q016215@scc.eng.rpath.com> changeset: 9de16e7fcee2 user: Elliot Peele <http://issues.rpath.com/> date: Wed, 09 Jul 2008 14:26:53 -0400 remove cook.py since it isn't used by anything committer: Elliot Peele <http://issues.rpath.com/> diff --git a/cook.py b/cook.py deleted file mode 100644 --- a/cook.py +++ /dev/null @@ -1,32 +0,0 @@ - def cook(self, group): - """ - Go through all the packages in a group that's on the current label and cook them. - """ - - self.repo() - from conary import display, queryrep, trove - - troveTups = queryrep.getTrovesToDisplay( - self.repos, [group], [], - versionFilter=queryrep.VERSION_FILTER_LATEST, - flavorFilter=queryrep.FLAVOR_FILTER_BEST, - labelPath=self.cfg.buildLabel, defaultFlavor=self.cfg.flavor, - affinityDb=None) - - dcfg = display.DisplayConfig(self.repos, None) - troveSource = dcfg.getTroveSource() - troves = troveSource.getTroves(troveTups, withFiles=False) - childTups = list(troves[0].iterTroveList(strongRefs=True)) - - import epdb - epdb.st() - - troveSpecs = [(group, None, None)] - self.repos.findTroves(self.cfg.buildLabel, troveSpecs) - - troveTups = [(group, None, None)] - - #t = troveSource.getTroves(troveTups) - ttup = self.repos.findTroves(self.cfg.buildLabel, troveTups)[troveTups[0]] - - From johnsonm at rpath.com Wed Aug 19 17:41:17 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:41:17 +0000 Subject: mirrorball: add support for fulling down gzipd files from an url Message-ID: <200908192141.n7JLfHVb019744@scc.eng.rpath.com> changeset: 90a09c837b23 user: Elliot Peele <https://issues.rpath.com/> date: Fri, 07 Nov 2008 16:17:55 -0500 add support for fulling down gzipd files from an url committer: Elliot Peele <https://issues.rpath.com/> diff --git a/pmap/__init__.py b/pmap/__init__.py --- a/pmap/__init__.py +++ b/pmap/__init__.py @@ -18,6 +18,12 @@ Parse advisories from pipermail archives. """ +import os +import gzip +import shutil +import urllib2 +import tempfile + from imputil import imp __all__ = ('InvalidBackendError', 'parse') @@ -26,6 +32,22 @@ class InvalidBackendError(Exception): pass +def __getFileObjFromUrl(url): + fn = tempfile.mktemp(prefix='pmap') + + # download file + inf = urllib2.urlopen(url) + outf = open(fn, 'w') + shutil.copyfileobj(inf, outf) + + if url.endswith('.gz'): + fh = gzip.open(fn) + else: + fh = open(fn) + + os.unlink(fn) + return fh + def __getBackend(backend): if backend not in __supported_backends: raise InvalidBackendError('%s is not a supported backend, please ' @@ -39,7 +61,8 @@ except ImportError, e: raise InvalidBackendError('Could not load %s backend: %s' % (backend, e)) -def parse(url, backend='ubuntu'): +def parse(url, backend='centos'): + fh = __getFileObjFromUrl(url) backend = __getBackend(backend) parser = backend.Parser() - return parser.parse(url) + return parser.parse(fh) From johnsonm at rpath.com Wed Aug 19 17:41:00 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:41:00 +0000 Subject: mirrorball: make configPath relative to updatebotrc location Message-ID: <200908192141.n7JLf0Ow019094@scc.eng.rpath.com> changeset: 402da312ae94 user: Elliot Peele <https://issues.rpath.com/> date: Wed, 22 Oct 2008 15:50:20 -0400 make configPath relative to updatebotrc location committer: Elliot Peele <https://issues.rpath.com/> diff --git a/updatebot/config.py b/updatebot/config.py --- a/updatebot/config.py +++ b/updatebot/config.py @@ -16,6 +16,8 @@ Configuration module for updatebot. """ +import os + from conary.lib import cfg from conary import versions from conary.conarycfg import CfgFlavor, CfgLabel @@ -50,8 +52,8 @@ # name of the product to use in advisories productName = CfgString - # path to configuration files (conaryrc, rmakerc) - configPath = CfgString + # path to configuration files relative to updatebotrc (conaryrc, rmakerc) + configPath = (CfgString, './') # type of upstream repostory to pull packages from, supported are apt and yum. repositoryFormat = (CfgString, 'yum') @@ -114,3 +116,11 @@ def __init__(self, *args, **kwargs): cfg.SectionedConfigFile.__init__(self) + + def read(self, *args, **kwargs): + ret = cfg.SectionedConfigFile.read(self, *args, **kwargs) + if not self.configPath.startswith(os.sep): + # configPath is relative + dirname = os.path.dirname(args[0]) + self.configPath = os.path.normpath(os.path.join(dirname, self.configPath)) + return ret From johnsonm at rpath.com Wed Aug 19 17:39:45 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:39:45 +0000 Subject: mirrorball: add script to cook groups as configured by updatebot Message-ID: <200908192139.n7JLdjWA016028@scc.eng.rpath.com> changeset: 928a9a12614a user: Elliot Peele <http://issues.rpath.com/> date: Sat, 28 Jun 2008 00:29:23 -0400 add script to cook groups as configured by updatebot committer: Elliot Peele <http://issues.rpath.com/> diff --git a/scripts/cookgroups.py b/scripts/cookgroups.py new file mode 100755 --- /dev/null +++ b/scripts/cookgroups.py @@ -0,0 +1,44 @@ +#!/usr/bin/python +# +# Copryright (c) 2008 rPath, Inc. +# + +""" +Script for cooking groups defined in the updatebot config. +""" + +import os +import sys + +sys.path.insert(0, os.environ['HOME'] + '/hg/rpath-xmllib') +sys.path.insert(0, os.environ['HOME'] + '/hg/conary') +sys.path.insert(0, os.environ['HOME'] + '/hg/mirrorball') + +from updatebot import log +from updatebot import build +from updatebot import config + +log.addRootLogger() +cfg = config.UpdateBotConfig() +cfg.read(os.environ['HOME'] + '/hg/mirrorball/config/updatebotrc') + +builder = build.Builder(cfg) + +grpTrvs = set() +for flavor in cfg.groupFlavors: + grpTrvs.add((cfg.topSourceGroup[0], cfg.topSourceGroup[1], flavor)) +grpTrvMap = builder.build(grpTrvs) + +print "built:\n" + +def displayTrove(nvf): + flavor = '' + if nvf[2] is not None: + flavor = '[%s]' % nvf[2] + + return '%s=%s%s' % (nvf[0], nvf[1], flavor) + +for srcTrv in grpTrvMap.iterkeys(): + print displayTrove(srcTrv) + for binTrv in grpTrvMap[srcTrv]: + print "\t", displayTrove(binTrv) From johnsonm at rpath.com Wed Aug 19 17:39:46 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:39:46 +0000 Subject: mirrorball: add command to cook a list of packages on the devel branch of a repository Message-ID: <200908192139.n7JLdk9P016062@scc.eng.rpath.com> changeset: bb7219528fb7 user: Elliot Peele <http://issues.rpath.com/> date: Wed, 09 Jul 2008 10:43:20 -0400 add command to cook a list of packages on the devel branch of a repository committer: Elliot Peele <http://issues.rpath.com/> diff --git a/scripts/cookpackages.py b/scripts/cookpackages.py new file mode 100755 --- /dev/null +++ b/scripts/cookpackages.py @@ -0,0 +1,46 @@ +#!/usr/bin/python +# +# Copryright (c) 2008 rPath, Inc. +# + +""" +Script for cooking groups defined in the updatebot config. +""" + +import os +import sys + +sys.path.insert(0, os.environ['HOME'] + '/hg/rpath-xmllib') +sys.path.insert(0, os.environ['HOME'] + '/hg/conary') +sys.path.insert(0, os.environ['HOME'] + '/hg/mirrorball') + +from updatebot import log +from updatebot import build +from updatebot import config + +log.addRootLogger() +cfg = config.UpdateBotConfig() +cfg.read(os.environ['HOME'] + '/hg/mirrorball/config/updatebotrc') + +builder = build.Builder(cfg) + + +trvs = set() +label = cfg.topSourceGroup[1] +for pkg in sys.argv[1:]: + trvs.add((pkg, label, None)) +trvMap = builder.build(trvs) + +print "built:\n" + +def displayTrove(nvf): + flavor = '' + if nvf[2] is not None: + flavor = '[%s]' % nvf[2] + + return '%s=%s%s' % (nvf[0], nvf[1], flavor) + +for srcTrv in trvMap.iterkeys(): + print displayTrove(srcTrv) + for binTrv in trvMap[srcTrv]: + print "\t", displayTrove(binTrv) From johnsonm at rpath.com Wed Aug 19 17:40:26 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:40:26 +0000 Subject: mirrorball: add support for sources of different versions Message-ID: <200908192140.n7JLeQDk017682@scc.eng.rpath.com> changeset: 6f1ccf2e317e user: Elliot Peele <http://bugs.rpath.com/> date: Sat, 23 Aug 2008 16:39:17 -0400 add support for sources of different versions committer: Elliot Peele <http://bugs.rpath.com/> diff --git a/updatebot/pkgsource/debsource.py b/updatebot/pkgsource/debsource.py --- a/updatebot/pkgsource/debsource.py +++ b/updatebot/pkgsource/debsource.py @@ -74,11 +74,22 @@ continue for binPkg in self.binNameMap[binPkgName]: - if (binPkg.version == srcPkg.version and - binPkg.release == srcPkg.release): + if ((binPkg.version == srcPkg.version and + binPkg.release == srcPkg.release) or + (binPkg.source == srcPkg.name and + binPkg.sourceVersion == srcPkg.version)): self.srcPkgMap[srcPkg].add(binPkg) self.srcPkgMap[srcPkg].add(srcPkg) for pkg in self.srcPkgMap[srcPkg]: self.binPkgMap[pkg] = srcPkg + + + # It seems that some packages have versions that don't match up with + # the source that they were built from. We need to handle that case. + for binPkgs in self.binNameMap.itervalues(): + for binPkg in binPkgs: + if binPkg not in self.binPkgMap: + pass + #log.warn('no source found for %s' % binPkg) From johnsonm at rpath.com Wed Aug 19 17:41:07 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:41:07 +0000 Subject: mirrorball: fix a couple of bugs Message-ID: <200908192141.n7JLf7Cn019316@scc.eng.rpath.com> changeset: bdd04d3d0fa6 user: Elliot Peele <https://issues.rpath.com/> date: Wed, 22 Oct 2008 17:14:36 -0400 fix a couple of bugs committer: Elliot Peele <https://issues.rpath.com/> diff --git a/scripts/findbinaries.py b/scripts/findbinaries.py old mode 100644 new mode 100755 --- a/scripts/findbinaries.py +++ b/scripts/findbinaries.py @@ -32,7 +32,7 @@ slog = logging.getLogger('findbinaries') cfg = config.UpdateBotConfig() -cfg.read(os.environ['HOME'] + '/hg/mirrorball/config/ubuntu/updatebotrc') +cfg.read(os.environ['HOME'] + '/hg/mirrorball/config/centos/updatebotrc') bot = bot.Bot(cfg) bot._populatePkgSource() @@ -40,7 +40,7 @@ updater = bot._updater helper = updater._conaryhelper -sources, failed = updater.create(cfg.package, buildAll=False) +sources, failed = updater.create(cfg.package, buildAll=True) pkgs = helper._repos.getTroveLeavesByLabel({None: {helper._ccfg.buildLabel: None}}) pkgSet = set([ x.split(':')[0] for x in pkgs ]) @@ -63,7 +63,7 @@ for src, binSet in helper._getSourceTroves((pkg, version, flavor)).iteritems(): src = (src[0].split(':')[0], src[1], src[2]) if src not in sources: - slog.warn('skipping %s, it not in souces' % src[0]) + slog.warn('skipping %s, it not in sources' % src[0]) continue if src not in srcDict: @@ -91,4 +91,4 @@ binLst.sort() for item in binLst: - print ' ' * 12, '\'%s\',' % item + print ' ' * 11, '\'%s\',' % item From johnsonm at rpath.com Wed Aug 19 17:42:16 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:42:16 +0000 Subject: mirrorball: add support for building everything from a package source Message-ID: <200908192142.n7JLgG8E022098@scc.eng.rpath.com> changeset: 9e73595ce4c2 user: Elliot Peele <https://issues.rpath.com/> date: Thu, 05 Feb 2009 23:54:22 -0500 add support for building everything from a package source committer: Elliot Peele <https://issues.rpath.com/> diff --git a/updatebot/bot.py b/updatebot/bot.py --- a/updatebot/bot.py +++ b/updatebot/bot.py @@ -68,8 +68,16 @@ # Populate rpm source object from yum metadata. self._pkgSource.load() + # Build list of packages + if self._cfg.packageAll: + toPackage = set([ x.name for x in + self._pkgSource.binPkgMap.iterkeys() if x.arch != 'src' ]) + toPackage = toPackage.difference(set(self._cfg.package)) + else: + toPackage = set(self._cfg.package) + # Import sources into repository. - toBuild, fail = self._updater.create(self._cfg.package, + toBuild, fail = self._updater.create(toPackage, buildAll=rebuild) if not rebuild: diff --git a/updatebot/config.py b/updatebot/config.py --- a/updatebot/config.py +++ b/updatebot/config.py @@ -123,6 +123,10 @@ # Packages to import package = (CfgList(CfgString), []) + # Include all packages, if this is set to true packages becomes an + # exclude list. + packageAll = (CfgBool, False) + # Factory to use for importing newPackageFactory = (CfgString, None) From johnsonm at rpath.com Wed Aug 19 17:42:33 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:42:33 +0000 Subject: mirrorball: use version cache instead of prodding the repo constantly Message-ID: <200908192142.n7JLgXmq022779@scc.eng.rpath.com> changeset: 418f1c874cbe user: Elliot Peele <https://issues.rpath.com/> date: Mon, 16 Mar 2009 11:25:01 -0400 use version cache instead of prodding the repo constantly committer: Elliot Peele <https://issues.rpath.com/> diff --git a/updatebot/update.py b/updatebot/update.py --- a/updatebot/update.py +++ b/updatebot/update.py @@ -245,17 +245,19 @@ # Update all of the unique sources. fail = set() toBuild = set() + verCache = self._conaryhelper.getLatestVersions() for pkg in toUpdate: - log.info('attempting to import %s' % pkg) + #log.info('attempting to import %s' % pkg) try: # Only import packages that haven't been imported before - version = self._conaryhelper.getLatestSourceVersion(pkg.name) + #version = self._conaryhelper.getLatestSourceVersion(pkg.name) + version = verCache.get('%s:source' % pkg.name) if not version: + log.info('attempting to import %s' % pkg) version = self.update((pkg.name, None, None), pkg) - if (not self._conaryhelper._getVersionsByName(pkg.name) or - buildAll): + if not verCache.get(pkg.name) or buildAll: toBuild.add((pkg.name, version, None)) else: log.info('not building %s' % pkg.name) From johnsonm at rpath.com Wed Aug 19 17:42:11 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:42:11 +0000 Subject: mirrorball: add more questions Message-ID: <200908192142.n7JLgCi8021911@scc.eng.rpath.com> changeset: 7d71b9a6603e user: Elliot Peele <https://issues.rpath.com/> date: Sun, 25 Jan 2009 21:52:57 -0500 add more questions committer: Elliot Peele <https://issues.rpath.com/> diff --git a/scripts/pcheck.py b/scripts/pcheck.py --- a/scripts/pcheck.py +++ b/scripts/pcheck.py @@ -1,6 +1,8 @@ #!/usr/bin/python +import os import sys +import time from conary.lib import util sys.excepthook = util.genExcepthook() @@ -13,6 +15,19 @@ log.setVerbosity(log.INFO) +def ask(prompt, default=None): + while True: + try: + prompt = '%s [%s]' % (prompt, default) + resp = raw_input(prompt + ' ') + except EOFError: + return None + + if not resp: + return default + + return resp + cfg = conarycfg.ConaryConfiguration(True) cfg.setContext('1-binary') @@ -93,9 +108,12 @@ log.critical('Failed to create promote changeset') sys.exit(1) -# Ask before committing. -okay = conaryclient.cmdline.askYn('commit changset? [y/N]', default=False) - # Commit changeset. -if okay: +if conaryclient.cmdline.askYn('commit changset? [y/N]', default=False): + start = time.time() client.repos.commitChangeSet(cs, callback=cb) + total = time.time() - start + log.info('commit time: %s' % total) +elif conaryclient.cmdline.askYn('save changeset? [Y/n]', default=True): + location = ask('where?', default=os.path.join(os.getcwd(), 'promote-changeset.ccs')) + cs.writeToFile(location) From johnsonm at rpath.com Wed Aug 19 17:41:56 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:41:56 +0000 Subject: mirrorball: add check for equality Message-ID: <200908192141.n7JLfux5021298@scc.eng.rpath.com> changeset: 93353ca5a3db user: Elliot Peele <https://issues.rpath.com/> date: Sun, 11 Jan 2009 18:11:24 -0500 add check for equality committer: Elliot Peele <https://issues.rpath.com/> diff --git a/aptmd/common.py b/aptmd/common.py --- a/aptmd/common.py +++ b/aptmd/common.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2008 rPath, Inc. +# Copyright (c) 2008-2009 rPath, Inc. # # This program is distributed under the terms of the Common Public License, # version 1.0. A copy of this license should have been distributed with this @@ -16,7 +16,7 @@ Common module for apt metadata parsers to inherit from. """ -from updatebot import util +from updatebot.lib import util from aptmd.container import Container from aptmd.parser import ContainerizedParser as Parser diff --git a/aptmd/container.py b/aptmd/container.py --- a/aptmd/container.py +++ b/aptmd/container.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2008 rPath, Inc. +# Copyright (c) 2008-2009 rPath, Inc. # # This program is distributed under the terms of the Common Public License, # version 1.0. A copy of this license should have been distributed with this @@ -31,6 +31,15 @@ self._data = {} + def __eq__(self, other): + """ + Check for equality. + """ + + if not isinstance(other, Container): + return False + return cmp(self, other) == 0 + def set(self, key, value): """ Set data in a local dictionary, also used for __setitem__. From johnsonm at rpath.com Wed Aug 19 17:41:08 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:41:08 +0000 Subject: mirrorball: vendor changes Message-ID: <200908192141.n7JLf8nJ019368@scc.eng.rpath.com> changeset: d5ef2212466b user: Elliot Peele <https://issues.rpath.com/> date: Tue, 28 Oct 2008 15:27:21 -0400 vendor changes committer: Elliot Peele <https://issues.rpath.com/> diff --git a/vendor/email/__init__.py b/vendor/email/__init__.py --- a/vendor/email/__init__.py +++ b/vendor/email/__init__.py @@ -121,13 +121,13 @@ for _name in _LOWERNAMES: importer = LazyImporter(_name.lower()) - sys.modules['vendor.email.' + _name] = importer - setattr(sys.modules['vendor.email'], _name, importer) + sys.modules['email.' + _name] = importer + setattr(sys.modules['email'], _name, importer) -import vendor.email.mime +import email.mime for _name in _MIMENAMES: importer = LazyImporter('mime.' + _name.lower()) - sys.modules['vendor.email.MIME' + _name] = importer - setattr(sys.modules['vendor.email'], 'MIME' + _name, importer) - setattr(sys.modules['vendor.email.mime'], _name, importer) + sys.modules['email.MIME' + _name] = importer + setattr(sys.modules['email'], 'MIME' + _name, importer) + setattr(sys.modules['email.mime'], _name, importer) diff --git a/vendor/mailbox.py b/vendor/mailbox.py --- a/vendor/mailbox.py +++ b/vendor/mailbox.py @@ -677,6 +677,7 @@ def get_message(self, key): """Return a Message representation or raise a KeyError.""" + import epdb; epdb.st() start, stop = self._lookup(key) self._file.seek(start) from_line = self._file.readline().replace(os.linesep, '') From johnsonm at rpath.com Wed Aug 19 17:42:42 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:42:42 +0000 Subject: mirrorball: script for finding missing srpms from the scientific linux repositories Message-ID: <200908192142.n7JLggcK023153@scc.eng.rpath.com> changeset: 0743f65918fb user: Elliot Peele <https://issues.rpath.com/> date: Thu, 09 Apr 2009 15:45:35 -0400 script for finding missing srpms from the scientific linux repositories committer: Elliot Peele <https://issues.rpath.com/> diff --git a/scripts/checksl.py b/scripts/checksl.py new file mode 100755 --- /dev/null +++ b/scripts/checksl.py @@ -0,0 +1,35 @@ +#!/usr/bin/python +# +# Copyright (c) 2009 rPath, Inc. +# +# This program is distributed under the terms of the Common Public License, +# version 1.0. A copy of this license should have been distributed with this +# source file in a file called LICENSE. If it is not present, the license +# is always available at http://www.rpath.com/permanent/licenses/CPL-1.0. +# +# This program is distributed in the hope that it will be useful, but +# without any warranty; without even the implied warranty of merchantability +# or fitness for a particular purpose. See the Common Public License for +# full details. +# + +import sys +from conary.lib import util + +sys.excepthook = util.genExcepthook() + +import os + +sys.path.insert(0, os.environ['HOME'] + '/hg/mirrorball') + +from updatebot import config +from updatebot import pkgsource +from updatebot import log as logger + +logger.addRootLogger() + +cfg = config.UpdateBotConfig() +cfg.read(os.environ['HOME'] + '/hg/mirrorball/config/scientific/updatebotrc') +pkgSource = pkgsource.PackageSource(cfg) + +pkgSource.load() From johnsonm at rpath.com Wed Aug 19 17:42:06 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:42:06 +0000 Subject: mirrorball: promote script to check rpl:1-rpl:1-qa promote Message-ID: <200908192142.n7JLg7uq021724@scc.eng.rpath.com> changeset: 5af03bd28bb9 user: Elliot Peele <https://issues.rpath.com/> date: Sun, 18 Jan 2009 23:07:54 -0500 promote script to check rpl:1-rpl:1-qa promote committer: Elliot Peele <https://issues.rpath.com/> diff --git a/scripts/pcheck.py b/scripts/pcheck.py new file mode 100755 --- /dev/null +++ b/scripts/pcheck.py @@ -0,0 +1,43 @@ +#!/usr/bin/python + +import sys +from conary.lib import util +sys.excepthook = util.genExcepthook() + +from conary import versions +from conary import callbacks +from conary import conarycfg +from conary import conaryclient + +cfg = conarycfg.ConaryConfiguration() +cfg.setContext('1-binary') + +client = conaryclient.ConaryClient(cfg) + +frmlabel = versions.VersionFromString('/conary.rpath.com at rpl:devel//1') +groupTrvs = client.repos.findTrove(['conary.rpath.com at rpl:1', ], ('group-os', frmlabel, None)) + +groupTrvs.sort() +groupTrvs.reverse() + +trvs = [ x for x in groupTrvs if x[1] == groupTrvs[0][1] ] + +labelMap = { + versions.VersionFromString('/conary.rpath.com at rpl:devel//1'): + versions.VersionFromString('/conary.rpath.com at rpl:devel//1-qa'), + versions.VersionFromString('/conary.rpath.com at rpl:devel//1-xen'): + versions.VersionFromString('/conary.rpath.com at rpl:devel//1-xen-qa'), +} + +cb = conaryclient.callbacks.CloneCallback(cfg) +success, cs = client.createSiblingCloneChangeSet( + labelMap, + trvs, + cloneSources=True, + callback=cb) + +import epdb +epdb.st() + + + From johnsonm at rpath.com Wed Aug 19 17:40:18 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:40:18 +0000 Subject: mirrorball: handle different repository types Message-ID: <200908192140.n7JLeIUR017324@scc.eng.rpath.com> changeset: 7a23f92844a2 user: Elliot Peele <http://issues.rpath.com/> date: Thu, 14 Aug 2008 22:37:33 -0400 handle different repository types committer: Elliot Peele <http://issues.rpath.com/> diff --git a/updatebot/bot.py b/updatebot/bot.py --- a/updatebot/bot.py +++ b/updatebot/bot.py @@ -19,6 +19,7 @@ import time import logging +import aptmd import repomd from updatebot import build @@ -56,14 +57,20 @@ if self._pkgSourcePopulated: return + if self._cfg.repositoryFormat == 'apt': + client = aptmd.Client(self._cfg.repositoryUrl) + for repo in self._cfg.repositoryPaths: log.info('loading repository data %s/%s' % (self._cfg.repositoryUrl, repo)) - client = repomd.Client(self._cfg.repositoryUrl + '/' + repo) + + if self._cfg.repositoryFormat == 'yum': + client = repomd.Client(self._cfg.repositoryUrl + '/' + repo) + self._pkgSource.loadFromClient(client, repo) self._clients[repo] = client + self._pkgSource.finalize() - self._pkgSourcePopulated = True def _populatePatchSource(self): @@ -111,7 +118,7 @@ # Import sources into repository. toBuild, fail = self._updater.create(self._cfg.package) -# import epdb; epdb.st() + import epdb; epdb.st() # Build all newly imported packages. #trvMap, failed = self._builder.buildmany(toBuild) From johnsonm at rpath.com Wed Aug 19 17:39:53 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:39:53 +0000 Subject: mirrorball: rename file, update to use update method Message-ID: <200908192139.n7JLdrGH016351@scc.eng.rpath.com> changeset: 9d6c5562efdc user: Elliot Peele <http://issues.rpath.com/> date: Thu, 10 Jul 2008 10:30:48 -0400 rename file, update to use update method committer: Elliot Peele <http://issues.rpath.com/> diff --git a/scripts/test.py b/scripts/test.py deleted file mode 100755 --- a/scripts/test.py +++ /dev/null @@ -1,21 +0,0 @@ -#!/usr/bin/python - -import os -import sys - -sys.path.insert(0, os.environ['HOME'] + '/hg/rpath-xmllib') -sys.path.insert(0, os.environ['HOME'] + '/hg/conary') -sys.path.insert(0, os.environ['HOME'] + '/hg/mirrorball') - -from conary.lib import util -sys.excepthook = util.genExcepthook() - -from updatebot import bot, config, log - -log.addRootLogger() -cfg = config.UpdateBotConfig() -cfg.read(os.environ['HOME'] + '/hg/mirrorball/config/updatebotrc') -obj = bot.Bot(cfg) -obj.run() - -import epdb ; epdb.st() diff --git a/scripts/update.py b/scripts/update.py new file mode 100755 --- /dev/null +++ b/scripts/update.py @@ -0,0 +1,21 @@ +#!/usr/bin/python + +import os +import sys + +sys.path.insert(0, os.environ['HOME'] + '/hg/rpath-xmllib') +sys.path.insert(0, os.environ['HOME'] + '/hg/conary') +sys.path.insert(0, os.environ['HOME'] + '/hg/mirrorball') + +from conary.lib import util +sys.excepthook = util.genExcepthook() + +from updatebot import bot, config, log + +log.addRootLogger() +cfg = config.UpdateBotConfig() +cfg.read(os.environ['HOME'] + '/hg/mirrorball/config/updatebotrc') +obj = bot.Bot(cfg) +obj.update() + +import epdb ; epdb.st() From johnsonm at rpath.com Wed Aug 19 17:40:17 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:40:17 +0000 Subject: mirrorball: cleanups Message-ID: <200908192140.n7JLeHpe017307@scc.eng.rpath.com> changeset: af96cc2f80a7 user: Elliot Peele <http://issues.rpath.com/> date: Thu, 14 Aug 2008 21:04:53 -0400 cleanups committer: Elliot Peele <http://issues.rpath.com/> diff --git a/updatebot/pkgsource/debsource.py b/updatebot/pkgsource/debsource.py --- a/updatebot/pkgsource/debsource.py +++ b/updatebot/pkgsource/debsource.py @@ -17,6 +17,8 @@ import aptmd from updatebot import util +log = logging.getLogger('updatebot.pkgsource') + class DebSource(object): def __init__(self, cfg): self._excludeArch = cfg.excludeArch @@ -31,7 +33,7 @@ self.locationMap = dict() def loadFromClient(self, client, path): - for pkg in self.client.parse(path): + for pkg in client.parse(path): if pkg.arch in self._excludeArch: continue @@ -66,8 +68,14 @@ self.srcPkgMap[srcPkg] = set() for binPkgName in srcPkg.binaries: + if binPkgName not in self.binNameMap: + # This means that we don't have a binary package that was + # built with srcPkg, but that is ok. + continue + for binPkg in self.binNameMap[binPkgName]: - if binPkg.version == srcPkg.version and binPkg.release == srcPkg.release: + if (binPkg.version == srcPkg.version and + binPkg.release == srcPkg.release): self.srcPkgMap[srcPkg].add(binPkg) self.srcPkgMap[srcPkg].add(srcPkg) From johnsonm at rpath.com Wed Aug 19 17:42:02 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:42:02 +0000 Subject: mirrorball: script changes Message-ID: <200908192142.n7JLg2O9021553@scc.eng.rpath.com> changeset: 0bcb59eb022e user: Elliot Peele <https://issues.rpath.com/> date: Thu, 05 Feb 2009 18:17:02 -0500 script changes committer: Elliot Peele <https://issues.rpath.com/> diff --git a/scripts/import.py b/scripts/import.py --- a/scripts/import.py +++ b/scripts/import.py @@ -27,7 +27,7 @@ log.addRootLogger() cfg = config.UpdateBotConfig() -cfg.read(os.environ['HOME'] + '/hg/mirrorball/config/centos/updatebotrc') +cfg.read(os.environ['HOME'] + '/hg/mirrorball/config/sles/updatebotrc') obj = bot.Bot(cfg) trvMap = obj.create() diff --git a/scripts/pkgsource.py b/scripts/pkgsource.py --- a/scripts/pkgsource.py +++ b/scripts/pkgsource.py @@ -3,11 +3,11 @@ import os import sys -sys.path.insert(0, os.environ['HOME'] + '/hg/26/rpath-xmllib') -sys.path.insert(0, os.environ['HOME'] + '/hg/26/conary') -sys.path.insert(0, os.environ['HOME'] + '/hg/26/mirrorball') -sys.path.insert(0, os.environ['HOME'] + '/hg/26/rmake') -sys.path.insert(0, os.environ['HOME'] + '/hg/26/epdb') +sys.path.insert(0, os.environ['HOME'] + '/hg/rpath-xmllib') +sys.path.insert(0, os.environ['HOME'] + '/hg/conary') +sys.path.insert(0, os.environ['HOME'] + '/hg/mirrorball') +sys.path.insert(0, os.environ['HOME'] + '/hg/rmake') +sys.path.insert(0, os.environ['HOME'] + '/hg/epdb') from conary.lib import util sys.excepthook = util.genExcepthook() @@ -18,8 +18,6 @@ updatebot.log.addRootLogger() log = logging.getLogger('test') -import aptmd -import repomd from updatebot import config from updatebot import pkgsource From johnsonm at rpath.com Wed Aug 19 17:40:03 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:40:03 +0000 Subject: mirrorball: uniquify getSourceTroves by using a set; avoid stomping on srcTrvs dict Message-ID: <200908192140.n7JLe37t016744@scc.eng.rpath.com> changeset: 0b6ed07d3354 user: Matt Wilson <https://issues.rpath.com/> date: Sat, 21 Jun 2008 16:50:09 -0400 uniquify getSourceTroves by using a set; avoid stomping on srcTrvs dict committer: Matt Wilson <https://issues.rpath.com/> diff --git a/updatebot/conaryhelper.py b/updatebot/conaryhelper.py --- a/updatebot/conaryhelper.py +++ b/updatebot/conaryhelper.py @@ -84,17 +84,18 @@ if len(latest) != 2: raise TooManyFlavorsFoundError(why=latest) - srcTrvs = {} + d = {} for trv in latest: log.info('querying %s for source troves' % (trv, )) srcTrvs = self._getSourceTroves(trv) for src, binLst in srcTrvs.iteritems(): - if src in srcTrvs: - srcTrvs[src].extend(binLst) + s = set(binLst) + if src in d: + d[src].update(s) else: - srcTrvs[src] = binLst + d[src] = s - return srcTrvs + return d @staticmethod def _findLatest(trvlst): @@ -149,8 +150,8 @@ strongRefs=True)): src = (sources[i](), v.getSourceVersion(), None) if src not in srcTrvs: - srcTrvs[src] = [] - srcTrvs[src].append((n, v, f)) + srcTrvs[src] = set() + srcTrvs[src].add((n, v, f)) return srcTrvs From johnsonm at rpath.com Wed Aug 19 17:39:21 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:39:21 +0000 Subject: mirrorball: Backfill the manifest with older binary rpms to ensure that we don't remove Message-ID: <200908192139.n7JLdLfW015089@scc.eng.rpath.com> changeset: e9341d2db250 user: Elliot Peele <http://issues.rpath.com/> date: Mon, 16 Jun 2008 11:02:05 -0400 Backfill the manifest with older binary rpms to ensure that we don't remove functionallity. committer: Elliot Peele <http://issues.rpath.com/> diff --git a/updatebot/update.py b/updatebot/update.py --- a/updatebot/update.py +++ b/updatebot/update.py @@ -146,6 +146,19 @@ # make sure we aren't trying to remove a package if binPkg.name not in newNames: - raise UpdateRemovesPackageError(why='%s not in %s' % (binPkg.name, newNames)) + # Novell releases updates to only the binary rpms of a package + # that have chnaged. We have to use binaries from the old srpm. + # Get the last version of the pkg and add it to the srcPkgMap. + log.warn('using old version of package %s' % binPkg) + pkgs = self._rpmSource.binNameMap[binPkg.name] + pkgs.sort(util.packagevercmp) + + # Raise an exception if the versions of the packages aren't equal. + if rpmvercmp(pkg[-1].version, binPkg.version) != 0: + raise UpdateRemovesPackageError(why='all rpms in the ' + 'manifest should have the same version, trying ' + 'to add %s' % (pkgs[-1], )) + + self._rpmSource.srcPkgMap[srpm].append(pkgs[-1]) return needsUpdate From johnsonm at rpath.com Wed Aug 19 17:41:38 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:41:38 +0000 Subject: mirrorball: strip arch and list header out of centos subjects Message-ID: <200908192141.n7JLfcxm020583@scc.eng.rpath.com> changeset: a0e17e8b178d user: Elliot Peele <https://issues.rpath.com/> date: Wed, 19 Nov 2008 11:27:14 -0500 strip arch and list header out of centos subjects committer: Elliot Peele <https://issues.rpath.com/> diff --git a/updatebot/advisories/centos.py b/updatebot/advisories/centos.py --- a/updatebot/advisories/centos.py +++ b/updatebot/advisories/centos.py @@ -89,6 +89,15 @@ if msg.pkgs is None: return + # Strip arch out of the subject + for arch in self.supportedArches: + if arch in msg.subject: + msg.subject = msg.subject.replace('%s ' % arch, '') + + # Strip subject. + msg.subject = msg.subject.replace('[CentOS-announce]', '') + msg.subject = msg.subject.strip() + for pkgName in msg.pkgs: # Toss out any arches that we don't know how to handle. if not self._supportedArch(pkgName): @@ -107,15 +116,6 @@ msg.packages = set() msg.packages.add(binPkg) - # Strip arch out of the subject - for arch in self.supportedArches: - if arch in msg.subject: - msg.subject = msg.subject.replace('%s ' % arch, '') - - # Strip subject. - msg.subject = msg.subject.replace('[CentOS-announce]', '') - msg.subject = msg.subject.strip() - def _supportedArch(self, pkgfn): """ Filter out unsupported arches based on rpm filename. From johnsonm at rpath.com Wed Aug 19 17:40:31 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:40:31 +0000 Subject: mirrorball: initial module for displaying infomration Message-ID: <200908192140.n7JLeVx6017869@scc.eng.rpath.com> changeset: a7bbc2f559e9 user: Elliot Peele <http://bugs.rpath.com/> date: Wed, 27 Aug 2008 17:30:49 -0400 initial module for displaying infomration committer: Elliot Peele <http://bugs.rpath.com/> diff --git a/updatebot/display.py b/updatebot/display.py new file mode 100644 --- /dev/null +++ b/updatebot/display.py @@ -0,0 +1,35 @@ +# +# Copyright (c) 2008 rPath, Inc. +# +# This program is distributed under the terms of the Common Public License, +# version 1.0. A copy of this license should have been distributed with this +# source file in a file called LICENSE. If it is not present, the license +# is always available at http://www.rpath.com/permanent/licenses/CPL-1.0. +# +# This program is distributed in the hope that it will be useful, but +# without any warranty; without even the implied warranty of merchantability +# or fitness for a particular purpose. See the Common Public License for +# full details. +# + +def displayTrovesForGroupRecipe(trvMap, indent=12): + """ + Formats a troveMap to a list of packages to be included in a group recipe. + @param trvMap dictionary of troves from a build + @type trvMap: {srcTrvs: set((n, v, f), ..)} + @return formatted string + """ + + names = set() + for trvSet in trvMap.itervalues(): + for trv in trvSet: + names.add(trv[0].split(':')[0]) + + names = list(names) + names.sort() + + ret = [] + for name in names: + ret.append(' ' * indent + '\'%s\',' % name) + + return '\n'.join(ret) From johnsonm at rpath.com Wed Aug 19 17:41:45 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:41:45 +0000 Subject: mirrorball: fix pylint errors Message-ID: <200908192141.n7JLfj8n020889@scc.eng.rpath.com> changeset: 2d4cd2398709 user: Elliot Peele <https://issues.rpath.com/> date: Thu, 20 Nov 2008 00:10:00 -0500 fix pylint errors committer: Elliot Peele <https://issues.rpath.com/> diff --git a/updatebot/advisories/common.py b/updatebot/advisories/common.py --- a/updatebot/advisories/common.py +++ b/updatebot/advisories/common.py @@ -89,7 +89,8 @@ smtp = self._smtpConnect() try: - results = smtp.sendmail(self._from, self._to + self._bcc, message.as_string()) + results = smtp.sendmail(self._from, self._to + self._bcc, + message.as_string()) except (SMTPRecipientsRefused, SMTPHeloError, SMTPSenderRefused, SMTPDataError), e: raise FailedToSendAdvisoryError(error=e) @@ -277,7 +278,7 @@ log.error('The %s backend does not implement %s' % (self.__class__.__name__, 'load')) - raise NotImplemntedError + raise NotImplementedError def _hasException(self, binPkg): """ @@ -307,7 +308,7 @@ log.error('The %s backend does not implement %s' % (self.__class__.__name__, '_isUpdateRepo')) - raise NotImplmentedError + raise NotImplementedError def _checkForDuplicates(self, patchSet): """ @@ -321,7 +322,7 @@ log.error('The %s backend does not implement %s' % (self.__class__.__name__, '_isUpdateRepo')) - raise NotImplmentedError + raise NotImplementedError def _filterPatch(self, patch): """ From johnsonm at rpath.com Wed Aug 19 17:42:18 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:42:18 +0000 Subject: mirrorball: fix typos Message-ID: <200908192142.n7JLgIUo022183@scc.eng.rpath.com> changeset: 615f9d69307e user: Elliot Peele <https://issues.rpath.com/> date: Sat, 07 Feb 2009 16:49:43 -0500 fix typos committer: Elliot Peele <https://issues.rpath.com/> diff --git a/updatebot/build.py b/updatebot/build.py --- a/updatebot/build.py +++ b/updatebot/build.py @@ -20,7 +20,7 @@ import logging import xml -from Queue import Queue +from Queue import Queue, Empty from threading import Thread, RLock from conary import conarycfg, conaryclient @@ -426,7 +426,7 @@ 0: 'log', 'log': 0, 1: 'results', - 'results': 1 + 'results': 1, 2: 'error', 'error': 2, } @@ -459,13 +459,13 @@ def run(self): while True: - self.trv = toBuild.get() + self.trv = self.toBuild.get() self.log('received trv') self.jobId = self.builder.start([self.trv, ]) - done, job = self._watch(): + done, job = self._watch() while not done: - done, job = self._watch(): + done, job = self._watch() if job.isFailed(): self.error('job failed') @@ -496,7 +496,7 @@ def error(self, msg): self._status(msg, type=MESSAGE_TYPES['error']) - def log(self, msg) + def log(self, msg): self._status(msg, type=MESSAGE_TYPES['log']) def results(self, res): @@ -547,7 +547,7 @@ while not done: try: log.debug('checking for status messages') - msg = self.status.get(timeout=5) + msg = self._status.get(timeout=5) except Empty: continue From johnsonm at rpath.com Wed Aug 19 17:40:53 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:40:53 +0000 Subject: mirrorball: checkpoint during initial centos package import Message-ID: <200908192140.n7JLerMS018821@scc.eng.rpath.com> changeset: c48daac2e8d9 user: Jeff Uphoff <https://issues.rpath.com/> date: Wed, 24 Sep 2008 15:27:29 -0400 checkpoint during initial centos package import committer: Jeff Uphoff <https://issues.rpath.com/> diff --git a/scripts/import.py b/scripts/import.py --- a/scripts/import.py +++ b/scripts/import.py @@ -27,7 +27,7 @@ log.addRootLogger() cfg = config.UpdateBotConfig() -cfg.read(os.environ['HOME'] + '/hg/mirrorball/config/ubuntu/updatebotrc') +cfg.read(os.environ['HOME'] + '/hg/mirrorball/config/centos/updatebotrc') obj = bot.Bot(cfg) trvMap = obj.create() diff --git a/scripts/pkgsource.py b/scripts/pkgsource.py --- a/scripts/pkgsource.py +++ b/scripts/pkgsource.py @@ -21,7 +21,7 @@ from updatebot import pkgsource cfg = config.UpdateBotConfig() -cfg.read(os.environ['HOME'] + '/hg/mirrorball/config/ubuntu/updatebotrc') +cfg.read(os.environ['HOME'] + '/hg/mirrorball/config/centos/updatebotrc') pkgSource = pkgsource.PackageSource(cfg) diff --git a/updatebot/bot.py b/updatebot/bot.py --- a/updatebot/bot.py +++ b/updatebot/bot.py @@ -117,11 +117,11 @@ toBuild, fail = self._updater.create(self._cfg.package, buildAll=False) # Build all newly imported packages. -# trvMap, failed = self._builder.buildmany(toBuild) + trvMap, failed = self._builder.buildmany(toBuild) -# import epdb; epdb.st() + import epdb; epdb.st() - trvMap = self._builder.build(toBuild) +## trvMap = self._builder.build(toBuild) #import epdb; epdb.st() #trvs = self._builder._formatInput(toBuild) From johnsonm at rpath.com Wed Aug 19 17:39:15 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:39:15 +0000 Subject: mirrorball: convert srpmToConaryVersion to using package objects Message-ID: <200908192139.n7JLdFu1014820@scc.eng.rpath.com> changeset: f4be48bdd2d5 user: Elliot Peele <http://issues.rpath.com/> date: Tue, 10 Jun 2008 16:31:23 -0400 convert srpmToConaryVersion to using package objects add function for comparing package objects committer: Elliot Peele <http://issues.rpath.com/> diff --git a/updatebot/util.py b/updatebot/util.py --- a/updatebot/util.py +++ b/updatebot/util.py @@ -18,6 +18,8 @@ import os +from rpmvercmp import rpmvercmp + def join(a, *b): """ Version of os.path.join that doesn't reroot when it finds a leading /. @@ -27,3 +29,39 @@ for path in b: root += os.sep + os.path.normpath(path) return root + +def srpmToConaryVersion(srcPkg): + """ + Get the equvialent conary version from a srcPkg object. + @param srcPkg: package object for a srpm + @type srcPkg: repomd.packagexml._Package + @return conary trailing version + """ + + version = srcPkg.version.replace('-', '_') + release = srcPkg.release.replace('-', '_') + cnyver = '_'.join([version, release]) + return cnyver + +def packagevercmp(a, b): + """ + Compare two package objects. + @param a: package object from repo metadata + @type a: repomd.packagexml._Package + @param b: package object from repo metadata + @type b: repomd.packagexml._Package + """ + + epochcmp = rpmvercmp(a.epoch, b.epoch) + if epochcmp != 0: + return epochcmp + + vercmp = rpmvercmp(a.version, b.version) + if vercmp != 0: + return vercmp + + relcmp = rpmvercmp(a.release, b.release) + if relcmp != 0: + return relcmp + + return 0 From johnsonm at rpath.com Wed Aug 19 17:39:07 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:39:07 +0000 Subject: mirrorball: cleanup pylint Message-ID: <200908192139.n7JLd7cR014521@scc.eng.rpath.com> changeset: 76ee73038a9a user: Elliot Peele <http://issues.rpath.com/> date: Mon, 02 Jun 2008 23:04:14 -0400 cleanup pylint committer: Elliot Peele <http://issues.rpath.com/> diff --git a/updatebot/conaryhelper.py b/updatebot/conaryhelper.py --- a/updatebot/conaryhelper.py +++ b/updatebot/conaryhelper.py @@ -63,7 +63,8 @@ return srcTrvs - def _findLatest(self, trvlst): + @classmethod + def _findLatest(cls, trvlst): """ Given a list of trove specs, find the most recent versions. @param trvlst: list of trove specs @@ -108,7 +109,8 @@ return srcTrvs - def _getTrove(self, cs, name, version, flavor): + @classmethod + def _getTrove(cls, cs, name, version, flavor): """ Get a trove object for a given name, version, flavor from a changeset. @param cs: conary changeset object @@ -126,17 +128,19 @@ trv = trove.Trove(troveCs, skipIntegrityChecks=True) return trv + if __name__ == '__main__': import sys from conary.lib import util as cnyutil sys.excepthook = cnyutil.genExcepthook() - import config - cfg = config.UpdateBotConfig() - cfg.topGroup = ('group-dist', 'sle.rpath.com at rpath:sle-devel', None) - cfg.configPath = '../' + from updatebot import config + Cfg = config.UpdateBotConfig() + Cfg.topGroup = ('group-dist', 'sle.rpath.com at rpath:sle-devel', None) + Cfg.configPath = '../' - obj = ConaryHelper(cfg) - srcTrvs = obj.getSourceTrovesInTopLevel() + Obj = ConaryHelper(Cfg) + SrcTrvs = Obj.getSourceTroves() - import epdb ; epdb.st() + import epdb + epdb.st() From johnsonm at rpath.com Wed Aug 19 17:42:06 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:42:06 +0000 Subject: mirrorball: add global config for common data Message-ID: <200908192142.n7JLg6n6021690@scc.eng.rpath.com> changeset: f06e688495a7 user: Elliot Peele <https://issues.rpath.com/> date: Fri, 16 Jan 2009 23:54:02 -0500 add global config for common data committer: Elliot Peele <https://issues.rpath.com/> diff --git a/updatebot/config.py b/updatebot/config.py --- a/updatebot/config.py +++ b/updatebot/config.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2008 rPath, Inc. +# Copyright (c) 2008-2009 rPath, Inc. # # This program is distributed under the terms of the Common Public License, # version 1.0. A copy of this license should have been distributed with this @@ -26,6 +26,8 @@ from rmake.build.buildcfg import CfgTroveSpec +from updatebot.lib import util + class CfgBranch(CfgLabel): """ Class for representing conary branches. @@ -156,6 +158,15 @@ emailBcc = (CfgList(CfgString), []) smtpServer = CfgString + # Jira Info + jiraUser = CfgString + jiraPassword = CfgString + jiraUrl = CfgString + jiraSecurityGroup = CfgString + + # Satis Info + satisUrl = CfgString + class UpdateBotConfig(cfg.SectionedConfigFile): """ @@ -175,6 +186,13 @@ Read specified file. """ + # If there is a global config, load it first. + cfgDir = os.path.dirname(args[0]) + cfgFile = util.join(cfgDir, '../', 'updatebotrc') + if os.path.exists(cfgFile): + cfg.SectionedConfigFile.read(self, cfgFile, **kwargs) + + # Find configPath. ret = cfg.SectionedConfigFile.read(self, *args, **kwargs) if not self.configPath.startswith(os.sep): # configPath is relative From johnsonm at rpath.com Wed Aug 19 17:42:22 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:42:22 +0000 Subject: mirrorball: more build changes Message-ID: <200908192142.n7JLgMvQ022319@scc.eng.rpath.com> changeset: 1ad796745afd user: Elliot Peele <https://issues.rpath.com/> date: Sun, 08 Feb 2009 06:29:08 +0000 more build changes committer: Elliot Peele <https://issues.rpath.com/> diff --git a/updatebot/build.py b/updatebot/build.py --- a/updatebot/build.py +++ b/updatebot/build.py @@ -451,10 +451,11 @@ class BuildWorker(Thread): BuilderClass = Builder - def __init__(self, cfg, toBuild, status, name=None): + def __init__(self, cfg, toBuild, status, name=None, offset=0): Thread.__init__(self, name=name) self.name = name + self.offset = offset self.toBuild = toBuild self.status = status self.builder = self.BuilderClass(cfg) @@ -463,6 +464,7 @@ self.jobId = None def run(self): + time.sleep(self.offset * 5) while True: self.trv = self.toBuild.get() self.log('received trv') @@ -478,10 +480,10 @@ continue built = True - if not built: - self.error('job failed') + if not built: + self.error('job failed') - self.toBuild.task_done() + self.toBuild.task_done() def _doBuild(self): self.jobId = self.builder.start([self.trv, ]) @@ -543,7 +545,7 @@ def provisionWorkers(self): for i in range(self._workerCount): worker = self.workerClass(self._cfg, self._toBuild, self._status, - name='Build Worker %s' % i) + name='Build Worker %s' % i, offset=i) self._workers.append(worker) def start(self): From johnsonm at rpath.com Wed Aug 19 17:42:59 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:42:59 +0000 Subject: mirrorball: for each instance use a different cache dir Message-ID: <200908192142.n7JLgxf6023851@scc.eng.rpath.com> changeset: 191209aa8c1f user: Elliot Peele <https://issues.rpath.com/> date: Wed, 13 May 2009 11:45:18 -0400 for each instance use a different cache dir committer: Elliot Peele <https://issues.rpath.com/> diff --git a/updatebot/conaryhelper.py b/updatebot/conaryhelper.py --- a/updatebot/conaryhelper.py +++ b/updatebot/conaryhelper.py @@ -73,6 +73,8 @@ self._newPkgFactory = cfg.newPackageFactory self._checkoutCache = {} + self._cacheDir = tempfile.mkdtemp( + prefix='conaryhelper-%s-' % cfg.platformName) def getConaryConfig(self): """ @@ -377,6 +379,16 @@ return recipeDir + def _getRecipeDir(self, pkgname): + """ + Make a temporary directory to create or checkout a package in. + @param pkgname: name of the package to checkout + @type pkgname: string + @return checkout directory + """ + + return tempfile.mkdtemp(prefix='%s-' % pkgname, dir=self._cacheDir) + def _checkout(self, pkgname): """ Checkout a source component from the repository. @@ -387,7 +399,7 @@ log.info('checking out %s' % pkgname) - recipeDir = tempfile.mkdtemp(prefix='conaryhelper-') + recipeDir = self._getRecipeDir(pkgname) checkin.checkout(self._repos, self._ccfg, recipeDir, [pkgname, ]) return recipeDir @@ -402,8 +414,7 @@ log.info('creating new package %s' % pkgname) - recipeDir = tempfile.mkdtemp(prefix='conaryhelper-') - + recipeDir = self._getRecipeDir(pkgname) cwd = os.getcwd() try: os.chdir(recipeDir) From johnsonm at rpath.com Wed Aug 19 17:40:56 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:40:56 +0000 Subject: mirrorball: add script for generating the manifest for a given package Message-ID: <200908192140.n7JLeuwk018924@scc.eng.rpath.com> changeset: 4138a70038ab user: Elliot Peele <https://issues.rpath.com/> date: Fri, 26 Sep 2008 15:04:03 -0400 add script for generating the manifest for a given package committer: Elliot Peele <https://issues.rpath.com/> diff --git a/scripts/genmanifest.py b/scripts/genmanifest.py new file mode 100755 --- /dev/null +++ b/scripts/genmanifest.py @@ -0,0 +1,39 @@ +#!/usr/bin/python +# +# Copyright (c) 2008 rPath, Inc. +# +# This program is distributed under the terms of the Common Public License, +# version 1.0. A copy of this license should have been distributed with this +# source file in a file called LICENSE. If it is not present, the license +# is always available at http://www.rpath.com/permanent/licenses/CPL-1.0. +# +# This program is distributed in the hope that it will be useful, but +# without any warranty; without even the implied warranty of merchantability +# or fitness for a particular purpose. See the Common Public License for +# full details. +# + +import os +import sys + +sys.path.insert(0, os.environ['HOME'] + '/hg/rpath-xmllib') +sys.path.insert(0, os.environ['HOME'] + '/hg/conary') +sys.path.insert(0, os.environ['HOME'] + '/hg/mirrorball') + +from conary.lib import util +sys.excepthook = util.genExcepthook() + +from updatebot import bot, config, log + +log.addRootLogger() +cfg = config.UpdateBotConfig() +cfg.read(os.environ['HOME'] + '/hg/mirrorball/config/centos/updatebotrc') +obj = bot.Bot(cfg) + +obj._populatePkgSource() + +pkgName = sys.argv[1] + +srcPkg = obj._updater._getPackagesToImport(pkgName) +manifest = obj._updater._getManifestFromPkgSource(srcPkg) +print '\n'.join(manifest) From johnsonm at rpath.com Wed Aug 19 17:42:16 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:42:16 +0000 Subject: mirrorball: take platform as argument Message-ID: <200908192142.n7JLgG0H022115@scc.eng.rpath.com> changeset: 37d21e5c4a27 user: Elliot Peele <https://issues.rpath.com/> date: Thu, 05 Feb 2009 23:55:21 -0500 take platform as argument committer: Elliot Peele <https://issues.rpath.com/> diff --git a/scripts/import.py b/scripts/import.py --- a/scripts/import.py +++ b/scripts/import.py @@ -30,7 +30,7 @@ log.addRootLogger() cfg = config.UpdateBotConfig() -cfg.read(os.environ['HOME'] + '/hg/mirrorball/config/sles/updatebotrc') +cfg.read(os.environ['HOME'] + '/hg/26/mirrorball/config/%s/updatebotrc' % sys.argv[1]) obj = bot.Bot(cfg) trvMap = obj.create() diff --git a/scripts/pkgsource.py b/scripts/pkgsource.py --- a/scripts/pkgsource.py +++ b/scripts/pkgsource.py @@ -3,11 +3,11 @@ import os import sys -sys.path.insert(0, os.environ['HOME'] + '/hg/rpath-xmllib') -sys.path.insert(0, os.environ['HOME'] + '/hg/conary') -sys.path.insert(0, os.environ['HOME'] + '/hg/mirrorball') -sys.path.insert(0, os.environ['HOME'] + '/hg/rmake') -sys.path.insert(0, os.environ['HOME'] + '/hg/epdb') +sys.path.insert(0, os.environ['HOME'] + '/hg/26/rpath-xmllib') +sys.path.insert(0, os.environ['HOME'] + '/hg/26/conary') +sys.path.insert(0, os.environ['HOME'] + '/hg/26/mirrorball') +sys.path.insert(0, os.environ['HOME'] + '/hg/26/rmake') +sys.path.insert(0, os.environ['HOME'] + '/hg/26/epdb') from conary.lib import util sys.excepthook = util.genExcepthook() @@ -22,7 +22,7 @@ from updatebot import pkgsource cfg = config.UpdateBotConfig() -cfg.read(os.environ['HOME'] + '/hg/26/mirrorball/config/sles/updatebotrc') +cfg.read(os.environ['HOME'] + '/hg/26/mirrorball/config/%s/updatebotrc' % sys.argv[1] ) pkgSource = pkgsource.PackageSource(cfg) pkgSource.load() From johnsonm at rpath.com Wed Aug 19 17:42:16 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:42:16 +0000 Subject: mirrorball: add fedora stub to make importer happy Message-ID: <200908192142.n7JLgGMN022081@scc.eng.rpath.com> changeset: c0c51b55475c user: Elliot Peele <https://issues.rpath.com/> date: Thu, 05 Feb 2009 23:54:06 -0500 add fedora stub to make importer happy committer: Elliot Peele <https://issues.rpath.com/> diff --git a/updatebot/advisories/__init__.py b/updatebot/advisories/__init__.py --- a/updatebot/advisories/__init__.py +++ b/updatebot/advisories/__init__.py @@ -26,7 +26,7 @@ Raised when an unsupported backend is used. """ -__supportedBackends = ('sles', 'centos', 'ubuntu') +__supportedBackends = ('sles', 'centos', 'ubuntu', 'fedora') def __getBackend(backend): if backend not in __supportedBackends: diff --git a/updatebot/advisories/fedora.py b/updatebot/advisories/fedora.py new file mode 100644 --- /dev/null +++ b/updatebot/advisories/fedora.py @@ -0,0 +1,30 @@ +# +# Copyright (c) 2008-2009 rPath, Inc. +# +# This program is distributed under the terms of the Common Public License, +# version 1.0. A copy of this license should have been distributed with this +# source file in a file called LICENSE. If it is not present, the license +# is always available at http://www.rpath.com/permanent/licenses/CPL-1.0. +# +# This program is distributed in the hope that it will be useful, but +# without any warranty; without even the implied warranty of merchantability +# or fitness for a particular purpose. See the Common Public License for +# full details. +# + +""" +Advisory module for Fedora. +""" + +import os +import pmap +import logging + +from updatebot.advisories.common import BaseAdvisor + +log = logging.getLogger('updatebot.advisories') + +class Advisor(BaseAdvisor): + """ + Class for processing Fedora advisory information. + """ From johnsonm at rpath.com Wed Aug 19 17:39:25 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:39:25 +0000 Subject: mirrorball: switch print statements to logging Message-ID: <200908192139.n7JLdP2V015293@scc.eng.rpath.com> changeset: 67c32e4c4d25 user: Elliot Peele <http://issues.rpath.com/> date: Mon, 16 Jun 2008 18:25:33 -0400 switch print statements to logging committer: Elliot Peele <http://issues.rpath.com/> diff --git a/rpmimport/recipemaker.py b/rpmimport/recipemaker.py --- a/rpmimport/recipemaker.py +++ b/rpmimport/recipemaker.py @@ -20,9 +20,12 @@ import os import shutil +import logging import tempfile from conary import cvc +log = logging.getLogger('rpmimport.recipemaker') + class RecipeMaker(object): """ Class for creating and managing rpm factory based source components. @@ -45,8 +48,8 @@ try: cvc.sourceCommand(self.cfg, ['cook'], {'no-deps': None}) except Exception, e: - print '++++++ error building', pkgname, str(e) - return + log.error('++++++ error building %s %s' % (pkgname, str(e))) + raise cvc.sourceCommand(self.cfg, [ 'commit' ], { 'message': @@ -61,7 +64,7 @@ Side effect: current working directory will be the checkout directory when this method returns """ - print 'creating initial template for', pkgname + log.info('creating initial template for %s' % pkgname) try: shutil.rmtree(pkgname) except OSError: @@ -81,7 +84,7 @@ Side effect: current working directory will be the checkout directory when this method returns """ - print 'updating', pkgname + log.info('updating %s' % pkgname) try: shutil.rmtree(pkgname) except OSError: From johnsonm at rpath.com Wed Aug 19 17:43:06 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:43:06 +0000 Subject: mirrorball: use common pkg filter Message-ID: <200908192143.n7JLh64t024124@scc.eng.rpath.com> changeset: 086d8adf4a76 user: Elliot Peele <https://issues.rpath.com/> date: Mon, 22 Jun 2009 10:35:42 -0400 use common pkg filter committer: Elliot Peele <https://issues.rpath.com/> diff --git a/updatebot/update.py b/updatebot/update.py --- a/updatebot/update.py +++ b/updatebot/update.py @@ -72,6 +72,19 @@ % (len(toUpdate), len(toAdvise))) return toAdvise, toUpdate + def _fltrPkg(self, pkgname): + """ + Return True if this is a package that should be filtered out. + """ + + if (name.startswith('info-') or + name.startswith('group-') or + name.startswith('factory-') or + name in self._cfg.excludePackages): + return True + + return False + def _findUpdatableTroves(self, group): """ Query a group to find packages that need to be updated. @@ -87,10 +100,7 @@ name = name.split(':')[0] # skip special packages - if (name.startswith('info-') or - name.startswith('group-') or - name.startswith('factory-') or - name in self._cfg.excludePackages): + if self._fltrPkg(name): continue latestSrpm = self._getLatestSource(name) @@ -267,8 +277,7 @@ if buildAll and pkgs: toBuild.update( [ (x, self._conaryhelper.getLatestSourceVersion(x), None) - for x in pkgs if not x.startswith('info-') - and x not in self._cfg.excludePackages ] + for x in pkgs if not self._fltrPkg(x) ] ) return toBuild, fail From johnsonm at rpath.com Wed Aug 19 17:42:44 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:42:44 +0000 Subject: mirrorball: better logging when there are no packages to build Message-ID: <200908192142.n7JLgiOe023255@scc.eng.rpath.com> changeset: b31d5ceb0825 user: Elliot Peele <https://issues.rpath.com/> date: Tue, 21 Apr 2009 13:36:52 -0400 better logging when there are no packages to build committer: Elliot Peele <https://issues.rpath.com/> diff --git a/updatebot/bot.py b/updatebot/bot.py --- a/updatebot/bot.py +++ b/updatebot/bot.py @@ -93,6 +93,7 @@ else: toPackage = set(self._cfg.package) + # Import sources into repository. toBuild, fail = self._updater.create(toPackage, buildAll=rebuild) @@ -100,15 +101,18 @@ log.info('failed to create %s packages' % len(fail)) log.info('found %s packages to build' % len(toBuild)) - if not rebuild: - # Build all newly imported packages. - trvMap, failed = self._builder.buildmany2(sorted(toBuild)) + if len(toBuild): + if not rebuild: + # Build all newly imported packages. + trvMap, failed = self._builder.buildmany2(sorted(toBuild)) + else: + # ReBuild all packages. + trvMap = self._builder.buildsplitarch(sorted(toBuild)) + log.info('import completed successfully') + log.info('imported %s source packages' % (len(toBuild), )) else: - # ReBuild all packages. - trvMap = self._builder.buildsplitarch(sorted(toBuild)) + log.info('no packages found to build') - log.info('import completed successfully') - log.info('imported %s source packages' % (len(toBuild), )) log.info('elapsed time %s' % (time.time() - start, )) return trvMap From johnsonm at rpath.com Wed Aug 19 17:40:45 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:40:45 +0000 Subject: mirrorball: add more frumptuus and some timing Message-ID: <200908192140.n7JLejUY018481@scc.eng.rpath.com> changeset: b803ec1ad40f user: Elliot Peele <https://issues.rpath.com/> date: Thu, 11 Sep 2008 15:50:25 -0400 add more frumptuus and some timing committer: Elliot Peele <https://issues.rpath.com/> diff --git a/scripts/promote-ubuntu.sh b/scripts/promote-ubuntu.sh --- a/scripts/promote-ubuntu.sh +++ b/scripts/promote-ubuntu.sh @@ -15,20 +15,21 @@ cfg="--config-file $HOME/hg/mirrorball/config/ubuntu/conaryrc -m promote" -cvc promote $cfg \ +time cvc promote $cfg \ {{auto,build,c,}package,deb-import,factory-deb}:source=ubuntu.rb.rpath.com at rpath:ubuntu-devel \ /ubuntu.rb.rpath.com at rpath:ubuntu-devel--/ubuntu.rpath.org at rpath:ubuntu-devel -cvc promote $cfg \ +time cvc promote $cfg \ group-appliance:source=ubuntu.rb.rpath.com at rpath:ubuntu-hardy-devel \ platform-definition:source=ubuntu.rb.rpath.com at rpath:ubuntu-hardy-devel \ group-os=ubuntu.rb.rpath.com at rpath:ubuntu-hardy-devel \ ubuntu.rb.rpath.com at rpath:ubuntu-hardy-devel--ubuntu.rpath.org at rpath:ubuntu-hardy-devel \ -#cvc promote $cfg \ +#time cvc promote $cfg \ # group-appliance:source=ubuntu.rpath.org at rpath:ubuntu-hardy-devel \ # platform-defintion:source=ubuntu.rpath.org at rpath:ubuntu-hardy-devel \ # group-os=ubuntu.rpath.org at rpath:ubuntu-hardy-devel \ # /ubuntu.rpath.org at rpath:ubuntu-hardy-devel--/ubuntu.rpath.org at rpath:ubuntu-hardy \ # /conary.rpath.com at rpl:devel//conary.rpath.com at rpl:2--/ubuntu.rpath.org at rpath:ubuntu-hardy \ # /conary.rpath.com at rpl:devel//conary.rpath.com at rpl:2//ubuntu.rpath.org at rpath:ubuntu-hardy-devel--/ubuntu.rpath.org at rpath:ubuntu-hardy +# /conary.rpath.com at rpl:devel//ubuntu.rpath.org at rpath:ubuntu-hardy-devel--/ubuntu.rpath.org at rpath:ubuntu-hardy From johnsonm at rpath.com Wed Aug 19 17:40:06 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:40:06 +0000 Subject: mirrorball: add emailFromName Message-ID: <200908192140.n7JLe6kE016847@scc.eng.rpath.com> changeset: 9cd47b67a444 user: Elliot Peele <http://bugs.rpath.com/> date: Mon, 11 Aug 2008 16:45:19 -0400 add emailFromName committer: Elliot Peele <http://bugs.rpath.com/> diff --git a/updatebot/advise.py b/updatebot/advise.py --- a/updatebot/advise.py +++ b/updatebot/advise.py @@ -217,6 +217,7 @@ def __init__(self, cfg): self._cfg = cfg + self._fromName = self._cfg.emailFromName self._from = self._cfg.emailFrom self._to = self._cfg.emailTo self._bcc = self._cfg.emailBcc @@ -226,8 +227,8 @@ if not self._cfg.productName: raise ProductNameNotDefinedError - if not self._from: - raise NoSenderFoundError(why='cfg.emailFrom not defined') + if not self._from or not self._fromName: + raise NoSenderFoundError(why='cfg.emailFrom or cfg.emailFromName not defined') if not self._to: raise NoRecipientsFoundError(why='cfg.emailTo not defined') @@ -268,7 +269,7 @@ msgText = self.template % self._data email = MIMEText(msgText) email['Subject'] = self._subject - email['From'] = self._from + email['From'] = '%s <%s>' % (self._fromName, self._from) email['To'] = self._formatList(self._to) email['Bcc'] = self._formatList(self._bcc) return email diff --git a/updatebot/config.py b/updatebot/config.py --- a/updatebot/config.py +++ b/updatebot/config.py @@ -103,6 +103,7 @@ groupFlavors = (CfgList(CfgFlavor), []) # email information for sending advisories + emailFromName = CfgString emailFrom = CfgString emailTo = (CfgList(CfgString), []) emailBcc = (CfgList(CfgString), []) From johnsonm at rpath.com Wed Aug 19 17:41:13 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:41:13 +0000 Subject: mirrorball: initial script to promote centos Message-ID: <200908192141.n7JLfDi2019590@scc.eng.rpath.com> changeset: b7d11ce3b9eb user: Elliot Peele <https://issues.rpath.com/> date: Fri, 31 Oct 2008 15:11:02 -0400 initial script to promote centos committer: Elliot Peele <https://issues.rpath.com/> diff --git a/scripts/promote-centos.sh b/scripts/promote-centos.sh new file mode 100755 --- /dev/null +++ b/scripts/promote-centos.sh @@ -0,0 +1,27 @@ +#!/bin/bash +# +# Copyright (c) 2008 rPath, Inc. +# +# This program is distributed under the terms of the Common Public License, +# version 1.0. A copy of this license should have been distributed with this +# source file in a file called LICENSE. If it is not present, the license +# is always available at http://www.rpath.com/permanent/licenses/CPL-1.0. +# +# This program is distributed in the hope that it will be useful, but +# without any warranty; without even the implied warranty of merchantability +# or fitness for a particular purpose. See the Common Public License for +# full details. +# + +cfg="--config-file $HOME/hg/mirrorball/config/centos/conaryrc -m promote --interactive" + +date +time cvc promote $cfg \ + group-appliance:source=centos.rpath.com at rpath:centos-5-devel \ + platform-definition:source=centos.rpath.com at rpath:centos-5-devel \ + group-os=centos.rpath.com at rpath:centos-5-devel \ + /centos.rpath.com at rpath:centos-5-devel--/centos.rpath.com at rpath:centos-5 \ + /centos.rpath.com at rpath:centos-devel//centos-5-devel--/centos.rpath.com at rpath:centos-devel//centos-5 \ + /conary.rpath.com at rpl:devel//2--/centos.rpath.com at rpath:centos-5 \ + /conary.rpath.com at rpl:devel//2//centos.rpath.com at rpath:centos-5-devel--/centos.rpath.com at rpath:centos-5 \ + /conary.rpath.com at rpl:devel//centos.rpath.com at rpath:centos-5-devel--/centos.rpath.com at rpath:centos-5 From johnsonm at rpath.com Wed Aug 19 17:40:27 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:40:27 +0000 Subject: mirrorball: special case pacakge names that contain version numbers Message-ID: <200908192140.n7JLeRM5017733@scc.eng.rpath.com> changeset: 6cc02c7cd0f1 user: Elliot Peele <http://bugs.rpath.com/> date: Mon, 25 Aug 2008 13:02:03 -0400 special case pacakge names that contain version numbers This is to handle the case where the source "linux" version 2.6.16-1 generates linux-2.6.16-1 and source "linux" version 2.6.16-2 generates "linux-2.6.16-2" which in this case need to be considered the "same" name. committer: Elliot Peele <http://bugs.rpath.com/> diff --git a/updatebot/update.py b/updatebot/update.py --- a/updatebot/update.py +++ b/updatebot/update.py @@ -275,13 +275,24 @@ latestSrpm = self._pkgSource.binPkgMap[latestRpm] pkgs = {} + pkgNames = set() for pkg in self._pkgSource.srcPkgMap[latestSrpm]: + pkgNames.add(pkg.name) pkgs[(pkg.name, pkg.arch)] = pkg for srpm in self._pkgSource.srcNameMap[latestSrpm.name]: if latestSrpm.epoch == srpm.epoch and \ latestSrpm.version == srpm.version: for pkg in self._pkgSource.srcPkgMap[srpm]: + # Add special handling for packages that have versions in + # the names. + # FIXME: This is specific to non rpm based platforms right + # now. It needs to be tested on rpm platforms to + # make nothing breaks. + if (self._cfg.repositoryFormat != 'rpm' + and pkg.name not in pkgNames + and pkg.version in pkg.name): + continue if (pkg.name, pkg.arch) not in pkgs: pkgs[(pkg.name, pkg.arch)] = pkg From johnsonm at rpath.com Wed Aug 19 17:41:24 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:41:24 +0000 Subject: mirrorball: fix errors from refactor Message-ID: <200908192141.n7JLfO3S020069@scc.eng.rpath.com> changeset: 6bcc7e41f824 user: Elliot Peele <https://issues.rpath.com/> date: Fri, 14 Nov 2008 18:34:32 -0500 fix errors from refactor committer: Elliot Peele <https://issues.rpath.com/> diff --git a/updatebot/pkgsource/debsource.py b/updatebot/pkgsource/debsource.py --- a/updatebot/pkgsource/debsource.py +++ b/updatebot/pkgsource/debsource.py @@ -21,7 +21,8 @@ class DebSource(object): def __init__(self, cfg): - self._excludeArch = cfg.excludeArch + self._cfg = cfg + self._excludeArch = self._cfg.excludeArch self._binPkgs = set() self._srcPkgs = set() @@ -51,7 +52,7 @@ client = repomd.Client(self._cfg.repositoryUrl) for repo in self._cfg.repositoryPaths: log.info('loading repository data %s' % repo) - self._pkgSource.loadFromClient(client, repo) + self.loadFromClient(client, repo) self._clients[repo] = client self.finalize() diff --git a/updatebot/pkgsource/rpmsource.py b/updatebot/pkgsource/rpmsource.py --- a/updatebot/pkgsource/rpmsource.py +++ b/updatebot/pkgsource/rpmsource.py @@ -30,7 +30,8 @@ """ def __init__(self, cfg): - self._excludeArch = cfg.excludeArch + self._cfg = cfg + self._excludeArch = self._cfg.excludeArch # {srcTup: srpm} self._srcMap = dict() @@ -77,7 +78,7 @@ for repo in self._cfg.repositoryPaths: log.info('loading repository data %s' % repo) client = repomd.Client(self._cfg.repositoryUrl + '/' + repo) - self._pkgSource.loadFromClient(client, repo) + self.loadFromClient(client, repo) self._clients[repo] = client self.finalize() From johnsonm at rpath.com Wed Aug 19 17:41:20 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:41:20 +0000 Subject: mirrorball: simplify pkgsource loading Message-ID: <200908192141.n7JLfKd3019898@scc.eng.rpath.com> changeset: 05cb37ef4c6d user: Elliot Peele <https://issues.rpath.com/> date: Fri, 14 Nov 2008 16:32:14 -0500 simplify pkgsource loading committer: Elliot Peele <https://issues.rpath.com/> diff --git a/scripts/pkgsource.py b/scripts/pkgsource.py --- a/scripts/pkgsource.py +++ b/scripts/pkgsource.py @@ -1,14 +1,17 @@ -#!/usr/bin/python +#!/usr/bin/python2.6 import os import sys + +sys.path.insert(0, os.environ['HOME'] + '/hg/26/rpath-xmllib') +sys.path.insert(0, os.environ['HOME'] + '/hg/26/conary') +sys.path.insert(0, os.environ['HOME'] + '/hg/26/mirrorball') +sys.path.insert(0, os.environ['HOME'] + '/hg/26/rmake') +sys.path.insert(0, os.environ['HOME'] + '/hg/26/epdb') + from conary.lib import util sys.excepthook = util.genExcepthook() -sys.path.insert(0, os.environ['HOME'] + '/hg/rpath-xmllib') -sys.path.insert(0, os.environ['HOME'] + '/hg/conary') -sys.path.insert(0, os.environ['HOME'] + '/hg/mirrorball') - import logging import updatebot.log @@ -21,21 +24,9 @@ from updatebot import pkgsource cfg = config.UpdateBotConfig() -cfg.read(os.environ['HOME'] + '/hg/mirrorball/config/centos/updatebotrc') +cfg.read(os.environ['HOME'] + '/hg/mirrorball/config/sles/updatebotrc') pkgSource = pkgsource.PackageSource(cfg) - -if cfg.repositoryFormat == 'apt': - client = aptmd.Client(cfg.repositoryUrl) - for path in cfg.repositoryPaths: - log.info('loading %s' % path) - pkgSource.loadFromClient(client, path) -else: - for path in cfg.repositoryPaths: - client = repomd.Client(cfg.repositoryUrl + '/' + path) - log.info('loading %s' % path) - pkgSource.loadFromClient(client, path) - -pkgSource.finalize() +pkgSource.load() import epdb; epdb.st() From johnsonm at rpath.com Wed Aug 19 17:42:12 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:42:12 +0000 Subject: mirrorball: handle 404 from pmap Message-ID: <200908192142.n7JLgCPM021945@scc.eng.rpath.com> changeset: 253eb91a1f83 user: Elliot Peele <https://issues.rpath.com/> date: Thu, 05 Feb 2009 16:38:34 -0500 handle 404 from pmap committer: Elliot Peele <https://issues.rpath.com/> diff --git a/updatebot/advisories/centos.py b/updatebot/advisories/centos.py --- a/updatebot/advisories/centos.py +++ b/updatebot/advisories/centos.py @@ -48,8 +48,11 @@ # Fetch all of the archives and process them. for url in self._getArchiveUrls(): log.info('parsing mail archive: %s' % url) - for msg in pmap.parse(url, backend=self._cfg.platformName): - self._loadOne(msg, pkgCache) + try: + for msg in pmap.parse(url, backend=self._cfg.platformName): + self._loadOne(msg, pkgCache) + except pmap.ArchiveNotFound, e: + log.warn('unable to retrieve archive for %s' % url) def _loadOne(self, msg, pkgCache): """ diff --git a/updatebot/advisories/ubuntu.py b/updatebot/advisories/ubuntu.py --- a/updatebot/advisories/ubuntu.py +++ b/updatebot/advisories/ubuntu.py @@ -53,8 +53,11 @@ # Fetch all of the archives and process them. for url in self._getArchiveUrls(): log.info('parsing mail archive: %s' % url) - for msg in pmap.parse(url, backend=self._cfg.platformName): - self._loadOne(msg, pkgCache) + try: + for msg in pmap.parse(url, backend=self._cfg.platformName): + self._loadOne(msg, pkgCache) + except pmap.ArchiveNotFoundError, e: + log.warn('unable to retrieve archive for %s' % url) def _loadOne(self, msg, pkgCache): """ From johnsonm at rpath.com Wed Aug 19 17:40:24 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:40:24 +0000 Subject: mirrorball: fixup log messages Message-ID: <200908192140.n7JLeOIP017614@scc.eng.rpath.com> changeset: aa4e957f1f14 user: Elliot Peele <http://issues.rpath.com/> date: Tue, 19 Aug 2008 11:43:40 -0400 fixup log messages use public methods where possible don't raise exceptions committer: Elliot Peele <http://issues.rpath.com/> diff --git a/updatebot/update.py b/updatebot/update.py --- a/updatebot/update.py +++ b/updatebot/update.py @@ -219,7 +219,7 @@ toUpdate = set() for pkg in pkgNames: if pkg not in self._pkgSource.binNameMap: - log.warn('no package named %s found in rpm source' % pkg) + log.warn('no package named %s found in package source' % pkg) continue srcPkg = self._getPackagesToImport(pkg) @@ -231,16 +231,13 @@ fail = set() toBuild = set() for pkg in toUpdate: - log.info('importing %s' % pkg) + log.info('attempting to import %s' % pkg) try: - # FIXME: Remove this once opensuse has groups. # Only import packages that haven't been imported before - version = self._conaryhelper._getVersionsByName('%s:source' % pkg.name) + version = self._conaryhelper.getLatestSourceVersion(pkg.name) if not version: version = self.update((pkg.name, None, None), pkg) - else: - version = version[0] if not self._conaryhelper._getVersionsByName(pkg.name) or buildAll: toBuild.add((pkg.name, version, None)) @@ -249,7 +246,6 @@ except Exception, e: log.error('failed to import %s: %s' % (pkg, e)) fail.add((pkg, e)) - raise return toBuild, fail From johnsonm at rpath.com Wed Aug 19 17:41:45 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:41:45 +0000 Subject: mirrorball: pylint fixes Message-ID: <200908192141.n7JLfjLK020855@scc.eng.rpath.com> changeset: 2c4cfe550767 user: Elliot Peele <https://issues.rpath.com/> date: Wed, 19 Nov 2008 23:40:02 -0500 pylint fixes committer: Elliot Peele <https://issues.rpath.com/> diff --git a/repomd/filelistsxml.py b/repomd/filelistsxml.py --- a/repomd/filelistsxml.py +++ b/repomd/filelistsxml.py @@ -8,8 +8,6 @@ __all__ = ('FilelistXml', ) -from rpath_common.xmllib import api1 as xmllib - from repomd.xmlcommon import XmlFileParser, SlotNode from repomd.packagexml import PackageXmlMixIn @@ -17,18 +15,24 @@ """ Python representation of filelists.xml from the repository metadata. """ + __slots__ = () def addChild(self, child): """ Parse children of filelists element. """ + if child.getName() == 'package': child.name = child.getAttribute('name') child.arch = child.getAttribute('arch') SlotNode.addChild(self, child) def getPackages(self): + """ + Get package objects with file information. + """ + return self.getChildren('package') @@ -41,5 +45,6 @@ """ Setup databinder to parse xml. """ + PackageXmlMixIn._registerTypes(self) self._databinder.registerType(_FileLists, name='filelists') diff --git a/repomd/packagexml.py b/repomd/packagexml.py --- a/repomd/packagexml.py +++ b/repomd/packagexml.py @@ -60,6 +60,11 @@ # R0915 - Too many statements # pylint: disable-msg=R0915 + + # E0203 - Access to member 'files' before its definition line 84 + # files is set to None by the superclasses __init__ + # pylint: disable-msg=E0203 + n = child.getName() if n == 'name': self.name = child.finalize() From johnsonm at rpath.com Wed Aug 19 17:42:55 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:42:55 +0000 Subject: mirrorball: add script for building recipe superclasses Message-ID: <200908192142.n7JLgt5L023664@scc.eng.rpath.com> changeset: 03c4e1525c66 user: Elliot Peele <https://issues.rpath.com/> date: Wed, 06 May 2009 15:33:15 -0400 add script for building recipe superclasses committer: Elliot Peele <https://issues.rpath.com/> diff --git a/scripts/buildautoloadrecipes b/scripts/buildautoloadrecipes new file mode 100755 --- /dev/null +++ b/scripts/buildautoloadrecipes @@ -0,0 +1,56 @@ +#!/bin/bash +# +# Copyright (c) 2009 rPath, Inc. +# +# This program is distributed under the terms of the Common Public License, +# version 1.0. A copy of this license should have been distributed with this +# source file in a file called LICENSE. If it is not present, the license +# is always available at http://www.rpath.com/permanent/licenses/CPL-1.0. +# +# This program is distributed in the hope that it will be useful, but +# without any warranty; without even the implied warranty of merchantability +# or fitness for a particular purpose. See the Common Public License for +# full details. +# + +platform=$1 +shift + +if [ "$2" != "" ] ; then + context=$2 +else + context="x86" +fi + +mirrorballpath="$HOME/hg/mirrorball" +platformConfig="$mirrorballpath/config/$platform/conaryrc" + +if [ ! -f $platformConfig ] ; then + echo "No conaryrc found for platform $platform in checkout $mirrorballpath" + exit 1 +fi + +autoLoad="" +autoLoadPackages=" + baserequires + package + buildpackage + cpackage + autopackage + derived + fileset + group + groupinfo + redirect + userinfo +" + +for pkg in $autoLoadPackages ; do + cvc cook \ + --config-file $platformConfig \ + --context $context \ + --no-deps \ + $autoLoad \ + $pkg + autoLoad="$autoLoad --config 'autoLoadRecipes $pkg'" +done From johnsonm at rpath.com Wed Aug 19 17:42:28 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:42:28 +0000 Subject: mirrorball: add better buildreq generation Message-ID: <200908192142.n7JLgSqB022575@scc.eng.rpath.com> changeset: 4cb0f2c3bd77 user: Elliot Peele <https://issues.rpath.com/> date: Fri, 20 Feb 2009 15:16:42 -0500 add better buildreq generation committer: Elliot Peele <https://issues.rpath.com/> diff --git a/updatebot/update.py b/updatebot/update.py --- a/updatebot/update.py +++ b/updatebot/update.py @@ -372,7 +372,7 @@ manifest = [] if self._cfg.repositoryFormat == 'yum' and self._cfg.buildFromSource: - manifest.append(pkg.location) + manifest.append(srcPkg.location) else: manifestPkgs = list(self._pkgSource.srcPkgMap[srcPkg]) for pkg in self._getLatestOfAvailableArches(manifestPkgs): @@ -411,7 +411,21 @@ reqs = [] for reqType in srcPkg.format: if reqType.getName() == 'rpm:requires': - reqs.extend([ x.name for x in reqType.iterChildren() ]) + names = [ x.name.split('(')[0] for x in reqType.iterChildren() + if not (hasattr(x, 'isspace') and x.isspace()) ] + + for name in names: + if name in self._pkgSource.binNameMap: + latest = self._getLatestBinary(name) + if latest not in self._pkgSource.binPkgMap: + log.warn('%s not found in binPkgMap' % latest) + continue + src = self._pkgSource.binPkgMap[latest] + srcname = src.name + else: + log.warn('found virtual requires %s in pkg %s' % (name, srcPkg.name)) + srcname = 'virtual' + reqs.append((name, srcname)) reqs = list(set(reqs)) return reqs From johnsonm at rpath.com Wed Aug 19 17:40:00 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:40:00 +0000 Subject: mirrorball: and a broken test Message-ID: <200908192140.n7JLe0vo016623@scc.eng.rpath.com> changeset: 099921727fd3 user: Elliot Peele <http://issues.rpath.com/> date: Fri, 25 Jul 2008 16:03:46 -0400 and a broken test committer: Elliot Peele <http://issues.rpath.com/> diff --git a/test/unit_test/updatebottest/advisetest.py b/test/unit_test/updatebottest/advisetest.py --- a/test/unit_test/updatebottest/advisetest.py +++ b/test/unit_test/updatebottest/advisetest.py @@ -22,10 +22,11 @@ class AdviseTest(slehelp.Helper): def testCheck(self): - mockRpmSource = mock.MockObject(stableReturnValues=True) - mockPatchSource = mock.MockObject(stableReturnValues=True) - mockHasException = mock.MockObject(stableReturnValues=True) - mockIsSecurity = mock.MockObject(stableReturnValues=True) + mockRpmSource = mock.MockObject() + mockPatchSource = mock.MockObject() + mockHasException = mock.MockObject() + mockIsSecurity = mock.MockObject() + mockMkAdvisory = mock.MockObject() mockSrcPkg1 = mock.MockObject() mockSrcPkg2 = mock.MockObject() @@ -54,14 +55,13 @@ mockPatchSource) self.mock(advisor, '_hasException', mockHasException) self.mock(advisor, '_isSecurity', mockIsSecurity) + self.mock(advisor, '_mkAdvisory', mockMkAdvisory) mockHasException._mock.setReturn(True, mockBinPkg2) mockHasException._mock.setReturn(False, mockBinPkg3) mockIsSecurity._mock.setReturn(False, mockBinPkg3) - expected = {(None, mockSrcPkg1): set([mockPatch1, ]), - (None, mockSrcPkg2): set(), - (None, mockSrcPkg3): set()} + expected = {(None, mockSrcPkg1): set([mockPatch1, ]),} # test normal case result = advisor.check(input) From johnsonm at rpath.com Wed Aug 19 17:40:11 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:40:11 +0000 Subject: mirrorball: cleanup file parsing Message-ID: <200908192140.n7JLeBGh017052@scc.eng.rpath.com> changeset: e175db256e3a user: Elliot Peele <http://issues.rpath.com/> date: Thu, 14 Aug 2008 16:52:20 -0400 cleanup file parsing committer: Elliot Peele <http://issues.rpath.com/> diff --git a/aptmd/sources.py b/aptmd/sources.py --- a/aptmd/sources.py +++ b/aptmd/sources.py @@ -21,14 +21,11 @@ class SourcesParser(BaseParser): - def __init__(self): BaseParser.__init__(self) self._containerClass = _SourcePackage - self._inFiles = False self._states.update({ - 'architecture' : self._architecture, 'binary' : self._binary, 'build-depends' : self._keyval, 'standards-version' : self._keyval, @@ -37,18 +34,9 @@ 'files' : self._files, 'homepage' : self._keyval, 'uploaders' : self._keyval, + '' : self._file, }) - def _parseLine(self, line): - BaseParser._parseLine(self, line) - - state = self._getState(self._line[0]) - if state != 'files' and state in self._states: - self._inFiles = False - - if self._inFiles: - self._file() - def _architecture(self): self._curObj.arch = 'src' @@ -60,11 +48,12 @@ self._curObj.directory = self._getLine() def _files(self): - self._inFiles = True + self._curObj.files = [] def _file(self): - if self._curObj.files is None: - self._curObj.files = [] + if len(self._line) != 4: + return - path = os.path.join(self._curObj.directory, self._line[2]) + fileName = self._line[3].strip() + path = os.path.join(self._curObj.directory, fileName) self._curObj.files.append(path) From johnsonm at rpath.com Wed Aug 19 17:41:17 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:41:17 +0000 Subject: mirrorball: add support for pulling in data from a mbox message Message-ID: <200908192141.n7JLfH0s019761@scc.eng.rpath.com> changeset: bde13bb92c29 user: Elliot Peele <https://issues.rpath.com/> date: Fri, 07 Nov 2008 16:18:33 -0500 add support for pulling in data from a mbox message committer: Elliot Peele <https://issues.rpath.com/> diff --git a/pmap/common.py b/pmap/common.py --- a/pmap/common.py +++ b/pmap/common.py @@ -12,20 +12,22 @@ # full details. # +import email +import shutil +import mailbox import tempfile -from vendor import mailbox - from aptmd.container import Container from aptmd.parser import ContainerizedParser as Parser class BaseContainer(Container): - __slots__ = ('fromAddr', 'fromName', 'timestamp') + __slots__ = ('fromAddr', 'fromName', 'timestamp', 'subject', 'msg') class BaseParser(Parser): def __init__(self): Parser.__init__(self) + self._curMsg = None self._containerClass = BaseContainer self._states.update({ }) @@ -40,7 +42,7 @@ def _getMbox(self, fileObj): tmpfn = tempfile.mktemp() tmpfh = open(tmpfn, 'w') - tmpfh.write(fileObj.read()) + shutil.copyfileobj(fileObj, tmpfh) tmpfh.close() mbox = mailbox.mbox(tmpfn) @@ -48,4 +50,17 @@ def _parseMsg(self, msg): self._newContainer() - + assert msg.get_content_type() == 'text/plain' + + self._curMsg = msg + self._curObj.msg = msg + + # Extract info from message + fromLine = msg['From'] + self._curObj.fromAddr = fromLine[:fromLine.find('(')].replace(' at ', '@') + self._curObj.fromName = fromLine[fromLine.find('('):].strip('()') + self._curObj.timestamp = ' '.join(msg.get_from().split()[4:]) + self._curObj.subject = msg['Subject'] + + for line in msg.get_payload().split('\n'): + self._parseLine(line) From johnsonm at rpath.com Wed Aug 19 17:40:13 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:40:13 +0000 Subject: mirrorball: rename some more methods Message-ID: <200908192140.n7JLeDb9017154@scc.eng.rpath.com> changeset: dc84f6fc607c user: Elliot Peele <http://issues.rpath.com/> date: Thu, 14 Aug 2008 18:02:10 -0400 rename some more methods committer: Elliot Peele <http://issues.rpath.com/> diff --git a/updatebot/bot.py b/updatebot/bot.py --- a/updatebot/bot.py +++ b/updatebot/bot.py @@ -48,7 +48,7 @@ self._patchSource) self._builder = build.Builder(self._cfg) - def _populateRpmSource(self): + def _populatePkgSource(self): """ Populate the rpm source data structures. """ @@ -104,7 +104,7 @@ log.info('starting import') # Populate rpm source object from yum metadata. - self._populateRpmSource() + self._populatePkgSource() #import epdb; epdb.st() @@ -162,7 +162,7 @@ log.info('starting update') # Populate rpm source object from yum metadata. - self._populateRpmSource() + self._populatePkgSource() # Get troves to update and send advisories. toAdvise, toUpdate = self._updater.getUpdates() diff --git a/updatebot/update.py b/updatebot/update.py --- a/updatebot/update.py +++ b/updatebot/update.py @@ -305,12 +305,12 @@ @return version of the updated source trove """ - manifest = self._getManifestFromRpmSource(srcPkg) + manifest = self._getManifestFromPkgSource(srcPkg) newVersion = self._conaryhelper.setManifest(nvf[0], manifest, commitMessage=self._cfg.commitMessage) return newVersion - def _getManifestFromRpmSource(self, srcPkg): + def _getManifestFromPkgSource(self, srcPkg): """ Get the contents of the a manifest file from the pkgSource object. @param srcPkg: source rpm package object From johnsonm at rpath.com Wed Aug 19 17:39:29 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:39:29 +0000 Subject: mirrorball: If any advisories are found for a given srpm, assume that they apply to all Message-ID: <200908192139.n7JLdTcL015413@scc.eng.rpath.com> changeset: 5d4f00d9c7a5 user: Elliot Peele <http://issues.rpath.com/> date: Wed, 18 Jun 2008 15:14:56 -0400 If any advisories are found for a given srpm, assume that they apply to all binaries committer: Elliot Peele <http://issues.rpath.com/> diff --git a/updatebot/advise.py b/updatebot/advise.py --- a/updatebot/advise.py +++ b/updatebot/advise.py @@ -45,20 +45,24 @@ for nvf, srcPkg in trvLst: patches = set() for binPkg in self._rpmSource.srcPkgMap[srcPkg]: - # Don't check srpms. - if binPkg is srcPkg: - continue - if binPkg in self._patchSource.pkgMap: patches.update(self._patchSource.pkgMap[binPkg]) + + for binPkg in self._rpmSource.srcPkgMap[srcPkg]: + # Don't check srpms. + if binPkg is srcPkg or binPkg in self._patchSource.pkgMap: + continue elif self._hasException(binPkg): log.info('found advisory exception for %s' % binPkg) log.debug(binPkg.location) elif not self._isSecurity(binPkg): log.info('package not in updates repository %s' % binPkg) log.debug(binPkg.location) + elif len(patches) > 0: + log.info('found package not mentioned in advisory %s' % binPkg) + log.debug(binPkg.location) else: - log.error('could not find patch for %s' % binPkg) + log.error('could not find advisory for %s' % binPkg) raise NoAdvisoryFoundError(why=binPkg) if (nvf, srcPkg) not in self._cache: From johnsonm at rpath.com Wed Aug 19 17:40:06 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:40:06 +0000 Subject: mirrorball: when doing initial import split arch builds Message-ID: <200908192140.n7JLe7qO016881@scc.eng.rpath.com> changeset: 5f4d9e397ca1 user: Elliot Peele <http://bugs.rpath.com/> date: Mon, 11 Aug 2008 18:17:53 -0400 when doing initial import split arch builds committer: Elliot Peele <http://bugs.rpath.com/> diff --git a/updatebot/bot.py b/updatebot/bot.py --- a/updatebot/bot.py +++ b/updatebot/bot.py @@ -106,7 +106,7 @@ # Populate rpm source object from yum metadata. self._populateRpmSource() -# import epdb; epdb.st() + #import epdb; epdb.st() # Import sources into repository. toBuild, fail = self._updater.create(self._cfg.package) @@ -114,8 +114,31 @@ # import epdb; epdb.st() # Build all newly imported packages. - trvMap, failed = self._builder.buildmany(toBuild) + #trvMap, failed = self._builder.buildmany(toBuild) #trvMap = self._builder.build(toBuild) + trvs = self._builder._formatInput(toBuild) + jobs = {} + for trv in trvs: + key = 'other' + if len(trv) == 4: + key = trv[3] + + if key not in jobs: + jobs[key] = [] + + jobs[key].append(trv) + + ids = {} + for job in jobs: + if job == 'other': + continue + + ids[job] = self._builder._startJob(jobs[job]) + + for job in ids: + self._builder._monitorJob(ids[job]) + + log.info('completed jobs %s' % (' '.join(ids.values()), )) import epdb; epdb.st() @@ -181,6 +204,8 @@ newTroves = self._updater.publish(toPublish, expected, self._cfg.targetLabel) + import epdb; epdb.st() + # Send advisories. self._advisor.send(toAdvise, newTroves) From johnsonm at rpath.com Wed Aug 19 17:41:35 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:41:35 +0000 Subject: mirrorball: call newContainer in the correct place Message-ID: <200908192141.n7JLfZKc020463@scc.eng.rpath.com> changeset: 730834d85b31 user: Elliot Peele <https://issues.rpath.com/> date: Tue, 18 Nov 2008 12:28:56 -0500 call newContainer in the correct place move __repr__ to a more appropriate class committer: Elliot Peele <https://issues.rpath.com/> diff --git a/pmap/centos.py b/pmap/centos.py --- a/pmap/centos.py +++ b/pmap/centos.py @@ -22,13 +22,10 @@ class CentOSAdvisory(BaseContainer): __slots__ = ('discard', 'archs', 'header', 'pkgs', 'upstreamAdvisoryUrl', ) - def __repr__(self): - return self.subject - def finalize(self): BaseContainer.finalize(self) - self.packages = set() + assert self.subject is not None self.summary = self.subject self.description = self.upstreamAdvisoryUrl diff --git a/pmap/common.py b/pmap/common.py --- a/pmap/common.py +++ b/pmap/common.py @@ -24,6 +24,10 @@ __slots__ = ('fromAddr', 'fromName', 'timestamp', 'subject', 'msg', 'description', 'summary', 'packages') + def __repr__(self): + return self.subject + + class BaseParser(Parser): def __init__(self): Parser.__init__(self) @@ -38,6 +42,11 @@ mbox = self._getMbox(fileObj) for msg in mbox: self._parseMsg(msg) + + # Make sure last object gets added to self._objects while allowing + # subclasses to have special handling in newContainer. + self._newContainer() + return self._objects def _getMbox(self, fileObj): @@ -65,7 +74,3 @@ for line in msg.get_payload().split('\n'): self._parseLine(line) - - # Make sure last object gets added to self._objects while allowing - # subclasses to have special handling in newContainer. - self._newContainer() From johnsonm at rpath.com Wed Aug 19 17:39:44 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:39:44 +0000 Subject: mirrorball: move update after advisory check Message-ID: <200908192139.n7JLdiXn015994@scc.eng.rpath.com> changeset: cb4df5a0b265 user: Elliot Peele <http://issues.rpath.com/> date: Sat, 28 Jun 2008 00:26:49 -0400 move update after advisory check committer: Elliot Peele <http://issues.rpath.com/> diff --git a/updatebot/bot.py b/updatebot/bot.py --- a/updatebot/bot.py +++ b/updatebot/bot.py @@ -96,12 +96,6 @@ # Get troves to update and send advisories. toAdvise, toUpdate = self._updater.getUpdates() - # Update source - for nvf, srcPkg in toUpdate: - toAdvise.remove((nvf, srcPkg)) - newVersion = self._updater.update(nvf, srcPkg) - toAdvise.append(((nvf[0], newVersion, nvf[2]), srcPkg)) - # Don't populate the patch source until we know that there are # updates. self._populatePatchSource() @@ -109,6 +103,23 @@ # Check to see if advisories exist for all required packages. self._advisor.check(toAdvise) + + # FIXME: this is a hack to work around some things. + for i in range(len(toAdvise)): + pkg = toAdvise[i] + version = self._updater._conaryhelper._getVersionsByName(pkg[0][0])[0] + toAdvise[i] = ((pkg[0][0], version, pkg[0][2]), pkg[1]) + if pkg in toUpdate: + toUpdate.remove(pkg) + toUpdate.append(((pkg[0][0], version, pkg[0][2]), pkg[1])) + import epdb; epdb.st() + + # Update source + for nvf, srcPkg in toUpdate: + toAdvise.remove((nvf, srcPkg)) + newVersion = self._updater.update(nvf, srcPkg) + toAdvise.append(((nvf[0], newVersion, nvf[2]), srcPkg)) + # Make sure to build everything in the toAdvise list, there may be # sources that have been updated, but not built. buildTroves = set([ x[0] for x in toAdvise ]) From johnsonm at rpath.com Wed Aug 19 17:43:00 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:43:00 +0000 Subject: mirrorball: improved source package synthesizing Message-ID: <200908192143.n7JLh0C8023885@scc.eng.rpath.com> changeset: 2aacc00bf0ea user: Elliot Peele <https://issues.rpath.com/> date: Sat, 30 May 2009 13:30:45 -0400 improved source package synthesizing committer: Elliot Peele <https://issues.rpath.com/> diff --git a/updatebot/config.py b/updatebot/config.py --- a/updatebot/config.py +++ b/updatebot/config.py @@ -187,8 +187,8 @@ writePackageMetadata = (CfgBool, False) # If sources are not available pkgSource will attempt to build artificial - # source information if this is set to False. - sourcesAvailable = (CfgBool, True) + # source information if this is set to True. + synthesizeSources = (CfgBool, False) # Number of troves at which to switch to a splitarch build. This is mostly # a magic number, but at least it is configurable? diff --git a/updatebot/pkgsource/rpmsource.py b/updatebot/pkgsource/rpmsource.py --- a/updatebot/pkgsource/rpmsource.py +++ b/updatebot/pkgsource/rpmsource.py @@ -154,7 +154,7 @@ # Build source structures from binaries if no sources are available from # the repository. - if not self._cfg.sourcesAvailable: + if self._cfg.synthesizeSources: self._createSrcMap() # Now that we have processed all of the rpms, build some more data @@ -249,7 +249,7 @@ """ # Return if sources should be available in repos. - if self._cfg.sourcesAvailable: + if not self._cfg.synthesizeSources: return PkgClass = repomd.packagexml._Package @@ -271,4 +271,6 @@ srcPkg.location = pkg.sourcerpm # add source to structures - self._procSrc(srcPkg) + if (name, epoch, version, release, arch) not in self._srcMap: + log.warn('synthesizing source package %s' % srcPkg) + self._procSrc(srcPkg) From johnsonm at rpath.com Wed Aug 19 17:40:46 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:40:46 +0000 Subject: mirrorball: promote to production Message-ID: <200908192140.n7JLekE3018532@scc.eng.rpath.com> changeset: 0d6c96958af5 user: Elliot Peele <https://issues.rpath.com/> date: Thu, 11 Sep 2008 23:27:07 -0400 promote to production committer: Elliot Peele <https://issues.rpath.com/> diff --git a/scripts/promote-ubuntu.sh b/scripts/promote-ubuntu.sh --- a/scripts/promote-ubuntu.sh +++ b/scripts/promote-ubuntu.sh @@ -25,11 +25,11 @@ group-os=ubuntu.rb.rpath.com at rpath:ubuntu-hardy-devel \ ubuntu.rb.rpath.com at rpath:ubuntu-hardy-devel--ubuntu.rpath.org at rpath:ubuntu-hardy-devel \ -#time cvc promote $cfg \ -# group-appliance:source=ubuntu.rpath.org at rpath:ubuntu-hardy-devel \ -# platform-defintion:source=ubuntu.rpath.org at rpath:ubuntu-hardy-devel \ -# group-os=ubuntu.rpath.org at rpath:ubuntu-hardy-devel \ -# /ubuntu.rpath.org at rpath:ubuntu-hardy-devel--/ubuntu.rpath.org at rpath:ubuntu-hardy \ -# /conary.rpath.com at rpl:devel//conary.rpath.com at rpl:2--/ubuntu.rpath.org at rpath:ubuntu-hardy \ -# /conary.rpath.com at rpl:devel//conary.rpath.com at rpl:2//ubuntu.rpath.org at rpath:ubuntu-hardy-devel--/ubuntu.rpath.org at rpath:ubuntu-hardy -# /conary.rpath.com at rpl:devel//ubuntu.rpath.org at rpath:ubuntu-hardy-devel--/ubuntu.rpath.org at rpath:ubuntu-hardy +time cvc promote $cfg \ + group-appliance:source=ubuntu.rpath.org at rpath:ubuntu-hardy-devel \ + platform-defintion:source=ubuntu.rpath.org at rpath:ubuntu-hardy-devel \ + group-os=ubuntu.rpath.org at rpath:ubuntu-hardy-devel \ + /ubuntu.rpath.org at rpath:ubuntu-hardy-devel--/ubuntu.rpath.org at rpath:ubuntu-hardy \ + /conary.rpath.com at rpl:devel//conary.rpath.com at rpl:2--/ubuntu.rpath.org at rpath:ubuntu-hardy \ + /conary.rpath.com at rpl:devel//conary.rpath.com at rpl:2//ubuntu.rpath.org at rpath:ubuntu-hardy-devel--/ubuntu.rpath.org at rpath:ubuntu-hardy \ + /conary.rpath.com at rpl:devel//ubuntu.rpath.org at rpath:ubuntu-hardy-devel--/ubuntu.rpath.org at rpath:ubuntu-hardy From johnsonm at rpath.com Wed Aug 19 17:42:58 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:42:58 +0000 Subject: mirrorball: switch to splitarch build for larger builds Message-ID: <200908192142.n7JLgwOY023817@scc.eng.rpath.com> changeset: d96eb1110c0c user: Elliot Peele <https://issues.rpath.com/> date: Tue, 12 May 2009 14:12:53 -0400 switch to splitarch build for larger builds committer: Elliot Peele <https://issues.rpath.com/> diff --git a/updatebot/bot.py b/updatebot/bot.py --- a/updatebot/bot.py +++ b/updatebot/bot.py @@ -179,7 +179,14 @@ # Make sure to build everything in the toAdvise list, there may be # sources that have been updated, but not built. buildTroves = set([ x[0] for x in toAdvise ]) - trvMap = self._builder.build(buildTroves) + + # Switch to splitarch if a build is larger than maxBuildSize. This + # number is kinda arbitrary. Builds tend to break when architectures + # are combind if the build is significantly large + if len(buildTroves) < self._cfg.maxBuildSize: + trvMap = self._builder.build(buildTroves) + else: + trvMap = self._builder.buildsplitarch(buildTroves) # Build group. grpTrvs = set() diff --git a/updatebot/config.py b/updatebot/config.py --- a/updatebot/config.py +++ b/updatebot/config.py @@ -21,7 +21,7 @@ from conary.lib import cfg from conary import versions from conary.conarycfg import CfgFlavor, CfgLabel -from conary.lib.cfgtypes import CfgString, CfgList, CfgRegExp, CfgBool, CfgDict +from conary.lib.cfgtypes import CfgString, CfgList, CfgRegExp, CfgBool, CfgDict, CfgInt from conary.lib.cfgtypes import ParseError from rmake.build.buildcfg import CfgTroveSpec @@ -190,6 +190,10 @@ # source information if this is set to False. sourcesAvailable = (CfgBool, True) + # Number of troves at which to switch to a splitarch build. This is mostly + # a magic number, but at least it is configurable? + maxBuildSize = (CfgInt, 10) + class UpdateBotConfig(cfg.SectionedConfigFile): """ From johnsonm at rpath.com Wed Aug 19 17:41:21 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:41:21 +0000 Subject: mirrorball: switch to new advisory model Message-ID: <200908192141.n7JLfLIJ019950@scc.eng.rpath.com> changeset: dd4c6a38c113 user: Elliot Peele <https://issues.rpath.com/> date: Fri, 14 Nov 2008 17:33:33 -0500 switch to new advisory model committer: Elliot Peele <https://issues.rpath.com/> diff --git a/updatebot/bot.py b/updatebot/bot.py --- a/updatebot/bot.py +++ b/updatebot/bot.py @@ -24,9 +24,8 @@ from updatebot import build from updatebot import update -from updatebot import advise from updatebot import pkgsource -from updatebot import patchsource +from updatebot import advisories log = logging.getLogger('updatebot.bot') @@ -42,26 +41,11 @@ self._clients = {} self._pkgSource = pkgsource.PackageSource(self._cfg) - self._patchSource = patchsource.PatchSource(self._cfg) self._updater = update.Updater(self._cfg, self._pkgSource) - self._advisor = advise.Advisor(self._cfg, self._pkgSource, - self._patchSource) + self._advisor = advisories.Advisor(self._cfg, self._pkgSource, + backend=self._cfg.platformName) self._builder = build.Builder(self._cfg) - def _populatePatchSource(self): - """ - Populate the patch source data structures. - """ - - if self._patchSourcePopulated: - return - - for path, client in self._clients.iteritems(): - log.info('loading patch information %s' % path) - self._patchSource.loadFromClient(client, path) - - self._patchSourcePopulated = True - @staticmethod def _flattenSetDict(setDict): """ @@ -155,7 +139,7 @@ # Populate patch source not that we know that there are updates # available. - self._populatePatchSource() + self._advisor.load() # Check to see if advisories exist for all required packages. self._advisor.check(toAdvise) From johnsonm at rpath.com Wed Aug 19 17:41:30 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:41:30 +0000 Subject: mirrorball: add support for building kernels Message-ID: <200908192141.n7JLfUWr020291@scc.eng.rpath.com> changeset: 5db0d556cd3b user: Elliot Peele <https://issues.rpath.com/> date: Sun, 09 Nov 2008 22:40:13 -0500 add support for building kernels committer: Elliot Peele <https://issues.rpath.com/> diff --git a/updatebot/build.py b/updatebot/build.py --- a/updatebot/build.py +++ b/updatebot/build.py @@ -201,6 +201,9 @@ # correct flavors. if name.startswith('group-'): troves.append((name, version, flavor)) + elif name == 'kernel' and self._cfg.kernelFlavors: + for context, flavor in self._cfg.kernelFlavors: + troves.append((name, version, flavor, context)) else: # Build all packages as x86 and x86_64. for context in self._cfg.archContexts: diff --git a/updatebot/config.py b/updatebot/config.py --- a/updatebot/config.py +++ b/updatebot/config.py @@ -40,6 +40,22 @@ except versions.ParseError, e: raise ParseError, e +class CfgContextFlavor(CfgFlavor): + """ + Class for representing both a flavor context name and a build flavor. + """ + + def parseString(self, val): + """ + Parse config string. + """ + + try: + context, flavorStr = val.split() + flavor = CfgFlavor.parseString(self, flavorStr) + return context, flavor + except versions.ParseError, e: + raise ParseError, e class UpdateBotConfig(cfg.SectionedConfigFile): """ @@ -107,6 +123,9 @@ # flavors to build the source group. groupFlavors = (CfgList(CfgFlavor), []) + # flavors to build kernels. + kernelFlavors = (CfgList(CfgContextFlavor), []) + # email information for sending advisories emailFromName = CfgString emailFrom = CfgString From johnsonm at rpath.com Wed Aug 19 17:40:03 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:40:03 +0000 Subject: mirrorball: adjust script to handle x86_64 and files that do not come from a RPM Message-ID: <200908192140.n7JLe3GX016761@scc.eng.rpath.com> changeset: c4744114f5fb user: Matt Wilson <https://issues.rpath.com/> date: Sat, 21 Jun 2008 16:50:51 -0400 adjust script to handle x86_64 and files that do not come from a RPM committer: Matt Wilson <https://issues.rpath.com/> diff --git a/scripts/report.py b/scripts/report.py --- a/scripts/report.py +++ b/scripts/report.py @@ -68,7 +68,7 @@ rpmname = os.path.basename(rpm) d = dict.fromkeys(((util.normpath(x), arch) for x in h.paths()), rpmname) pathMap.update(d) - binaries = sorted(pkgMap[src]) + binaries = pkgMap[src] flist = [] for binary in binaries: name, version, flavor = binary @@ -77,12 +77,13 @@ continue if 'debuginfo' in name: continue - if 'is: x86_64(' in str(flavorStr): + if 'is: x86_64' in str(flavorStr): arch = 'x86_64' elif 'is: x86(' in str(flavorStr): arch = 'i586' else: arch = 'noarch' + print name, version, flavor cs = conaryclient.createChangeSet([(name, (None, None), (version, flavor), True)], @@ -96,10 +97,15 @@ cs.reset() cont = cs.getFileContents(pathId, fileId)[1] md5 = md5ToString(md5String(cont.get().read())) - rpm = pathMap[(path, arch)] + try: + rpm = pathMap[(path, arch)] + except KeyError: + # some file we added, like a tag handler + continue flist.append((binary[0], rpm, path, md5)) # sort based on path flist.sort(key=operator.itemgetter(2)) for info in flist: outfile.write('\t'.join(info)) outfile.write('\n') + outfile.flush() From johnsonm at rpath.com Wed Aug 19 17:39:52 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:39:52 +0000 Subject: mirrorball: cleanup final bits of advisor Message-ID: <200908192139.n7JLdqYD016300@scc.eng.rpath.com> changeset: 019a372f11d8 user: Elliot Peele <http://issues.rpath.com/> date: Wed, 09 Jul 2008 15:22:34 -0400 cleanup final bits of advisor remove break points committer: Elliot Peele <http://issues.rpath.com/> diff --git a/updatebot/advise.py b/updatebot/advise.py --- a/updatebot/advise.py +++ b/updatebot/advise.py @@ -152,8 +152,6 @@ newTroveMap = self._mkNewTroveMap(trvLst, newTroves) - import epdb; epdb.st() - toSend = set() for patch, advisory in self._advisories.iteritems(): binNames = [ x.name for x in patch.packages ] @@ -172,8 +170,6 @@ advisory.setUpdateTroves(newTroveMap[srpm]) toSend.add(advisory) - import epdb; epdb.st() - for advisory in toSend: log.info('sending advisory: %s' % advisory) advisory.send() @@ -252,7 +248,7 @@ smtp = self._smtpConnect() try: - results = smtp.sendmail(self._from, self._to, message) + results = smtp.sendmail(self._from, self._to, message.as_string()) except (SMTPRecipientsRefused, SMTPHeloError, SMTPSenderRefused, SMTPDataError), e: raise FailedToSendAdvisoryError(error=e) @@ -291,7 +287,11 @@ """ server = smtplib.SMTP(self._cfg.smtpServer) - server.connect() + + rootLogger = logging.getLogger('') + if rootLogger.level == logging.DEBUG: + server.set_debuglevel(1) + return server def setUpdateTroves(self, troves): @@ -327,7 +327,10 @@ # W0613 - Unused argument 'f' # pylint: disable-msg=W0613 - return '%s=%s' % (n, v) + label = v.trailingLabel().asString() + revision = v.trailingRevision().asString() + + return '%s=%s/%s' % (n, label, revision) def setSubject(self, subject): """ From johnsonm at rpath.com Wed Aug 19 17:39:53 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:39:53 +0000 Subject: mirrorball: only populate rpm and patch datastructures once Message-ID: <200908192139.n7JLdrBn016334@scc.eng.rpath.com> changeset: b199abb71cd5 user: Elliot Peele <http://issues.rpath.com/> date: Thu, 10 Jul 2008 10:29:44 -0400 only populate rpm and patch datastructures once committer: Elliot Peele <http://issues.rpath.com/> diff --git a/updatebot/bot.py b/updatebot/bot.py --- a/updatebot/bot.py +++ b/updatebot/bot.py @@ -37,6 +37,9 @@ def __init__(self, cfg): self._cfg = cfg + self._rpmSourcePopulated = False + self._patchSourcePopulated = False + self._clients = {} self._rpmSource = rpmsource.RpmSource() self._patchSource = patchsource.PatchSource(self._cfg) @@ -50,6 +53,9 @@ Populate the rpm source data structures. """ + if self._rpmSourcePopulated: + return + for repo in self._cfg.repositoryPaths: log.info('loading repository data %s/%s' % (self._cfg.repositoryUrl, repo)) @@ -58,16 +64,23 @@ self._clients[repo] = client self._rpmSource.finalize() + self._rpmSourcePopulated = True + def _populatePatchSource(self): """ Populate the patch source data structures. """ + if self._patchSourcePopulated: + return + for path, client in self._clients.iteritems(): log.info('loading patch information %s/%s' % (self._cfg.repositoryUrl, path)) self._patchSource.loadFromClient(client, path) + self._patchSourcePopulated = True + @staticmethod def _flattenSetDict(setDict): """ @@ -82,7 +95,12 @@ lst.extend(list(trvSet)) return lst - def run(self): + def create(self): + """ + Do initial imports. + """ + + def update(self): """ Update the conary repository from the yum repositories. """ From johnsonm at rpath.com Wed Aug 19 17:39:08 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:39:08 +0000 Subject: mirrorball: simplify cvc command excution Message-ID: <200908192139.n7JLd8Kc014573@scc.eng.rpath.com> changeset: 21f5a44ba032 user: Elliot Peele <http://issues.rpath.com/> date: Mon, 02 Jun 2008 23:28:19 -0400 simplify cvc command excution committer: Elliot Peele <http://issues.rpath.com/> diff --git a/rpmimport/recipemaker.py b/rpmimport/recipemaker.py --- a/rpmimport/recipemaker.py +++ b/rpmimport/recipemaker.py @@ -32,6 +32,13 @@ self.repos = repos self.rpmSource = rpmSource + def _cvc(self, *args, **kwargs): + """ + Run cvc command. + """ + + cvc.sourceCommand(self.cfg, *args, **kwargs) + def _updateSourceComponent(self, pkgname, manifestContents, comment): """ @@ -43,14 +50,11 @@ f.write(manifestContents) f.close() try: - cvc.sourceCommand(self.cfg, ['cook'], {'no-deps': None}) + self._cvc('cook', no-deps=None) except Exception, e: print '++++++ error building', pkgname, str(e) return - cvc.sourceCommand(self.cfg, - [ 'commit' ], - { 'message': - '%s of %s:source' % (comment, pkgname)}) + self._cvc('commit', message='%s of %s:source' % (comment, pkgname)) def _newpkg(self, pkgname): """ @@ -66,12 +70,11 @@ shutil.rmtree(pkgname) except OSError: pass - cvc.sourceCommand(self.cfg, [ "newpkg", pkgname ], - {'factory':'sle-rpm'}) + self._cvc('newpkg', pkgname, factory='sle-rpm') os.chdir(pkgname) f = open('manifest', 'w') f.close() - cvc.sourceCommand(self.cfg, [ 'add', 'manifest' ], {'text':True}) + self._cvc('add', 'manifest', text=True) def _checkout(self, pkgname): """ @@ -86,7 +89,7 @@ shutil.rmtree(pkgname) except OSError: pass - cvc.sourceCommand(self.cfg, [ 'co', pkgname ], {}) + self._cvc('co', pkgname) os.chdir(pkgname) def _createOrUpdate(self, pkgname, srpm, create=False, update=False): From johnsonm at rpath.com Wed Aug 19 17:42:20 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:42:20 +0000 Subject: mirrorball: build until done Message-ID: <200908192142.n7JLgKk9022234@scc.eng.rpath.com> changeset: 3727756443f9 user: Elliot Peele <https://issues.rpath.com/> date: Sun, 08 Feb 2009 04:11:27 +0000 build until done committer: Elliot Peele <https://issues.rpath.com/> diff --git a/updatebot/build.py b/updatebot/build.py --- a/updatebot/build.py +++ b/updatebot/build.py @@ -441,7 +441,12 @@ self.type = type def __str__(self): - return '%(name)s: %(trv)s [%(jobId)s] - %(message)s' % self.__dict__ + msg = '$(name)s: %(trv)s [%(jobId)s] - ' + if self.type == MESSAGE_TYPES['results']: + msg += 'done' + else: + msg += '%(message)s' + return msg % self.__dict__ class BuildWorker(Thread): BuilderClass = Builder @@ -458,27 +463,38 @@ self.jobId = None def run(self): + done = False while True: self.trv = self.toBuild.get() self.log('received trv') - self.jobId = self.builder.start([self.trv, ]) + built = False + while not built: + try: + self._doBuild() + except Exception, e: + built = False + built = True + + self.toBuild.task_done() + + def _doBuild(self): + self.jobId = self.builder.start([self.trv, ]) + + done, job = self._watch() + while not done: done, job = self._watch() - while not done: - done, job = self._watch() - if job.isFailed(): + if job.isFailed(): + self.error('job failed') + + else: + try: + res = self.builder.commit(self.jobId) + self.results(res) + except JobFailedError: self.error('job failed') - else: - try: - res = self.builder.commit(self.jobId) - self.results(res) - except JobFailedError: - self.error('job failed') - - self.toBuild.task_done() - def _watch(self): try: job = self.builder._helper.getJob(self.jobId) From johnsonm at rpath.com Wed Aug 19 17:42:30 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:42:30 +0000 Subject: mirrorball: add scripts for sync fedora and scientific linux mirrors Message-ID: <200908192142.n7JLgUi5022677@scc.eng.rpath.com> changeset: 1cf8e996b61a user: Elliot Peele <https://issues.rpath.com/> date: Mon, 16 Mar 2009 11:20:55 -0400 add scripts for sync fedora and scientific linux mirrors committer: Elliot Peele <https://issues.rpath.com/> diff --git a/scripts/sync-fedora-devel.sh b/scripts/sync-fedora-devel.sh new file mode 100755 --- /dev/null +++ b/scripts/sync-fedora-devel.sh @@ -0,0 +1,22 @@ +#!/bin/bash +# +# Copyright (c) 2008 rPath, Inc. +# +# This program is distributed under the terms of the Common Public License, +# version 1.0. A copy of this license should have been distributed with this +# source file in a file called LICENSE. If it is not present, the license +# is always available at http://www.rpath.com/permanent/licenses/CPL-1.0. +# +# This program is distributed in the hope that it will be useful, but +# without any warranty; without even the implied warranty of merchantability +# or fitness for a particular purpose. See the Common Public License for +# full details. +# + +SOURCE=rsync://mirrors.kernel.org/fedora/development +DEST=/l/fedora/ + +date +rsync -arv --progress --bwlimit=800 --exclude ppc --export ppc64 $SOURCE $DEST + +./hardlink.py $DEST diff --git a/scripts/sync-scientific.sh b/scripts/sync-scientific.sh new file mode 100755 --- /dev/null +++ b/scripts/sync-scientific.sh @@ -0,0 +1,22 @@ +#!/bin/bash +# +# Copyright (c) 2009 rPath, Inc. +# +# This program is distributed under the terms of the Common Public License, +# version 1.0. A copy of this license should have been distributed with this +# source file in a file called LICENSE. If it is not present, the license +# is always available at http://www.rpath.com/permanent/licenses/CPL-1.0. +# +# This program is distributed in the hope that it will be useful, but +# without any warranty; without even the implied warranty of merchantability +# or fitness for a particular purpose. See the Common Public License for +# full details. +# + +SOURCE=rsync://rsync.scientificlinux.org/scientific/53 +DEST=/l/scientific/ + +date +rsync -arv --progress --bwlimit=1024 --exclude iso $SOURCE $DEST + +./hardlink.py $DEST From johnsonm at rpath.com Wed Aug 19 17:39:49 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:39:49 +0000 Subject: mirrorball: add test for rpmvercmp Message-ID: <200908192139.n7JLdnvl016181@scc.eng.rpath.com> changeset: 36cffda3e8ee user: Elliot Peele <http://issues.rpath.com/> date: Wed, 09 Jul 2008 14:16:36 -0400 add test for rpmvercmp committer: Elliot Peele <http://issues.rpath.com/> diff --git a/test/unit_test/rpmutiltest/__init__.py b/test/unit_test/rpmutiltest/__init__.py new file mode 100644 diff --git a/test/unit_test/rpmutiltest/vercmptest.py b/test/unit_test/rpmutiltest/vercmptest.py new file mode 100644 --- /dev/null +++ b/test/unit_test/rpmutiltest/vercmptest.py @@ -0,0 +1,53 @@ +# +# Copyright (c) 2008 rPath, Inc. +# +# This program is distributed under the terms of the Common Public License, +# version 1.0. A copy of this license should have been distributed with this +# source file in a file called LICENSE. If it is not present, the license +# is always available at http://www.rpath.com/permanent/licenses/CPL-1.0. +# +# This program is distributed in the hope that it will be useful, but +# without any warranty; without even the implied warranty of merchantability +# or fitness for a particular purpose. See the Common Public License for +# full details. +# + +import testsetup + +import mock +import slehelp + +from rpmutils import rpmvercmp + +class VerCmpTest(slehelp.Helper): + def testVerCmp(self): + def _test(a, b, expected): + result = rpmvercmp(a, b) + self.failUnlessEqual(result, expected) + + _test('1', '1', 0) + _test('1', '2', -1) + _test('2', '1', 1) + _test('a', 'a', 0) + _test('a', 'b', -1) + _test('b', 'a', 1) + _test('1.2', '1.3', -1) + _test('1.3', '1.1', 1) + _test('1.a', '1.a', 0) + _test('1.a', '1.b', -1) + _test('1.b', '1.a', 1) + _test('1.2+', '1.2', 0) + _test('1.0010', '1.0', 1) + _test('1.05', '1.5', 0) + _test('1.0', '1', 1) + _test('2.50', '2.5', 1) + _test('fc4', 'fc.4', 0) + _test('FC5', 'fc5', -1) + _test('2a', '2.0', -1) + _test('1.0', '1.fc4', 1) + _test('3.0.0_fc', '3.0.0.fc', 0) + _test('1++', '1_', 0) + _test('+', '_', -1) + _test('_', '+', 1) + +testsetup.main() From johnsonm at rpath.com Wed Aug 19 17:43:03 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:43:03 +0000 Subject: mirrorball: add support for building kernel modules in all of the flavors that kernels are Message-ID: <200908192143.n7JLh4pZ024038@scc.eng.rpath.com> changeset: da91ec5d7b9c user: Elliot Peele <https://issues.rpath.com/> date: Fri, 12 Jun 2009 14:26:42 -0400 add support for building kernel modules in all of the flavors that kernels are built in, except debug committer: Elliot Peele <https://issues.rpath.com/> diff --git a/updatebot/build.py b/updatebot/build.py --- a/updatebot/build.py +++ b/updatebot/build.py @@ -23,6 +23,7 @@ from Queue import Queue, Empty from threading import Thread, RLock +from conary.deps import deps from conary import conarycfg, conaryclient from rmake import plugins @@ -295,8 +296,16 @@ # correct flavors. if name.startswith('group-'): troves.append((name, version, flavor)) - elif name == 'kernel' and self._cfg.kernelFlavors: + elif ((name == 'kernel' or name in self._cfg.kernelModules) + and self._cfg.kernelFlavors): for context, flavor in self._cfg.kernelFlavors: + # Replace flag name to match package + if name != 'kernel': + flavor = deps.parseFlavor(str(flavor).replace('kernel', name)) + # Don't build kernel modules with a .debug flag, that + # is only for kernels. + if flavor.stronglySatisfies(deps.parseFlavor('%s.debug' % name)): + continue troves.append((name, version, flavor, context)) elif name in self._cfg.packageFlavors: for context, flavor in self._cfg.packageFlavors[name]: diff --git a/updatebot/config.py b/updatebot/config.py --- a/updatebot/config.py +++ b/updatebot/config.py @@ -160,6 +160,9 @@ # flavors to build kernels. kernelFlavors = (CfgList(CfgContextFlavor), []) + # packages other than "kernel" to be built in kernelFlavers + kernelModules = (CfgList(CfgString), []) + # flavors to build packages in for packages that need specific flavoring. packageFlavors = (CfgDict(CfgList(CfgContextFlavor)), {}) From johnsonm at rpath.com Wed Aug 19 17:42:03 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:42:03 +0000 Subject: mirrorball: use new splitarch building in the builder Message-ID: <200908192142.n7JLg3Lb021604@scc.eng.rpath.com> changeset: 5696438d83d6 user: Elliot Peele <https://issues.rpath.com/> date: Wed, 14 Jan 2009 11:37:12 -0500 use new splitarch building in the builder committer: Elliot Peele <https://issues.rpath.com/> diff --git a/updatebot/bot.py b/updatebot/bot.py --- a/updatebot/bot.py +++ b/updatebot/bot.py @@ -57,7 +57,7 @@ lst.extend(list(trvSet)) return lst - def create(self): + def create(self, rebuild=False): """ Do initial imports. """ @@ -69,46 +69,14 @@ self._pkgSource.load() # Import sources into repository. - toBuild, fail = self._updater.create(self._cfg.package, buildAll=False) + toBuild, fail = self._updater.create(self._cfg.package, buildAll=rebuild) - # Build all newly imported packages. - trvMap, failed = self._builder.buildmany(toBuild) - -# import epdb; epdb.st() - -## trvMap = self._builder.build(toBuild) - #import epdb; epdb.st() - - #trvs = self._builder._formatInput(toBuild) - #jobs = {} - #for trv in trvs: - # key = 'other' - # if len(trv) == 4: - # key = trv[3] - - # if key not in jobs: - # jobs[key] = [] - - # jobs[key].append(trv) - - #ids = {} - #for job in jobs: - # if job == 'other': - # continue - - # ids[job] = self._builder._startJob(jobs[job]) - - #for job in ids: - # self._builder._monitorJob(ids[job]) - - #log.info('completed jobs %s' % (' '.join(ids.values()), )) - - #import epdb; epdb.st() - - #for trv in self._flattenSetDict(trvMap): - # log.info('built: %s' % trv) - - #import epdb; epdb.st() + if not rebuild: + # Build all newly imported packages. + trvMap, failed = self._builder.buildmany(toBuild) + else: + # ReBuild all packages. + trvMap = self._builder.buildsplitarch(toBuild) log.info('import completed successfully') log.info('imported %s source packages' % (len(toBuild), )) From johnsonm at rpath.com Wed Aug 19 17:42:27 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:42:27 +0000 Subject: mirrorball: only generate manifests with srpms if building from source Message-ID: <200908192142.n7JLgRqG022524@scc.eng.rpath.com> changeset: 1f8e46eaaeb6 user: Elliot Peele <https://issues.rpath.com/> date: Sat, 14 Feb 2009 15:55:53 -0500 only generate manifests with srpms if building from source committer: Elliot Peele <https://issues.rpath.com/> diff --git a/updatebot/config.py b/updatebot/config.py --- a/updatebot/config.py +++ b/updatebot/config.py @@ -179,8 +179,8 @@ # Satis Info satisUrl = CfgString - # Tell the updater to write build requires information - generateBuildRequires = (CfgBool, False) + # Try to build from source rpms + buildFromSource = (CfgBool, False) class UpdateBotConfig(cfg.SectionedConfigFile): diff --git a/updatebot/update.py b/updatebot/update.py --- a/updatebot/update.py +++ b/updatebot/update.py @@ -354,7 +354,7 @@ metadata = self._getMetadataFromPkgSource(srcPkg) self._conaryhelper.setMetadata(nvf[0], metadata) - if self._cfg.repositoryFormat == 'yum' and self._cfg.generateBuildRequires: + if self._cfg.repositoryFormat == 'yum' and self._cfg.buildFromSource: buildrequires = self._getBuildRequiresFromPkgSource(srcPkg) self._conaryhelper.setBuildRequires(nvf[0], buildrequires) @@ -370,12 +370,16 @@ """ manifest = [] - manifestPkgs = list(self._pkgSource.srcPkgMap[srcPkg]) - for pkg in self._getLatestOfAvailableArches(manifestPkgs): - if hasattr(pkg, 'location'): - manifest.append(pkg.location) - elif hasattr(pkg, 'files'): - manifest.extend(pkg.files) + + if self._cfg.repositoryFormat == 'yum' and self._cfg.buildFromSource: + manifest.append(pkg.location) + else: + manifestPkgs = list(self._pkgSource.srcPkgMap[srcPkg]) + for pkg in self._getLatestOfAvailableArches(manifestPkgs): + if hasattr(pkg, 'location'): + manifest.append(pkg.location) + elif hasattr(pkg, 'files'): + manifest.extend(pkg.files) return manifest def _getMetadataFromPkgSource(self, srcPkg): From johnsonm at rpath.com Wed Aug 19 17:43:04 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:43:04 +0000 Subject: mirrorball: clean up _formatInput Message-ID: <200908192143.n7JLh4rm024056@scc.eng.rpath.com> changeset: 561caa3b942a user: Elliot Peele <https://issues.rpath.com/> date: Mon, 15 Jun 2009 22:14:14 -0400 clean up _formatInput make sure names are std strings, not unicode committer: Elliot Peele <https://issues.rpath.com/> diff --git a/updatebot/build.py b/updatebot/build.py --- a/updatebot/build.py +++ b/updatebot/build.py @@ -292,24 +292,34 @@ # Build all troves in defined contexts. troves = [] for name, version, flavor in troveSpecs: + # Make sure name is not an unicode string, it causes breakage in + # the deps modules in conary. + name = name.encode() + # Don't set context for groups, they will already have the # correct flavors. if name.startswith('group-'): troves.append((name, version, flavor)) + + # Kernels are special. elif ((name == 'kernel' or name in self._cfg.kernelModules) and self._cfg.kernelFlavors): for context, flavor in self._cfg.kernelFlavors: # Replace flag name to match package if name != 'kernel': - flavor = deps.parseFlavor(str(flavor).replace('kernel', name)) # Don't build kernel modules with a .debug flag, that # is only for kernels. - if flavor.stronglySatisfies(deps.parseFlavor('%s.debug' % name)): + if flavor.stronglySatisfies(deps.parseFlavor('kernel.debug')): continue + flavor = deps.parseFlavor(str(flavor).replace('kernel', name) troves.append((name, version, flavor, context)) + + # Handle special package flavors when specified. elif name in self._cfg.packageFlavors: for context, flavor in self._cfg.packageFlavors[name]: troves.append((name, version, flavor, context)) + + # All other packages. else: # Build all packages as x86 and x86_64. for context in self._cfg.archContexts: From johnsonm at rpath.com Wed Aug 19 17:43:06 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:43:06 +0000 Subject: mirrorball: branch merge Message-ID: <200908192143.n7JLh6DP024158@scc.eng.rpath.com> changeset: 642c0cb18ba5 user: Elliot Peele <https://issues.rpath.com/> date: Mon, 06 Jul 2009 14:41:49 -0400 branch merge committer: Elliot Peele <https://issues.rpath.com/> diff --git a/scripts/buildsplit b/scripts/buildsplit new file mode 100755 --- /dev/null +++ b/scripts/buildsplit @@ -0,0 +1,23 @@ +#!/usr/bin/python +# +# Copryright (c) 2008-2009 rPath, Inc. +# + +""" +Script for cooking groups defined in the updatebot config. +""" + +from header import * + +if len(sys.argv) < 3: + usage() + +trvs = set() +label = cfg.topSourceGroup[1] +for pkg in sys.argv[2:]: + trvs.add((pkg, label, None)) +trvMap = builder.buildsplitarch(trvs) + +print "built:\n" + +display(trvMap) diff --git a/updatebot/update.py b/updatebot/update.py --- a/updatebot/update.py +++ b/updatebot/update.py @@ -72,6 +72,19 @@ % (len(toUpdate), len(toAdvise))) return toAdvise, toUpdate + def _fltrPkg(self, pkgname): + """ + Return True if this is a package that should be filtered out. + """ + + if (pkgname.startswith('info-') or + pkgname.startswith('group-') or + pkgname.startswith('factory-') or + pkgname in self._cfg.excludePackages): + return True + + return False + def _findUpdatableTroves(self, group): """ Query a group to find packages that need to be updated. @@ -87,10 +100,7 @@ name = name.split(':')[0] # skip special packages - if (name.startswith('info-') or - name.startswith('group-') or - name.startswith('factory-') or - name in self._cfg.excludePackages): + if self._fltrPkg(name): continue latestSrpm = self._getLatestSource(name) @@ -267,8 +277,7 @@ if buildAll and pkgs: toBuild.update( [ (x, self._conaryhelper.getLatestSourceVersion(x), None) - for x in pkgs if not x.startswith('info-') - and x not in self._cfg.excludePackages ] + for x in pkgs if not self._fltrPkg(x) ] ) return toBuild, fail From johnsonm at rpath.com Wed Aug 19 17:40:08 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:40:08 +0000 Subject: mirrorball: fix typo Message-ID: <200908192140.n7JLe8Fk016949@scc.eng.rpath.com> changeset: c8682417726d user: Elliot Peele <http://bugs.rpath.com/> date: Wed, 13 Aug 2008 14:22:54 -0400 fix typo committer: Elliot Peele <http://bugs.rpath.com/> diff --git a/scripts/sync-ubuntu.sh b/scripts/sync-ubuntu.sh new file mode 100644 --- /dev/null +++ b/scripts/sync-ubuntu.sh @@ -0,0 +1,21 @@ +#!/bin/bash +# +# Copyright (c) 2008 rPath, Inc. +# +# This program is distributed under the terms of the Common Public License, +# version 1.0. A copy of this license should have been distributed with this +# source file in a file called LICENSE. If it is not present, the license +# is always available at http://www.rpath.com/permanent/licenses/CPL-1.0. +# +# This program is distributed in the hope that it will be useful, but +# without any warranty; without even the implied warranty of merchantability +# or fitness for a particular purpose. See the Common Public License for +# full details. +# + +SOURCE=rsync://mirrors.us.kernel.org/ubuntu/ +DEST=/l/ubuntu/ + +rsync -arv --progress --exclude dapper* --exclude feisty* --exclude gustsy* --delete --exclude pool/universe/* --exclude pool/restricted/* --exclude pool/multiverse/* $SOURCE $DEST + +./hardlink.py $DEST diff --git a/scripts/sync-ubunutu.sh b/scripts/sync-ubunutu.sh deleted file mode 100644 --- a/scripts/sync-ubunutu.sh +++ /dev/null @@ -1,21 +0,0 @@ -#!/bin/bash -# -# Copyright (c) 2008 rPath, Inc. -# -# This program is distributed under the terms of the Common Public License, -# version 1.0. A copy of this license should have been distributed with this -# source file in a file called LICENSE. If it is not present, the license -# is always available at http://www.rpath.com/permanent/licenses/CPL-1.0. -# -# This program is distributed in the hope that it will be useful, but -# without any warranty; without even the implied warranty of merchantability -# or fitness for a particular purpose. See the Common Public License for -# full details. -# - -SOURCE=rsync://mirrors.us.kernel.org/ubuntu/ -DEST=/l/ubuntu/ - -rsync -arv --progress --exclude dapper* --exclude feisty* --exclude gustsy* --delete --exclude pool/universe/* --exclude pool/restricted/* --exclude pool/multiverse/* $SOURCE $DEST - -./hardlink.py $DEST From johnsonm at rpath.com Wed Aug 19 17:40:35 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:40:35 +0000 Subject: mirrorball: switch to single package building as that should be the default Message-ID: <200908192140.n7JLeZ6G018056@scc.eng.rpath.com> changeset: bf85ce954dd2 user: Elliot Peele <https://issues.rpath.com/> date: Tue, 02 Sep 2008 23:17:34 -0400 switch to single package building as that should be the default committer: Elliot Peele <https://issues.rpath.com/> diff --git a/updatebot/bot.py b/updatebot/bot.py --- a/updatebot/bot.py +++ b/updatebot/bot.py @@ -121,38 +121,39 @@ # import epdb; epdb.st() -# trvMap = self._builder.build(toBuild) -# import epdb; epdb.st() - trvs = self._builder._formatInput(toBuild) - jobs = {} - for trv in trvs: - key = 'other' - if len(trv) == 4: - key = trv[3] + trvMap = self._builder.build(toBuild) + #import epdb; epdb.st() - if key not in jobs: - jobs[key] = [] + #trvs = self._builder._formatInput(toBuild) + #jobs = {} + #for trv in trvs: + # key = 'other' + # if len(trv) == 4: + # key = trv[3] - jobs[key].append(trv) + # if key not in jobs: + # jobs[key] = [] - ids = {} - for job in jobs: - if job == 'other': - continue + # jobs[key].append(trv) - ids[job] = self._builder._startJob(jobs[job]) + #ids = {} + #for job in jobs: + # if job == 'other': + # continue - for job in ids: - self._builder._monitorJob(ids[job]) + # ids[job] = self._builder._startJob(jobs[job]) - log.info('completed jobs %s' % (' '.join(ids.values()), )) + #for job in ids: + # self._builder._monitorJob(ids[job]) - import epdb; epdb.st() + #log.info('completed jobs %s' % (' '.join(ids.values()), )) - for trv in self._flattenSetDict(trvMap): - log.info('built: %s' % trv) + #import epdb; epdb.st() - import epdb; epdb.st() + #for trv in self._flattenSetDict(trvMap): + # log.info('built: %s' % trv) + + #import epdb; epdb.st() log.info('import completed successfully') log.info('imported %s source packages' % (len(toBuild), )) From johnsonm at rpath.com Wed Aug 19 17:42:02 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:42:02 +0000 Subject: mirrorball: add support for alternate package flavors Message-ID: <200908192142.n7JLg2k9021536@scc.eng.rpath.com> changeset: 2642bdda87cf user: Elliot Peele <https://issues.rpath.com/> date: Thu, 05 Feb 2009 16:33:16 -0500 add support for alternate package flavors committer: Elliot Peele <https://issues.rpath.com/> diff --git a/updatebot/build.py b/updatebot/build.py --- a/updatebot/build.py +++ b/updatebot/build.py @@ -210,6 +210,9 @@ elif name == 'kernel' and self._cfg.kernelFlavors: for context, flavor in self._cfg.kernelFlavors: troves.append((name, version, flavor, context)) + elif name in self._cfg.packageFlavors: + for context, flavor in self._cfg.packageFlavors[name]: + troves.append((name, version, flavor, context)) else: # Build all packages as x86 and x86_64. for context in self._cfg.archContexts: diff --git a/updatebot/config.py b/updatebot/config.py --- a/updatebot/config.py +++ b/updatebot/config.py @@ -21,7 +21,7 @@ from conary.lib import cfg from conary import versions from conary.conarycfg import CfgFlavor, CfgLabel -from conary.lib.cfgtypes import CfgString, CfgList, CfgRegExp, CfgBool +from conary.lib.cfgtypes import CfgString, CfgList, CfgRegExp, CfgBool, CfgDict from conary.lib.cfgtypes import ParseError from rmake.build.buildcfg import CfgTroveSpec @@ -53,8 +53,13 @@ """ try: - context, flavorStr = val.split() - flavor = CfgFlavor.parseString(self, flavorStr) + splt = val.split() + if len(splt) == 1: + context = val + flavor = None + else: + context, flavorStr = splt + flavor = CfgFlavor.parseString(self, flavorStr) return context, flavor except versions.ParseError, e: raise ParseError, e @@ -146,6 +151,9 @@ # flavors to build kernels. kernelFlavors = (CfgList(CfgContextFlavor), []) + # flavors to build packages in for packages that need specific flavoring. + packageFlavors = (CfgDict(CfgList(CfgContextFlavor)), {}) + # email information for sending advisories emailFromName = CfgString emailFrom = CfgString From johnsonm at rpath.com Wed Aug 19 17:43:02 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:43:02 +0000 Subject: mirrorball: move package cmp and hash to an external class, no longer hash on epoch, leave Message-ID: <200908192143.n7JLh2Tk023970@scc.eng.rpath.com> changeset: 244551a57c63 user: Elliot Peele <https://issues.rpath.com/> date: Mon, 08 Jun 2009 22:12:44 -0400 move package cmp and hash to an external class, no longer hash on epoch, leave it up to cmp to deal with collisions committer: Elliot Peele <https://issues.rpath.com/> diff --git a/repomd/packagexml.py b/repomd/packagexml.py --- a/repomd/packagexml.py +++ b/repomd/packagexml.py @@ -16,7 +16,7 @@ Module for parsing package sections of xml files from the repository metadata. """ -__all__ = ('PackageXmlMixIn', ) +__all__ = ('PackageXmlMixIn', 'PackageCompare', ) import os @@ -27,7 +27,22 @@ from repomd.errors import UnknownElementError, UnknownAttributeError from repomd.xmlcommon import SlotNode -class _Package(SlotNode): +class PackageCompare(object): + def __str__(self): + return '-'.join([self.name, self.epoch, self.version, self.release, + self.arch]) + + def __hash__(self): + # NOTE: We do not hash on epoch, because not all package objects will + # have epoch set. This will cause hash collisions that should be + # handled in __cmp__. + return hash((self.name, self.version, self.release, self.arch)) + + def __cmp__(self, other): + return util.packageCompare(self, other) + + +class _Package(SlotNode, PackageCompare): """ Python representation of package section of xml files from the repository metadata. @@ -132,25 +147,13 @@ else: raise UnknownElementError(child) - def __str__(self): - return '-'.join([self.name, self.epoch, self.version, self.release, - self.arch]) - def __repr__(self): return os.path.basename(self.location) - def __hash__(self): - return hash((self.name, self.epoch, self.version, self.release, - self.arch)) - def __cmp__(self, other): - pkgvercmp = util.packagevercmp(self, other) - if pkgvercmp != 0: - return pkgvercmp - - archcmp = cmp(self.arch, other.arch) - if archcmp != 0: - return archcmp + pkgcmp = PackageCompare.__cmp__(self, other) + if pkgcmp != 0: + return pkgcmp return cmp(self.location, other.location) From johnsonm at rpath.com Wed Aug 19 17:40:41 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:40:41 +0000 Subject: mirrorball: add ubuntu promote script Message-ID: <200908192140.n7JLefLP018294@scc.eng.rpath.com> changeset: 4d44b7dea151 user: Elliot Peele <https://issues.rpath.com/> date: Mon, 08 Sep 2008 22:05:39 -0400 add ubuntu promote script committer: Elliot Peele <https://issues.rpath.com/> diff --git a/scripts/promote-ubuntu.sh b/scripts/promote-ubuntu.sh new file mode 100644 --- /dev/null +++ b/scripts/promote-ubuntu.sh @@ -0,0 +1,34 @@ +#!/bin/bash +# +# Copyright (c) 2008 rPath, Inc. +# +# This program is distributed under the terms of the Common Public License, +# version 1.0. A copy of this license should have been distributed with this +# source file in a file called LICENSE. If it is not present, the license +# is always available at http://www.rpath.com/permanent/licenses/CPL-1.0. +# +# This program is distributed in the hope that it will be useful, but +# without any warranty; without even the implied warranty of merchantability +# or fitness for a particular purpose. See the Common Public License for +# full details. +# + +cfg="--config-file $HOME/hg/mirrorball/config/ubuntu/conaryrc --interactive" + +cvc promote $cfg \ +{{auto,build,c,}package,deb-import,factory-deb}:source=ubuntu.rb.rpath.com at rpath:ubuntu-devel \ + /ubuntu.rb.rpath.com at rpath:ubuntu-devel--/ubuntu.rpath.org at rpath:ubuntu-devel + +cvc promote $cfg \ + group-os=ubuntu.rb.rpath.com at rpath:ubuntu-hardy-devel \ + platform-definition:source=ubuntu.rb.rpath.com at rpath:ubuntu-hardy-devel \ + /ubuntu.rb.rpath.com at rpath:ubuntu-hardy-devel--/ubuntu.rpath.org at rpath:ubuntu-hardy-devel \ + /ubuntu.rb.rpath.com at rpath:ubuntu-devel//ubuntu.rb.rpath.com at rpath:ubuntu-hardy-devel--/ubuntu.rpath.org at rpath:ubuntu-devel//ubuntu.rpath.org at rpath:ubuntu-hardy-devel \ + /conary.rpath.com at rpl:devel//conary.rpath.com at rpl:2//ubuntu.rb.rpath.com at rpath:ubuntu-hardy-devel--/conary.rpath.com at rpl:devel//conary.rpath.com at rpl:2//ubuntu.rpath.org at rpath:ubuntu-hardy-devel + +cvc promote $cfg \ + group-os=ubuntu.rpath.org at rpath:ubuntu-hardy-devel \ + platform-defintion:source=ubuntu.rpath.org at rpath:ubuntu-hardy-devel \ + /ubuntu.rpath.org at rpath:ubuntu-hardy-devel--/ubuntu.rpath.org at rpath:ubuntu-hardy \ + /conary.rpath.com at rpl:devel//conary.rpath.com at rpl:2--/ubuntu.rpath.org at rpath:ubuntu-hardy + /conary.rpath.com at rpl:devel//conary.rpath.com at rpl:2//ubuntu.rpath.org at rpath:ubuntu-hardy-devel--/ubuntu.rpath.org at rpath:ubuntu-hardy From johnsonm at rpath.com Wed Aug 19 17:41:12 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:41:12 +0000 Subject: mirrorball: try to match up binary rpms with srpms for binary rpms that epochs that differ Message-ID: <200908192141.n7JLfCDq019556@scc.eng.rpath.com> changeset: 73700cb5bd4e user: Elliot Peele <https://issues.rpath.com/> date: Thu, 30 Oct 2008 13:09:20 -0400 try to match up binary rpms with srpms for binary rpms that epochs that differ from there srpm committer: Elliot Peele <https://issues.rpath.com/> diff --git a/updatebot/pkgsource/rpmsource.py b/updatebot/pkgsource/rpmsource.py --- a/updatebot/pkgsource/rpmsource.py +++ b/updatebot/pkgsource/rpmsource.py @@ -32,7 +32,10 @@ def __init__(self, cfg): self._excludeArch = cfg.excludeArch - # {srpm: {rpm: path} + # {srcTup: srpm} + self._srcMap = dict() + + # {srcTup: {rpm: path} self._rpmMap = dict() # set of all src pkg objects @@ -110,6 +113,7 @@ self.locationMap[package.location] = package self._srcPkgs.add(package) + self._srcMap[(package.name, package.epoch, package.version, package.release, package.arch)] = package def _procBin(self, package): """ @@ -163,11 +167,33 @@ self.srcPkgMap[pkg] = self._rpmMap[key] self.srcPkgMap[pkg].add(pkg) + del self._rpmMap[key] for binPkg in self.srcPkgMap[pkg]: self.binPkgMap[binPkg] = pkg - log.warn('found %s source rpms without matching binary rpms' % count) + if count > 0: + log.warn('found %s source rpms without matching binary rpms' % count) + + # Attempt to match up remaining binaries with srpms. + for srcTup in self._rpmMap.keys(): + srcKey = list(srcTup) + epoch = int(srcKey[1]) + while epoch >= 0: + srcKey[1] = str(epoch) + key = tuple(srcKey) + if key in self._srcMap: + srcPkg = self._srcMap[key] + for binPkg in self._rpmMap[srcTup]: + self.srcPkgMap[srcPkg].add(binPkg) + self.binPkgMap[binPkg] = srcPkg + del self._rpmMap[srcTup] + break + epoch -= 1 + + if self._rpmMap: + count = sum([ len(x) for x in self._rpmMap.itervalues() ]) + log.warn('found %s binary rpms without matching srpms' % count) def loadFileLists(self, client, basePath): for pkg in client.getFileLists(): From johnsonm at rpath.com Wed Aug 19 17:39:02 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:39:02 +0000 Subject: mirrorball: add rpmvercmp.py Message-ID: <200908192139.n7JLd2ZL014303@scc.eng.rpath.com> changeset: f614dc7000bf user: Matt Wilson <https://issues.rpath.com/> date: Mon, 02 Jun 2008 19:57:45 -0400 add rpmvercmp.py committer: Matt Wilson <https://issues.rpath.com/> diff --git a/rpmvercmp.py b/rpmvercmp.py new file mode 100644 --- /dev/null +++ b/rpmvercmp.py @@ -0,0 +1,90 @@ +#!/usr/bin/python + +def _rpmversplit(s): + l = [] + isNumericHunk = s[0].isdigit() + + i = 1 + start = 0 + + + while i < len(s): + if not s[i].isalnum(): + l.append(s[start:i]) + start = i + 1 + elif isNumericHunk != s[i].isdigit(): + l.append(s[start:i]) + start = i + + i += 1 + + l.append(s[start:i]) + + # filter out empty strings + return [ x for x in l if x ] + +def rpmvercmp(ver1string, ver2string): + + ver1list = _rpmversplit(ver1string) + ver2list = _rpmversplit(ver2string) + + while ver1list or ver2list: + if not ver1list: + return -1 + elif not ver2list: + return 1 + + v1 = ver1list.pop(0) + v2 = ver2list.pop(0) + + if v1.isdigit() and v2.isdigit(): + v1 = int(v1) + v2 = int(v2) + elif v1.isdigit() and not v2.isdigit(): + # numbers are newer than letters + return 1 + elif not v1.isdigit() and v2.isdigit(): + return -1 + + if v1 < v2: + return -1 + elif v1 > v2: + return 1 + + return 0 + +if __name__ == '__main__': + def _test(a, b, expected): + result = rpmvercmp(a, b) + if result == -1: + print a, '<', b + elif result == 1: + print a, '>', b + else: + print a, '==', b + assert(result == expected) + + _test('1', '1', 0) + _test('1', '2', -1) + _test('2', '1', 1) + _test('a', 'a', 0) + _test('a', 'b', -1) + _test('b', 'a', 1) + _test('1.2', '1.3', -1) + _test('1.3', '1.1', 1) + _test('1.a', '1.a', 0) + _test('1.a', '1.b', -1) + _test('1.b', '1.a', 1) + _test('1.2+', '1.2', 0) + _test('1.0010', '1.0', 1) + _test('1.05', '1.5', 0) + _test('1.0', '1', 1) + _test('2.50', '2.5', 1) + _test('fc4', 'fc.4', 0) + _test('FC5', 'fc5', -1) + _test('2a', '2.0', -1) + _test('1.0', '1.fc4', 1) + _test('3.0.0_fc', '3.0.0.fc', 0) + _test('1++', '1_', 0) + _test('+', '_', -1) + _test('_', '+', -1) From johnsonm at rpath.com Wed Aug 19 17:40:20 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:40:20 +0000 Subject: mirrorball: add getLatestSourceVersion method Message-ID: <200908192140.n7JLeKbD017426@scc.eng.rpath.com> changeset: a5dbdf75d698 user: Elliot Peele <http://bugs.rpath.com/> date: Mon, 18 Aug 2008 22:01:52 -0400 add getLatestSourceVersion method remove debuggers committer: Elliot Peele <http://bugs.rpath.com/> diff --git a/updatebot/conaryhelper.py b/updatebot/conaryhelper.py --- a/updatebot/conaryhelper.py +++ b/updatebot/conaryhelper.py @@ -210,7 +210,7 @@ log.info('setting manifest for %s' % pkgname) # Figure out if we should create or update. - if not self._getVersionsByName('%s:source' % pkgname): + if not self.getLatestSourceVersion(pkgname): recipeDir = self._newpkg(pkgname) else: recipeDir = self._checkout(pkgname) @@ -234,9 +234,9 @@ util.rmtree(recipeDir) # Get new version of the source trove. - versions = self._getVersionsByName('%s:source' % pkgname) - assert len(versions) == 1 - return versions[0] + version = self.getLatestSourceVersion(pkgname) + assert version is not None + return version def _checkout(self, pkgname): """ @@ -329,6 +329,21 @@ versions = verMap.keys() return versions + def getLatestSourceVersion(self, pkgname): + """ + Finds the latest version of pkgname:source. + @param pkgname: name of package to look for + @type pkgname: string + """ + + versions = self._getVersionsByName('%s:source' % pkgname) + assert len(versions) in (0, 1) + + if len(versions) == 1: + return versions[0] + + return None + def promote(self, trvLst, expected, sourceLabels, targetLabel, checkPackageList=True): """ @@ -360,7 +375,6 @@ assert(label is not None) labelMap[label] = targetLabel - import epdb; epdb.st() success, cs = self._client.createSiblingCloneChangeSet( labelMap, trvLst, @@ -381,7 +395,6 @@ # that we think should be available to promote. Note that all packages # in expected will not be promoted because not all packages are # included in the groups. - import epdb; epdb.st() difference = newPkgs.difference(oldPkgs) grpTrvs = set([ (x[0], x[2]) for x in trvLst if not x[0].endswith(':source') ]) if checkPackageList and difference != grpTrvs: From johnsonm at rpath.com Wed Aug 19 17:42:44 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:42:44 +0000 Subject: mirrorball: branch merge Message-ID: <200908192142.n7JLgiDB023221@scc.eng.rpath.com> changeset: a69bd80dafb5 user: Elliot Peele <https://issues.rpath.com/> date: Mon, 20 Apr 2009 15:25:22 -0400 branch merge committer: Elliot Peele <https://issues.rpath.com/> diff --git a/scripts/import b/scripts/import --- a/scripts/import +++ b/scripts/import @@ -16,11 +16,7 @@ import os import sys -sys.path.insert(0, os.environ['HOME'] + '/hg/xobj/py') -sys.path.insert(0, os.environ['HOME'] + '/hg/rpath-xmllib') -sys.path.insert(0, os.environ['HOME'] + '/hg/conary') sys.path.insert(0, os.environ['HOME'] + '/hg/mirrorball') -sys.path.insert(0, os.environ['HOME'] + '/hg/rmake') from conary.lib import util sys.excepthook = util.genExcepthook() diff --git a/scripts/sync-scientific.sh b/scripts/sync-scientific.sh --- a/scripts/sync-scientific.sh +++ b/scripts/sync-scientific.sh @@ -1,6 +1,6 @@ #!/bin/bash # -# Copyright (c) 2008 rPath, Inc. +# Copyright (c) 2008-2009 rPath, Inc. # # This program is distributed under the terms of the Common Public License, # version 1.0. A copy of this license should have been distributed with this diff --git a/updatebot/pkgsource/rpmsource.py b/updatebot/pkgsource/rpmsource.py --- a/updatebot/pkgsource/rpmsource.py +++ b/updatebot/pkgsource/rpmsource.py @@ -178,9 +178,9 @@ for binPkg in self.srcPkgMap[pkg]: self.binPkgMap[binPkg] = pkg - if count > 0: - log.warn('found %s source rpms without matching binary ' - 'rpms' % count) + #if count > 0: + # log.warn('found %s source rpms without matching binary ' + # 'rpms' % count) # Defer deletes, contents of rpmMap are used more than once. for key in toDelete: @@ -205,6 +205,19 @@ if self._rpmMap: count = sum([ len(x) for x in self._rpmMap.itervalues() ]) log.warn('found %s binary rpms without matching srpms' % count) + srcs = {} + for x in self._rpmMap.itervalues(): + for y in x: + if y.sourcerpm not in srcs: + srcs[y.sourcerpm] = set() + srcs[y.sourcerpm].add(y.location) + + for src, locs in srcs.iteritems(): + log.warn('missing srpm: %s' % src) + log.warn('for rpm(s):') + for loc in sorted(locs): + log.warn('\t%s' % loc) + #srcs = {} #for x in self._rpmMap.itervalues(): From johnsonm at rpath.com Wed Aug 19 17:40:59 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:40:59 +0000 Subject: mirrorball: refactor parser Message-ID: <200908192141.n7JLexW9019043@scc.eng.rpath.com> changeset: 9a62cf0e3a7c user: Elliot Peele <https://issues.rpath.com/> date: Thu, 09 Oct 2008 18:14:07 -0400 refactor parser committer: Elliot Peele <https://issues.rpath.com/> diff --git a/aptmd/common.py b/aptmd/common.py --- a/aptmd/common.py +++ b/aptmd/common.py @@ -14,8 +14,8 @@ from updatebot import util -from aptmd.parser import Parser from aptmd.container import Container +from aptmd.parser import ContainerizedParser as Parser class BaseContainer(Container): __slots__ = ('name', 'arch', 'epoch', 'version', 'release') @@ -39,7 +39,6 @@ Parser.__init__(self) self._containerClass = BaseContainer - self._objects = [] self._states.update({ 'package' : self._package, @@ -51,18 +50,6 @@ 'original-maintainer' : self._keyval, }) - def parse(self, fileObj): - self._objects = [] - Parser.parse(self, fileObj) - return self._objects - - @staticmethod - def _getState(key): - key = key.strip() - if key.endswith(':'): - key = key[:-1] - return key.lower() - def _package(self): if self._curObj is not None: if hasattr(self._curObj, 'finalize'): diff --git a/aptmd/parser.py b/aptmd/parser.py --- a/aptmd/parser.py +++ b/aptmd/parser.py @@ -64,7 +64,6 @@ self._curObj = None self._lineTokenizer = _QuotedLineTokenizer() - self._containerClass = None self._states = {} def parse(self, fn): @@ -105,3 +104,36 @@ key = self._getState(self._line[0]) value = ' '.join(self._line[1:]).strip() self._curObj.set(key, value) + + +class ContainerizedParser(Parser): + def __init__(self): + Parser.__init__(self) + + self._objects = [] + self._containerClass = None + self._stateFilters = { + } + + def _filter(self, filter, state): + self._stateFilters[re.compile(filter)] = state + + def _getState(key): + key = key.strip() + key = key.lower() + if key.endswith(':'): + key = key[:-1] + + if key in self._states: + return key + + for filter, state in self._stateFilters.iteritems(): + if filter.match(key): + return state + + return key + + def parse(self, fileObj): + self._objects = [] + Parser.parse(self, fileObj) + return self._objects From johnsonm at rpath.com Wed Aug 19 17:39:34 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:39:34 +0000 Subject: mirrorball: add more advisory related exceptions Message-ID: <200908192139.n7JLdYpc015635@scc.eng.rpath.com> changeset: ecf2b5421efa user: Elliot Peele <http://issues.rpath.com/> date: Mon, 23 Jun 2008 17:07:38 -0400 add more advisory related exceptions committer: Elliot Peele <http://issues.rpath.com/> diff --git a/updatebot/errors.py b/updatebot/errors.py --- a/updatebot/errors.py +++ b/updatebot/errors.py @@ -118,7 +118,7 @@ promote all expected pacakges. """ - _parms = ['expected', 'actual'] + _params = ['expected', 'actual'] _template = ('Expected to promote %(expected)s, actually tried to promote' ' %(actual)s.') @@ -134,3 +134,62 @@ NoAdvisoryFoundError, raised when the bot can not find an advisory for an updated package. """ + +class ProductNameNotDefinedError(AdvisoryError): + """ + ProductNameNotDefinedError, raised when the product name is not defined + in the config. + """ + + _template = 'Product name not defined' + +class NoSenderFoundError(AdvisoryError): + """ + NoSenderFoundError, raised when no sender is defined in the config file. + """ + + _template = 'No sender defined for advisory emails: %(why)s' + +class NoRecipientsFoundError(AdvisoryError): + """ + NoRecipientsFoundError, raised when no recipients are defined in the + config file. + """ + + _template = 'No recipients defined for advisory emails: %(why)s' + +class FailedToSendAdvisoryError(AdvisoryError): + """ + FailedToSendAdvisoryError, raised when the smtp server fails to send the + advisory. + """ + + _params = ['error', ] + _template = 'Failed to send advisory: %(error)s' + +class AdvisoryRecipientRefusedError(FailedToSendAdvisoryError): + """ + AdvisoryRecipientRefusedError, raised when the smtp server refuses one or + more recipients, but not all of them. + """ + + _params = ['data', ] + _template = 'One or more recipients was refused by the smtp server: %(data)s' + +class NoPackagesFoundForAdvisory(AdvisoryError): + """ + NoPackagesFoundForAdvisory, raised when the bot can't find promoted + binary versions of a package. + """ + + _params = ['what', ] + _template = 'Could not find binary packages for %(what)s' + +class MultipleAdvisoriesFoundError(AdvisoryError): + """ + MultipleAdvisoriesFoundError, raised when multiple advisories are found + for one source. + """ + + _params = ['what', 'advisories'] + _template = 'Found multiple advisories for %(what)s: %(advisories)s' From johnsonm at rpath.com Wed Aug 19 17:40:59 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:40:59 +0000 Subject: mirrorball: beginings of a piper mail archive parser Message-ID: <200908192141.n7JLexBU019060@scc.eng.rpath.com> changeset: 04188ee2a240 user: Elliot Peele <https://issues.rpath.com/> date: Thu, 09 Oct 2008 18:20:39 -0400 beginings of a piper mail archive parser committer: Elliot Peele <https://issues.rpath.com/> diff --git a/pmap/__init__.py b/pmap/__init__.py new file mode 100644 --- /dev/null +++ b/pmap/__init__.py @@ -0,0 +1,19 @@ +# +# Copyright (c) 2008 rPath, Inc. +# +# This program is distributed under the terms of the Common Public License, +# version 1.0. A copy of this license should have been distributed with this +# source file in a file called LICENSE. If it is not present, the license +# is always available at http://www.rpath.com/permanent/licenses/CPL-1.0. +# +# This program is distributed in the hope that it will be useful, but +# without any warranty; without even the implied warranty of merchantability +# or fitness for a particular purpose. See the Common Public License for +# full details. +# + +""" +PMAP - PiperMail Archive Parser + +Parse advisories from pipermail archives. +""" diff --git a/pmap/common.py b/pmap/common.py new file mode 100644 --- /dev/null +++ b/pmap/common.py @@ -0,0 +1,38 @@ +# +# Copyright (c) 2008 rPath, Inc. +# +# This program is distributed under the terms of the Common Public License, +# version 1.0. A copy of this license should have been distributed with this +# source file in a file called LICENSE. If it is not present, the license +# is always available at http://www.rpath.com/permanent/licenses/CPL-1.0. +# +# This program is distributed in the hope that it will be useful, but +# without any warranty; without even the implied warranty of merchantability +# or fitness for a particular purpose. See the Common Public License for +# full details. +# + +from aptmd.container import Container +from aptmd.parser import ContainerizedParser as Parser + +class BaseContainer(Container): + pass + +class BaseParser(Parser): + def __init__(self): + Parser.__init__(self) + + self._containerClass = BaseContainer + self._objects = [] + + self._states.update({ + '--------------' : self._messagesep, + }) + + def _messagesep(self): + if self._line[1] == 'next' and self._line[2] == 'part': + if self._curObj is not None: + if hasattr(self._curObj, 'finalize'): + self._curObj.finalize() + self._objects.append(self._curObj) + self._curObj = self._containerClass() From johnsonm at rpath.com Wed Aug 19 17:42:30 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:42:30 +0000 Subject: mirrorball: add script for discovering virtual buildreqs/provides Message-ID: <200908192142.n7JLgUgc022660@scc.eng.rpath.com> changeset: 4077a7be2a5c user: Elliot Peele <https://issues.rpath.com/> date: Mon, 16 Mar 2009 11:20:31 -0400 add script for discovering virtual buildreqs/provides committer: Elliot Peele <https://issues.rpath.com/> diff --git a/scripts/virtdepfinder.py b/scripts/virtdepfinder.py new file mode 100755 --- /dev/null +++ b/scripts/virtdepfinder.py @@ -0,0 +1,66 @@ +#!/usr/bin/python2.6 + +import os +import sys + +sys.path.insert(0, os.environ['HOME'] + '/hg/26/rpath-xmllib') +sys.path.insert(0, os.environ['HOME'] + '/hg/26/conary') +sys.path.insert(0, os.environ['HOME'] + '/hg/26/mirrorball') +sys.path.insert(0, os.environ['HOME'] + '/hg/26/rmake') +sys.path.insert(0, os.environ['HOME'] + '/hg/26/epdb') +sys.path.insert(0, os.environ['HOME'] + '/hg/26/xobj/py') + +from conary.lib import util +sys.excepthook = util.genExcepthook() + +import logging +import updatebot.log + +updatebot.log.addRootLogger() +log = logging.getLogger('test') + +from updatebot import config +from updatebot.update import Updater +from updatebot import pkgsource + +cfg = config.UpdateBotConfig() +cfg.read(os.environ['HOME'] + '/hg/26/mirrorball/config/%s/updatebotrc' % sys.argv[1] ) + +pkgSource = pkgsource.PackageSource(cfg) +pkgSource.load() + +updater = Updater(cfg, pkgSource) + +virtreqs = {} +for srcName in pkgSource.srcNameMap.iterkeys(): + if not pkgSource.srcNameMap[srcName]: + continue + srcPkg = updater._getLatestSource(srcName) + for req in updater._getBuildRequiresFromPkgSource(srcPkg): + if req[1] == 'virtual': + if req[0] not in virtreqs: + virtreqs[req[0]] = set() + virtreqs[req[0]].add(srcName) + +provides = {} +for binName in pkgSource.binNameMap.iterkeys(): + binPkg = updater._getLatestBinary(binName) + for reqType in binPkg.format: + if reqType.getName() == 'rpm:provides': + for req in reqType.iterChildren(): + if hasattr(req, 'isspace') and req.isspace(): + continue + if '/' in req.name: + continue + if req.name not in provides: + provides[req.name] = set() + provides[req.name].add(binName) + +virtreqMap = {} +for req in virtreqs.iterkeys(): + if req not in provides: + log.warn('could not find provide %s for %s' % (req, virtreqs[req])) + continue + virtreqMap[req] = provides[req] + +import epdb; epdb.st() From johnsonm at rpath.com Wed Aug 19 17:39:08 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:39:08 +0000 Subject: mirrorball: cleanups for pylint Message-ID: <200908192139.n7JLd86N014556@scc.eng.rpath.com> changeset: 6c87df2fe48f user: Elliot Peele <http://issues.rpath.com/> date: Mon, 02 Jun 2008 23:21:25 -0400 cleanups for pylint committer: Elliot Peele <http://issues.rpath.com/> diff --git a/rpmimport/recipemaker.py b/rpmimport/recipemaker.py --- a/rpmimport/recipemaker.py +++ b/rpmimport/recipemaker.py @@ -64,15 +64,14 @@ print 'creating initial template for', pkgname try: shutil.rmtree(pkgname) - except OSError, e: + except OSError: pass cvc.sourceCommand(self.cfg, [ "newpkg", pkgname ], {'factory':'sle-rpm'}) os.chdir(pkgname) - f = open(manifest, 'w') + f = open('manifest', 'w') f.close() - addfiles.append(manifest) - cvc.sourceCommand(self.cfg, addfiles, {'text':True}) + cvc.sourceCommand(self.cfg, [ 'add', 'manifest' ], {'text':True}) def _checkout(self, pkgname): """ @@ -85,12 +84,25 @@ print 'updating', pkgname try: shutil.rmtree(pkgname) - except OSError, e: + except OSError: pass cvc.sourceCommand(self.cfg, [ 'co', pkgname ], {}) os.chdir(pkgname) def _createOrUpdate(self, pkgname, srpm, create=False, update=False): + """ + Manage a package manifest file. + NOTE: either create or update must be True. + @param pkgname: name of the package + @type pkgname: string + @param srpm: name of hte source RPM file + @type srpm: string + @param create: create a package + @type create: boolean + @param update: update a package + @type update: boolean + """ + assert(create or update) manifest = self.rpmSource.createManifest(srpm) @@ -107,7 +119,23 @@ os.chdir(cwd) def createManifest(self, pkgname, srpm): + """ + Create a manifest file. + @param pkgname: name of the package + @type pkgname: string + @param srpm: name of hte source RPM file + @type srpm: string + """ + self._createOrUpdate(pkgname, srpm, create=True) def updateManifest(self, pkgname, srpm): + """ + Update a manifest file. + @param pkgname: name of the package + @type pkgname: string + @param srpm: name of hte source RPM file + @type srpm: string + """ + self._createOrUpdate(pkgname, srpm, update=True) From johnsonm at rpath.com Wed Aug 19 17:42:11 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:42:11 +0000 Subject: mirrorball: handle metadata in a more consistent manner Message-ID: <200908192142.n7JLgB7E021877@scc.eng.rpath.com> changeset: 0dc702f73c37 user: Elliot Peele <https://issues.rpath.com/> date: Fri, 23 Jan 2009 19:33:39 -0500 handle metadata in a more consistent manner committer: Elliot Peele <https://issues.rpath.com/> diff --git a/updatebot/lib/util.py b/updatebot/lib/util.py --- a/updatebot/lib/util.py +++ b/updatebot/lib/util.py @@ -97,3 +97,26 @@ return nameCmp return packagevercmp(a, b) + +class Metadata(object): + """ + Base class for repository metadata. + """ + + def __init__(self, pkgs): + self.pkgs = pkgs + self.locationMap = {} + self.binPkgMap = {} + + src = None + for pkg in self.pkgs: + if hasattr(pkg, 'location'): + self.locationMap[pkg.location] = pkg + elif hasattr(pkg, 'files'): + for path in pkg.files: + self.locationMap[path] = pkg + if pkg.arch == 'src': + src = pkg + + for pkg in self.pkgs: + self.binPkgMap[pkg] = src diff --git a/updatebot/update.py b/updatebot/update.py --- a/updatebot/update.py +++ b/updatebot/update.py @@ -148,7 +148,8 @@ srcPkg = self._pkgSource.binPkgMap[binPkg] else: if metadata is None: - metadata = self._getMetadataFromConaryRepository(nvf[0]) + pkgs = self._getMetadataFromConaryRepository(nvf[0]) + metadata = util.Metadata(pkgs) if metadata: binPkg = metadata.locationMap[line] srcPkg = metadata.binPkgMap[binPkg] @@ -390,25 +391,7 @@ @return dictionary of infomation that looks like a pkgsource. """ - metadata = {} - metadata['pkgs'] = self._conaryhelper.getMetadata(pkgName) - metadata['locationMap'] = {} - metadata['binPkgMap'] = {} - - src = None - for pkg in metadata['pkgs']: - if hasattr(pkg, 'location'): - metadata['locationMap'][pkg.location] = pkg - elif hasattr(pkg, 'files'): - for path in pkg.files: - metadata['locationMap'][path] = pkg - if pkg.arch == 'src': - src = pkg - - for pkg in metadata['pkgs']: - metadata['binPkgMap'][pkg] = src - - return metadata + return self._conaryhelper.getMetadata(pkgName) def publish(self, trvLst, expected, targetLabel, checkPackageList=True): """ From johnsonm at rpath.com Wed Aug 19 17:40:43 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:40:43 +0000 Subject: mirrorball: update Message-ID: <200908192140.n7JLehS4018379@scc.eng.rpath.com> changeset: 4bef2809c11a user: Elliot Peele <https://issues.rpath.com/> date: Wed, 10 Sep 2008 18:34:45 -0400 update committer: Elliot Peele <https://issues.rpath.com/> diff --git a/scripts/promote-ubuntu.sh b/scripts/promote-ubuntu.sh old mode 100644 new mode 100755 --- a/scripts/promote-ubuntu.sh +++ b/scripts/promote-ubuntu.sh @@ -13,22 +13,22 @@ # full details. # -cfg="--config-file $HOME/hg/mirrorball/config/ubuntu/conaryrc --interactive" +cfg="--config-file $HOME/hg/mirrorball/config/ubuntu/conaryrc -m promote" cvc promote $cfg \ {{auto,build,c,}package,deb-import,factory-deb}:source=ubuntu.rb.rpath.com at rpath:ubuntu-devel \ /ubuntu.rb.rpath.com at rpath:ubuntu-devel--/ubuntu.rpath.org at rpath:ubuntu-devel cvc promote $cfg \ + group-appliance:source=ubuntu.rb.rpath.com at rpath:ubuntu-hardy-devel \ + platform-definition:source=ubuntu.rb.rpath.com at rpath:ubuntu-hardy-devel \ group-os=ubuntu.rb.rpath.com at rpath:ubuntu-hardy-devel \ - platform-definition:source=ubuntu.rb.rpath.com at rpath:ubuntu-hardy-devel \ - /ubuntu.rb.rpath.com at rpath:ubuntu-hardy-devel--/ubuntu.rpath.org at rpath:ubuntu-hardy-devel \ - /ubuntu.rb.rpath.com at rpath:ubuntu-devel//ubuntu.rb.rpath.com at rpath:ubuntu-hardy-devel--/ubuntu.rpath.org at rpath:ubuntu-devel//ubuntu.rpath.org at rpath:ubuntu-hardy-devel \ - /conary.rpath.com at rpl:devel//conary.rpath.com at rpl:2//ubuntu.rb.rpath.com at rpath:ubuntu-hardy-devel--/conary.rpath.com at rpl:devel//conary.rpath.com at rpl:2//ubuntu.rpath.org at rpath:ubuntu-hardy-devel + ubuntu.rb.rpath.com at rpath:ubuntu-hardy-devel--ubuntu.rpath.org at rpath:ubuntu-hardy-devel \ -cvc promote $cfg \ - group-os=ubuntu.rpath.org at rpath:ubuntu-hardy-devel \ - platform-defintion:source=ubuntu.rpath.org at rpath:ubuntu-hardy-devel \ - /ubuntu.rpath.org at rpath:ubuntu-hardy-devel--/ubuntu.rpath.org at rpath:ubuntu-hardy \ - /conary.rpath.com at rpl:devel//conary.rpath.com at rpl:2--/ubuntu.rpath.org at rpath:ubuntu-hardy - /conary.rpath.com at rpl:devel//conary.rpath.com at rpl:2//ubuntu.rpath.org at rpath:ubuntu-hardy-devel--/ubuntu.rpath.org at rpath:ubuntu-hardy +#cvc promote $cfg \ +# group-appliance:source=ubuntu.rpath.org at rpath:ubuntu-hardy-devel \ +# platform-defintion:source=ubuntu.rpath.org at rpath:ubuntu-hardy-devel \ +# group-os=ubuntu.rpath.org at rpath:ubuntu-hardy-devel \ +# /ubuntu.rpath.org at rpath:ubuntu-hardy-devel--/ubuntu.rpath.org at rpath:ubuntu-hardy \ +# /conary.rpath.com at rpl:devel//conary.rpath.com at rpl:2--/ubuntu.rpath.org at rpath:ubuntu-hardy \ +# /conary.rpath.com at rpl:devel//conary.rpath.com at rpl:2//ubuntu.rpath.org at rpath:ubuntu-hardy-devel--/ubuntu.rpath.org at rpath:ubuntu-hardy From johnsonm at rpath.com Wed Aug 19 17:41:02 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:41:02 +0000 Subject: mirrorball: more parser work Message-ID: <200908192141.n7JLf201019145@scc.eng.rpath.com> changeset: 371766555f0a user: Elliot Peele <https://issues.rpath.com/> date: Wed, 22 Oct 2008 15:54:25 -0400 more parser work committer: Elliot Peele <https://issues.rpath.com/> diff --git a/pmap/__init__.py b/pmap/__init__.py --- a/pmap/__init__.py +++ b/pmap/__init__.py @@ -17,3 +17,29 @@ Parse advisories from pipermail archives. """ + +from imputil import imp + +__all__ = ('InvalidBackendError', 'parse') +__supported_backends = ('ubuntu', 'centos') + +class InvalidBackendError(Exception): + pass + +def __getBackend(backend): + if backend not in __supported_backends: + raise InvalidBackendError('%s is not a supported backend, please ' + 'choose from %s' % (backend, ','.join(__supported_backends))) + + try: + path = [imp.find_module('pmap')[1], ] + mod = imp.find_module(backend, path) + loaded = imp.load_module(backend, mod[0], mod[1], mod[2]) + return loaded + except ImportError, e: + raise InvalidBackendError('Could not load %s backend: %s' % (backend, e)) + +def parse(url, backend='ubuntu'): + backend = __getBackend(backend) + parser = backend.Parser() + return parser.parse(url) diff --git a/pmap/common.py b/pmap/common.py --- a/pmap/common.py +++ b/pmap/common.py @@ -12,27 +12,40 @@ # full details. # +import tempfile + +from upstream import mailbox + from aptmd.container import Container from aptmd.parser import ContainerizedParser as Parser class BaseContainer(Container): - pass + __slots__ = ('fromAddr', 'fromName', 'timestamp') class BaseParser(Parser): def __init__(self): Parser.__init__(self) self._containerClass = BaseContainer - self._objects = [] - self._states.update({ - '--------------' : self._messagesep, }) - def _messagesep(self): - if self._line[1] == 'next' and self._line[2] == 'part': - if self._curObj is not None: - if hasattr(self._curObj, 'finalize'): - self._curObj.finalize() - self._objects.append(self._curObj) - self._curObj = self._containerClass() + def parse(self, fileObj): + self._objects = [] + mbox = self._getMbox(fileObj) + for msg in mbox: + self._parseMsg(msg) + return self._objects + + def _getMbox(self, fileObj): + tmpfn = tempfile.mktemp() + tmpfh = open(tmpfn, 'w') + tmpfh.write(fileObj.read()) + tmpfh.close() + + mbox = mailbox.mbox(tmpfn) + return mbox + + def _parseMsg(self, msg): + self._newContainer() + From johnsonm at rpath.com Wed Aug 19 17:42:49 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:42:49 +0000 Subject: mirrorball: add support for synthesizing source package objects Message-ID: <200908192142.n7JLgnm2023460@scc.eng.rpath.com> changeset: 6b6b7761475f user: Elliot Peele <https://issues.rpath.com/> date: Wed, 29 Apr 2009 16:38:00 -0400 add support for synthesizing source package objects committer: Elliot Peele <https://issues.rpath.com/> diff --git a/updatebot/config.py b/updatebot/config.py --- a/updatebot/config.py +++ b/updatebot/config.py @@ -186,6 +186,10 @@ # package format. writePackageMetadata = (CfgBool, False) + # If sources are not available pkgSource will attempt to build artificial + # source information if this is set to False. + sourcesAvailable = (CfgBool, True) + class UpdateBotConfig(cfg.SectionedConfigFile): """ diff --git a/updatebot/pkgsource/rpmsource.py b/updatebot/pkgsource/rpmsource.py --- a/updatebot/pkgsource/rpmsource.py +++ b/updatebot/pkgsource/rpmsource.py @@ -152,6 +152,11 @@ Make some final datastructures now that we are done populating object. """ + # Build source structures from binaries if no sources are available from + # the repository. + if not self._cfg.sourcesAvailable: + self._createSrcMap() + # Now that we have processed all of the rpms, build some more data # structures. count = 0 @@ -227,7 +232,6 @@ for loc in sorted(locs): log.warn('\t%s' % loc) - def loadFileLists(self, client, basePath): """ Parse file information. @@ -238,3 +242,33 @@ if util.packageCompare(pkg, binPkg) == 0: binPkg.files = pkg.files break + + def _createSrcMap(self): + """ + Create a source map from the binary map if no sources are available. + """ + + # Return if sources should be available in repos. + if self._cfg.sourcesAvailable: + return + + PkgClass = repomd.packagexml._Package + + # Create a fake source rpm object for each key in the rpmMap. + for (name, epoch, version, release, arch), bins in self._rpmMap.iteritems(): + srcPkg = PkgClass() + srcPkg.name = name + srcPkg.epoch = epoch + srcPkg.version = version + srcPkg.release = release + srcPkg.arch = arch + + # grab the first binary package + pkg = sorted(bins)[0] + + # Set the location of the fake source package to just be the name + # of the file. The factory will take care of the rest. + srcPkg.location = pkg.sourcerpm + + # add source to structures + self._procSrc(srcPkg) From johnsonm at rpath.com Wed Aug 19 17:40:12 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:40:12 +0000 Subject: mirrorball: intial debsource Message-ID: <200908192140.n7JLeCRn017103@scc.eng.rpath.com> changeset: 8b0709511e2e user: Elliot Peele <http://issues.rpath.com/> date: Thu, 14 Aug 2008 17:49:23 -0400 intial debsource committer: Elliot Peele <http://issues.rpath.com/> diff --git a/updatebot/debsource.py b/updatebot/debsource.py new file mode 100644 --- /dev/null +++ b/updatebot/debsource.py @@ -0,0 +1,75 @@ +# +# Copyright (c) 2008 rPath, Inc. +# +# This program is distributed under the terms of the Common Public License, +# version 1.0. A copy of this license should have been distributed with this +# source file in a file called LICENSE. If it is not present, the license +# is always available at http://www.rpath.com/permanent/licenses/CPL-1.0. +# +# This program is distributed in the hope that it will be useful, but +# without any warranty; without even the implied warranty of merchantability +# or fitness for a particular purpose. See the Common Public License for +# full details. +# + +import logging + +import aptmd +from updatebot import util + +class DebSource(object): + def __init__(self, cfg): + self._excludeArch = cfg.excludeArch + + self._binPkgs = set() + self._srcPkgs = set() + + self.srcPkgMap = dict() + self.binPkgMap = dict() + self.srcNameMap = dict() + self.binNameMap = dict() + self.locationMap = dict() + + def loadFromClient(self, client, path): + for pkg in self.client.parse(path): + if pkg.arch in self._excludeArch: + continue + + if pkg.arch == 'src': + self._procSrc(pkg) + else: + self._procBin(pkg) + + def _procSrc(self, pkg): + if pkg.name not in self.srcNameMap: + self.srcNameMap[pkg.name] = set() + self.srcNameMap[pkg.name].add(pkg) + + for location in pkg.files: + self.locationMap[location] = pkg + + self._srcPkgs.add(pkg) + + def _procBin(self, pkg): + if pkg.name not in self.binNameMap: + self.binNameMap[pkg.name] = set() + self.binNameMap[pkg.name].add(pkg) + + self.locationMap[pkg.location] = pkg + + self._binPkgs.add(pkg) + + def finalize(self): + for srcPkg in self._srcPkgs: + if srcPkg in self.srcPkgMap: + continue + + self.srcPkgMap[srcPkg] = set() + for binPkgName in srcPkg.binaries: + for binPkg in self.binNameMap[binPkgName]: + if binPkg.version == srcPkg.version and binPkg.release = srcPkg.release: + self.srcPkgMap[srcPkg].add(binPkg) + self.srcPkgMap[srcPkg].add(srcPkg) + + for pkg in self.srcPkgMap[srcPkg]: + self.binPkgMap[pkg] = srcPkg From johnsonm at rpath.com Wed Aug 19 17:38:58 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:38:58 +0000 Subject: mirrorball: add support for some more elements Message-ID: <200908192139.n7JLcwKP014083@scc.eng.rpath.com> changeset: 07bf75cf5d6b user: Elliot Peele <http://issues.rpath.com/> date: Fri, 30 May 2008 15:50:49 -0400 add support for some more elements committer: Elliot Peele <http://issues.rpath.com/> diff --git a/repomd/packagexml.py b/repomd/packagexml.py --- a/repomd/packagexml.py +++ b/repomd/packagexml.py @@ -56,6 +56,7 @@ sourcerpm = None headerStart = None headerEnd = None + licenseToConfirm = None def addChild(self, child): ''' @@ -115,7 +116,9 @@ self.headerEnd = node.getAttribute('end') elif node.getName() in ('rpm:provides', 'rpm:requires', 'rpm:obsoletes', 'rpm:recommends', - 'rpm:conflicts', 'suse:freshens'): + 'rpm:conflicts', 'suse:freshens', + 'rpm:enhances', 'rpm:supplements', + 'rpm:suggests', ): self.format.append(node) elif node.getName() == 'file': pass @@ -123,6 +126,8 @@ raise UnknownElementError(node) elif child.getName() == 'pkgfiles': pass + elif child.getName() == 'suse:license-to-confirm': + self.licenseToConfirm = child.finalize() else: raise UnknownElementError(child) @@ -195,6 +200,24 @@ ''' +class _RpmEnhances(_RpmRequires): + ''' + Parse rpm:enhances children. + ''' + + +class _RpmSupplements(_RpmRequires): + ''' + Parse rpm:supplements children. + ''' + + +class _RpmSuggests(_RpmRequires): + ''' + Parse rpm:suggests children. + ''' + + class _SuseFreshens(_RpmRequires): ''' Parse suse:freshens children. @@ -242,5 +265,14 @@ namespace='rpm') self._databinder.registerType(_RpmConflicts, name='conflicts', namespace='rpm') + self._databinder.registerType(_RpmEnhances, name='enhances', + namespace='rpm') + self._databinder.registerType(_RpmSupplements, name='supplements', + namespace='rpm') + self._databinder.registerType(_RpmSuggests, name='suggests', + namespace='rpm') self._databinder.registerType(_SuseFreshens, name='freshens', namespace='suse') + self._databinder.registerType(xmllib.StringNode, + name='license-to-confirm', + namespace='suse') From johnsonm at rpath.com Wed Aug 19 17:43:00 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:43:00 +0000 Subject: mirrorball: beginings of a script to compare source troves of two platforms for copying Message-ID: <200908192143.n7JLh0Zk023868@scc.eng.rpath.com> changeset: 24ab6b431b88 user: Elliot Peele <https://issues.rpath.com/> date: Wed, 13 May 2009 11:46:06 -0400 beginings of a script to compare source troves of two platforms for copying recipe modifications committer: Elliot Peele <https://issues.rpath.com/> diff --git a/scripts/compare b/scripts/compare new file mode 100755 --- /dev/null +++ b/scripts/compare @@ -0,0 +1,91 @@ +#!/usr/bin/python +# +# Copyright (c) 2009 rPath, Inc. +# +# This program is distributed under the terms of the Common Public License, +# version 1.0. A copy of this license should have been distributed with this +# source file in a file called LICENSE. If it is not present, the license +# is always available at http://www.rpath.com/permanent/licenses/CPL-1.0. +# +# This program is distributed in the hope that it will be useful, but +# without any warranty; without even the implied warranty of merchantability +# or fitness for a particular purpose. See the Common Public License for +# full details. +# + +""" +Compare source components between two platforms to help fill in modifications +for new versions of a platform. + +Usage: + +compare sles10 sles11 + +""" + +import os +import sys + +sys.path.insert(0, os.environ['HOME'] + '/hg/mirrorball') + +from conary.lib import util +sys.excepthook = util.genExcepthook() + +from updatebot import log +from updatebot import config +from updatebot import conaryhelper + +log.addRootLogger() + +import logging +slog = logging.getLogger('script') + +pfma = sys.argv[1] +pfmb = sys.argv[2] + +pfmPath = os.environ['HOME'] + '/hg/mirrorball/config/%s/updatebotrc' + +cfga = config.UpdateBotConfig() +cfga.read(pfmPath % pfma) + +cfgb = config.UpdateBotConfig() +cfgb.read(pfmPath % pfmb) + +helpera = conaryhelper.ConaryHelper(cfga) +helperb = conaryhelper.ConaryHelper(cfgb) + +slog.info('getting latest versions for %s' % pfma) +versionsa = helpera.getLatestVersions() + +slog.info('getting latest versions for %s' % pfmb) +versionsb = helperb.getLatestVersions() + +sourceNames = set() +for pkgname in versionsa.iterkeys(): + if pkgname.endswith(':source') and pkgname in versionsb: + name = pkgname.split(':')[0] + sourceNames.add(name) + +def checkPackage(name): + slog.info('comparing %s' % name) + coa = helpera._edit(name) + cob = helperb._edit(name) + + lsa = set(os.listdir(coa)) + lsb = set(os.listdir(cob)) + + if len(lsa) == len(lsb): + return + + for fn in lsa.difference(lsb): + slog.info('found new file %s in %s' % (fn, name)) + + return lsa.difference(lsb) + +res = {} +for source in sourceNames: + diffSet = checkPackage(source) + if diffSet: + res[source] = diffSet + +import epdb; epdb.st() From johnsonm at rpath.com Wed Aug 19 17:39:51 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:39:51 +0000 Subject: mirrorball: cleanup for pylint, move tests into testsuite Message-ID: <200908192139.n7JLdpC8016249@scc.eng.rpath.com> changeset: a0c5e1c0d097 user: Elliot Peele <http://issues.rpath.com/> date: Wed, 09 Jul 2008 14:28:30 -0400 cleanup for pylint, move tests into testsuite committer: Elliot Peele <http://issues.rpath.com/> diff --git a/rpmutils/__init__.py b/rpmutils/__init__.py --- a/rpmutils/__init__.py +++ b/rpmutils/__init__.py @@ -12,6 +12,10 @@ # full details. # +""" +Module that provides common utility functions for interacting with rpms. +""" + __ALL__ = ('rpmvercmp', 'readHeader', ) from rpmutils.vercmp import rpmvercmp diff --git a/rpmutils/vercmp.py b/rpmutils/vercmp.py --- a/rpmutils/vercmp.py +++ b/rpmutils/vercmp.py @@ -1,6 +1,26 @@ -#!/usr/bin/python +# +# Copyright (c) 2008 rPath, Inc. +# +# This program is distributed under the terms of the Common Public License, +# version 1.0. A copy of this license should have been distributed with this +# source file in a file called LICENSE. If it is not present, the license +# is always available at http://www.rpath.com/permanent/licenses/CPL-1.0. +# +# This program is distributed in the hope that it will be useful, but +# without any warranty; without even the implied warranty of merchantability +# or fitness for a particular purpose. See the Common Public License for +# full details. +# + +""" +Module that implements rpm version comparison. +""" def _rpmversplit(s): + """ + Split version strings. + """ + l = [] isNumericHunk = s[0].isdigit() @@ -24,6 +44,12 @@ return [ x for x in l if x ] def rpmvercmp(ver1string, ver2string): + """ + Compare rpm version strings. + """ + + # R0911 - Too many return statements + # pylint: disable-msg=R0911 ver1list = _rpmversplit(ver1string) ver2list = _rpmversplit(ver2string) @@ -52,39 +78,3 @@ return 1 return 0 - -if __name__ == '__main__': - def _test(a, b, expected): - result = rpmvercmp(a, b) - if result == -1: - print a, '<', b - elif result == 1: - print a, '>', b - else: - print a, '==', b - assert(result == expected) - - _test('1', '1', 0) - _test('1', '2', -1) - _test('2', '1', 1) - _test('a', 'a', 0) - _test('a', 'b', -1) - _test('b', 'a', 1) - _test('1.2', '1.3', -1) - _test('1.3', '1.1', 1) - _test('1.a', '1.a', 0) - _test('1.a', '1.b', -1) - _test('1.b', '1.a', 1) - _test('1.2+', '1.2', 0) - _test('1.0010', '1.0', 1) - _test('1.05', '1.5', 0) - _test('1.0', '1', 1) - _test('2.50', '2.5', 1) - _test('fc4', 'fc.4', 0) - _test('FC5', 'fc5', -1) - _test('2a', '2.0', -1) - _test('1.0', '1.fc4', 1) - _test('3.0.0_fc', '3.0.0.fc', 0) - _test('1++', '1_', 0) - _test('+', '_', -1) - _test('_', '+', -1) From johnsonm at rpath.com Wed Aug 19 17:41:53 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:41:53 +0000 Subject: mirrorball: move function to common code Message-ID: <200908192141.n7JLfsd1021213@scc.eng.rpath.com> changeset: 1f1797b2e2da user: Elliot Peele <https://issues.rpath.com/> date: Fri, 19 Dec 2008 14:33:10 -0500 move function to common code committer: Elliot Peele <https://issues.rpath.com/> diff --git a/updatebot/advisories/centos.py b/updatebot/advisories/centos.py --- a/updatebot/advisories/centos.py +++ b/updatebot/advisories/centos.py @@ -13,11 +13,9 @@ # import os -import time import pmap import logging -from updatebot.update import Updater from updatebot.advisories.common import BaseAdvisor log = logging.getLogger('updatebot.advisories') @@ -45,38 +43,6 @@ for msg in pmap.parse(url, backend=self._cfg.platformName): self._loadOne(msg, pkgCache) - def _getArchiveUrls(self): - """ - Compute archive urls to load. - """ - - base = self._cfg.listArchiveBaseUrl - startDate = self._cfg.listArchiveStartDate - timeTup = list(time.strptime(startDate, '%Y%m')) - - startYear = timeTup[0] - startMonth = timeTup[1] - - endYear = time.localtime()[0] - endMonth = time.localtime()[1] - - while timeTup[0] <= endYear: - if timeTup[0] != startYear: - timeTup[1] = 1 - - if timeTup[0] == endYear: - end = endMonth - else: - end = 12 - - while timeTup[1] <= end: - fname = time.strftime('%Y-%B.txt.gz', timeTup) - yield '/'.join([base, fname]) - - timeTup[1] += 1 - - timeTup[0] += 1 - def _loadOne(self, msg, pkgCache): """ Handles matching one message to any mentioned packages. diff --git a/updatebot/advisories/common.py b/updatebot/advisories/common.py --- a/updatebot/advisories/common.py +++ b/updatebot/advisories/common.py @@ -403,3 +403,35 @@ raise NoPackagesFoundForAdvisory(what=(nvf, srcPkg)) return res + + def _getArchiveUrls(self): + """ + Compute archive urls to load. + """ + + base = self._cfg.listArchiveBaseUrl + startDate = self._cfg.listArchiveStartDate + timeTup = list(time.strptime(startDate, '%Y%m')) + + startYear = timeTup[0] + startMonth = timeTup[1] + + endYear = time.localtime()[0] + endMonth = time.localtime()[1] + + while timeTup[0] <= endYear: + if timeTup[0] != startYear: + timeTup[1] = 1 + + if timeTup[0] == endYear: + end = endMonth + else: + end = 12 + + while timeTup[1] <= end: + fname = time.strftime('%Y-%B.txt.gz', timeTup) + yield '/'.join([base, fname]) + + timeTup[1] += 1 + + timeTup[0] += 1 From johnsonm at rpath.com Wed Aug 19 17:40:10 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:40:10 +0000 Subject: mirrorball: don't call child.getName() so much Message-ID: <200908192140.n7JLeANO017018@scc.eng.rpath.com> changeset: faae15ebdd80 user: Elliot Peele <http://issues.rpath.com/> date: Thu, 14 Aug 2008 14:46:27 -0400 don't call child.getName() so much committer: Elliot Peele <http://issues.rpath.com/> diff --git a/repomd/patchxml.py b/repomd/patchxml.py --- a/repomd/patchxml.py +++ b/repomd/patchxml.py @@ -40,32 +40,33 @@ # R0912 - Too many branches # pylint: disable-msg=R0912 - if child.getName() == 'yum:name': + n = child.getName() + if n == 'yum:name': self.name = child.finalize() - elif child.getName() == 'summary': + elif n == 'summary': if child.getAttribute('lang') == 'en': self.summary = child.finalize() - elif child.getName() == 'description': + elif n == 'description': if child.getAttribute('lang') == 'en': self.description = child.finalize() - elif child.getName() == 'yum:version': + elif n == 'yum:version': self.version = child.getAttribute('ver') self.release = child.getAttribute('rel') - elif child.getName() == 'rpm:requires': + elif n == 'rpm:requires': self.requires = child.getChildren('entry', namespace='rpm') - elif child.getName() == 'rpm:recommends': + elif n == 'rpm:recommends': self.recommends = child.getChildren('entry', namespace='rpm') - elif child.getName() == 'rpm:obsoletes': + elif n == 'rpm:obsoletes': self.obsoletes = child.getChildren('entry', namespace='rpm') - elif child.getName() == 'reboot-needed': + elif n == 'reboot-needed': self.rebootNeeded = True - elif child.getName() == 'license-to-confirm': + elif n == 'license-to-confirm': self.licenseToConfirm = child.finalize() - elif child.getName() == 'package-manager': + elif n == 'package-manager': self.packageManager = True - elif child.getName() == 'category': + elif n == 'category': self.category = child.finalize() - elif child.getName() == 'atoms': + elif n == 'atoms': self.packages = child.getChildren('package') else: raise UnknownElementError(child) @@ -108,12 +109,13 @@ Parse children of atoms element. """ - if child.getName() == 'package': + n = child.getName() + if n == 'package': child.type = child.getAttribute('type') xmllib.BaseNode.addChild(self, child) - elif child.getName() == 'message': + elif n == 'message': pass - elif child.getName() == 'script': + elif n == 'script': pass else: raise UnknownElementError(child) From johnsonm at rpath.com Wed Aug 19 17:41:29 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:41:29 +0000 Subject: mirrorball: refactor how the pkgCache works so that all required information is provided Message-ID: <200908192141.n7JLfTue020257@scc.eng.rpath.com> changeset: d64be11f76ed user: Elliot Peele <https://issues.rpath.com/> date: Mon, 17 Nov 2008 18:35:05 -0500 refactor how the pkgCache works so that all required information is provided to the advisory message committer: Elliot Peele <https://issues.rpath.com/> diff --git a/updatebot/advisories/centos.py b/updatebot/advisories/centos.py --- a/updatebot/advisories/centos.py +++ b/updatebot/advisories/centos.py @@ -17,6 +17,7 @@ import pmap import logging +from updatebot.update import Updater from updatebot.advisories.common import BaseAdvisor log = logging.getLogger('updatebot.advisories') @@ -30,33 +31,13 @@ object to patch object for a given platform into self._pkgMap. """ - def compLocation(pkg1, pkg2): - loc1 = pkg1.location.split('/') - loc1.pop(2) - loc2 = pkg2.location.split('/') - loc2.pop(2) - - if loc1 == loc2: - return True - return False - # Build data structure for looking up packages based on basename. pkgCache = {} for binPkg in self._pkgSource.binPkgMap: baseName = os.path.basename(binPkg.location) - if baseName in pkgCache: - if pkgCache[baseName] is binPkg: - log.info('found multiple instances of the same package in binPkgMap') - continue - elif pkgCache[baseName].location == binPkg.location: - log.info('found multiple package objects with the same location') - continue - elif compLocation(pkgCache[baseName], binPkg): - continue - else: - log.warn('found %s already in pkgCache' % baseName) - continue - pkgCache[baseName] = binPkg + if baseName not in pkgCache: + pkgCache[baseName] = set() + pkgCache[baseName].add(binPkg) # Fetch all of the archives and process them. for url in self._getArchiveUrls(): @@ -117,14 +98,11 @@ #log.warn('found %s in msg, but not in pkgCache' % pkgName) continue - binPkg = pkgCache[pkgName] - if binPkg not in self._pkgMap: - self._pkgMap[binPkg] = set() - self._pkgMap[binPkg].add(msg) - - if msg.packages is None: - msg.packages = set() - msg.packages.add(binPkg) + for binPkg in pkgCache[pkgName]: + if binPkg not in self._pkgMap: + self._pkgMap[binPkg] = set() + self._pkgMap[binPkg].add(msg) + msg.packages.add(binPkg) # Strip arch out of the subject for arch in self.supportedArches: From johnsonm at rpath.com Wed Aug 19 17:41:36 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:41:36 +0000 Subject: mirrorball: allow a advisory backend to raise an error if it does not expect there to be Message-ID: <200908192141.n7JLfa6u020497@scc.eng.rpath.com> changeset: 0fab215a27ae user: Elliot Peele <https://issues.rpath.com/> date: Tue, 18 Nov 2008 12:30:16 -0500 allow a advisory backend to raise an error if it does not expect there to be packages released that are not mentioned in an advisory committer: Elliot Peele <https://issues.rpath.com/> diff --git a/updatebot/advisories/common.py b/updatebot/advisories/common.py --- a/updatebot/advisories/common.py +++ b/updatebot/advisories/common.py @@ -33,6 +33,7 @@ from updatebot.errors import NoRecipientsFoundError from updatebot.errors import NoSenderFoundError from updatebot.errors import ProductNameNotDefinedError +from updatebot.errors import ExtraPackagesFoundInUpdateError class BaseAdvisory(object): """ @@ -193,6 +194,7 @@ """ _advisoryClass = BaseAdvisory + allowExtraPackages = False def __init__(self, cfg, pkgSource): self._cfg = cfg @@ -239,9 +241,13 @@ log.info('package not in updates repository %s' % binPkg) log.debug(binPkg.location) elif len(patches) > 0: + import epdb; epdb.st() log.info('found package not mentioned in advisory %s' % binPkg) log.debug(binPkg.location) + if not self.allowExtraPacakges: + raise ExtraPackagesFoundInUpdateError(pkg=binPkg, + src=srcPkg, advisory=list(patches)[0]) else: log.error('could not find advisory for %s' % binPkg) raise NoAdvisoryFoundError(why=binPkg) diff --git a/updatebot/advisories/sles.py b/updatebot/advisories/sles.py --- a/updatebot/advisories/sles.py +++ b/updatebot/advisories/sles.py @@ -19,6 +19,8 @@ log = logging.getLogger('updatebot.advisories') class Advisor(BaseAdvisor): + allowExtraPackages = True + def load(self): """ Parse the required data to generate a mapping of binary package diff --git a/updatebot/errors.py b/updatebot/errors.py --- a/updatebot/errors.py +++ b/updatebot/errors.py @@ -196,6 +196,16 @@ _params = ['what', ] _template = 'Could not find binary packages for %(what)s' +class ExtraPackagesFoundInUpdateError(AdvisoryError): + """ + ExtraPackagesFoundInUpdateError, raised when packages are found for an + update that are not mentioned in the advisory. + """ + + _params = ['pkg', 'src', 'advisory'] + _template = ('At least one (%(pkg)s) was found that is not mentioned in the' + ' advisory for %(src)s, %(advisory)s') + class MultipleAdvisoriesFoundError(AdvisoryError): """ MultipleAdvisoriesFoundError, raised when multiple advisories are found From johnsonm at rpath.com Wed Aug 19 17:41:53 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:41:53 +0000 Subject: mirrorball: switch to using xobj Message-ID: <200908192141.n7JLfrjg021179@scc.eng.rpath.com> changeset: ec83848e86dc user: Elliot Peele <https://issues.rpath.com/> date: Fri, 19 Dec 2008 14:31:56 -0500 switch to using xobj committer: Elliot Peele <https://issues.rpath.com/> diff --git a/aptmd/common.py b/aptmd/common.py --- a/aptmd/common.py +++ b/aptmd/common.py @@ -27,7 +27,7 @@ metadata. """ - __slots__ = ('name', 'arch', 'epoch', 'version', 'release') + _slots = ('name', 'arch', 'epoch', 'version', 'release') def __repr__(self): # Instance of 'BaseContainer' has no 'name' member diff --git a/aptmd/container.py b/aptmd/container.py --- a/aptmd/container.py +++ b/aptmd/container.py @@ -21,12 +21,12 @@ Base container class. """ - __slots__ = ('_data', ) + _slots = ('_data', ) def __init__(self): for cls in self.__class__.__mro__: - if hasattr(cls, '__slots__'): - for item in cls.__slots__: + if hasattr(cls, '_slots'): + for item in cls._slots: setattr(self, item, None) self._data = {} diff --git a/aptmd/packages.py b/aptmd/packages.py --- a/aptmd/packages.py +++ b/aptmd/packages.py @@ -23,8 +23,8 @@ Package container class. """ - __slots__ = ('source', 'sourceVersion', 'location', 'summary', - 'description') + _slots = ('source', 'sourceVersion', 'location', 'summary', + 'description') class PackagesParser(BaseParser): diff --git a/aptmd/sources.py b/aptmd/sources.py --- a/aptmd/sources.py +++ b/aptmd/sources.py @@ -25,7 +25,7 @@ Container for source data. """ - __slots__ = ('binaries', 'directory', 'files') + _slots = ('binaries', 'directory', 'files') class SourcesParser(BaseParser): diff --git a/pmap/centos.py b/pmap/centos.py --- a/pmap/centos.py +++ b/pmap/centos.py @@ -28,7 +28,7 @@ Container class for CentOS advisories. """ - __slots__ = ('discard', 'archs', 'header', 'pkgs', 'upstreamAdvisoryUrl', ) + _slots = ('discard', 'archs', 'header', 'pkgs', 'upstreamAdvisoryUrl', ) def finalize(self): """ diff --git a/pmap/common.py b/pmap/common.py --- a/pmap/common.py +++ b/pmap/common.py @@ -28,8 +28,8 @@ Base MBox Message container class. """ - __slots__ = ('fromAddr', 'fromName', 'timestamp', 'subject', 'msg', - 'description', 'summary', 'packages') + _slots = ('fromAddr', 'fromName', 'timestamp', 'subject', 'msg', + 'description', 'summary', 'packages') def __repr__(self): return self.subject diff --git a/pmap/ubuntu.py b/pmap/ubuntu.py --- a/pmap/ubuntu.py +++ b/pmap/ubuntu.py @@ -24,7 +24,7 @@ Ubuntu specific container class. """ - __slots__ = ('pkgs', 'pkgNameVersion') + _slots = ('pkgs', 'pkgNameVersion') def finalize(self): BaseContainer.finalize(self) From johnsonm at rpath.com Wed Aug 19 17:42:15 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:42:15 +0000 Subject: mirrorball: add support for version of repomd used by fedora Message-ID: <200908192142.n7JLgFZj022047@scc.eng.rpath.com> changeset: d7a00d1e0ba9 user: Elliot Peele <https://issues.rpath.com/> date: Thu, 05 Feb 2009 23:52:33 -0500 add support for version of repomd used by fedora committer: Elliot Peele <https://issues.rpath.com/> diff --git a/repomd/repomdxml.py b/repomd/repomdxml.py --- a/repomd/repomdxml.py +++ b/repomd/repomdxml.py @@ -32,6 +32,8 @@ Python representation of repomd.xml from the repository metadata. """ + __slots__ = ('revision', ) + def addChild(self, child): """ Parse children of repomd element. @@ -40,7 +42,10 @@ # W0212 - Access to a protected member _parser of a client class # pylint: disable-msg=W0212 - if child.getName() == 'data': + name = child.getName() + if name == 'revision': + self.revision = child.finalize() + elif name == 'data': child.type = child.getAttribute('type') if child.type == 'patches': child._parser = PatchesXml(None, child.location) @@ -80,7 +85,7 @@ Parser for repomd.xml data elements. """ __slots__ = ('location', 'checksum', 'checksumType', 'timestamp', - 'openChecksum', 'openChecksumType') + 'openChecksum', 'openChecksumType', 'databaseVersion', ) # All attributes are defined in __init__ by iterating over __slots__, # this confuses pylint. @@ -92,16 +97,19 @@ Parse children of data element. """ - if child.getName() == 'location': + name = child.getName() + if name == 'location': self.location = child.getAttribute('href') - elif child.getName() == 'checksum': + elif name == 'checksum': self.checksum = child.finalize() self.checksumType = child.getAttribute('type') - elif child.getName() == 'timestamp': + elif name == 'timestamp': self.timestamp = child.finalize() - elif child.getName() == 'open-checksum': + elif name == 'open-checksum': self.openChecksum = child.finalize() self.openChecksumType = child.getAttribute('type') + elif name == 'database_version': + self.databaseVersion = child.finalize() else: raise UnknownElementError(child) @@ -118,6 +126,8 @@ self._databinder.registerType(_RepoMd, name='repomd') self._databinder.registerType(_RepoMdDataElement, name='data') + self._databinder.registerType(xmllib.StringNode, name='revision') self._databinder.registerType(xmllib.StringNode, name='checksum') self._databinder.registerType(xmllib.IntegerNode, name='timestamp') self._databinder.registerType(xmllib.StringNode, name='open-checksum') + self._databinder.registerType(xmllib.StringNode, name='database_version') From johnsonm at rpath.com Wed Aug 19 17:39:06 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:39:06 +0000 Subject: mirrorball: cleaup pylint Message-ID: <200908192139.n7JLd60S014504@scc.eng.rpath.com> changeset: 2f0ae71db072 user: Elliot Peele <http://issues.rpath.com/> date: Mon, 02 Jun 2008 22:52:13 -0400 cleaup pylint add exception for var not defined in __init__, all variables are defined in __slots__ and __init__ iterates over the contents of __slots__ setting them all to None. committer: Elliot Peele <http://issues.rpath.com/> diff --git a/repomd/packagexml.py b/repomd/packagexml.py --- a/repomd/packagexml.py +++ b/repomd/packagexml.py @@ -36,6 +36,11 @@ 'sourcerpm', 'headerStart', 'headerEnd', 'licenseToConfirm') + # All attributes are defined in __init__ by iterating over __slots__, + # this confuses pylint. + # W0201 - Attribute $foo defined outside __init__ + # pylint: disable-msg=W0201 + def addChild(self, child): """ Parse children of package element. diff --git a/repomd/patchesxml.py b/repomd/patchesxml.py --- a/repomd/patchesxml.py +++ b/repomd/patchesxml.py @@ -61,6 +61,11 @@ """ __slots__ = ('id', 'checksum', 'checksumType', 'location') + # All attributes are defined in __init__ by iterating over __slots__, + # this confuses pylint. + # W0201 - Attribute $foo defined outside __init__ + # pylint: disable-msg=W0201 + def addChild(self, child): """ Parse children of patch element. diff --git a/repomd/patchxml.py b/repomd/patchxml.py --- a/repomd/patchxml.py +++ b/repomd/patchxml.py @@ -23,6 +23,11 @@ 'licenseToConfirm', 'packageManager', 'category', 'packages') + # All attributes are defined in __init__ by iterating over __slots__, + # this confuses pylint. + # W0201 - Attribute $foo defined outside __init__ + # pylint: disable-msg=W0201 + def addChild(self, child): """ Parse children of patch element. diff --git a/repomd/repomdxml.py b/repomd/repomdxml.py --- a/repomd/repomdxml.py +++ b/repomd/repomdxml.py @@ -78,6 +78,11 @@ __slots__ = ('location', 'checksum', 'checksumType', 'timestamp', 'openChecksum', 'openChecksumType') + # All attributes are defined in __init__ by iterating over __slots__, + # this confuses pylint. + # W0201 - Attribute $foo defined outside __init__ + # pylint: disable-msg=W0201 + def addChild(self, child): """ Parse children of data element. diff --git a/repomd/xmlcommon.py b/repomd/xmlcommon.py --- a/repomd/xmlcommon.py +++ b/repomd/xmlcommon.py @@ -16,7 +16,7 @@ Base module for common super classes for repomd. """ -__all__ = ('XmlFileParser', ) +__all__ = ('XmlFileParser', 'SlotNode') from rpath_common.xmllib import api1 as xmllib @@ -65,6 +65,10 @@ class SlotNode(xmllib.BaseNode): + """ + XML node class that initializes all __slots__ entries to None. + """ + __slots__ = () def __init__(self, *args, **kw): From johnsonm at rpath.com Wed Aug 19 17:41:57 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:41:57 +0000 Subject: mirrorball: clean ups Message-ID: <200908192141.n7JLfvJI021366@scc.eng.rpath.com> changeset: b2d0db25dcf8 user: Elliot Peele <https://issues.rpath.com/> date: Sun, 11 Jan 2009 23:14:31 -0500 clean ups committer: Elliot Peele <https://issues.rpath.com/> diff --git a/scripts/validatemanifests.py b/scripts/validatemanifests.py --- a/scripts/validatemanifests.py +++ b/scripts/validatemanifests.py @@ -17,12 +17,8 @@ import sys import logging -sys.path.insert(0, os.environ['HOME'] + '/hg/26/epdb') sys.path.insert(0, os.environ['HOME'] + '/hg/26/xobj/py') -sys.path.insert(0, os.environ['HOME'] + '/hg/26/rmake') -sys.path.insert(0, os.environ['HOME'] + '/hg/26/conary') sys.path.insert(0, os.environ['HOME'] + '/hg/26/mirrorball') -sys.path.insert(0, os.environ['HOME'] + '/hg/26/rpath-xmllib') from conary.lib import util sys.excepthook = util.genExcepthook() @@ -52,7 +48,7 @@ not n.startswith('info-') and not n.startswith('factory-') and not n in cfg.excludePackages): - #v = helper.getLatestSourceVersion(n) + v = helper.getLatestSourceVersion(n) pkgs.append((n, v.getSourceVersion())) changed = {} @@ -71,41 +67,7 @@ changed[pkg] = srcPkg continue - srcPkg = map[key] - #manifest = b._updater._getManifestFromPkgSource(srcPkg) - #repoManifest = b._updater._conaryhelper.getManifest(pkg) - - #if len(manifest) == len(repoManifest): - # offt = 0 - # for i in range(len(manifest)): - # index = i - offt - # if manifest[index] == repoManifest[index]: - # manifest.pop(index) - # repoManifest.pop(index) - # offt += 1 - - #if not manifest and not repoManifest: - # changed[pkg] = srcPkg - # continue - - #if manifest != repoManifest: - # removed = [] - # for item in repoManifest: - # if item in manifest: - # manifest.remove(item) - # removed.append(item) - - # for item in removed: - # repoManifest.remove(item) - - # debug = False - # for item in manifest: - # if 'universe' in item: - # log.info('%s: new packages found in universe' % pkg) - # else: - # debug = True - - changed[pkg] = srcPkg + changed[pkg] = map[key] #import epdb; epdb.st() @@ -116,12 +78,15 @@ helper.setManifest(pkg, manifest) metadata = b._updater._getMetadataFromPkgSource(srcPkg) helper.setMetadata(pkg, metadata) -# helper.commit(pkg, commitMessage=cfg.commitMessage) - toBuild.add((pkg, cfg.topSourceGroup[1], None)) new = helper.getMetadata(pkg) + assert metadata == new - assert metadata == new + newManifest = helper.getManifest(pkg) + assert manifest == newManifest + + helper.commit(pkg, commitMessage=cfg.commitMessage) + toBuild.add((pkg, cfg.topSourceGroup[1], None)) import epdb; epdb.st() From johnsonm at rpath.com Wed Aug 19 17:41:17 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:41:17 +0000 Subject: mirrorball: add support for parsing centos advisories Message-ID: <200908192141.n7JLfHDc019778@scc.eng.rpath.com> changeset: 136d09f811a3 user: Elliot Peele <https://issues.rpath.com/> date: Fri, 07 Nov 2008 16:19:02 -0500 add support for parsing centos advisories committer: Elliot Peele <https://issues.rpath.com/> diff --git a/pmap/centos.py b/pmap/centos.py --- a/pmap/centos.py +++ b/pmap/centos.py @@ -12,14 +12,79 @@ # full details. # +import re +import logging +log = logging.getLogger('pmap.centos') + from pmap.common import BaseParser from pmap.common import BaseContainer -class Container(BaseContainer): - pass +class CentOSAdvisory(BaseContainer): + __slots__ = ('discard', 'archs', 'header', 'pkgs', 'upstreamAdvisoryUrl', ) + + def __repr__(self): + return self.subject + class Parser(BaseParser): def __init__(self): BaseParser.__init__(self) - self._containerClass = Container + self._containerClass = CentOSAdvisory + self._states.update({ + 'rhnurl' : self._rhnurl, + 'updates' : self._updates, + 'supportedarch' : self._supportedarch, + 'unsupportedarch' : self._unsupportedarch, + 'centos' : self._header, + 'upstream' : self._rhnurl, + 'sha1' : self._sha1, + }) + + self._supportedArchRE = '(src|i386|i686|x86_64)' + self._suparch = re.compile(self._supportedArchRE) + + self._filter('^.*rhn\.redhat\.com.*$', 'rhnurl') + self._filter('^updates.*', 'updates') + self._filter('^%s' % self._supportedArchRE, 'supportedarch') + self._filter('(ia64|s390|s390x)', 'unsupported') + self._filter('^[a-z0-9]{32}$', 'sha1') + + def _newContainer(self): + if self._curObj and self._curObj.discard: + self._curObj = None + BaseParser._newContainer(self) + + def _discard(self, force=False): + if self._curObj.discard is None: + log.debug('discarding message: %s' % self._curObj.subject) + self._curObj.discard = True + + def _addPkg(self, pkg): + if self._curObj.pkgs is None: + self._curObj.pkgs = set() + self._curObj.pkgs.add(pkg) + + def _rhnurl(self): + line = self._getFullLine() + self._curObj.upstreamAdvisoryUrl = line[line.find('http'):line.find('html')+4] + + def _updates(self): + pkg = self._getFullLine().split('/')[-1] + self._addPkg(pkg) + + def _supportedarch(self): + self._curObj.discard = False + if self._curObj.archs is None: + self._curObj.archs = set() + self._curObj.archs.add(self._line[0]) + + def _unsupportedarch(self): + self._discard() + + def _header(self): + if self._line[1] == 'Errata' and self._line[3] == 'Security': + self._curObj.header = self._getFullLine() + + def _sha1(self): + self._addPkg(self._line[-1]) From johnsonm at rpath.com Wed Aug 19 17:39:13 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:39:13 +0000 Subject: mirrorball: add method for retrieving conary config object Message-ID: <200908192139.n7JLdDQt014768@scc.eng.rpath.com> changeset: 59c4ce5cccca user: Elliot Peele <http://issues.rpath.com/> date: Tue, 10 Jun 2008 16:28:51 -0400 add method for retrieving conary config object import performance of parsing data from conary repo committer: Elliot Peele <http://issues.rpath.com/> diff --git a/updatebot/conaryhelper.py b/updatebot/conaryhelper.py --- a/updatebot/conaryhelper.py +++ b/updatebot/conaryhelper.py @@ -43,22 +43,35 @@ self._client = conaryclient.ConaryClient(self._ccfg) self._repos = self._client.getRepos() - def getSourceTroves(self, group=None): + def getConaryConfig(self): + """ + Get a conary config instance. + @return conary configuration object + """ + + return self._ccfg + + def getSourceTroves(self, group): """ Find all of the source troves included in group. If group is None use the top level group config option. - @param group: optional argument to specify the group to query + @param group: group to query @type group: None or troveTuple (name, versionStr, flavorStr) @return set of source trove specs """ - if not group: - group = self._cfg.topGroup - trvlst = self._repos.findTrove(self._ccfg.buildLabel, group) + latest = self._findLatest(trvlst) + + # Magic number should probably be a config option. + # 2 here is the number of flavors expected. + if len(latest) != 2: + raise TooManyFlavorsFoundError(why=latest) + srcTrvs = set() - for trv in self._findLatest(trvlst): + for trv in latest: + log.info('querying %s for source troves' % (trv, )) srcTrvs.update(self._getSourceTroves(trv)) return srcTrvs @@ -98,14 +111,15 @@ cl = [ (name, (None, None), (version, flavor), True) ] cs = self._client.createChangeSet(cl, withFiles=False, withFileContents=False, - skipNotByDefault=False) + recurse=False) topTrove = self._getTrove(cs, name, version, flavor) srcTrvs = set() - for n, v, f in topTrove.iterTroveList(weakRefs=True): - trv = self._getTrove(cs, n, v, f) - srcTrvs.add((trv.getSourceName(), v.getSourceVersion(), None)) + sources = self._repos.getTroveInfo(trove._TROVEINFO_TAG_SOURCENAME, + list(topTrove.iterTroveList(weakRefs=True))) + for i, (n, v, f) in enumerate(topTrove.iterTroveList(weakRefs=True)): + srcTrvs.add((sources[i](), v.getSourceVersion(), None)) return srcTrvs @@ -124,6 +138,7 @@ @return conary.trove.Trove object """ + #log.debug('getting trove for (%s, %s, %s)' % (name, version, flavor)) troveCs = cs.getNewTroveVersion(name, version, flavor) trv = trove.Trove(troveCs, skipIntegrityChecks=True) return trv From johnsonm at rpath.com Wed Aug 19 17:40:57 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:40:57 +0000 Subject: mirrorball: branch merge Message-ID: <200908192140.n7JLevLl018958@scc.eng.rpath.com> changeset: ab76a98441ea user: Elliot Peele <https://issues.rpath.com/> date: Fri, 26 Sep 2008 15:53:05 -0400 branch merge committer: Elliot Peele <https://issues.rpath.com/> diff --git a/scripts/promote-ubuntu.sh b/scripts/promote-ubuntu.sh --- a/scripts/promote-ubuntu.sh +++ b/scripts/promote-ubuntu.sh @@ -13,24 +13,28 @@ # full details. # -cfg="--config-file $HOME/hg/mirrorball/config/ubuntu/conaryrc -m promote" +cfg="--config-file $HOME/hg/mirrorball/config/ubuntu/conaryrc -m promote --interactive" -#time cvc promote $cfg \ -#{{auto,build,c,}package,deb-import,factory-deb}:source=ubuntu.rb.rpath.com at rpath:ubuntu-devel \ -# /ubuntu.rb.rpath.com at rpath:ubuntu-devel--/ubuntu.rpath.org at rpath:ubuntu-devel +date +time cvc promote $cfg \ +{{auto,build,c,}package,deb-import,factory-deb}:source=ubuntu.rpath.org at rpath:ubuntu-devel \ + /ubuntu.rpath.org at rpath:ubuntu-devel--/ubuntu.rpath.com at rpath:ubuntu-devel -#time cvc promote $cfg \ -# group-appliance:source=ubuntu.rb.rpath.com at rpath:ubuntu-hardy-devel \ -# platform-definition:source=ubuntu.rb.rpath.com at rpath:ubuntu-hardy-devel \ -# group-os=ubuntu.rb.rpath.com at rpath:ubuntu-hardy-devel \ -# ubuntu.rb.rpath.com at rpath:ubuntu-hardy-devel--ubuntu.rpath.org at rpath:ubuntu-hardy-devel \ - +date time cvc promote $cfg \ group-appliance:source=ubuntu.rpath.org at rpath:ubuntu-hardy-devel \ platform-definition:source=ubuntu.rpath.org at rpath:ubuntu-hardy-devel \ group-os=ubuntu.rpath.org at rpath:ubuntu-hardy-devel \ - /ubuntu.rpath.org at rpath:ubuntu-hardy-devel--/ubuntu.rpath.org at rpath:ubuntu-hardy \ - /ubuntu.rpath.org at rpath:ubuntu-devel//ubuntu-hardy-devel--/ubuntu.rpath.org at rpath:ubuntu-devel//ubuntu-hardy \ - /conary.rpath.com at rpl:devel//conary.rpath.com at rpl:2--/ubuntu.rpath.org at rpath:ubuntu-hardy \ - /conary.rpath.com at rpl:devel//conary.rpath.com at rpl:2//ubuntu.rpath.org at rpath:ubuntu-hardy-devel--/ubuntu.rpath.org at rpath:ubuntu-hardy \ - /conary.rpath.com at rpl:devel//ubuntu.rpath.org at rpath:ubuntu-hardy-devel--/ubuntu.rpath.org at rpath:ubuntu-hardy + ubuntu.rpath.org at rpath:ubuntu-hardy-devel--ubuntu.rpath.com at rpath:ubuntu-hardy-devel \ + +date +time cvc promote $cfg \ + group-appliance:source=ubuntu.rpath.com at rpath:ubuntu-hardy-devel \ + platform-definition:source=ubuntu.rpath.com at rpath:ubuntu-hardy-devel \ + group-os=ubuntu.rpath.com at rpath:ubuntu-hardy-devel \ + /ubuntu.rpath.com at rpath:ubuntu-hardy-devel--/ubuntu.rpath.com at rpath:ubuntu-hardy \ + /ubuntu.rpath.com at rpath:ubuntu-devel//ubuntu-hardy-devel--/ubuntu.rpath.com at rpath:ubuntu-devel//ubuntu-hardy \ + /conary.rpath.com at rpl:devel//conary.rpath.com at rpl:2--/ubuntu.rpath.com at rpath:ubuntu-hardy \ + /conary.rpath.com at rpl:devel//conary.rpath.com at rpl:2//ubuntu.rpath.com at rpath:ubuntu-hardy-devel--/ubuntu.rpath.com at rpath:ubuntu-hardy \ + /conary.rpath.com at rpl:devel//ubuntu.rpath.com at rpath:ubuntu-hardy-devel--/ubuntu.rpath.com at rpath:ubuntu-hardy \ + /ubuntu.rpath.org at rpath:ubuntu-devel//ubuntu.rpath.com at rpath:ubuntu-hardy-devel--/ubuntu.rpath.com at rpath:ubuntu-hardy From johnsonm at rpath.com Wed Aug 19 17:40:57 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:40:57 +0000 Subject: mirrorball: branch merge Message-ID: <200908192140.n7JLev3n018975@scc.eng.rpath.com> changeset: e6ebcc59ee4f user: Elliot Peele <https://issues.rpath.com/> date: Fri, 26 Sep 2008 16:35:03 -0400 branch merge committer: Elliot Peele <https://issues.rpath.com/> diff --git a/scripts/promote-ubuntu.sh b/scripts/promote-ubuntu.sh --- a/scripts/promote-ubuntu.sh +++ b/scripts/promote-ubuntu.sh @@ -13,24 +13,28 @@ # full details. # -cfg="--config-file $HOME/hg/mirrorball/config/ubuntu/conaryrc -m promote" +cfg="--config-file $HOME/hg/mirrorball/config/ubuntu/conaryrc -m promote --interactive" -#time cvc promote $cfg \ -#{{auto,build,c,}package,deb-import,factory-deb}:source=ubuntu.rb.rpath.com at rpath:ubuntu-devel \ -# /ubuntu.rb.rpath.com at rpath:ubuntu-devel--/ubuntu.rpath.org at rpath:ubuntu-devel +date +time cvc promote $cfg \ +{{auto,build,c,}package,deb-import,factory-deb}:source=ubuntu.rpath.org at rpath:ubuntu-devel \ + /ubuntu.rpath.org at rpath:ubuntu-devel--/ubuntu.rpath.com at rpath:ubuntu-devel -#time cvc promote $cfg \ -# group-appliance:source=ubuntu.rb.rpath.com at rpath:ubuntu-hardy-devel \ -# platform-definition:source=ubuntu.rb.rpath.com at rpath:ubuntu-hardy-devel \ -# group-os=ubuntu.rb.rpath.com at rpath:ubuntu-hardy-devel \ -# ubuntu.rb.rpath.com at rpath:ubuntu-hardy-devel--ubuntu.rpath.org at rpath:ubuntu-hardy-devel \ - +date time cvc promote $cfg \ group-appliance:source=ubuntu.rpath.org at rpath:ubuntu-hardy-devel \ platform-definition:source=ubuntu.rpath.org at rpath:ubuntu-hardy-devel \ group-os=ubuntu.rpath.org at rpath:ubuntu-hardy-devel \ - /ubuntu.rpath.org at rpath:ubuntu-hardy-devel--/ubuntu.rpath.org at rpath:ubuntu-hardy \ - /ubuntu.rpath.org at rpath:ubuntu-devel//ubuntu-hardy-devel--/ubuntu.rpath.org at rpath:ubuntu-devel//ubuntu-hardy \ - /conary.rpath.com at rpl:devel//conary.rpath.com at rpl:2--/ubuntu.rpath.org at rpath:ubuntu-hardy \ - /conary.rpath.com at rpl:devel//conary.rpath.com at rpl:2//ubuntu.rpath.org at rpath:ubuntu-hardy-devel--/ubuntu.rpath.org at rpath:ubuntu-hardy \ - /conary.rpath.com at rpl:devel//ubuntu.rpath.org at rpath:ubuntu-hardy-devel--/ubuntu.rpath.org at rpath:ubuntu-hardy + ubuntu.rpath.org at rpath:ubuntu-hardy-devel--ubuntu.rpath.com at rpath:ubuntu-hardy-devel \ + +date +time cvc promote $cfg \ + group-appliance:source=ubuntu.rpath.com at rpath:ubuntu-hardy-devel \ + platform-definition:source=ubuntu.rpath.com at rpath:ubuntu-hardy-devel \ + group-os=ubuntu.rpath.com at rpath:ubuntu-hardy-devel \ + /ubuntu.rpath.com at rpath:ubuntu-hardy-devel--/ubuntu.rpath.com at rpath:ubuntu-hardy \ + /ubuntu.rpath.com at rpath:ubuntu-devel//ubuntu-hardy-devel--/ubuntu.rpath.com at rpath:ubuntu-devel//ubuntu-hardy \ + /conary.rpath.com at rpl:devel//conary.rpath.com at rpl:2--/ubuntu.rpath.com at rpath:ubuntu-hardy \ + /conary.rpath.com at rpl:devel//conary.rpath.com at rpl:2//ubuntu.rpath.com at rpath:ubuntu-hardy-devel--/ubuntu.rpath.com at rpath:ubuntu-hardy \ + /conary.rpath.com at rpl:devel//ubuntu.rpath.com at rpath:ubuntu-hardy-devel--/ubuntu.rpath.com at rpath:ubuntu-hardy \ + /ubuntu.rpath.org at rpath:ubuntu-devel//ubuntu.rpath.com at rpath:ubuntu-hardy-devel--/ubuntu.rpath.com at rpath:ubuntu-hardy From johnsonm at rpath.com Wed Aug 19 17:40:56 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:40:56 +0000 Subject: mirrorball: flatten more branches Message-ID: <200908192140.n7JLeuJD018941@scc.eng.rpath.com> changeset: 0ec573e2c149 user: Elliot Peele <https://issues.rpath.com/> date: Fri, 26 Sep 2008 15:52:45 -0400 flatten more branches committer: Elliot Peele <https://issues.rpath.com/> diff --git a/scripts/promote-ubuntu.sh b/scripts/promote-ubuntu.sh --- a/scripts/promote-ubuntu.sh +++ b/scripts/promote-ubuntu.sh @@ -13,24 +13,28 @@ # full details. # -cfg="--config-file $HOME/hg/mirrorball/config/ubuntu/conaryrc -m promote" +cfg="--config-file $HOME/hg/mirrorball/config/ubuntu/conaryrc -m promote --interactive" -#time cvc promote $cfg \ -#{{auto,build,c,}package,deb-import,factory-deb}:source=ubuntu.rb.rpath.com at rpath:ubuntu-devel \ -# /ubuntu.rb.rpath.com at rpath:ubuntu-devel--/ubuntu.rpath.org at rpath:ubuntu-devel +date +time cvc promote $cfg \ +{{auto,build,c,}package,deb-import,factory-deb}:source=ubuntu.rpath.org at rpath:ubuntu-devel \ + /ubuntu.rpath.org at rpath:ubuntu-devel--/ubuntu.rpath.com at rpath:ubuntu-devel -#time cvc promote $cfg \ -# group-appliance:source=ubuntu.rb.rpath.com at rpath:ubuntu-hardy-devel \ -# platform-definition:source=ubuntu.rb.rpath.com at rpath:ubuntu-hardy-devel \ -# group-os=ubuntu.rb.rpath.com at rpath:ubuntu-hardy-devel \ -# ubuntu.rb.rpath.com at rpath:ubuntu-hardy-devel--ubuntu.rpath.org at rpath:ubuntu-hardy-devel \ - +date time cvc promote $cfg \ group-appliance:source=ubuntu.rpath.org at rpath:ubuntu-hardy-devel \ platform-definition:source=ubuntu.rpath.org at rpath:ubuntu-hardy-devel \ group-os=ubuntu.rpath.org at rpath:ubuntu-hardy-devel \ - /ubuntu.rpath.org at rpath:ubuntu-hardy-devel--/ubuntu.rpath.org at rpath:ubuntu-hardy \ - /ubuntu.rpath.org at rpath:ubuntu-devel//ubuntu-hardy-devel--/ubuntu.rpath.org at rpath:ubuntu-devel//ubuntu-hardy \ - /conary.rpath.com at rpl:devel//conary.rpath.com at rpl:2--/ubuntu.rpath.org at rpath:ubuntu-hardy \ - /conary.rpath.com at rpl:devel//conary.rpath.com at rpl:2//ubuntu.rpath.org at rpath:ubuntu-hardy-devel--/ubuntu.rpath.org at rpath:ubuntu-hardy \ - /conary.rpath.com at rpl:devel//ubuntu.rpath.org at rpath:ubuntu-hardy-devel--/ubuntu.rpath.org at rpath:ubuntu-hardy + ubuntu.rpath.org at rpath:ubuntu-hardy-devel--ubuntu.rpath.com at rpath:ubuntu-hardy-devel \ + +date +time cvc promote $cfg \ + group-appliance:source=ubuntu.rpath.com at rpath:ubuntu-hardy-devel \ + platform-definition:source=ubuntu.rpath.com at rpath:ubuntu-hardy-devel \ + group-os=ubuntu.rpath.com at rpath:ubuntu-hardy-devel \ + /ubuntu.rpath.com at rpath:ubuntu-hardy-devel--/ubuntu.rpath.com at rpath:ubuntu-hardy \ + /ubuntu.rpath.com at rpath:ubuntu-devel//ubuntu-hardy-devel--/ubuntu.rpath.com at rpath:ubuntu-devel//ubuntu-hardy \ + /conary.rpath.com at rpl:devel//conary.rpath.com at rpl:2--/ubuntu.rpath.com at rpath:ubuntu-hardy \ + /conary.rpath.com at rpl:devel//conary.rpath.com at rpl:2//ubuntu.rpath.com at rpath:ubuntu-hardy-devel--/ubuntu.rpath.com at rpath:ubuntu-hardy \ + /conary.rpath.com at rpl:devel//ubuntu.rpath.com at rpath:ubuntu-hardy-devel--/ubuntu.rpath.com at rpath:ubuntu-hardy \ + /ubuntu.rpath.org at rpath:ubuntu-devel//ubuntu.rpath.com at rpath:ubuntu-hardy-devel--/ubuntu.rpath.com at rpath:ubuntu-hardy From johnsonm at rpath.com Wed Aug 19 17:40:30 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:40:30 +0000 Subject: mirrorball: add script for getting a list binaries on a label that should go in a group Message-ID: <200908192140.n7JLeUOt017835@scc.eng.rpath.com> changeset: cf560dbda861 user: Elliot Peele <http://issues.rpath.com/> date: Tue, 26 Aug 2008 19:52:26 -0400 add script for getting a list binaries on a label that should go in a group committer: Elliot Peele <http://issues.rpath.com/> diff --git a/scripts/findbinaries.py b/scripts/findbinaries.py new file mode 100644 --- /dev/null +++ b/scripts/findbinaries.py @@ -0,0 +1,94 @@ +#!/usr/bin/python +# +# Copyright (c) 2008 rPath, Inc. +# +# This program is distributed under the terms of the Common Public License, +# version 1.0. A copy of this license should have been distributed with this +# source file in a file called LICENSE. If it is not present, the license +# is always available at http://www.rpath.com/permanent/licenses/CPL-1.0. +# +# This program is distributed in the hope that it will be useful, but +# without any warranty; without even the implied warranty of merchantability +# or fitness for a particular purpose. See the Common Public License for +# full details. +# + +import os +import sys + +sys.path.insert(0, os.environ['HOME'] + '/hg/rpath-xmllib') +sys.path.insert(0, os.environ['HOME'] + '/hg/conary') +sys.path.insert(0, os.environ['HOME'] + '/hg/mirrorball') + +import logging + +from updatebot import log +from updatebot import bot +from updatebot import config +from updatebot import conaryhelper + +log.addRootLogger() + +slog = logging.getLogger('findbinaries') + +cfg = config.UpdateBotConfig() +cfg.read(os.environ['HOME'] + '/hg/mirrorball/config/ubuntu/updatebotrc') + +bot = bot.Bot(cfg) +bot._populatePkgSource() + +updater = bot._updater +helper = updater._conaryhelper + +sources, failed = updater.create(cfg.package, buildAll=True) + +pkgs = helper._repos.getTroveLeavesByLabel({None: {helper._ccfg.buildLabel: None}}) +pkgSet = set([ x.split(':')[0] for x in pkgs ]) + +#import epdb; epdb.st() + +srcDict = {} +for pkg in pkgSet: + if pkg not in pkgs: + slog.warn('skipping %s, not in packages' % pkg) + continue + version = pkgs[pkg].keys()[0] + flavors = pkgs[pkg][version] + if len(flavors) == 0: + flavor = None + else: + flavor = flavors[0] + + slog.info('getting binaries for %s' % pkg) + for src, binSet in helper._getSourceTroves((pkg, version, flavor)).iteritems(): + src = (src[0].split(':')[0], src[1], src[2]) + if src not in sources: + slog.warn('skipping %s, it not in souces' % src[0]) + continue + + if src not in srcDict: + srcDict[src] = set() + + for bin in binSet: + name = bin[0].split(':')[0] + srcDict[src].add((name, bin[1], bin[2])) + +bins = set() +for src, binSet in srcDict.iteritems(): + latest = None + for bin in binSet: + if not latest: + latest = bin[1] + + if bin[1] > latest: + latest = bin[1] + + for bin in binSet: + if bin[1] == latest: + bins.add(bin[0]) + +binLst = list(bins) +binLst.sort() + +for item in binLst: + print ' ' * 12, '\'%s\',' % item From johnsonm at rpath.com Wed Aug 19 17:43:08 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:43:08 +0000 Subject: mirrorball: add support for scientific advisories Message-ID: <200908192143.n7JLh8AE024226@scc.eng.rpath.com> changeset: e1fb7af74137 user: Elliot Peele <https://issues.rpath.com/> date: Thu, 30 Jul 2009 15:27:48 -0400 add support for scientific advisories committer: Elliot Peele <https://issues.rpath.com/> diff --git a/updatebot/advisories/__init__.py b/updatebot/advisories/__init__.py --- a/updatebot/advisories/__init__.py +++ b/updatebot/advisories/__init__.py @@ -21,7 +21,7 @@ log = logging.getLogger('updatebot.advisories') -_supportedBackends = ('sles', 'sles11', 'centos', 'ubuntu', ) +_supportedBackends = ('sles', 'sles11', 'centos', 'ubuntu', 'scientific', ) class InvalidBackendError(Exception): """ diff --git a/updatebot/advisories/centos.py b/updatebot/advisories/centos.py --- a/updatebot/advisories/centos.py +++ b/updatebot/advisories/centos.py @@ -50,9 +50,10 @@ for url in self._getArchiveUrls(): log.info('parsing mail archive: %s' % url) try: - for msg in pmap.parse(url, backend=self._cfg.platformName): + for msg in pmap.parse(url, backend=self._cfg.platformName, + productVersion=self._cfg.upstreamProductVersion): self._loadOne(msg, pkgCache) - except pmap.ArchiveNotFound, e: + except pmap.ArchiveNotFoundError, e: log.warn('unable to retrieve archive for %s' % url) def _loadOne(self, msg, pkgCache): diff --git a/updatebot/advisories/scientific.py b/updatebot/advisories/scientific.py new file mode 100644 --- /dev/null +++ b/updatebot/advisories/scientific.py @@ -0,0 +1,44 @@ +# +# Copyright (c) 2009 rPath, Inc. +# +# This program is distributed under the terms of the Common Public License, +# version 1.0. A copy of this license should have been distributed with this +# source file in a file called LICENSE. If it is not present, the license +# is always available at http://www.rpath.com/permanent/licenses/CPL-1.0. +# +# This program is distributed in the hope that it will be useful, but +# without any warranty; without even the implied warranty of merchantability +# or fitness for a particular purpose. See the Common Public License for +# full details. +# + +""" +Advisory module for Scientific Linux. +""" + +from updatebot.advisories.centos import Advisor as BaseAdvisor + +class Advisor(BaseAdvisor): + def _loadOne(self, msg, pkgCache): + """ + Handles matching one message to any mentioned packages. + """ + + # Filter out messages that don't have a summary or description. + if msg.summary is None or msg.description is None: + return + + BaseAdvisor._loadOne(self, msg, pkgCache) + + def _isUpdatesRepo(self, binPkg): + """ + Check the repository name. If this package didn't come from a updates + repository it is probably not security related. + @param binPkg: binary package object + @type binPkg: repomd.packagexml._Package + """ + + parts = binPkg.location.split('/') + if parts[2] == 'updates' and not parts[3] == 'fastbugs': + return True + return False From johnsonm at rpath.com Wed Aug 19 17:42:35 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:42:35 +0000 Subject: mirrorball: rename import.py to import Message-ID: <200908192142.n7JLgZwu022864@scc.eng.rpath.com> changeset: d6f444243182 user: Elliot Peele <https://issues.rpath.com/> date: Mon, 16 Mar 2009 17:26:49 -0400 rename import.py to import committer: Elliot Peele <https://issues.rpath.com/> diff --git a/scripts/import b/scripts/import new file mode 100755 --- /dev/null +++ b/scripts/import @@ -0,0 +1,40 @@ +#!/usr/bin/python +# +# Copyright (c) 2008-2009 rPath, Inc. +# +# This program is distributed under the terms of the Common Public License, +# version 1.0. A copy of this license should have been distributed with this +# source file in a file called LICENSE. If it is not present, the license +# is always available at http://www.rpath.com/permanent/licenses/CPL-1.0. +# +# This program is distributed in the hope that it will be useful, but +# without any warranty; without even the implied warranty of merchantability +# or fitness for a particular purpose. See the Common Public License for +# full details. +# + +import os +import sys + +sys.path.insert(0, os.environ['HOME'] + '/hg/xobj/py') +sys.path.insert(0, os.environ['HOME'] + '/hg/rpath-xmllib') +sys.path.insert(0, os.environ['HOME'] + '/hg/conary') +sys.path.insert(0, os.environ['HOME'] + '/hg/mirrorball') +sys.path.insert(0, os.environ['HOME'] + '/hg/rmake') + +from conary.lib import util +sys.excepthook = util.genExcepthook() + +from updatebot import bot, config, log + +log.addRootLogger() +cfg = config.UpdateBotConfig() +cfg.read(os.environ['HOME'] + '/hg/mirrorball/config/%s/updatebotrc' % sys.argv[1]) +obj = bot.Bot(cfg) +trvMap = obj.create() + +import epdb ; epdb.st() + +for source in trvMap: + for bin in trvMap[source]: + print '%s=%s[%s]' % bin diff --git a/scripts/import.py b/scripts/import.py deleted file mode 100755 --- a/scripts/import.py +++ /dev/null @@ -1,40 +0,0 @@ -#!/usr/bin/python -# -# Copyright (c) 2008-2009 rPath, Inc. -# -# This program is distributed under the terms of the Common Public License, -# version 1.0. A copy of this license should have been distributed with this -# source file in a file called LICENSE. If it is not present, the license -# is always available at http://www.rpath.com/permanent/licenses/CPL-1.0. -# -# This program is distributed in the hope that it will be useful, but -# without any warranty; without even the implied warranty of merchantability -# or fitness for a particular purpose. See the Common Public License for -# full details. -# - -import os -import sys - -sys.path.insert(0, os.environ['HOME'] + '/hg/xobj/py') -sys.path.insert(0, os.environ['HOME'] + '/hg/rpath-xmllib') -sys.path.insert(0, os.environ['HOME'] + '/hg/conary') -sys.path.insert(0, os.environ['HOME'] + '/hg/mirrorball') -sys.path.insert(0, os.environ['HOME'] + '/hg/rmake') - -from conary.lib import util -sys.excepthook = util.genExcepthook() - -from updatebot import bot, config, log - -log.addRootLogger() -cfg = config.UpdateBotConfig() -cfg.read(os.environ['HOME'] + '/hg/mirrorball/config/%s/updatebotrc' % sys.argv[1]) -obj = bot.Bot(cfg) -trvMap = obj.create() - -import epdb ; epdb.st() - -for source in trvMap: - for bin in trvMap[source]: - print '%s=%s[%s]' % bin From johnsonm at rpath.com Wed Aug 19 17:41:52 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:41:52 +0000 Subject: mirrorball: handle more different advisory messages Message-ID: <200908192141.n7JLfqvv021144@scc.eng.rpath.com> changeset: 5b89f1343386 user: Elliot Peele <https://issues.rpath.com/> date: Fri, 19 Dec 2008 14:30:51 -0500 handle more different advisory messages committer: Elliot Peele <https://issues.rpath.com/> diff --git a/pmap/ubuntu.py b/pmap/ubuntu.py --- a/pmap/ubuntu.py +++ b/pmap/ubuntu.py @@ -48,16 +48,23 @@ self._curDistroVer = None self._inDetails = False + self._inHeader = False + self._endHeader = False self._states.update({ - 'ubuntu' : self._ubuntu, - 'pkgnamever' : self._pkgnamever, - 'repourl' : self._repourl, - 'details' : self._details, - 'updated' : self._updated, + 'headersep' : self._headersep, + 'setdescription' : self._the, + 'ubuntu' : self._ubuntu, + 'pkgnamever' : self._pkgnamever, + 'repourl' : self._repourl, + 'details' : self._details, + 'updated' : self._updated, + 'source' : self._source, }) self._filter('^.*security\.ubuntu\.com/ubuntu.*$', 'repourl') + self._filterLine('^=*$', 'headersep') + self._filterLine('^The problem can be corrected.*$', 'setdescription') def _newContainer(self): if self._curObj and not self._curObj.pkgs: @@ -78,17 +85,31 @@ return state + def _headersep(self): + if len(self._line) != 1: + return + + if not self._inHeader and not self._endHeader: + self._inHeader = True + else: + self._endHeader = True + self._inHeader = False + + def _the(self): + if self._endHeader and self._text.strip(): + self._curObj.description = self._text.strip() + def _ubuntu(self): if '.' in self._line[1] and len(self._line[1].split('.')) == 2: year, month = self._line[1].strip(':').split('.') if year.isdigit() and month.isdigit(): - self._curDistroVer = self._line[1] + self._curDistroVer = self._line[1].strip(':') def _pkgnamever(self): # looks like a version line = [ x for x in self._line if x ] - if len(line) == 2 and '-' in line[1]: + if len(line) == 2: name, version = line if self._curObj.pkgNameVersion is None: self._curObj.pkgNameVersion = {} @@ -112,7 +133,18 @@ if self._line[1] == 'follow:': self._curDistroVer = None self._inDetails = True + self._endHeader = False def _updated(self): - if self._line[1] == 'packages' and self._inDetails: - self._curObj.description = self._text + if self._line[1] == 'packages': + self._processDetails() + + def _source(self): + if self._line[1] == 'archives:': + self._processDetails() + + def _processDetails(self): + if self._inDetails or self._endHeader: + self._inDetails = False + if self._curObj.description is None: + self._curObj.description = self._text.strip() From johnsonm at rpath.com Wed Aug 19 17:42:09 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:42:09 +0000 Subject: mirrorball: rpl 1 -> 1-qa promote script Message-ID: <200908192142.n7JLg9dv021809@scc.eng.rpath.com> changeset: b12f9420a013 user: Elliot Peele <https://issues.rpath.com/> date: Fri, 23 Jan 2009 14:04:44 -0500 rpl 1 -> 1-qa promote script committer: Elliot Peele <https://issues.rpath.com/> diff --git a/scripts/pcheck.py b/scripts/pcheck.py --- a/scripts/pcheck.py +++ b/scripts/pcheck.py @@ -4,12 +4,14 @@ from conary.lib import util sys.excepthook = util.genExcepthook() +from conary.lib import log +from conary import trove from conary import versions from conary import callbacks from conary import conarycfg from conary import conaryclient -cfg = conarycfg.ConaryConfiguration() +cfg = conarycfg.ConaryConfiguration(True) cfg.setContext('1-binary') client = conaryclient.ConaryClient(cfg) @@ -20,25 +22,69 @@ groupTrvs.sort() groupTrvs.reverse() -trvs = [ x for x in groupTrvs if x[1] == groupTrvs[0][1] ] +grpTrvs = [ x for x in groupTrvs if x[1] == groupTrvs[0][1] ] + +# Find all of the troves in the groups that are to be promoted. +log.info('Create group changesets') +grpCsReq = [(x, (None, None), (y, z), True) for x, y, z in grpTrvs ] +grpCs = client.createChangeSet(grpCsReq, withFiles=False, withFileContents=False, recurse=False) + +oldTrvSpecs = set() +for topLevelCs in grpCs.iterNewTroveList(): + trv = trove.Trove(topLevelCs, skipIntegrityChecks=True) + oldTrvSpecs.update(set([ x for x in trv.iterTroveList(weakRefs=True, strongRefs=True) ])) + + +# Build mapping of frumptuus. labelMap = { versions.VersionFromString('/conary.rpath.com at rpl:devel//1'): versions.VersionFromString('/conary.rpath.com at rpl:devel//1-qa'), + versions.VersionFromString('/conary.rpath.com at rpl:1'): + versions.VersionFromString('/conary.rpath.com at rpl:1-qa'), versions.VersionFromString('/conary.rpath.com at rpl:devel//1-xen'): versions.VersionFromString('/conary.rpath.com at rpl:devel//1-xen-qa'), + versions.VersionFromString('/conary.rpath.com at rpl:devel//1-vmware'): + versions.VersionFromString('/conary.rpath.com at rpl:devel//1-vmware-qa'), } -cb = conaryclient.callbacks.CloneCallback(cfg) +# List of labels that should not be promoted. +hiddenLabels = [ + versions.Label('conary.rpath.com at rpl:1-compat'), +] + + +# Find troves that have labels that are not in the label map. +log.info('Searching for troves that will not be promoted') +branches = labelMap.keys() +for name, version, flavor in sorted(oldTrvSpecs): + if version.branch() not in branches: + match = False + for label in hiddenLabels: + if label in version.versions: + match = True + if not match: + log.warning('not promoting %s=%s[%s]' % (name, version, flavor)) + + +# Make the promote changeset. +log.info('Creating promote changeset') +cb = conaryclient.callbacks.CloneCallback(cfg, 'automated commit') success, cs = client.createSiblingCloneChangeSet( labelMap, - trvs, + grpTrvs, cloneSources=True, callback=cb, updateBuildInfo=False) -import epdb -epdb.st() +# Check status +if not success: + log.critical('Failed to create promote changeset') + sys.exit(1) +# Ask before committing. +okay = conaryclient.cmdline.askYn('continue with clone? [y/N]', default=False) - +# Commit changeset. +if okay: + client.repos.commitChangeSet(cs, callback=callback) From johnsonm at rpath.com Wed Aug 19 17:41:51 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:41:51 +0000 Subject: mirrorball: add ubuntu archive parsing Message-ID: <200908192141.n7JLfp2u021110@scc.eng.rpath.com> changeset: 7a7a846682a1 user: Elliot Peele <https://issues.rpath.com/> date: Wed, 17 Dec 2008 16:08:44 -0500 add ubuntu archive parsing committer: Elliot Peele <https://issues.rpath.com/> diff --git a/pmap/ubuntu.py b/pmap/ubuntu.py --- a/pmap/ubuntu.py +++ b/pmap/ubuntu.py @@ -24,6 +24,18 @@ Ubuntu specific container class. """ + __slots__ = ('pkgs', 'pkgNameVersion') + + def finalize(self): + BaseContainer.finalize(self) + + assert self.subject is not None + assert self.pkgs is not None + assert self.description is not None + assert self.pkgNameVersion is not None + + self.summary = self.subject + class Parser(BaseParser): """ Class for parsing Ubuntu mbox mail archives. @@ -32,8 +44,75 @@ def __init__(self): BaseParser.__init__(self) - self._containerClass = Container + self._containerClass = UbuntuContainer + + self._curDistroVer = None + self._inDetails = False self._states.update({ + 'ubuntu' : self._ubuntu, + 'pkgnamever' : self._pkgnamever, + 'repourl' : self._repourl, + 'details' : self._details, + 'updated' : self._updated, + }) - }) + self._filter('^.*security\.ubuntu\.com/ubuntu.*$', 'repourl') + + def _newContainer(self): + if self._curObj and not self._curObj.pkgs: + self._curObj = None + BaseParser._newContainer(self) + + def _parseLine(self, cfgline): + cfgline = cfgline.strip() + return BaseParser._parseLine(self, cfgline) + + def _getState(self, state): + state = BaseParser._getState(self, state) + if state in self._states: + return state + + if self._curDistroVer is not None: + return 'pkgnamever' + + return state + + def _ubuntu(self): + if '.' in self._line[1] and len(self._line[1].split('.')) == 2: + year, month = self._line[1].strip(':').split('.') + if year.isdigit() and month.isdigit(): + self._curDistroVer = self._line[1] + + def _pkgnamever(self): + # looks like a version + line = [ x for x in self._line if x ] + + if len(line) == 2 and '-' in line[1]: + name, version = line + if self._curObj.pkgNameVersion is None: + self._curObj.pkgNameVersion = {} + + if self._curDistroVer not in self._curObj.pkgNameVersion: + self._curObj.pkgNameVersion[self._curDistroVer] = set() + + self._curObj.pkgNameVersion[self._curDistroVer].add((name, version)) + + def _repourl(self): + url = self._line[0] + parts = url.split('/') + + # probaby a repository url + if parts[3] == 'ubuntu' and parts[4] == 'pool': + if self._curObj.pkgs is None: + self._curObj.pkgs = set() + self._curObj.pkgs.add('/'.join(parts[4:])) + + def _details(self): + if self._line[1] == 'follow:': + self._curDistroVer = None + self._inDetails = True + + def _updated(self): + if self._line[1] == 'packages' and self._inDetails: + self._curObj.description = self._text From johnsonm at rpath.com Wed Aug 19 17:40:16 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:40:16 +0000 Subject: mirrorball: remove too few public methods exceptions Message-ID: <200908192140.n7JLeGbk017256@scc.eng.rpath.com> changeset: e0c92f7e8a0c user: Elliot Peele <http://issues.rpath.com/> date: Thu, 14 Aug 2008 18:18:34 -0400 remove too few public methods exceptions committer: Elliot Peele <http://issues.rpath.com/> diff --git a/repomd/filelistsxml.py b/repomd/filelistsxml.py --- a/repomd/filelistsxml.py +++ b/repomd/filelistsxml.py @@ -37,9 +37,6 @@ Handle registering all types for parsing filelists.xml files. """ - # R0903 - Too few public methods - # pylint: disable-msg=R0903 - def _registerTypes(self): """ Setup databinder to parse xml. diff --git a/repomd/packagexml.py b/repomd/packagexml.py --- a/repomd/packagexml.py +++ b/repomd/packagexml.py @@ -244,9 +244,6 @@ Handle registering all types for parsing package elements. """ - # R0903 - Too few public methods - # pylint: disable-msg=R0903 - def _registerTypes(self): """ Setup databinder to parse xml. diff --git a/repomd/patchesxml.py b/repomd/patchesxml.py --- a/repomd/patchesxml.py +++ b/repomd/patchesxml.py @@ -85,9 +85,6 @@ Handle registering all types for parsing patches.xml. """ - # R0903 - Too few public methods - # pylint: disable-msg=R0903 - def _registerTypes(self): """ Setup databinder to parse xml. diff --git a/repomd/patchxml.py b/repomd/patchxml.py --- a/repomd/patchxml.py +++ b/repomd/patchxml.py @@ -126,9 +126,6 @@ Handle registering all types for parsing patch-*.xml files. """ - # R0903 - Too few public methods - # pylint: disable-msg=R0903 - def _registerTypes(self): """ Setup databinder to parse xml. diff --git a/repomd/primaryxml.py b/repomd/primaryxml.py --- a/repomd/primaryxml.py +++ b/repomd/primaryxml.py @@ -44,9 +44,6 @@ Handle registering all types for parsing primary.xml.gz. """ - # R0903 - Too few public methods - # pylint: disable-msg=R0903 - def _registerTypes(self): """ Setup databinder to parse xml. diff --git a/repomd/repomdxml.py b/repomd/repomdxml.py --- a/repomd/repomdxml.py +++ b/repomd/repomdxml.py @@ -111,9 +111,6 @@ Handle registering all types for parsing repomd.xml file. """ - # R0903 - Too few public methods - # pylint: disable-msg=R0903 - def _registerTypes(self): """ Setup databinder to parse xml. diff --git a/repomd/repository.py b/repomd/repository.py --- a/repomd/repository.py +++ b/repomd/repository.py @@ -29,9 +29,6 @@ Access files from the repository. """ - # R0903 - Too few public methods - # pylint: disable-msg=R0903 - def __init__(self, repoUrl): self._repoUrl = repoUrl diff --git a/repomd/xmlcommon.py b/repomd/xmlcommon.py --- a/repomd/xmlcommon.py +++ b/repomd/xmlcommon.py @@ -25,9 +25,6 @@ Base class for handling databinder setup. """ - # R0903 - Too few public methods - # pylint: disable-msg=R0903 - def __init__(self, repository, path): self._repository = repository self._path = path diff --git a/updatebot/patchsource.py b/updatebot/patchsource.py --- a/updatebot/patchsource.py +++ b/updatebot/patchsource.py @@ -25,9 +25,6 @@ Store patch related mappings. """ - # R0903 - Too few public methods - # pylint: disable-msg=R0903 - def __init__(self, cfg): self._cfg = cfg From johnsonm at rpath.com Wed Aug 19 17:39:41 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:39:41 +0000 Subject: mirrorball: add tests for util Message-ID: <200908192139.n7JLdfnL015858@scc.eng.rpath.com> changeset: d8dcf02e8e2f user: Elliot Peele <http://issues.rpath.com/> date: Tue, 24 Jun 2008 17:24:11 -0400 add tests for util committer: Elliot Peele <http://issues.rpath.com/> diff --git a/test/unit_test/updatebottest/utiltest.py b/test/unit_test/updatebottest/utiltest.py new file mode 100644 --- /dev/null +++ b/test/unit_test/updatebottest/utiltest.py @@ -0,0 +1,94 @@ +# +# Copyright (c) 2008 rPath, Inc. +# +# This program is distributed under the terms of the Common Public License, +# version 1.0. A copy of this license should have been distributed with this +# source file in a file called LICENSE. If it is not present, the license +# is always available at http://www.rpath.com/permanent/licenses/CPL-1.0. +# +# This program is distributed in the hope that it will be useful, but +# without any warranty; without even the implied warranty of merchantability +# or fitness for a particular purpose. See the Common Public License for +# full details. +# + +import testsetup + +import mock +import slehelp + +from updatebot import util + +class UtilTest(slehelp.Helper): + def testJoin(self): + def t(input, output): + result = util.join(*input) + self.failUnlessEqual(result, output) + + t(['/foo', 'bar'], + '/foo/bar') + t(['/foo/../', 'bar/'], + '/bar') + t(['/foo', '/bar'], + '/foo/bar') + + def testSrpmToConaryVersion(self): + mockSrcPkg = mock.MockObject() + + mockSrcPkg._mock.set(version='a-b') + mockSrcPkg._mock.set(release='b-c') + + expected = 'a_b_b_c' + result = util.srpmToConaryVersion(mockSrcPkg) + self.failUnlessEqual(result, expected) + + mockSrcPkg._mock.set(version='a_b') + result = util.srpmToConaryVersion(mockSrcPkg) + self.failUnlessEqual(result, expected) + + def testPackagevercmp(self): + def t(expected, kw1, kw2): + mockPkg1 = mock.MockObject() + mockPkg2 = mock.MockObject() + + for kw in kw1.iterkeys(): + mockPkg1._mock.set(**{kw:kw1[kw]}) + for kw in kw2.iterkeys(): + mockPkg2._mock.set(**{kw:kw2[kw]}) + + result = util.packagevercmp(mockPkg1, mockPkg2) + self.failUnlessEqual(result, expected) + + t(1, {'epoch': '1'}, + {'epoch': '0'}) + t(-1, {'epoch': '0'}, + {'epoch': '1'}) + t(1, {'epoch': '0', + 'version': '1'}, + {'epoch': '0', + 'version': '0'}) + t(-1, {'epoch': '0', + 'version': '0'}, + {'epoch': '0', + 'version': '1'}) + t(1, {'epoch': '0', + 'version': '0', + 'release': '1'}, + {'epoch': '0', + 'version': '0', + 'release': '0'}) + t(-1, {'epoch': '0', + 'version': '0', + 'release': '0'}, + {'epoch': '0', + 'version': '0', + 'release': '1'}) + t(0, {'epoch': '0', + 'version': '0', + 'release': '0'}, + {'epoch': '0', + 'version': '0', + 'release': '0'}) + + +testsetup.main() diff --git a/updatebot/util.py b/updatebot/util.py --- a/updatebot/util.py +++ b/updatebot/util.py @@ -30,6 +30,8 @@ """ root = os.path.normpath(a) + if root == '/': + root = '' for path in b: root += os.sep + os.path.normpath(path) return root From johnsonm at rpath.com Wed Aug 19 17:39:32 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:39:32 +0000 Subject: mirrorball: add some more comments Message-ID: <200908192139.n7JLdWWk015567@scc.eng.rpath.com> changeset: bed2f033c2c8 user: Elliot Peele <http://issues.rpath.com/> date: Fri, 20 Jun 2008 00:46:11 -0400 add some more comments committer: Elliot Peele <http://issues.rpath.com/> diff --git a/updatebot/bot.py b/updatebot/bot.py --- a/updatebot/bot.py +++ b/updatebot/bot.py @@ -53,7 +53,7 @@ """ for repo in self._cfg.repositoryPaths: - log.info('loading %s/%s repository data' + log.info('loading repository data %s/%s' % (self._cfg.repositoryUrl, repo)) client = repomd.Client(self._cfg.repositoryUrl + '/' + repo) self._rpmSource.loadFromClient(client, repo) @@ -66,7 +66,7 @@ """ for path, client in self._clients.iteritems(): - log.info('loading %s/%s patch information' + log.info('loading patch information %s/%s' % (self._cfg.repositoryUrl, path)) self._patchSource.loadFromClient(client, path) @@ -83,35 +83,42 @@ # Get troves to update and send advisories. toAdvise, toUpdate = self._updater.getUpdates() - # Don't populate the patch source until we knoe that there are - # updatas. - self._populatePatchSource() + # Don't populate the patch source until we know that there are + # updates. + #self._populatePatchSource() # Check to see if advisories exist for all required packages. - self._advisor.check(toAdvise) + #self._advisor.check(toAdvise) # Update sources. - for nvf, srcPkg in toUpdate: - self._updater.update(nvf, srcPkg) + #for nvf, srcPkg in toUpdate: + # self._updater.update(nvf, srcPkg) # Make sure to build everything in the toAdvise list, there may be # sources that have been updated, but not built. - buildTroves = [ x[0] for x in toAdvise ] - trvMap = self._builder.build(buildTroves) + #buildTroves = set([ x[0] for x in toAdvise ]) + #trvMap = self._builder.build(buildTroves) + import epdb; epdb.st() # Build group. - grpTrvMap = self._builder.build(self._cfg.topGroup) + grpTrvs = set() + for flavor in self._cfg.groupFlavors: + grpTrvs.add((self._cfg.topSourceGroup[0], + self._cfg.topSourceGroup[1], + flavor)) + grpTrvMap = self._builder.build(grpTrvs) + import epdb; epdb.st() # Promote group. - # FIXME: should be able to get the new versions of packages from - # promote. - helper = self._updater.getConaryHelper() - newTroves = helper.promote(grpTrvMap.values(), self._cfg.targetLabel) + # We expect that everything that was built will be published. + expected = [ x for x in y for y in trvMap.itervalues() ] + toPublish = [ x for x in y for y in grpTrvMap.itervalues() ] + newTroves = self._updater.publish(toPublish, expected, + self._cfg.targetLabel) - # FIXME: build a trvMap from source tove nvf to new binary trove nvf - + import epdb; epdb.st() # Send advisories. - self._advisor.send(trvMap, toAdvise) + self._advisor.send(newTroves, toAdvise) log.info('update completed successfully') log.info('updated %s packages and sent %s advisories' From johnsonm at rpath.com Wed Aug 19 17:41:57 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:41:57 +0000 Subject: mirrorball: update validatemanifests Message-ID: <200908192141.n7JLfvSe021349@scc.eng.rpath.com> changeset: b491e78b179e user: Elliot Peele <https://issues.rpath.com/> date: Sun, 11 Jan 2009 18:21:41 -0500 update validatemanifests committer: Elliot Peele <https://issues.rpath.com/> diff --git a/scripts/validatemanifests.py b/scripts/validatemanifests.py --- a/scripts/validatemanifests.py +++ b/scripts/validatemanifests.py @@ -28,7 +28,7 @@ sys.excepthook = util.genExcepthook() from updatebot import bot -from updatebot import util +from updatebot.lib import util from updatebot import config from updatebot import log as logger @@ -56,8 +56,7 @@ pkgs.append((n, v.getSourceVersion())) changed = {} -for pkg, v in pkgs: - +for i, (pkg, v) in enumerate(pkgs): srpms = list(b._pkgSource.srcNameMap[pkg]) map = {} @@ -65,62 +64,68 @@ key = '%s_%s' % (x.version, x.release) map[key] = x - srcPkg = map[v.trailingRevision().asString().split('-')[0]] - manifest = b._updater._getManifestFromPkgSource(srcPkg) - repoManifest = b._updater._conaryhelper.getManifest(pkg) - - if len(manifest) == len(repoManifest): - offt = 0 - for i in range(len(manifest)): - index = i - offt - if manifest[index] == repoManifest[index]: - manifest.pop(index) - repoManifest.pop(index) - offt += 1 - - if not manifest and not repoManifest: + key = v.trailingRevision().asString().split('-')[0] + if key not in map: + log.info('%s (%s) version not found, using latest' % (pkg, key)) + srcPkg = b._updater._getLatestSource(pkg) + changed[pkg] = srcPkg continue - if manifest != repoManifest: - for item in repoManifest: - if item in manifest: - manifest.remove(item) + srcPkg = map[key] + #manifest = b._updater._getManifestFromPkgSource(srcPkg) + #repoManifest = b._updater._conaryhelper.getManifest(pkg) - debug = False - for item in manifest: - if 'universe' in item: - print '%s: new packages found in universe' % pkg - else: - debug = True + #if len(manifest) == len(repoManifest): + # offt = 0 + # for i in range(len(manifest)): + # index = i - offt + # if manifest[index] == repoManifest[index]: + # manifest.pop(index) + # repoManifest.pop(index) + # offt += 1 - if debug: - import epdb; epdb.st() - # assert len(manifest) == len(repoManifest) + #if not manifest and not repoManifest: + # changed[pkg] = srcPkg + # continue - # baseNames1 = [ os.path.basename(x) for x in manifest ] - # baseNames2 = [ os.path.basename(x) for x in repoManifest ] + #if manifest != repoManifest: + # removed = [] + # for item in repoManifest: + # if item in manifest: + # manifest.remove(item) + # removed.append(item) - # baseNames1.sort() - # baseNames2.sort() + # for item in removed: + # repoManifest.remove(item) - # assert baseNames1 == baseNames2 + # debug = False + # for item in manifest: + # if 'universe' in item: + # log.info('%s: new packages found in universe' % pkg) + # else: + # debug = True - # changed[pkg] = [manifest, repoManifest, srcPkg] + changed[pkg] = srcPkg + + +#import epdb; epdb.st() +toBuild = set() +for pkg in changed: + srcPkg = changed[pkg] + manifest = b._updater._getManifestFromPkgSource(srcPkg) + helper.setManifest(pkg, manifest) + metadata = b._updater._getMetadataFromPkgSource(srcPkg) + helper.setMetadata(pkg, metadata) +# helper.commit(pkg, commitMessage=cfg.commitMessage) + toBuild.add((pkg, cfg.topSourceGroup[1], None)) + + new = helper.getMetadata(pkg) + + assert metadata == new import epdb; epdb.st() -trvs = set() -for pkg in changed: - srcPkg = changed[pkg][2] - manifest = b._updater._getManifestFromPkgSource(srcPkg) - helper.setManifest(pkg, manifest) - helper.commit(pkg, commitMessage=cfg.commitMessage) - trvs.add((pkg, cfg.topSourceGroup[1], None)) - -import epdb; epdb.st() - -#trvMap = b._builder.build(trvs) -trvMap = b._builder.buildmany(trvs) +trvMap = b._builder.buildmany(toBuild) def displayTrove(nvf): flavor = '' From johnsonm at rpath.com Wed Aug 19 17:40:09 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:40:09 +0000 Subject: mirrorball: add named argument for disabling promote checks Message-ID: <200908192140.n7JLe9UH016983@scc.eng.rpath.com> changeset: b000b3102ef7 user: Elliot Peele <http://bugs.rpath.com/> date: Wed, 13 Aug 2008 15:53:50 -0400 add named argument for disabling promote checks committer: Elliot Peele <http://bugs.rpath.com/> diff --git a/updatebot/conaryhelper.py b/updatebot/conaryhelper.py --- a/updatebot/conaryhelper.py +++ b/updatebot/conaryhelper.py @@ -329,7 +329,8 @@ versions = verMap.keys() return versions - def promote(self, trvLst, expected, sourceLabels, targetLabel): + def promote(self, trvLst, expected, sourceLabels, targetLabel, + checkPackageList=True): """ Promote a group and its contents to a target label. @param trvLst: list of troves to publish @@ -341,6 +342,9 @@ @type sourceLabels: [labelObject, ... ] @param targetLabel: table to publish to @type targetLabel: conary Label object + @param checkPackageList: verify the list of packages being promoted or + not. + @type checkPackageList: boolean """ start = time.time() @@ -353,8 +357,10 @@ # Build the label map. labelMap = {fromLabel: targetLabel} for label in sourceLabels: + assert(label is not None) labelMap[label] = targetLabel + import epdb; epdb.st() success, cs = self._client.createSiblingCloneChangeSet( labelMap, trvLst, @@ -375,9 +381,10 @@ # that we think should be available to promote. Note that all packages # in expected will not be promoted because not all packages are # included in the groups. + import epdb; epdb.st() difference = newPkgs.difference(oldPkgs) grpTrvs = set([ (x[0], x[2]) for x in trvLst if not x[0].endswith(':source') ]) - if difference != grpTrvs: + if checkPackageList and difference != grpTrvs: raise PromoteMismatchError(expected=oldPkgs, actual=newPkgs) log.info('committing changeset') diff --git a/updatebot/update.py b/updatebot/update.py --- a/updatebot/update.py +++ b/updatebot/update.py @@ -321,7 +321,7 @@ pkgs = self._getLatestOfAvailableArches(manifestPkgs) return [ x.location for x in pkgs ] - def publish(self, trvLst, expected, targetLabel): + def publish(self, trvLst, expected, targetLabel, checkPackageList=True): """ Publish a group and its contents to a target label. @param trvLst: list of troves to publish @@ -330,7 +330,10 @@ @type expected: [(name, version, flavor), ...] @param targetLabel: table to publish to @type targetLabel: conary Label object + @param checkPackageList: verify list of packages being promoted or not. + @type checkPackageList: boolean """ return self._conaryhelper.promote(trvLst, expected, - self._cfg.sourceLabel, targetLabel) + self._cfg.sourceLabel, targetLabel, + checkPackageList=checkPackageList) From johnsonm at rpath.com Wed Aug 19 17:41:40 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:41:40 +0000 Subject: mirrorball: find versions and flavors for everything to promote Message-ID: <200908192141.n7JLfe4v020651@scc.eng.rpath.com> changeset: 7f61bc1dde44 user: Elliot Peele <https://issues.rpath.com/> date: Wed, 19 Nov 2008 13:26:16 -0500 find versions and flavors for everything to promote add mirror functionallity committer: Elliot Peele <https://issues.rpath.com/> diff --git a/updatebot/conaryhelper.py b/updatebot/conaryhelper.py --- a/updatebot/conaryhelper.py +++ b/updatebot/conaryhelper.py @@ -23,8 +23,12 @@ import tempfile import conary +from conary import trove +from conary import checkin from conary.build import use -from conary import conaryclient, conarycfg, trove, checkin +from conary import conarycfg +from conary import conaryclient +from conary.conaryclient import mirror from updatebot import util from updatebot.errors import GroupNotFound @@ -32,6 +36,7 @@ from updatebot.errors import NoManifestFoundError from updatebot.errors import PromoteFailedError from updatebot.errors import PromoteMismatchError +from updatebot.errors import MirrorFailedError log = logging.getLogger('updatebot.conaryhelper') @@ -49,6 +54,12 @@ # Have to initialize flavors to commit to the repository. self._ccfg.initializeFlavors() + self._mcfg = None + mcfgfn = util.join(cfg.configPath, 'mirror.conf') + if os.path.exists(mcfgfn): + self._mcfg = mirror.MirrorFileConfiguration() + self._mcfg.read(mcfgfn) + self._client = conaryclient.ConaryClient(self._ccfg) self._repos = self._client.getRepos() @@ -412,7 +423,14 @@ # mix in the extra troves for n, v, f in extraPromoteTroves: - trvLst.append((n, None, None)) + trvs = self._repos.findTrove(fromLabel, (n, v, f)) + latestVer = trvs[0][1] + for name, version, flavor in trvs: + if version > latestVer: + latestVer = version + for name, version, flavor in trvs: + if version == latestVer: + trvLst.append((name, version, flavor)) success, cs = self._client.createSiblingCloneChangeSet( labelMap, @@ -449,3 +467,28 @@ log.info('promote complete, elapsed time %s' % (time.time() - start, )) return packageList + + def mirror(self): + """ + Mirror the current platform to the external repository if a + mirror.conf exists. + """ + + if self._mcfg is None: + log.info('mirroring disabled, no mirror.conf found for this platform') + return + + from conary.lib import log as clog + + # Always use DEBUG logging when mirroring + curLevel = clog.fmtLogger.level + clog.setVerbosity(clog.DEBUG) + + callback = mirror.ChangesetCallback() + rc = mirror.mainWorkflow(cfg=self._mcfg, callback=callback) + + if rc is not None and rc != 0: + raise MirrorFailedError(rc=rc) + + # Reset loglevel + clog.setVerbosity(curLevel) diff --git a/updatebot/errors.py b/updatebot/errors.py --- a/updatebot/errors.py +++ b/updatebot/errors.py @@ -131,6 +131,14 @@ _template = ('Expected to promote %(expected)s, actually tried to promote' ' %(actual)s.') +class MirrorFailedError(UnhandledUpdateError): + """ + MirrorFailedError, raised when the mirror process fails. + """ + + _params = ['rc', ] + _template = 'Mirror process exited with code %(rc)s' + class AdvisoryError(UnhandledUpdateError): """ Base error for other advisory errors to inherit from. From johnsonm at rpath.com Wed Aug 19 17:41:30 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:41:30 +0000 Subject: mirrorball: add support for promote packages other than what is contained within the top level group Message-ID: <200908192141.n7JLfUvm020308@scc.eng.rpath.com> changeset: fd79c3f5c2c0 user: Elliot Peele <https://issues.rpath.com/> date: Mon, 10 Nov 2008 00:22:45 -0500 add support for promote packages other than what is contained within the top level group committer: Elliot Peele <https://issues.rpath.com/> diff --git a/updatebot/conaryhelper.py b/updatebot/conaryhelper.py --- a/updatebot/conaryhelper.py +++ b/updatebot/conaryhelper.py @@ -373,7 +373,7 @@ return None def promote(self, trvLst, expected, sourceLabels, targetLabel, - checkPackageList=True): + checkPackageList=True, extraPromoteTroves=None): """ Promote a group and its contents to a target label. @param trvLst: list of troves to publish @@ -388,12 +388,19 @@ @param checkPackageList: verify the list of packages being promoted or not. @type checkPackageList: boolean + @param extraPromoteTroves: troves to promote in addition to the troves + that have been built. + @type extraPromoteTroves: list of trove specs. """ start = time.time() log.info('starting promote') log.info('creating changeset') + # make extraPromoteTroves a list if it was not specified. + if extraPromoteTroves is None: + extraPromoteTroves = [] + # Get the label that the group is on. fromLabel = trvLst[0][1].trailingLabel() @@ -403,6 +410,10 @@ assert(label is not None) labelMap[label] = targetLabel + # mix in the extra troves + for n, v, f in extraPromoteTroves: + trvLst.append((n, None, None)) + success, cs = self._client.createSiblingCloneChangeSet( labelMap, trvLst, @@ -423,9 +434,11 @@ # that we think should be available to promote. Note that all packages # in expected will not be promoted because not all packages are # included in the groups. - difference = newPkgs.difference(oldPkgs) + trvDiff = newPkgs.difference(oldPkgs) grpTrvs = set([ (x[0], x[2]) for x in trvLst if not x[0].endswith(':source') ]) - if checkPackageList and difference != grpTrvs: + grpDiff = set([ x[0] for x in trvDiff.difference(grpTrvs) ]) + extraTroves = set([ x[0] for x in extraPromoteTroves ]) + if checkPackageList and not grpDiff.difference(extraTroves): raise PromoteMismatchError(expected=oldPkgs, actual=newPkgs) log.info('committing changeset') diff --git a/updatebot/config.py b/updatebot/config.py --- a/updatebot/config.py +++ b/updatebot/config.py @@ -96,6 +96,9 @@ # Label to promote to targetLabel = CfgBranch + # Packages other than the topGroup that need to be promoted. + extraPromoteTroves = (CfgList(CfgTroveSpec), []) + # Packages to import package = (CfgList(CfgString), []) diff --git a/updatebot/update.py b/updatebot/update.py --- a/updatebot/update.py +++ b/updatebot/update.py @@ -364,4 +364,5 @@ return self._conaryhelper.promote(trvLst, expected, self._cfg.sourceLabel, targetLabel, - checkPackageList=checkPackageList) + checkPackageList=checkPackageList, + extraPromoteTroves=self._cfg.extraPromoteTroves) From johnsonm at rpath.com Wed Aug 19 17:39:00 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:39:00 +0000 Subject: mirrorball: 1) use """ instead of ''' for doc strings Message-ID: <200908192139.n7JLd0HS014212@scc.eng.rpath.com> changeset: cb088cb66554 user: Matt Wilson <https://issues.rpath.com/> date: Mon, 02 Jun 2008 15:42:08 -0400 1) use """ instead of ''' for doc strings 2) append the package location when building the manifest, not the package object 3) avoid adding the same RPM multiple times to the rpmMap to handle cases where several repositories hold the same binary RPM 4) remove unused code committer: Matt Wilson <https://issues.rpath.com/> diff --git a/rpmimport/rpmsource.py b/rpmimport/rpmsource.py --- a/rpmimport/rpmsource.py +++ b/rpmimport/rpmsource.py @@ -13,18 +13,18 @@ # full details. # -''' +""" Module for interacting with packages in multiple yum repositories. -''' +""" import os import repomd class PackageChecksumMismatchError(Exception): - ''' + """ Exception for packages that have checksums that don't match. - ''' + """ def __init__(self, pkg1, pkg2): Exception.__init__(self) @@ -38,9 +38,9 @@ class RpmSource(object): - ''' + """ Class that builds maps of packages from multiple yum repositories. - ''' + """ def __init__(self): # {srpm: {rpm: path} @@ -52,30 +52,14 @@ # {srpm: path} self.srcPath = dict() - @classmethod - def transformName(cls, name): + def _procSrc(self, basePath, package): """ - In name, - => _, + => _plus. - """ - - return name.replace('-', '_').replace('+', '_plus') - - @classmethod - def quoteSequence(cls, seq): - """ - [a, b] => 'a', 'b' - """ - - return ', '.join("'%s'" % x for x in sorted(seq)) - - def _procSrc(self, basePath, package): - ''' Process source rpms. @param basePath: path to yum repository. @type basePath: string @param package: package object @type package: repomd.packagexml._Package - ''' + """ shortSrpm = os.path.basename(package.location) longLoc = basePath + '/' + package.location if shortSrpm in self.srcPath: @@ -90,17 +74,23 @@ self.srcPath[shortSrpm] = package def _procBin(self, basePath, package): - ''' + """ Process binary rpms. @param basePath: path to yum repository. @type basePath: string @param package: package object @type package: repomd.packagexml._Package - ''' + """ srpm = package.sourcerpm - longLoc = basePath + '/' + package.location + longLoc = basePath + '/' + package.location package.location = longLoc if self.rpmMap.has_key(srpm): + shortLoc = os.path.basename(package.location) + # remove duplicates of this binary RPM - last one wins + existing = [ x for x in self.rpmMap[srpm].iterkeys() + if os.path.basename(x) == shortLoc ] + for key in existing: + del self.rpmMap[srpm][key] self.rpmMap[srpm][longLoc] = package else: self.rpmMap[srpm] = {longLoc: package} @@ -184,14 +174,12 @@ return 'i686', archMap['i686'] return None, None - def createManifest(self, srpm, prefix): + def createManifest(self, srpm): """ @return the text for the manifest file. """ l = [] l.append(self.srcPath[srpm].location) - l.extend([x for x in self.getRPMS(srpm)]) - if prefix: - l = [ x[len(prefix):] for x in l] + l.extend([x.location for x in self.getRPMS(srpm)]) # add a trailing newline return '\n'.join(sorted(l) + ['']) From johnsonm at rpath.com Wed Aug 19 17:39:15 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:39:15 +0000 Subject: mirrorball: add top level bot class with basic outline of execution Message-ID: <200908192139.n7JLdFZV014841@scc.eng.rpath.com> changeset: 258d60c0bf93 user: Elliot Peele <http://issues.rpath.com/> date: Tue, 10 Jun 2008 16:33:42 -0400 add top level bot class with basic outline of execution committer: Elliot Peele <http://issues.rpath.com/> diff --git a/updatebot/bot.py b/updatebot/bot.py new file mode 100644 --- /dev/null +++ b/updatebot/bot.py @@ -0,0 +1,101 @@ +# +# Copyright (c) 2008 rPath, Inc. +# +# This program is distributed under the terms of the Common Public License, +# version 1.0. A copy of this license should have been distributed with this +# source file in a file called LICENSE. If it is not present, the license +# is always available at http://www.rpath.com/permanent/licenses/CPL-1.0. +# +# This program is distributed in the hope that it will be useful, but +# without any warranty; without even the implied warranty of merchantability +# or fitness for a particular purpose. See the Common Public License for +# full details. +# + +""" +Module for driving the update process. +""" + +import logging + +import repomd +from rpmvercmp import rpmvercmp +from rpmimport import rpmsource + +from updatebot import util +from updatebot import build +from updatebot import update +from updatebot import advise +from updatebot.errors import * + +log = logging.getLogger('updatebot.bot') + +class Bot(object): + """ + Top level object for driving update process. + """ + + def __init__(self, cfg): + self._cfg = cfg + + self._clients = {} + self._rpmSource = rpmsource.RpmSource() + self._updater = update.Updater(self._cfg, self._rpmSource) + self._advisor = advise.Advisor(self._cfg, self._rpmSource) + self._builder = build.Builder(self._cfg) + + def _populateRpmSource(self): + """ + Populate the rpm source data structures. + """ + + for repo in self._cfg.repositoryPaths: + log.info('loading %s/%s repository data' + % (self._cfg.repositoryUrl, repo)) + client = repomd.Client(self._cfg.repositoryUrl + '/' + repo) + self._rpmSource.loadFromClient(client, repo) + self._clients[repo] = client + self._rpmSource.finalize() + + def run(self): + """ + Update the conary repository from the yum repositories. + """ + + log.info('starting update') + + # Populate rpm source object from yum metadata. + self._populateRpmSource() + + # Get troves to update and send advisories. + toAdvise, toUpdate = self._updater.getUpdates() + + # Check to see if advisories exist for all required packages. + self._advisor.check(toAdvise) + + # Update sources. + for nvf, srcPkg in toUpdate: + self._updater.update(nvf, srcPkg) + + # Make sure to build everything in the toAdvise list, there may be + # sources that have been updated, but not built. + buildTroves = [ x[0] for x in toAdvise ] + trvMap = self._builder.build(buildTroves) + + # Build group. + grpTrvMap = self._builder.build(self._cfg.topGroup) + + # Promote group. + # FIXME: should be able to get the new versions of packages from + # promote. + helper = self._updater.getConaryHelper() + newTroves = helper.promote(grpTrvMap.values(), self._cfg.targetLabel) + + # FIXME: build a trvMap from source tove nvf to new binary trove nvf + + # Send advisories. + self._advisor.send(trvMap, toAdvise) + + log.info('update completed successfully') + log.info('updated %s packages and sent %s advisories' + % (len(toUpdate), len(toAdvise))) From johnsonm at rpath.com Wed Aug 19 17:42:03 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:42:03 +0000 Subject: mirrorball: add split arch building, allow for commits of multiple jobs at once Message-ID: <200908192142.n7JLg3rW021587@scc.eng.rpath.com> changeset: 3f50940cbcb8 user: Elliot Peele <https://issues.rpath.com/> date: Wed, 14 Jan 2009 11:27:14 -0500 add split arch building, allow for commits of multiple jobs at once committer: Elliot Peele <https://issues.rpath.com/> diff --git a/updatebot/build.py b/updatebot/build.py --- a/updatebot/build.py +++ b/updatebot/build.py @@ -108,7 +108,7 @@ jobs[index].append(trv) - if i % 40 == 0: + if i % 50 == 0: index += 1 failed = set() @@ -157,6 +157,52 @@ return results, failed + def buildsplitarch(self, troveSpecs): + """ + Build a list of packages, in N jobs where N is the number of + configured arch contexts. + @param troveSpecs: list of trove specs + @type troveSpecs: [(name, versionObj, flavorObj), ...] + @return troveMap: dictionary of troveSpecs to built troves + """ + + # Split troves by context. + jobs = {} + for trv in self._formatInput(troveSpecs): + if len(trv) != 4: + continue + + key = trv[3] + if key not in jobs: + jobs[key] = [] + jobs[key].append(trv) + + # Start all build jobs. + jobIds = {} + for ctx, job in jobs.iteritems(): + jobIds[ctx] = self._startJob(job) + + fmtstr = ','.join([ '%s:%s' % (x, y) for x, y in jobIds.iteritems()]) + log.info('Started %s' % fmtstr) + + # Wait for the jobs to finish. + log.info('Waiting for jobs to complete') + for jobId in jobIds.itervalues(): + job = self._helper.getJob(jobId) + while not job.isFinished() and not job.isFailed(): + time.sleep(1) + job = self._helper.getJob(jobId) + + # Sanity check all jobs. + for jobId in jobIds.itervalues(): + self._sanityCheckJob(jobId) + + # Commit if all jobs were successfull. + trvMap = self._commitJob(jobIds.values()) + + ret = self._formatOutput(trvMap) + return ret + def start(self, troveSpecs): """ Public version of start job that starts a job without monitoring. @@ -271,23 +317,30 @@ @return troveMap: dictionary of troveSpecs to built troves """ + if type(jobId) != list: + jobIds = [ jobId, ] + else: + jobIds = jobId + + jobIdsStr = ','.join(jobIds) + # Do the commit startTime = time.time() - job = self._helper.getJob(jobId) - log.info('Starting commit of job %d', jobId) + jobs = [ self._helper.getJob(x) for x in jobIds ] + log.info('Starting commit of job %s', jobIdsStr) - self._helper.client.startCommit([jobId, ]) + self._helper.client.startCommit(jobIds) succeeded, data = commit.commitJobs(self._helper.getConaryClient(), - [job, ], + jobs, self._rmakeCfg.reposName, self._cfg.commitMessage) if not succeeded: - self._helper.client.commitFailed([jobId, ], data) - raise CommitFailedError(jobId=job.jobId, why=data) + self._helper.client.commitFailed(jobIds, data) + raise CommitFailedError(jobId=jobIdsStr, why=data) - log.info('Commit of job %d completed in %.02f seconds', - jobId, time.time() - startTime) + log.info('Commit of job %s completed in %.02f seconds', + jobIdsStr, time.time() - startTime) troveMap = {} for troveTupleDict in data.itervalues(): From johnsonm at rpath.com Wed Aug 19 17:40:49 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:40:49 +0000 Subject: mirrorball: testsuite.py Message-ID: <200908192140.n7JLenVn018634@scc.eng.rpath.com> changeset: 0d326cb11d90 user: Elliot Peele <https://issues.rpath.com/> date: Mon, 15 Sep 2008 16:10:47 -0400 testsuite.py committer: Elliot Peele <https://issues.rpath.com/> diff --git a/test/bootstrap.py b/test/bootstrap.py --- a/test/bootstrap.py +++ b/test/bootstrap.py @@ -1,7 +1,7 @@ import os import sys -testUtilDir = os.environ.get('TESTUTILS_PATH', '../testutils') +testUtilDir = os.environ.get('TESTUTILS_PATH', '../../testutils') if os.path.exists(testUtilDir): sys.path.insert(0, testUtilDir) diff --git a/test/testsuite.py b/test/testsuite.py --- a/test/testsuite.py +++ b/test/testsuite.py @@ -54,20 +54,21 @@ sys.path.insert(0, thisPath) return thisPath - conaryDir = setPathFromEnv('CONARY_PATH', 'conary') - conaryTestPath = setPathFromEnv('CONARY_TEST_PATH', 'conary-test') + testutilsPath = setPathFromEnv('TESTUTILS_PATH', '../testutils') + conaryDir = setPathFromEnv('CONARY_PATH', '../conary') + conaryTestPath = setPathFromEnv('CONARY_TEST_PATH', '../conary-test') setPathFromEnv('CONARY_POLICY_PATH', '/usr/lib/conary/policy') mirrorballPath = setPathFromEnv('SLEESTACK_PATH', '') - rmakePath = setPathFromEnv('RMAKE_PATH', 'rmake') - rmakeTestPath = setPathFromEnv('RMAKE_TEST_PATH', 'rmake-private/test') - xmllibPath = setPathFromEnv('XMLLIB_PATH', 'rpath-xmllib') + rmakePath = setPathFromEnv('RMAKE_PATH', '../rmake') + rmakeTestPath = setPathFromEnv('RMAKE_TEST_PATH', '../rmake-private') + xmllibPath = setPathFromEnv('XMLLIB_PATH', '../rpath-xmllib') pluginPath = os.path.realpath(rmakeTestPath + '/rmake_plugins') # Insert the following paths into the python path and sys path in # listed order. paths = (mirrorballPath, rmakePath, conaryDir, conaryTestPath, - rmakeTestPath, xmllibPath) + rmakeTestPath, xmllibPath, testutilsPath) pythonPath = os.environ.get('PYTHONPATH', "") for p in reversed(paths): if p in sys.path: diff --git a/updatebot/cmdline/command.py b/updatebot/cmdline/command.py --- a/updatebot/cmdline/command.py +++ b/updatebot/cmdline/command.py @@ -29,7 +29,6 @@ class BotCommand(options.AbstractCommand): defaultGroup = 'Common Options' - commandGroup = CG_MISC docs = {'config' : (VERBOSE_HELP, "Set config KEY to VALUE", "'KEY VALUE'"), diff --git a/updatebot/cmdline/main.py b/updatebot/cmdline/main.py --- a/updatebot/cmdline/main.py +++ b/updatebot/cmdline/main.py @@ -15,8 +15,10 @@ import sys import logging +from conary.lib import util from conary.lib import options +from updatebot import errors from updatebot import config from updatebot import constants from updatebot import log as logger @@ -27,7 +29,7 @@ version = constants.version abstractCommand = command.BotCommand - configClass = config.UpdateBotConfiguration + configClass = config.UpdateBotConfig useConaryOptions = False @@ -49,13 +51,12 @@ try: argv = list(argv) debugAll = '--debug-all' in argv + debuggerException = errors.UpdateBotError if debugAll: debuggerException = Exception argv.remove('--debug-all') - else: - debuggerException = errors.UpdateBotError - sys.excepthook = errors.genExcepthook(debug=debugAll, - debugCtrlC=debugAll) + sys.excepthook = util.genExcepthook(debug=debugAll, + debugCtrlC=debugAll) return BotMain().main(argv, debuggerException=debuggerException) except debuggerException, err: raise From johnsonm at rpath.com Wed Aug 19 17:42:50 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:42:50 +0000 Subject: mirrorball: better advisory module that raises exceptions on access rather than Message-ID: <200908192142.n7JLgo6I023477@scc.eng.rpath.com> changeset: 0ee0dc651868 user: Elliot Peele <https://issues.rpath.com/> date: Wed, 29 Apr 2009 16:55:39 -0400 better advisory module that raises exceptions on access rather than instantiation, remove unneeded stubs committer: Elliot Peele <https://issues.rpath.com/> diff --git a/updatebot/advisories/__init__.py b/updatebot/advisories/__init__.py --- a/updatebot/advisories/__init__.py +++ b/updatebot/advisories/__init__.py @@ -21,17 +21,33 @@ log = logging.getLogger('updatebot.advisories') +_supportedBackends = ('sles', 'centos', 'ubuntu', ) + class InvalidBackendError(Exception): """ Raised when an unsupported backend is used. """ -__supportedBackends = ('sles', 'centos', 'ubuntu', 'fedora', 'scientific') + +class _UnsupportedAdivsor(object): + """ + Stub object to raise exception on access rather than instantiation. + """ + + class Advisor(object): + def __init__(self, cfg, backend): + self.backend = backend + + def __getattr__(self, name): + if name == 'backend': + return getattr(self, name) + raise InvalidBackendError('%s is not a supported backend, please ' + 'choose from %s' % (self.backend, ','.join(_supportedBackends))) + def __getBackend(backend): - if backend not in __supportedBackends: - raise InvalidBackendError('%s is not a supported backend, please ' - 'choose from %s' % (backend, ','.join(__supportedBackends))) + if backend not in _supportedBackends: + return _UnsupportedAdivsor try: updatebotPath = [imp.find_module('updatebot')[1], ] diff --git a/updatebot/advisories/fedora.py b/updatebot/advisories/fedora.py deleted file mode 100644 --- a/updatebot/advisories/fedora.py +++ /dev/null @@ -1,30 +0,0 @@ -# -# Copyright (c) 2008-2009 rPath, Inc. -# -# This program is distributed under the terms of the Common Public License, -# version 1.0. A copy of this license should have been distributed with this -# source file in a file called LICENSE. If it is not present, the license -# is always available at http://www.rpath.com/permanent/licenses/CPL-1.0. -# -# This program is distributed in the hope that it will be useful, but -# without any warranty; without even the implied warranty of merchantability -# or fitness for a particular purpose. See the Common Public License for -# full details. -# - -""" -Advisory module for Fedora. -""" - -import os -import pmap -import logging - -from updatebot.advisories.common import BaseAdvisor - -log = logging.getLogger('updatebot.advisories') - -class Advisor(BaseAdvisor): - """ - Class for processing Fedora advisory information. - """ diff --git a/updatebot/advisories/scientific.py b/updatebot/advisories/scientific.py deleted file mode 100644 --- a/updatebot/advisories/scientific.py +++ /dev/null @@ -1,30 +0,0 @@ -# -# Copyright (c) 2008-2009 rPath, Inc. -# -# This program is distributed under the terms of the Common Public License, -# version 1.0. A copy of this license should have been distributed with this -# source file in a file called LICENSE. If it is not present, the license -# is always available at http://www.rpath.com/permanent/licenses/CPL-1.0. -# -# This program is distributed in the hope that it will be useful, but -# without any warranty; without even the implied warranty of merchantability -# or fitness for a particular purpose. See the Common Public License for -# full details. -# - -""" -Advisory module for Scientific Linux. -""" - -import os -import pmap -import logging - -from updatebot.advisories.common import BaseAdvisor - -log = logging.getLogger('updatebot.advisories') - -class Advisor(BaseAdvisor): - """ - Class for processing Scientific Linux advisory information. - """ From johnsonm at rpath.com Wed Aug 19 17:42:52 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:42:52 +0000 Subject: mirrorball: add support for creating a package Message-ID: <200908192142.n7JLgqDt023562@scc.eng.rpath.com> changeset: ca46a2a8cd42 user: Elliot Peele <https://issues.rpath.com/> date: Mon, 04 May 2009 14:31:58 -0400 add support for creating a package committer: Elliot Peele <https://issues.rpath.com/> diff --git a/scripts/recreate b/scripts/recreate new file mode 100755 --- /dev/null +++ b/scripts/recreate @@ -0,0 +1,34 @@ +#!/usr/bin/python +# +# Copyright (c) 2009 rPath, Inc. +# +# This program is distributed under the terms of the Common Public License, +# version 1.0. A copy of this license should have been distributed with this +# source file in a file called LICENSE. If it is not present, the license +# is always available at http://www.rpath.com/permanent/licenses/CPL-1.0. +# +# This program is distributed in the hope that it will be useful, but +# without any warranty; without even the implied warranty of merchantability +# or fitness for a particular purpose. See the Common Public License for +# full details. +# + +import os +import sys + +sys.path.insert(0, os.environ['HOME'] + '/hg/mirrorball') + +from updatebot import log +from updatebot import bot +from updatebot import config + +log.addRootLogger() + +cfg = config.UpdateBotConfig() +cfg.read(os.environ['HOME'] + '/hg/mirrorball/config/%s/updatebotrc' % sys.argv[1]) + +obj = bot.Bot(cfg) + +pkgs = sys.argv[2:] + +obj.create(recreate=pkgs) diff --git a/updatebot/bot.py b/updatebot/bot.py --- a/updatebot/bot.py +++ b/updatebot/bot.py @@ -57,7 +57,7 @@ lst.extend(list(trvSet)) return lst - def create(self, rebuild=False): + def create(self, rebuild=False, recreate=None): """ Do initial imports. """ @@ -69,7 +69,9 @@ self._pkgSource.load() # Build list of packages - if self._cfg.packageAll: + if recreate: + toPackage = set(recreate) + elif self._cfg.packageAll: toPackage = set() for srcName, srcSet in self._pkgSource.srcNameMap.iteritems(): if len(srcSet) == 0: @@ -96,7 +98,8 @@ # Import sources into repository. toBuild, fail = self._updater.create(toPackage, - buildAll=rebuild) + buildAll=rebuild, + recreate=bool(recreate)) log.info('failed to create %s packages' % len(fail)) log.info('found %s packages to build' % len(toBuild)) diff --git a/updatebot/update.py b/updatebot/update.py --- a/updatebot/update.py +++ b/updatebot/update.py @@ -217,13 +217,15 @@ return ret - def create(self, pkgNames, buildAll=False): + def create(self, pkgNames, buildAll=False, recreate=False): """ Import a new package into the repository. @param pkgNames: list of packages to import @type pkgNames: list @param buildAll: return a list of all troves found rather than just the new ones. @type buildAll: boolean + @param recreate: a package manifest even if it already exists. + @type recreate: boolean @return new source [(name, version, flavor), ... ] """ @@ -250,11 +252,11 @@ try: # Only import packages that haven't been imported before version = verCache.get('%s:source' % pkg.name) - if not version: + if not version or recreate: log.info('attempting to import %s' % pkg) version = self.update((pkg.name, None, None), pkg) - if not verCache.get(pkg.name) or buildAll: + if not verCache.get(pkg.name) or buildAll or recreate: toBuild.add((pkg.name, version, None)) else: log.info('not building %s' % pkg.name) From johnsonm at rpath.com Wed Aug 19 17:39:30 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:39:30 +0000 Subject: mirrorball: setup rmake helper and config correctly Message-ID: <200908192139.n7JLdVwB015499@scc.eng.rpath.com> changeset: d3cbbdb47e1d user: Elliot Peele <http://issues.rpath.com/> date: Thu, 19 Jun 2008 21:38:30 -0400 setup rmake helper and config correctly filter trove list on the way in and the out committer: Elliot Peele <http://issues.rpath.com/> diff --git a/updatebot/build.py b/updatebot/build.py --- a/updatebot/build.py +++ b/updatebot/build.py @@ -19,8 +19,13 @@ import time import logging +from conary import conarycfg, conaryclient + +from rmake import plugins +from rmake.build import buildcfg from rmake.cmdline import helper, monitor, commit +from updatebot import util from updatebot.errors import JobFailedError, CommitFailedError log = logging.getLogger('updateBot.build') @@ -39,7 +44,25 @@ def __init__(self, cfg): self._cfg = cfg - self._helper = helper.rMakeHelper(root=self._cfg.configPath) + self._ccfg = conarycfg.ConaryConfiguration(readConfigFiles=False) + self._ccfg.read(util.join(self._cfg.configPath, 'conaryrc')) + self._ccfg.initializeFlavors() + + self._client = conaryclient.ConaryClient(self._ccfg) + + # Get default pluginDirs from the rmake cfg object, setup the plugin + # manager, then create a new rmake config object so that rmakeUser + # will be parsed correctly. + rmakeCfg = buildcfg.BuildConfiguration(readConfigFiles=False) + pluginMgr = plugins.PluginManager(rmakeCfg.pluginDirs) + pluginMgr.loadPlugins() + pluginMgr.callClientHook('client_preInit', self, []) + + self._rmakeCfg = buildcfg.BuildConfiguration(readConfigFiles=False) + self._rmakeCfg.read(util.join(self._cfg.configPath, 'rmakerc')) + self._rmakeCfg.useConaryConfig(self._ccfg) + + self._helper = helper.rMakeHelper(buildConfig=self._rmakeCfg) def build(self, troveSpecs): """ @@ -49,12 +72,29 @@ @return troveMap: dictionary of troveSpecs to built troves """ - jobId = self._startJob(troveSpecs) + # Build all troves in defined contexts. + troves = [] + for name, version, flavor in troveSpecs: + for context in self._cfg.archContexts: + troves.append((name, version, flavor, context)) + + jobId = self._startJob(troves) self._monitorJob(jobId) self._sanityCheckJob(jobId) trvMap = self._commitJob(jobId) - return trvMap + # Format trvMap into something more usefull. + # {(name, version, None): set([(name, version, flavor), ...])} + ret = {} + for sn, sv, sf, c in trvMap.iterkeys(): + n = sn.split(':')[0] + if (n, sv, None) not in ret: + ret[(n, sv, None)] = set() + for name, version, flavor in trvMap[(sn, sv, sf)]: + if name == n: + ret[(n, v, None)].add((name, version, flavor)) + + return ret def _startJob(self, troveSpecs): """ @@ -114,10 +154,10 @@ startTime = time.time() job = self._helper.getJob(jobId) log.info('Starting commit of job %d', jobId) - self._helper.client.startCommit(jobId) - succeeded, data = commit.commitJobs(self._helper.getConaryClient(), + self._helper.client.startCommit([jobId, ]) + succeeded, data = commit.commitJobs(self._client, [job, ], - self._helper.buildConfig.reposName, + self._rmakeCfg.reposName, self._cfg.commitMessage) if not succeeded: self._helper.client.commitFailed([jobId, ], data) @@ -135,6 +175,9 @@ return troveMap + def _registerCommand(self, *args, **kwargs): + 'Fake rMake hook' + class _StatusOnlyDisplay(monitor.JobLogDisplay): """ From johnsonm at rpath.com Wed Aug 19 17:39:36 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:39:36 +0000 Subject: mirrorball: add test for promote Message-ID: <200908192139.n7JLda0V015687@scc.eng.rpath.com> changeset: 3fb4801a7150 user: Elliot Peele <http://issues.rpath.com/> date: Mon, 23 Jun 2008 18:49:29 -0400 add test for promote committer: Elliot Peele <http://issues.rpath.com/> diff --git a/test/unit_test/updatebottest/conaryhelpertest.py b/test/unit_test/updatebottest/conaryhelpertest.py --- a/test/unit_test/updatebottest/conaryhelpertest.py +++ b/test/unit_test/updatebottest/conaryhelpertest.py @@ -265,7 +265,7 @@ result = helper.setManifest(trvName, manifestLst, commitMessage) self.failUnlessEqual(result, []) self.failUnless(os.path.exists('foo/manifest')) - self.failUnlessEqual(open('foo/manifest').read(), 'bar\nbaz') + self.failUnlessEqual(open('foo/manifest').read(), 'bar\nbaz\n') mockGetVersionsByName._mock.assertCalled('%s:source' % trvName) mockNewPkg._mock.assertCalled(trvName) mockCheckout._mock.assertNotCalled() @@ -280,7 +280,7 @@ result = helper.setManifest(trvName, manifestLst, commitMessage) self.failUnlessEqual(result, ['bar', ]) self.failUnless(os.path.exists('bar/manifest')) - self.failUnlessEqual(open('bar/manifest').read(), 'bar\nbaz') + self.failUnlessEqual(open('bar/manifest').read(), 'bar\nbaz\n') mockGetVersionsByName._mock.assertCalled('%s:source' % trvName) mockNewPkg._mock.assertNotCalled() mockCheckout._mock.assertCalled(trvName) @@ -358,5 +358,49 @@ mockRepos.getTroveLeavesByLabel._mock.assertCalled( {pkgName: {mockLabel: None}}) + def testPromote(self): + mockFromLabel = mock.MockObject() + mockToLabel = mock.MockObject() + mockVersion = mock.MockObject() + mockCs = mock.MockObject() + mockCsNewTrove = mock.MockObject() + mockRepos = mock.MockObject() + + trvLst = [('foo', mockVersion, None), ] + + mockVersion.trailingLabel._mock.setReturn(mockFromLabel) + mockCs.iterNewTroveList._mock.setReturn([mockCsNewTrove, ]) + mockRepos.commitChangeSet._mock.setReturn(None, mockCs) + + def getMockClient(returnValue): + mockClient = mock.MockObject() + mockClient.createSiblingCloneChangeSet._mock.setReturn( + returnValue, {mockFromLabel: mockToLabel}, trvLst, + cloneSources=True) + return mockClient + + helper = conaryhelper.ConaryHelper(self.updateBotCfg) + helper._repos = mockRepos + + # normal case + expected = ('foo', None, None) + helper._client = getMockClient((True, mockCs)) + mockCsNewTrove.getNewNameVersionFlavor._mock.setReturn(expected) + result = helper.promote(trvLst, trvLst, mockToLabel) + self.failUnlessEqual(result, [expected, ]) + mockVersion.trailingLabel._mock.assertCalled() + mockCs.iterNewTroveList._mock.assertCalled() + mockCsNewTrove.getNewNameVersionFlavor._mock.assertCalled() + mockRepos.commitChangeSet._mock.assertCalled(mockCs) + + # test PromoteFailedError exception + helper._client = getMockClient((False, mockCs)) + self.failUnlessRaises(errors.PromoteFailedError, helper.promote, + trvLst, trvLst, mockToLabel) + + # test PromoteMismatchError exception + helper._client = getMockClient((True, mockCs)) + self.failUnlessRaises(errors.PromoteMismatchError, helper.promote, + trvLst, [('bar', None, None), ], mockToLabel) testsetup.main() diff --git a/updatebot/conaryhelper.py b/updatebot/conaryhelper.py --- a/updatebot/conaryhelper.py +++ b/updatebot/conaryhelper.py @@ -305,6 +305,7 @@ {fromLabel:targetLabel}, trvLst, cloneSources=True) + if not success: raise PromoteFailedError(what=trvLst) From johnsonm at rpath.com Wed Aug 19 17:40:01 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:40:01 +0000 Subject: mirrorball: initial report.py Message-ID: <200908192140.n7JLe2Ex016693@scc.eng.rpath.com> changeset: 328b3a794cd5 user: Matt Wilson <https://issues.rpath.com/> date: Sat, 21 Jun 2008 16:09:48 -0400 initial report.py committer: Matt Wilson <https://issues.rpath.com/> diff --git a/scripts/report.py b/scripts/report.py new file mode 100755 --- /dev/null +++ b/scripts/report.py @@ -0,0 +1,105 @@ +#!/usr/bin/python +# +# Copyright (c) 2008 rPath, Inc. +# +# This program is distributed under the terms of the Common Public License, +# version 1.0. A copy of this license should have been distributed with this +# source file in a file called LICENSE. If it is not present, the license +# is always available at http://www.rpath.com/permanent/licenses/CPL-1.0. +# +# This program is distributed in the hope that it will be useful, but +# without any warranty; without even the implied warranty of merchantability +# or fitness for a particular purpose. See the Common Public License for +# full details. +# + +import os +import sys +import operator + +sys.path.insert(0, os.environ['HOME'] + '/hg/rpath-xmllib') +sys.path.insert(0, os.environ['HOME'] + '/hg/conary') +sys.path.insert(0, os.environ['HOME'] + '/hg/mirrorball') + +from conary.lib import util +from conary.lib.sha1helper import md5String, md5ToString +from conary import rpmhelper, files, updatecmd +sys.excepthook = util.genExcepthook() + +from updatebot import bot, conaryhelper, config, log +from rpmutils import readHeader + +outfile = open(sys.argv[1], 'w') +cb = updatecmd.UpdateCallback() + +#import cProfile +#prof = cProfile.Profile() +#prof.enable() + +log.addRootLogger() +cfg = config.UpdateBotConfig() +cfg.read(os.environ['HOME'] + '/hg/mirrorball/config/sles/updatebotrc') +obj = bot.Bot(cfg) +obj._populateRpmSource() + +#prof.disable() +#prof.dump_stats('report.lsprof') + +conaryhelper = obj._updater._conaryhelper +conaryclient = conaryhelper._client + +pkgMap = conaryhelper.getSourceTroves(cfg.topGroup) +sources = sorted(pkgMap.keys()) + +for src in sources: + srcName = src[0].split(':')[0] + if srcName in cfg.excludePackages and srcName != 'kernel': + continue + manifest = conaryhelper.getManifest(srcName) + + pathMap = {} + # collect up a path -> rpm package map + for rpm in manifest: + if 'src.rpm' in rpm: + continue + rpmloc = '%s/%s' %(cfg.repositoryUrl, rpm) + h = readHeader(rpmloc) + arch = h[rpmhelper.ARCH] + rpmname = os.path.basename(rpm) + d = dict.fromkeys(((util.normpath(x), arch) for x in h.paths()), rpmname) + pathMap.update(d) + binaries = sorted(pkgMap[src]) + flist = [] + for binary in binaries: + name, version, flavor = binary + flavorStr = str(flavor) + if ':' not in name: + continue + if 'debuginfo' in name: + continue + if 'is: x86_64(' in str(flavorStr): + arch = 'x86_64' + elif 'is: x86(' in str(flavorStr): + arch = 'i586' + else: + arch = 'noarch' + cs = conaryclient.createChangeSet([(name, + (None, None), + (version, flavor), True)], + withFiles=True, + withFileContents=True, + callback=cb) + tcs = [x for x in cs.iterNewTroveList()][0] + for (pathId, path, fileId, version) in tcs.newFiles: + f = files.ThawFile(cs.getFileChange(None, fileId), pathId) + if hasattr(f, 'contents') and f.contents: + cs.reset() + cont = cs.getFileContents(pathId, fileId)[1] + md5 = md5ToString(md5String(cont.get().read())) + rpm = pathMap[(path, arch)] + flist.append((binary[0], rpm, path, md5)) + # sort based on path + flist.sort(key=operator.itemgetter(2)) + for info in flist: + outfile.write('\t'.join(info)) + outfile.write('\n') From johnsonm at rpath.com Wed Aug 19 17:39:22 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:39:22 +0000 Subject: mirrorball: add test for update sanity check changes Message-ID: <200908192139.n7JLdM1D015123@scc.eng.rpath.com> changeset: e051242c3fe9 user: Elliot Peele <http://issues.rpath.com/> date: Mon, 16 Jun 2008 12:44:00 -0400 add test for update sanity check changes committer: Elliot Peele <http://issues.rpath.com/> diff --git a/test/unit_test/updatebottest/updatetest.py b/test/unit_test/updatebottest/updatetest.py --- a/test/unit_test/updatebottest/updatetest.py +++ b/test/unit_test/updatebottest/updatetest.py @@ -148,20 +148,43 @@ oldBinPkg1 = mock.MockObject(stableReturnValues=True) oldBinPkg2 = mock.MockObject(stableReturnValues=True) + oldBinPkg3 = mock.MockObject(stableReturnValues=True) + oldBinPkg4 = mock.MockObject(stableReturnValues=True) oldBinPkg1._mock.set(name='foo') oldBinPkg2._mock.set(name='foo-devel') + oldBinPkg3._mock.set(name='bar') + oldBinPkg3._mock.set(version='1') + oldBinPkg4._mock.set(name='bar') + oldBinPkg4._mock.set(version='0') newSrcPkg = mock.MockObject() oldSrcPkg = mock.MockObject() + oldSrcPkg2 = mock.MockObject() - srcPkgMap = {newSrcPkg: [newBinPkg1, newBinPkg2]} - locationMap = {'a': oldBinPkg1, 'b': oldBinPkg2} - binPkgMap = {oldBinPkg1: oldSrcPkg, oldBinPkg2: oldSrcPkg} + srcPkgMap = {newSrcPkg: [newBinPkg1, + newBinPkg2], + oldSrcPkg: [oldBinPkg1, + oldBinPkg2, + oldBinPkg3], + oldSrcPkg2: [oldBinPkg4, ]} + + locationMap = {'a': oldBinPkg1, + 'b': oldBinPkg2, + 'c': oldBinPkg3} + + binPkgMap = {oldBinPkg1: oldSrcPkg, + oldBinPkg2: oldSrcPkg, + oldBinPkg3: oldSrcPkg, + oldBinPkg4: oldSrcPkg2} + + binNameMap = {'bar': [oldBinPkg3, + oldBinPkg4]} mockRpmSource._mock.set(srcPkgMap=srcPkgMap) mockRpmSource._mock.set(locationMap=locationMap) mockRpmSource._mock.set(binPkgMap=binPkgMap) + mockRpmSource._mock.set(binNameMap=binNameMap) mockRecipeMaker = mock.MockObject(stableReturnValues=True) mockRecipeMaker.getManifest._mock.setReturn(['a', 'b'], 'foo') @@ -194,9 +217,26 @@ mockPackageVerCmp._mock.assertCalled(newSrcPkg, oldSrcPkg) mockPackageVerCmp._mock.assertCalled(newSrcPkg, oldSrcPkg) - # test remove package exception - oldBinPkg2._mock.set(name='bar') - mockPackageVerCmp._mock.setReturn(1, newSrcPkg, oldSrcPkg) + # test remove package without exception + mockRecipeMaker.getManifest._mock.setReturn(['a', 'c'], 'foo') + mockPackageVerCmp._mock.setReturn(0, newSrcPkg, oldSrcPkg) + mockPackageVerCmp._mock.setReturn(1, oldBinPkg3, oldBinPkg4) + mockPackageVerCmp._mock.setReturn(-1, oldBinPkg4, oldBinPkg3) + result = updater._sanitizeTrove(trvSpec, newSrcPkg) + self.failUnlessEqual(result, False) + mockRecipeMaker.getManifest._mock.assertCalled('foo') + mockPackageVerCmp._mock.assertCalled(newSrcPkg, oldSrcPkg) + mockPackageVerCmp._mock.assertCalled(newSrcPkg, oldSrcPkg) + self.failUnless(oldBinPkg3 in srcPkgMap[newSrcPkg]) + # reset srcPkgMap + srcPkgMap[newSrcPkg].remove(oldBinPkg3) + + # test remove package with exception + oldBinPkg4._mock.set(version='2') + mockRecipeMaker.getManifest._mock.setReturn(['a', 'c'], 'foo') + mockPackageVerCmp._mock.setReturn(0, newSrcPkg, oldSrcPkg) + mockPackageVerCmp._mock.setReturn(-1, oldBinPkg3, oldBinPkg4) + mockPackageVerCmp._mock.setReturn(1, oldBinPkg4, oldBinPkg3) self.failUnlessRaises(errors.UpdateRemovesPackageError, updater._sanitizeTrove, trvSpec, newSrcPkg) mockRecipeMaker.getManifest._mock.assertCalled('foo') From johnsonm at rpath.com Wed Aug 19 17:41:43 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:41:43 +0000 Subject: mirrorball: plumb in mirror support Message-ID: <200908192141.n7JLfhie020770@scc.eng.rpath.com> changeset: b85ac183e6c3 user: Elliot Peele <https://issues.rpath.com/> date: Wed, 19 Nov 2008 22:44:40 -0500 plumb in mirror support add support for only updating a single package while ignoring update sanity checks committer: Elliot Peele <https://issues.rpath.com/> diff --git a/updatebot/bot.py b/updatebot/bot.py --- a/updatebot/bot.py +++ b/updatebot/bot.py @@ -119,11 +119,17 @@ return trvMap - def update(self): + def update(self, force=None): """ Update the conary repository from the yum repositories. + @param force: list of packages to update without exception + @type force: list(pkgName, pkgName, ...) """ + if force is not None: + self._cfg.disableUpdateSanity = True + assert isinstance(force, list) + start = time.time() log.info('starting update') @@ -133,6 +139,20 @@ # Get troves to update and send advisories. toAdvise, toUpdate = self._updater.getUpdates() + # If forcing an update, make sure that all packages are listed in + # toAdvise and toUpdate as needed. + if force: + advise = list() + update = list() + for pkg in toAdvise: + if pkg[1].name in force: + advise.append(pkg) + for pkg in toUpdate: + if pkg[1].name in force: + update.append(pkg) + toAdvise = advise + toUpdate = update + if len(toAdvise) == 0: log.info('no updates available') return @@ -170,12 +190,12 @@ newTroves = self._updater.publish(toPublish, expected, self._cfg.targetLabel) + # Mirror out content + self._updater.mirror() + # Send advisories. self._advisor.send(toAdvise, newTroves) - # Mirror out content - self._update.mirror() - log.info('update completed successfully') log.info('updated %s packages and sent %s advisories' % (len(toUpdate), len(toAdvise))) diff --git a/updatebot/config.py b/updatebot/config.py --- a/updatebot/config.py +++ b/updatebot/config.py @@ -21,7 +21,7 @@ from conary.lib import cfg from conary import versions from conary.conarycfg import CfgFlavor, CfgLabel -from conary.lib.cfgtypes import CfgString, CfgList, CfgRegExp, ParseError +from conary.lib.cfgtypes import CfgString, CfgList, CfgRegExp, CfgBool, ParseError from rmake.build.buildcfg import CfgTroveSpec @@ -71,6 +71,10 @@ # platform short name platformName = CfgString + # disables checks for update completeness, this should only be enabled if + # you know what you are doing and have a good reason. + disableUpdateSanity = CfgBool + # path to configuration files relative to updatebotrc (conaryrc, rmakerc) configPath = (CfgString, './') diff --git a/updatebot/update.py b/updatebot/update.py --- a/updatebot/update.py +++ b/updatebot/update.py @@ -153,7 +153,8 @@ raise UpdateGoesBackwardsError(why=(srcPkg, srpm)) # make sure we aren't trying to remove a package - if (binPkg.name, binPkg.arch) not in newNames: + if ((binPkg.name, binPkg.arch) not in newNames and + not self._cfg.disableUpdateSanity): # Novell releases updates to only the binary rpms of a package # that have chnaged. We have to use binaries from the old srpm. # Get the last version of the pkg and add it to the srcPkgMap. @@ -367,3 +368,10 @@ self._cfg.sourceLabel, targetLabel, checkPackageList=checkPackageList, extraPromoteTroves=self._cfg.extraPromoteTroves) + + def mirror(self): + """ + If a mirror is configured, mirror out any changes. + """ + + return self._conaryhelper.mirror() From johnsonm at rpath.com Wed Aug 19 17:42:14 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:42:14 +0000 Subject: mirrorball: branch merge Message-ID: <200908192142.n7JLgEHJ022013@scc.eng.rpath.com> changeset: dba5a235529a user: Elliot Peele <https://issues.rpath.com/> date: Thu, 05 Feb 2009 18:18:39 -0500 branch merge committer: Elliot Peele <https://issues.rpath.com/> diff --git a/scripts/import.py b/scripts/import.py --- a/scripts/import.py +++ b/scripts/import.py @@ -1,4 +1,4 @@ -#!/usr/bin/python2.6 +#!/usr/bin/python # # Copyright (c) 2008 rPath, Inc. # @@ -16,11 +16,9 @@ import os import sys -sys.path.insert(0, os.environ['HOME'] + '/hg/26/rpath-xmllib') -sys.path.insert(0, os.environ['HOME'] + '/hg/26/conary') -sys.path.insert(0, os.environ['HOME'] + '/hg/26/mirrorball') -sys.path.insert(0, os.environ['HOME'] + '/hg/26/rmake') -sys.path.insert(0, os.environ['HOME'] + '/hg/26/epdb') +sys.path.insert(0, os.environ['HOME'] + '/hg/rpath-xmllib') +sys.path.insert(0, os.environ['HOME'] + '/hg/conary') +sys.path.insert(0, os.environ['HOME'] + '/hg/mirrorball') from conary.lib import util sys.excepthook = util.genExcepthook() @@ -29,7 +27,7 @@ log.addRootLogger() cfg = config.UpdateBotConfig() -cfg.read(os.environ['HOME'] + '/hg/26/mirrorball/config/sles/updatebotrc') +cfg.read(os.environ['HOME'] + '/hg/mirrorball/config/sles/updatebotrc') obj = bot.Bot(cfg) trvMap = obj.create() diff --git a/scripts/pkgsource.py b/scripts/pkgsource.py --- a/scripts/pkgsource.py +++ b/scripts/pkgsource.py @@ -3,11 +3,11 @@ import os import sys -sys.path.insert(0, os.environ['HOME'] + '/hg/26/rpath-xmllib') -sys.path.insert(0, os.environ['HOME'] + '/hg/26/conary') -sys.path.insert(0, os.environ['HOME'] + '/hg/26/mirrorball') -sys.path.insert(0, os.environ['HOME'] + '/hg/26/rmake') -sys.path.insert(0, os.environ['HOME'] + '/hg/26/epdb') +sys.path.insert(0, os.environ['HOME'] + '/hg/rpath-xmllib') +sys.path.insert(0, os.environ['HOME'] + '/hg/conary') +sys.path.insert(0, os.environ['HOME'] + '/hg/mirrorball') +sys.path.insert(0, os.environ['HOME'] + '/hg/rmake') +sys.path.insert(0, os.environ['HOME'] + '/hg/epdb') from conary.lib import util sys.excepthook = util.genExcepthook() @@ -18,8 +18,6 @@ updatebot.log.addRootLogger() log = logging.getLogger('test') -import aptmd -import repomd from updatebot import config from updatebot import pkgsource diff --git a/updatebot/build.py b/updatebot/build.py --- a/updatebot/build.py +++ b/updatebot/build.py @@ -210,6 +210,9 @@ elif name == 'kernel' and self._cfg.kernelFlavors: for context, flavor in self._cfg.kernelFlavors: troves.append((name, version, flavor, context)) + elif name in self._cfg.packageFlavors: + for context, flavor in self._cfg.packageFlavors[name]: + troves.append((name, version, flavor, context)) else: # Build all packages as x86 and x86_64. for context in self._cfg.archContexts: diff --git a/updatebot/config.py b/updatebot/config.py --- a/updatebot/config.py +++ b/updatebot/config.py @@ -21,7 +21,7 @@ from conary.lib import cfg from conary import versions from conary.conarycfg import CfgFlavor, CfgLabel -from conary.lib.cfgtypes import CfgString, CfgList, CfgRegExp, CfgBool +from conary.lib.cfgtypes import CfgString, CfgList, CfgRegExp, CfgBool, CfgDict from conary.lib.cfgtypes import ParseError from rmake.build.buildcfg import CfgTroveSpec @@ -53,8 +53,13 @@ """ try: - context, flavorStr = val.split() - flavor = CfgFlavor.parseString(self, flavorStr) + splt = val.split() + if len(splt) == 1: + context = val + flavor = None + else: + context, flavorStr = splt + flavor = CfgFlavor.parseString(self, flavorStr) return context, flavor except versions.ParseError, e: raise ParseError, e @@ -149,6 +154,9 @@ # flavors to build kernels. kernelFlavors = (CfgList(CfgContextFlavor), []) + # flavors to build packages in for packages that need specific flavoring. + packageFlavors = (CfgDict(CfgList(CfgContextFlavor)), {}) + # email information for sending advisories emailFromName = CfgString emailFrom = CfgString From johnsonm at rpath.com Wed Aug 19 17:40:02 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:40:02 +0000 Subject: mirrorball: branch merge Message-ID: <200908192140.n7JLe2nw016727@scc.eng.rpath.com> changeset: 2060e1ed4675 user: Matt Wilson <https://issues.rpath.com/> date: Sat, 21 Jun 2008 16:10:19 -0400 branch merge committer: Matt Wilson <https://issues.rpath.com/> diff --git a/scripts/report.py b/scripts/report.py new file mode 100755 --- /dev/null +++ b/scripts/report.py @@ -0,0 +1,105 @@ +#!/usr/bin/python +# +# Copyright (c) 2008 rPath, Inc. +# +# This program is distributed under the terms of the Common Public License, +# version 1.0. A copy of this license should have been distributed with this +# source file in a file called LICENSE. If it is not present, the license +# is always available at http://www.rpath.com/permanent/licenses/CPL-1.0. +# +# This program is distributed in the hope that it will be useful, but +# without any warranty; without even the implied warranty of merchantability +# or fitness for a particular purpose. See the Common Public License for +# full details. +# + +import os +import sys +import operator + +sys.path.insert(0, os.environ['HOME'] + '/hg/rpath-xmllib') +sys.path.insert(0, os.environ['HOME'] + '/hg/conary') +sys.path.insert(0, os.environ['HOME'] + '/hg/mirrorball') + +from conary.lib import util +from conary.lib.sha1helper import md5String, md5ToString +from conary import rpmhelper, files, updatecmd +sys.excepthook = util.genExcepthook() + +from updatebot import bot, conaryhelper, config, log +from rpmutils import readHeader + +outfile = open(sys.argv[1], 'w') +cb = updatecmd.UpdateCallback() + +#import cProfile +#prof = cProfile.Profile() +#prof.enable() + +log.addRootLogger() +cfg = config.UpdateBotConfig() +cfg.read(os.environ['HOME'] + '/hg/mirrorball/config/sles/updatebotrc') +obj = bot.Bot(cfg) +obj._populateRpmSource() + +#prof.disable() +#prof.dump_stats('report.lsprof') + +conaryhelper = obj._updater._conaryhelper +conaryclient = conaryhelper._client + +pkgMap = conaryhelper.getSourceTroves(cfg.topGroup) +sources = sorted(pkgMap.keys()) + +for src in sources: + srcName = src[0].split(':')[0] + if srcName in cfg.excludePackages and srcName != 'kernel': + continue + manifest = conaryhelper.getManifest(srcName) + + pathMap = {} + # collect up a path -> rpm package map + for rpm in manifest: + if 'src.rpm' in rpm: + continue + rpmloc = '%s/%s' %(cfg.repositoryUrl, rpm) + h = readHeader(rpmloc) + arch = h[rpmhelper.ARCH] + rpmname = os.path.basename(rpm) + d = dict.fromkeys(((util.normpath(x), arch) for x in h.paths()), rpmname) + pathMap.update(d) + binaries = sorted(pkgMap[src]) + flist = [] + for binary in binaries: + name, version, flavor = binary + flavorStr = str(flavor) + if ':' not in name: + continue + if 'debuginfo' in name: + continue + if 'is: x86_64(' in str(flavorStr): + arch = 'x86_64' + elif 'is: x86(' in str(flavorStr): + arch = 'i586' + else: + arch = 'noarch' + cs = conaryclient.createChangeSet([(name, + (None, None), + (version, flavor), True)], + withFiles=True, + withFileContents=True, + callback=cb) + tcs = [x for x in cs.iterNewTroveList()][0] + for (pathId, path, fileId, version) in tcs.newFiles: + f = files.ThawFile(cs.getFileChange(None, fileId), pathId) + if hasattr(f, 'contents') and f.contents: + cs.reset() + cont = cs.getFileContents(pathId, fileId)[1] + md5 = md5ToString(md5String(cont.get().read())) + rpm = pathMap[(path, arch)] + flist.append((binary[0], rpm, path, md5)) + # sort based on path + flist.sort(key=operator.itemgetter(2)) + for info in flist: + outfile.write('\t'.join(info)) + outfile.write('\n') From johnsonm at rpath.com Wed Aug 19 17:43:03 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:43:03 +0000 Subject: mirrorball: add support for sles11 advisories Message-ID: <200908192143.n7JLh3aW024004@scc.eng.rpath.com> changeset: d1630c4e42cd user: Elliot Peele <https://issues.rpath.com/> date: Mon, 08 Jun 2009 22:15:18 -0400 add support for sles11 advisories committer: Elliot Peele <https://issues.rpath.com/> diff --git a/updatebot/advisories/__init__.py b/updatebot/advisories/__init__.py --- a/updatebot/advisories/__init__.py +++ b/updatebot/advisories/__init__.py @@ -21,7 +21,7 @@ log = logging.getLogger('updatebot.advisories') -_supportedBackends = ('sles', 'centos', 'ubuntu', ) +_supportedBackends = ('sles', 'sles11', 'centos', 'ubuntu', ) class InvalidBackendError(Exception): """ diff --git a/updatebot/advisories/sles11.py b/updatebot/advisories/sles11.py new file mode 100644 --- /dev/null +++ b/updatebot/advisories/sles11.py @@ -0,0 +1,108 @@ +# +# Copyright (c) 2008-2009 rPath, Inc. +# +# This program is distributed under the terms of the Common Public License, +# version 1.0. A copy of this license should have been distributed with this +# source file in a file called LICENSE. If it is not present, the license +# is always available at http://www.rpath.com/permanent/licenses/CPL-1.0. +# +# This program is distributed in the hope that it will be useful, but +# without any warranty; without even the implied warranty of merchantability +# or fitness for a particular purpose. See the Common Public License for +# full details. +# + +""" +Advisory module for SLES. +""" + +import logging + +from updatebot.advisories.common import BaseAdvisory +from updatebot.advisories.sles import Advisor as BaseAdvisor + +log = logging.getLogger('updatebot.advisories') + +class SLES11Advisory(BaseAdvisory): + template = BaseAdvisory.template + """\ + +References: +%(references)s +""" + + def setReferences(self, refs): + """ + populate advisory data based on list of urls. + """ + + self._data['references'] = self._indentFormatList(refs) + + +class Advisor(BaseAdvisor): + """ + Class for processing SLES advisory information. + """ + + _advisoryClass = SLES11Advisory + + def load(self): + """ + Parse the required data to generate a mapping of binary package + object to patch object for a given platform into self._pkgMap. + """ + + for path, client in self._pkgSource.getClients().iteritems(): + log.info('loading patch information %s' % path) + for patch in client.getUpdateInfo(): + patch.summary = patch.title + patch.packages = patch.pkglist + self._loadOne(patch, path) + + def _mkAdvisory(self, patch): + """ + Create and populate advisory object for a given package. + """ + + advisory = BaseAdvisor._mkAdvisory(self, patch) + advisory.setReferences([ x.href for x in patch.references]) + return advisory + + def _hasException(self, binPkg): + """ + Check the config for repositories with exceptions for sending + advisories. (io. repositories that we generated metadata for.) + @param binPkg: binary package object + @type binPkg: repomd.packagexml._Package + """ + + res = BaseAdvisor._hasException(self, binPkg) + if res: + return True + + for n in ('-debuginfo', '-debugsource'): + if n in binPkg.location: + return True + + return False + + def _checkForDuplicates(self, patchSet): + """ + Check a set of "patch" objects for duplicates. If there are duplicates + combine any required information into the first object in the set and + return True, otherwise return False. + """ + + patchLst = list(patchSet) + + if not len(patchLst): + return False + + patch = patchLst.pop(0) + + for p in patchLst: + if patch.title == p.title and patch.description == p.description: + for pkg in p.pkglist: + if pkg not in patch.pkglist: + patch.pkglist.append(pkg) + + return True From johnsonm at rpath.com Wed Aug 19 17:39:13 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:39:13 +0000 Subject: mirrorball: add maps for updatebot Message-ID: <200908192139.n7JLdDOd014751@scc.eng.rpath.com> changeset: bea7a2dd1ffb user: Elliot Peele <http://issues.rpath.com/> date: Tue, 10 Jun 2008 16:26:51 -0400 add maps for updatebot committer: Elliot Peele <http://issues.rpath.com/> diff --git a/rpmimport/rpmsource.py b/rpmimport/rpmsource.py --- a/rpmimport/rpmsource.py +++ b/rpmimport/rpmsource.py @@ -18,24 +18,11 @@ """ import os +import logging import repomd -class PackageChecksumMismatchError(Exception): - """ - Exception for packages that have checksums that don't match. - """ - - def __init__(self, pkg1, pkg2): - Exception.__init__(self) - self.pkg1 = pkg1 - self.pkg2 = pkg2 - - def __str___(self): - return ('The checksums of %s (%s) and %s (%s) do not match.' - % (self.pkg1, self.pkg1.checksum, self.pkg2, - self.pkg2.checksum)) - +log = logging.getLogger('rpmimport.rpmsource') class RpmSource(object): """ @@ -52,6 +39,21 @@ # {srpm: path} self.srcPath = dict() + # {location: srpm} + self.locationMap = dict() + + # {srcPkg: srpmname} + self.srcPkgNameMap = dict() + + # {srcPkg: [binPkg, ... ] } + self.srcPkgMap = dict() + + # {binPkg: srcPkg} + self.binPkgMap = dict() + + # {srcName: [srcPkg, ... ] } + self.srcNameMap = dict() + def _procSrc(self, basePath, package): """ Process source rpms. @@ -62,17 +64,16 @@ """ shortSrpm = os.path.basename(package.location) longLoc = basePath + '/' + package.location - if shortSrpm in self.srcPath: - #if package.checksum != self.srcPath[shortSrpm].checksum: - # raise PackageChecksumMismatchError(package, - # self.srcPath[shortSrpm]) - #else: - # return - pass - else: - package.location = longLoc + package.location = longLoc + if shortSrpm not in self.srcPath: self.srcPath[shortSrpm] = package + self.srcPkgNameMap[package] = shortSrpm + + if package.name not in self.srcNameMap: + self.srcNameMap[package.name] = [] + self.srcNameMap[package.name].append(package) + def _procBin(self, basePath, package): """ Process binary rpms. @@ -87,10 +88,10 @@ if self.rpmMap.has_key(srpm): shortLoc = os.path.basename(package.location) # remove duplicates of this binary RPM - last one wins - existing = [ x for x in self.rpmMap[srpm].iterkeys() - if os.path.basename(x) == shortLoc ] - for key in existing: - del self.rpmMap[srpm][key] + #existing = [ x for x in self.rpmMap[srpm].iterkeys() + # if os.path.basename(x) == shortLoc ] + #for key in existing: + # del self.rpmMap[srpm][key] self.rpmMap[srpm][longLoc] = package else: self.rpmMap[srpm] = {longLoc: package} @@ -130,6 +131,30 @@ else: self._procBin(basePath, pkg) + def finalize(self): + # Now that we have processed all of the rpms, build some more data + # structures. + count = 0 + for pkg, shortSrpm in self.srcPkgNameMap.iteritems(): + if pkg in self.srcPkgMap: + continue + + if shortSrpm not in self.rpmMap: + count += 1 + #log.warn('found source without binary rpms: %s' % pkg) + if pkg in self.srcNameMap[pkg.name]: + self.srcNameMap[pkg.name].remove(pkg) + continue + + self.srcPkgMap[pkg] = self.rpmMap[shortSrpm].values() + self.srcPkgMap[pkg].append(pkg) + + for binPkg in self.srcPkgMap[pkg]: + self.locationMap[binPkg.location] = binPkg + self.binPkgMap[binPkg] = pkg + + log.warn('found %s source rpms without matching binary rpms' % count) + def getNames(self, src): """ @return list that goes into the rpms line in the recipe. From johnsonm at rpath.com Wed Aug 19 17:39:42 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:39:42 +0000 Subject: mirrorball: do update earlier so that we have the new source versions Message-ID: <200908192139.n7JLdgSZ015909@scc.eng.rpath.com> changeset: cfa2ee5505a3 user: Elliot Peele <http://issues.rpath.com/> date: Wed, 25 Jun 2008 19:23:02 -0400 do update earlier so that we have the new source versions committer: Elliot Peele <http://issues.rpath.com/> diff --git a/updatebot/bot.py b/updatebot/bot.py --- a/updatebot/bot.py +++ b/updatebot/bot.py @@ -16,18 +16,16 @@ Module for driving the update process. """ +import time import logging import repomd -from rpmvercmp import rpmvercmp from rpmimport import rpmsource -from updatebot import util from updatebot import build from updatebot import update from updatebot import advise from updatebot import patchsource -from updatebot.errors import * log = logging.getLogger('updatebot.bot') @@ -41,7 +39,7 @@ self._clients = {} self._rpmSource = rpmsource.RpmSource() - self._patchSource = patchsource.PatchSource() + self._patchSource = patchsource.PatchSource(self._cfg) self._updater = update.Updater(self._cfg, self._rpmSource) self._advisor = advise.Advisor(self._cfg, self._rpmSource, self._patchSource) @@ -70,11 +68,26 @@ % (self._cfg.repositoryUrl, path)) self._patchSource.loadFromClient(client, path) + @staticmethod + def _flattenSetDict(setDict): + """ + Convert a dictionary with values of sets to a list. + @param setList: dictionary of sets + @type setList: [set(), set(), ...] + @return list of items that were in the sets + """ + + lst = [] + for trvSet in setDict.itervalues(): + lst.extend(list(trvSet)) + return lst + def run(self): """ Update the conary repository from the yum repositories. """ + start = time.time() log.info('starting update') # Populate rpm source object from yum metadata. @@ -83,23 +96,24 @@ # Get troves to update and send advisories. toAdvise, toUpdate = self._updater.getUpdates() + # Update source + for nvf, srcPkg in toUpdate: + toAdvise.remove((nvf, srcPkg)) + newVersion = self._updater.update(nvf, srcPkg) + toAdvise.append(((nvf[0], newVersion, nvf[2]), srcPkg)) + # Don't populate the patch source until we know that there are # updates. - #self._populatePatchSource() + self._populatePatchSource() # Check to see if advisories exist for all required packages. - #self._advisor.check(toAdvise) - - # Update sources. - #for nvf, srcPkg in toUpdate: - # self._updater.update(nvf, srcPkg) + self._advisor.check(toAdvise) # Make sure to build everything in the toAdvise list, there may be # sources that have been updated, but not built. - #buildTroves = set([ x[0] for x in toAdvise ]) - #trvMap = self._builder.build(buildTroves) + buildTroves = set([ x[0] for x in toAdvise ]) + trvMap = self._builder.build(buildTroves) - import epdb; epdb.st() # Build group. grpTrvs = set() for flavor in self._cfg.groupFlavors: @@ -108,18 +122,18 @@ flavor)) grpTrvMap = self._builder.build(grpTrvs) - import epdb; epdb.st() # Promote group. # We expect that everything that was built will be published. - expected = [ x for x in y for y in trvMap.itervalues() ] - toPublish = [ x for x in y for y in grpTrvMap.itervalues() ] + expected = self._flattenSetDict(trvMap) + toPublish = self._flattenSetDict(grpTrvMap) newTroves = self._updater.publish(toPublish, expected, self._cfg.targetLabel) import epdb; epdb.st() # Send advisories. - self._advisor.send(newTroves, toAdvise) + self._advisor.send(toAdvise, newTroves) log.info('update completed successfully') log.info('updated %s packages and sent %s advisories' % (len(toUpdate), len(toAdvise))) + log.info('elapsed time %s' % (time.time() - start, )) From johnsonm at rpath.com Wed Aug 19 17:40:48 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:40:48 +0000 Subject: mirrorball: Added test for package control file parser Message-ID: <200908192140.n7JLemQm018617@scc.eng.rpath.com> changeset: ff43b9963e6f user: Mihai Ibanescu <https://issues.rpath.com/> date: Mon, 15 Sep 2008 16:35:32 -0400 Added test for package control file parser committer: Mihai Ibanescu <https://issues.rpath.com/> diff --git a/aptmd/parser.py b/aptmd/parser.py --- a/aptmd/parser.py +++ b/aptmd/parser.py @@ -28,7 +28,7 @@ self._singleQuotedString = False self._doubleQuotedString = False - self._list = [''] + self._list = [[]] for char in line: self._cur = char if char in self._states: @@ -36,14 +36,14 @@ else: self._states['other']() - if self._list[-1] == '': - self._list = self._list[:-1] + if not self._list[-1]: + del self._list[-1] - return self._list + return [ ''.join(x) for x in self._list ] def _space(self): if not self._singleQuotedString and not self._doubleQuotedString: - self._list.append('') + self._list.append([]) def _singleQuote(self): self._add() @@ -54,7 +54,7 @@ self._doubleQuotedString = not self._doubleQuotedString def _add(self): - self._list[-1] += self._cur + self._list[-1].append(self._cur) class Parser(object): diff --git a/test/archive/control-files/ascii.control b/test/archive/control-files/ascii.control new file mode 100644 --- /dev/null +++ b/test/archive/control-files/ascii.control @@ -0,0 +1,17 @@ +Package: ascii +Version: 3.8-2 +Section: utils +Priority: optional +Architecture: i386 +Depends: libc6 (>= 2.3.4-1) +Installed-Size: 72 +Maintainer: Florian Ernst <florian at debian.org> +Description: interactive ASCII name and synonym chart + The ascii utility provides easy conversion between various byte representations + and the American Standard Code for Information Interchange (ASCII) character + table. It knows about a wide variety of hex, binary, octal, Teletype mnemonic, + ISO/ECMA code point, slang names, XML entity names, and other representations. + Given any one on the command line, it will try to display all others. Called + with no arguments it displays a handy small ASCII chart. + . + Homepage: http://www.catb.org/~esr/ascii/ diff --git a/test/unit_test/aptmdtest/__init__.py b/test/unit_test/aptmdtest/__init__.py new file mode 100644 diff --git a/test/unit_test/aptmdtest/packagestest.py b/test/unit_test/aptmdtest/packagestest.py new file mode 100644 --- /dev/null +++ b/test/unit_test/aptmdtest/packagestest.py @@ -0,0 +1,49 @@ +# +# Copyright (c) 2008 rPath, Inc. +# +# This program is distributed under the terms of the Common Public License, +# version 1.0. A copy of this license should have been distributed with this +# source file in a file called LICENSE. If it is not present, the license +# is always available at http://www.rpath.com/permanent/licenses/CPL-1.0. +# +# This program is distributed in the hope that it will be useful, but +# without any warranty; without even the implied warranty of merchantability +# or fitness for a particular purpose. See the Common Public License for +# full details. +# + +import testsetup +from testrunner import resources + +import os +import StringIO + +from aptmd import packages + +import slehelp + +class PackagesTest(slehelp.Helper): + def testMetadataParse1(self): + ctrlFile = os.path.join(resources.archivePath, 'control-files', + 'ascii.control') + + parser = packages.PackagesParser() + parser.parse(ctrlFile) + self.failUnlessEqual(parser._curObj.name, 'ascii') + self.failUnlessEqual(parser._curObj.version, '3.8') + self.failUnlessEqual(parser._curObj.release, '2') + self.failUnlessEqual(parser._curObj.arch, 'i386') + self.failUnlessEqual(parser._curObj.get('installed-size'), '72') + self.failUnlessEqual(parser._curObj.summary, 'interactive ASCII name and synonym chart') + self.failUnlessEqual(parser._curObj.description, """\ + The ascii utility provides easy conversion between various byte representations + and the American Standard Code for Information Interchange (ASCII) character + table. It knows about a wide variety of hex, binary, octal, Teletype mnemonic, + ISO/ECMA code point, slang names, XML entity names, and other representations. + Given any one on the command line, it will try to display all others. Called + with no arguments it displays a handy small ASCII chart. + . + Homepage: http://www.catb.org/~esr/ascii/ +""") + +testsetup.main() From johnsonm at rpath.com Wed Aug 19 17:41:26 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:41:26 +0000 Subject: mirrorball: script for checking and fixing manifests Message-ID: <200908192141.n7JLfQ96020154@scc.eng.rpath.com> changeset: 88a1ed511615 user: Elliot Peele <https://issues.rpath.com/> date: Mon, 17 Nov 2008 16:31:46 -0500 script for checking and fixing manifests committer: Elliot Peele <https://issues.rpath.com/> diff --git a/scripts/validatemanifests.py b/scripts/validatemanifests.py new file mode 100755 --- /dev/null +++ b/scripts/validatemanifests.py @@ -0,0 +1,128 @@ +#!/usr/bin/python2.6 +# +# Copyright (c) 2008 rPath, Inc. +# +# This program is distributed under the terms of the Common Public License, +# version 1.0. A copy of this license should have been distributed with this +# source file in a file called LICENSE. If it is not present, the license +# is always available at http://www.rpath.com/permanent/licenses/CPL-1.0. +# +# This program is distributed in the hope that it will be useful, but +# without any warranty; without even the implied warranty of merchantability +# or fitness for a particular purpose. See the Common Public License for +# full details. +# + +import os +import sys +import logging + +sys.path.insert(0, os.environ['HOME'] + '/hg/26/epdb') +sys.path.insert(0, os.environ['HOME'] + '/hg/26/rmake') +sys.path.insert(0, os.environ['HOME'] + '/hg/26/conary') +sys.path.insert(0, os.environ['HOME'] + '/hg/26/mirrorball') +sys.path.insert(0, os.environ['HOME'] + '/hg/26/rpath-xmllib') + +from conary.lib import util +sys.excepthook = util.genExcepthook() + +from updatebot import bot +from updatebot import util +from updatebot import config +from updatebot import log as logger + +logger.addRootLogger() + +log = logging.getLogger('verifymanifest') + +cfg = config.UpdateBotConfig() +cfg.read(os.environ['HOME'] + '/hg/26/mirrorball/config/centos/updatebotrc') +b = bot.Bot(cfg) + +b._pkgSource.load() + +helper = b._updater._conaryhelper + +pkgs = [] +for n, v, f in helper.getSourceTroves(cfg.topGroup): + n = n.split(':')[0] + if len(v.versions) < 3 and v.trailingLabel().asString() == cfg.topGroup[1]: + if (not n.startswith('group-') and + not n.startswith('info-') and + not n.startswith('factory-') and + not n in cfg.excludePackages): + #v = helper.getLatestSourceVersion(n) + pkgs.append((n, v.getSourceVersion())) + +changed = {} +for pkg, v in pkgs: + + srpms = list(b._pkgSource.srcNameMap[pkg]) + + map = {} + for x in srpms: + key = '%s_%s' % (x.version, x.release) + map[key] = x + + srcPkg = map[v.trailingRevision().asString().split('-')[0]] + manifest = b._updater._getManifestFromPkgSource(srcPkg) + repoManifest = b._updater._conaryhelper.getManifest(pkg) + + if len(manifest) == len(repoManifest): + offt = 0 + for i in range(len(manifest)): + index = i - offt + if manifest[index] == repoManifest[index]: + manifest.pop(index) + repoManifest.pop(index) + offt += 1 + + if not manifest and not repoManifest: + continue + + if 'system-config-securitylevel' in repoManifest[0] and pkg != 'system-config-securitylevel': + changed[pkg] = [manifest, repoManifest, srcPkg] + + #if manifest != repoManifest: + # assert len(manifest) == len(repoManifest) + + # baseNames1 = [ os.path.basename(x) for x in manifest ] + # baseNames2 = [ os.path.basename(x) for x in repoManifest ] + + # baseNames1.sort() + # baseNames2.sort() + + # assert baseNames1 == baseNames2 + + # changed[pkg] = [manifest, repoManifest, srcPkg] + +import epdb; epdb.st() + +trvs = set() +for pkg in changed: + srcPkg = changed[pkg][2] + manifest = b._updater._getManifestFromPkgSource(srcPkg) + helper.setManifest(pkg, manifest, commitMessage=cfg.commitMessage) + trvs.add((pkg, cfg.topSourceGroup[1], None)) + +import epdb; epdb.st() + +#trvMap = b._builder.build(trvs) +trvMap = b._builder.buildmany(trvs) + +def displayTrove(nvf): + flavor = '' + if nvf[2] is not None: + flavor = '[%s]' % nvf[2] + + return '%s=%s%s' % (nvf[0], nvf[1], flavor) + +def display(trvMap): + for srcTrv in trvMap.iterkeys(): + print displayTrove(srcTrv) + for binTrv in trvMap[srcTrv]: + print " " * 4, displayTrove(binTrv) + +display(trvMap) + +import epdb; epdb.st() From johnsonm at rpath.com Wed Aug 19 17:42:11 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:42:11 +0000 Subject: mirrorball: keep track of the path to the metadata files for later filtering Message-ID: <200908192142.n7JLgB95021894@scc.eng.rpath.com> changeset: dd07469d84e0 user: Elliot Peele <https://issues.rpath.com/> date: Fri, 23 Jan 2009 19:34:20 -0500 keep track of the path to the metadata files for later filtering committer: Elliot Peele <https://issues.rpath.com/> diff --git a/aptmd/__init__.py b/aptmd/__init__.py --- a/aptmd/__init__.py +++ b/aptmd/__init__.py @@ -45,8 +45,8 @@ fh = self._repo.get(path) basename = os.path.basename(path) if basename.startswith('Packages'): - return self._packages.parse(fh) + return self._packages.parse(fh, path) elif basename.startswith('Sources'): - return self._sources.parse(fh) + return self._sources.parse(fh, path) else: raise UnsupportedFileError(path) diff --git a/aptmd/common.py b/aptmd/common.py --- a/aptmd/common.py +++ b/aptmd/common.py @@ -27,7 +27,7 @@ metadata. """ - _slots = ('name', 'arch', 'epoch', 'version', 'release') + _slots = ('name', 'arch', 'epoch', 'version', 'release', 'mdpath') def __repr__(self): # Instance of 'BaseContainer' has no 'name' member @@ -58,6 +58,7 @@ Parser.__init__(self) self._containerClass = BaseContainer + self._mdPath = None self._states.update({ 'package' : self._package, @@ -69,16 +70,29 @@ 'original-maintainer' : self._keyval, }) + def parse(self, fn, path): + """ + Parse repository metadata. + """ + + self._mdPath = path + return Parser.parse(self, fn) + + def _newContainer(self): + """ + Create a new container object and store the finished one. + """ + + if self._curObj is not None: + self._curObj.mdpath = self._mdPath + return Parser._newContainer(self) + def _package(self): """ Parse package info. """ - if self._curObj is not None: - if hasattr(self._curObj, 'finalize'): - self._curObj.finalize() - self._objects.append(self._curObj) - self._curObj = self._containerClass() + self._newContainer() self._curObj.name = self._getLine() def _architecture(self): diff --git a/aptmd/packages.py b/aptmd/packages.py --- a/aptmd/packages.py +++ b/aptmd/packages.py @@ -54,17 +54,19 @@ 'task' : self._keyval, }) - def parse(self, fn): + def parse(self, fn, path): """ Parse a given file or file like object line by line. @param fn: filename or file like object to parse. @type fn: string or file like object. + @param path: path to the metadata file + @type path: string """ # Attribute 'description' defined outside __init__ # pylint: disable-msg=W0201 - ret = BaseParser.parse(self, fn) + ret = BaseParser.parse(self, fn, path) # If there is any text left, collect it in the description if self._text: self._curObj.description = self._text diff --git a/updatebot/advisories/ubuntu.py b/updatebot/advisories/ubuntu.py --- a/updatebot/advisories/ubuntu.py +++ b/updatebot/advisories/ubuntu.py @@ -72,8 +72,7 @@ #log.warn('found path (%s) not in cache' % path) continue - #import epdb; epdb.st() - log.info('found path (%s) in cache' % path) + #log.info('found path (%s) in cache' % path) nvMap = msg.pkgNameVersion[self._cfg.upstreamProductVersion] for pkg in pkgCache[path]: nv = (pkg.name, '-'.join([pkg.version, pkg.release])) @@ -113,7 +112,11 @@ # W0613 - Unused argument # pylint: disable-msg=W0613 - return True + for path in ('-security', ): + if path in binPkg.mdpath: + return True + + return False def _checkForDuplicates(self, patchSet): """ From johnsonm at rpath.com Wed Aug 19 17:39:37 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:39:37 +0000 Subject: mirrorball: filter out some patches Message-ID: <200908192139.n7JLdb4j015738@scc.eng.rpath.com> changeset: 8698bf13b830 user: Elliot Peele <http://issues.rpath.com/> date: Tue, 24 Jun 2008 14:50:56 -0400 filter out some patches committer: Elliot Peele <http://issues.rpath.com/> diff --git a/test/unit_test/updatebottest/patchsourcetest.py b/test/unit_test/updatebottest/patchsourcetest.py --- a/test/unit_test/updatebottest/patchsourcetest.py +++ b/test/unit_test/updatebottest/patchsourcetest.py @@ -28,7 +28,7 @@ mockLoadOne = mock.MockObject(stableReturnValues=True) mockLoadOne._mock.setReturn(None, mockPatch, '/foo') - patchSource = patchsource.PatchSource() + patchSource = patchsource.PatchSource(self.updateBotCfg) self.mock(patchSource, '_loadOne', mockLoadOne) result = patchSource.loadFromClient(mockClient, '/foo') @@ -43,13 +43,56 @@ mockPatch = mock.MockObject(stableReturnValues=True) mockPatch._mock.set(packages=[mockPackage, ]) + mockFilterPatch = mock.MockObject() + mockFilterPatch._mock.setReturn(False, mockPatch) + expectedResult = {mockPackage: set([mockPatch, ])} - patchSource = patchsource.PatchSource() + patchSource = patchsource.PatchSource(self.updateBotCfg) + self.mock(patchSource, '_filterPatch', mockFilterPatch) + result = patchSource._loadOne(mockPatch, 'baz') self.failUnlessEqual(result, None) self.failUnlessEqual(patchSource.pkgMap, expectedResult) self.failUnlessEqual(mockPackage.location, 'baz/foo/bar') + mockFilterPatch._mock.assertCalled(mockPatch) + + mockFilterPatch._mock.setReturn(True, mockPatch) + result = patchSource._loadOne(mockPatch, 'baz') + self.failUnlessEqual(result, None) + self.failUnlessEqual(patchSource.pkgMap, expectedResult) + mockFilterPatch._mock.assertCalled(mockPatch) + + def testFilterPatch(self): + mockConfig = mock.MockObject() + mockFilter = mock.MockObject() + mockPatch = mock.MockObject() + + mockPatch._mock.set(summary='foo') + mockPatch._mock.set(description='bar') + + mockConfig._mock.set(patchFilter=[(None, mockFilter), ]) + + patchSource = patchsource.PatchSource(mockConfig) + + mockFilter.match._mock.setReturn(True, 'foo') + result = patchSource._filterPatch(mockPatch) + self.failUnlessEqual(result, True) + mockFilter.match._mock.assertCalled('foo') + + mockFilter.match._mock.setReturn(False, 'foo') + mockFilter.match._mock.setReturn(True, 'bar') + result = patchSource._filterPatch(mockPatch) + self.failUnlessEqual(result, True) + mockFilter.match._mock.assertCalled('foo') + mockFilter.match._mock.assertCalled('bar') + + mockFilter.match._mock.setReturn(False, 'foo') + mockFilter.match._mock.setReturn(False, 'bar') + result = patchSource._filterPatch(mockPatch) + self.failUnlessEqual(result, False) + mockFilter.match._mock.assertCalled('foo') + mockFilter.match._mock.assertCalled('bar') testsetup.main() diff --git a/updatebot/patchsource.py b/updatebot/patchsource.py --- a/updatebot/patchsource.py +++ b/updatebot/patchsource.py @@ -28,7 +28,9 @@ # R0903 - Too few public methods # pylint: disable-msg=R0903 - def __init__(self): + def __init__(self, cfg): + self._cfg = cfg + # {binPkg: patchObj} self.pkgMap = dict() @@ -53,8 +55,26 @@ @type path: string """ + if self._filterPatch(patch): + return + for package in patch.packages: package.location = path + '/' + package.location if package not in self.pkgMap: self.pkgMap[package] = set() self.pkgMap[package].add(patch) + + def _filterPatch(self, patch): + """ + Filter out patches that match filters in config. + @param patch: repomd patch object + @type patch: repomd.patchxml._Patch + """ + + for _, filter in self._cfg.patchFilter: + if filter.match(patch.summary): + return True + if filter.match(patch.description): + return True + + return False From johnsonm at rpath.com Wed Aug 19 17:40:49 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:40:49 +0000 Subject: mirrorball: Branch merge Message-ID: <200908192140.n7JLeo4d018668@scc.eng.rpath.com> changeset: 771b7d01bd11 user: Mihai Ibanescu <https://issues.rpath.com/> date: Mon, 15 Sep 2008 16:36:07 -0400 Branch merge committer: Mihai Ibanescu <https://issues.rpath.com/> diff --git a/aptmd/parser.py b/aptmd/parser.py --- a/aptmd/parser.py +++ b/aptmd/parser.py @@ -28,7 +28,7 @@ self._singleQuotedString = False self._doubleQuotedString = False - self._list = [''] + self._list = [[]] for char in line: self._cur = char if char in self._states: @@ -36,14 +36,14 @@ else: self._states['other']() - if self._list[-1] == '': - self._list = self._list[:-1] + if not self._list[-1]: + del self._list[-1] - return self._list + return [ ''.join(x) for x in self._list ] def _space(self): if not self._singleQuotedString and not self._doubleQuotedString: - self._list.append('') + self._list.append([]) def _singleQuote(self): self._add() @@ -54,7 +54,7 @@ self._doubleQuotedString = not self._doubleQuotedString def _add(self): - self._list[-1] += self._cur + self._list[-1].append(self._cur) class Parser(object): diff --git a/test/archive/control-files/ascii.control b/test/archive/control-files/ascii.control new file mode 100644 --- /dev/null +++ b/test/archive/control-files/ascii.control @@ -0,0 +1,17 @@ +Package: ascii +Version: 3.8-2 +Section: utils +Priority: optional +Architecture: i386 +Depends: libc6 (>= 2.3.4-1) +Installed-Size: 72 +Maintainer: Florian Ernst <florian at debian.org> +Description: interactive ASCII name and synonym chart + The ascii utility provides easy conversion between various byte representations + and the American Standard Code for Information Interchange (ASCII) character + table. It knows about a wide variety of hex, binary, octal, Teletype mnemonic, + ISO/ECMA code point, slang names, XML entity names, and other representations. + Given any one on the command line, it will try to display all others. Called + with no arguments it displays a handy small ASCII chart. + . + Homepage: http://www.catb.org/~esr/ascii/ diff --git a/test/unit_test/aptmdtest/packagestest.py b/test/unit_test/aptmdtest/packagestest.py new file mode 100644 --- /dev/null +++ b/test/unit_test/aptmdtest/packagestest.py @@ -0,0 +1,49 @@ +# +# Copyright (c) 2008 rPath, Inc. +# +# This program is distributed under the terms of the Common Public License, +# version 1.0. A copy of this license should have been distributed with this +# source file in a file called LICENSE. If it is not present, the license +# is always available at http://www.rpath.com/permanent/licenses/CPL-1.0. +# +# This program is distributed in the hope that it will be useful, but +# without any warranty; without even the implied warranty of merchantability +# or fitness for a particular purpose. See the Common Public License for +# full details. +# + +import testsetup +from testrunner import resources + +import os +import StringIO + +from aptmd import packages + +import slehelp + +class PackagesTest(slehelp.Helper): + def testMetadataParse1(self): + ctrlFile = os.path.join(resources.archivePath, 'control-files', + 'ascii.control') + + parser = packages.PackagesParser() + parser.parse(ctrlFile) + self.failUnlessEqual(parser._curObj.name, 'ascii') + self.failUnlessEqual(parser._curObj.version, '3.8') + self.failUnlessEqual(parser._curObj.release, '2') + self.failUnlessEqual(parser._curObj.arch, 'i386') + self.failUnlessEqual(parser._curObj.get('installed-size'), '72') + self.failUnlessEqual(parser._curObj.summary, 'interactive ASCII name and synonym chart') + self.failUnlessEqual(parser._curObj.description, """\ + The ascii utility provides easy conversion between various byte representations + and the American Standard Code for Information Interchange (ASCII) character + table. It knows about a wide variety of hex, binary, octal, Teletype mnemonic, + ISO/ECMA code point, slang names, XML entity names, and other representations. + Given any one on the command line, it will try to display all others. Called + with no arguments it displays a handy small ASCII chart. + . + Homepage: http://www.catb.org/~esr/ascii/ +""") + +testsetup.main() From johnsonm at rpath.com Wed Aug 19 17:41:07 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:41:07 +0000 Subject: mirrorball: simplify scripts Message-ID: <200908192141.n7JLf7FN019351@scc.eng.rpath.com> changeset: c928428c7f68 user: Elliot Peele <https://issues.rpath.com/> date: Tue, 28 Oct 2008 15:26:48 -0400 simplify scripts committer: Elliot Peele <https://issues.rpath.com/> diff --git a/scripts/buildgroups b/scripts/buildgroups --- a/scripts/buildgroups +++ b/scripts/buildgroups @@ -7,22 +7,7 @@ Script for cooking groups defined in the updatebot config. """ -import os -import sys - -sys.path.insert(0, os.environ['HOME'] + '/hg/rpath-xmllib') -sys.path.insert(0, os.environ['HOME'] + '/hg/conary') -sys.path.insert(0, os.environ['HOME'] + '/hg/mirrorball') - -from updatebot import log -from updatebot import build -from updatebot import config - -log.addRootLogger() -cfg = config.UpdateBotConfig() -cfg.read(os.environ['HOME'] + '/hg/mirrorball/config/ubuntu/updatebotrc') - -builder = build.Builder(cfg) +from header import * grpTrvs = set() for flavor in cfg.groupFlavors: @@ -31,14 +16,4 @@ print "built:\n" -def displayTrove(nvf): - flavor = '' - if nvf[2] is not None: - flavor = '[%s]' % nvf[2] - - return '%s=%s%s' % (nvf[0], nvf[1], flavor) - -for srcTrv in grpTrvMap.iterkeys(): - print displayTrove(srcTrv) - for binTrv in grpTrvMap[srcTrv]: - print " " * 4, displayTrove(binTrv) +display(grpTrvMap) diff --git a/scripts/buildpackages b/scripts/buildpackages --- a/scripts/buildpackages +++ b/scripts/buildpackages @@ -7,29 +7,10 @@ Script for cooking groups defined in the updatebot config. """ -import os -import sys +from header import * -sys.path.insert(0, os.environ['HOME'] + '/hg/rpath-xmllib') -sys.path.insert(0, os.environ['HOME'] + '/hg/conary') -sys.path.insert(0, os.environ['HOME'] + '/hg/mirrorball') - -from conary.lib import util -sys.excepthook = util.genExcepthook() - -from updatebot import log -from updatebot import build -from updatebot import config - -if len(sys.argv) < 3 or sys.argv[1] not in os.listdir(os.environ['HOME'] + '/hg/mirrorball/config'): - print 'usage: buildpackages <platform> [pkg1, pkg2, ...]' - sys.exit(1) - -log.addRootLogger() -cfg = config.UpdateBotConfig() -cfg.read(os.environ['HOME'] + '/hg/mirrorball/config/%s/updatebotrc' % sys.argv[1]) - -builder = build.Builder(cfg) +if len(sys.argv) < 3: + usage() trvs = set() label = cfg.topSourceGroup[1] @@ -39,14 +20,4 @@ print "built:\n" -def displayTrove(nvf): - flavor = '' - if nvf[2] is not None: - flavor = '[%s]' % nvf[2] - - return '%s=%s%s' % (nvf[0], nvf[1], flavor) - -for srcTrv in trvMap.iterkeys(): - print displayTrove(srcTrv) - for binTrv in trvMap[srcTrv]: - print " " * 4, displayTrove(binTrv) +display(trvMap) diff --git a/scripts/header.py b/scripts/header.py new file mode 100644 --- /dev/null +++ b/scripts/header.py @@ -0,0 +1,50 @@ +# +# Copyright (c) 2008 rPath, Inc. +# +# This program is distributed under the terms of the Common Public License, +# version 1.0. A copy of this license should have been distributed with this +# source file in a file called LICENSE. If it is not present, the license +# is always available at http://www.rpath.com/permanent/licenses/CPL-1.0. +# +# This program is distributed in the hope that it will be useful, but +# without any warranty; without even the implied warranty of merchantability +# or fitness for a particular purpose. See the Common Public License for +# full details. +# + +import os +import sys + +sys.path.insert(0, os.environ['HOME'] + '/hg/rpath-xmllib') +sys.path.insert(0, os.environ['HOME'] + '/hg/conary') +sys.path.insert(0, os.environ['HOME'] + '/hg/mirrorball') + +from updatebot import log +from updatebot import build +from updatebot import config + +def usage(): + print 'usage: buildpackages <platform> [pkg1, pkg2, ...]' + sys.exit(1) + +if len(sys.argv) < 2 or sys.argv[1] not in os.listdir(os.environ['HOME'] + '/hg/mirrorball/config'): + usage() + +log.addRootLogger() +cfg = config.UpdateBotConfig() +cfg.read(os.environ['HOME'] + '/hg/mirrorball/config/ubuntu/updatebotrc') + +builder = build.Builder(cfg) + +def displayTrove(nvf): + flavor = '' + if nvf[2] is not None: + flavor = '[%s]' % nvf[2] + + return '%s=%s%s' % (nvf[0], nvf[1], flavor) + +def display(trvMap): + for srcTrv in trvMap.iterkeys(): + print displayTrove(srcTrv) + for binTrv in trvMap[srcTrv]: + print " " * 4, displayTrove(binTrv) From johnsonm at rpath.com Wed Aug 19 17:41:04 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:41:04 +0000 Subject: mirrorball: branch merge Message-ID: <200908192141.n7JLf5Ne019248@scc.eng.rpath.com> changeset: 4a045f56dc46 user: Elliot Peele <https://issues.rpath.com/> date: Wed, 22 Oct 2008 15:23:29 -0400 branch merge committer: Elliot Peele <https://issues.rpath.com/> diff --git a/aptmd/common.py b/aptmd/common.py --- a/aptmd/common.py +++ b/aptmd/common.py @@ -14,8 +14,8 @@ from updatebot import util -from aptmd.parser import Parser from aptmd.container import Container +from aptmd.parser import ContainerizedParser as Parser class BaseContainer(Container): __slots__ = ('name', 'arch', 'epoch', 'version', 'release') @@ -39,7 +39,6 @@ Parser.__init__(self) self._containerClass = BaseContainer - self._objects = [] self._states.update({ 'package' : self._package, @@ -51,18 +50,6 @@ 'original-maintainer' : self._keyval, }) - def parse(self, fileObj): - self._objects = [] - Parser.parse(self, fileObj) - return self._objects - - @staticmethod - def _getState(key): - key = key.strip() - if key.endswith(':'): - key = key[:-1] - return key.lower() - def _package(self): if self._curObj is not None: if hasattr(self._curObj, 'finalize'): diff --git a/aptmd/parser.py b/aptmd/parser.py --- a/aptmd/parser.py +++ b/aptmd/parser.py @@ -64,7 +64,6 @@ self._curObj = None self._lineTokenizer = _QuotedLineTokenizer() - self._containerClass = None self._states = {} def parse(self, fn): @@ -105,3 +104,36 @@ key = self._getState(self._line[0]) value = ' '.join(self._line[1:]).strip() self._curObj.set(key, value) + + +class ContainerizedParser(Parser): + def __init__(self): + Parser.__init__(self) + + self._objects = [] + self._containerClass = None + self._stateFilters = { + } + + def _filter(self, filter, state): + self._stateFilters[re.compile(filter)] = state + + def _getState(key): + key = key.strip() + key = key.lower() + if key.endswith(':'): + key = key[:-1] + + if key in self._states: + return key + + for filter, state in self._stateFilters.iteritems(): + if filter.match(key): + return state + + return key + + def parse(self, fileObj): + self._objects = [] + Parser.parse(self, fileObj) + return self._objects diff --git a/pmap/__init__.py b/pmap/__init__.py new file mode 100644 --- /dev/null +++ b/pmap/__init__.py @@ -0,0 +1,19 @@ +# +# Copyright (c) 2008 rPath, Inc. +# +# This program is distributed under the terms of the Common Public License, +# version 1.0. A copy of this license should have been distributed with this +# source file in a file called LICENSE. If it is not present, the license +# is always available at http://www.rpath.com/permanent/licenses/CPL-1.0. +# +# This program is distributed in the hope that it will be useful, but +# without any warranty; without even the implied warranty of merchantability +# or fitness for a particular purpose. See the Common Public License for +# full details. +# + +""" +PMAP - PiperMail Archive Parser + +Parse advisories from pipermail archives. +""" diff --git a/pmap/common.py b/pmap/common.py new file mode 100644 --- /dev/null +++ b/pmap/common.py @@ -0,0 +1,38 @@ +# +# Copyright (c) 2008 rPath, Inc. +# +# This program is distributed under the terms of the Common Public License, +# version 1.0. A copy of this license should have been distributed with this +# source file in a file called LICENSE. If it is not present, the license +# is always available at http://www.rpath.com/permanent/licenses/CPL-1.0. +# +# This program is distributed in the hope that it will be useful, but +# without any warranty; without even the implied warranty of merchantability +# or fitness for a particular purpose. See the Common Public License for +# full details. +# + +from aptmd.container import Container +from aptmd.parser import ContainerizedParser as Parser + +class BaseContainer(Container): + pass + +class BaseParser(Parser): + def __init__(self): + Parser.__init__(self) + + self._containerClass = BaseContainer + self._objects = [] + + self._states.update({ + '--------------' : self._messagesep, + }) + + def _messagesep(self): + if self._line[1] == 'next' and self._line[2] == 'part': + if self._curObj is not None: + if hasattr(self._curObj, 'finalize'): + self._curObj.finalize() + self._objects.append(self._curObj) + self._curObj = self._containerClass() From johnsonm at rpath.com Wed Aug 19 17:41:00 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:41:00 +0000 Subject: mirrorball: branch merge Message-ID: <200908192141.n7JLf0lk019077@scc.eng.rpath.com> changeset: a4de7dc81ddc user: Elliot Peele <https://issues.rpath.com/> date: Thu, 09 Oct 2008 18:20:59 -0400 branch merge committer: Elliot Peele <https://issues.rpath.com/> diff --git a/aptmd/common.py b/aptmd/common.py --- a/aptmd/common.py +++ b/aptmd/common.py @@ -14,8 +14,8 @@ from updatebot import util -from aptmd.parser import Parser from aptmd.container import Container +from aptmd.parser import ContainerizedParser as Parser class BaseContainer(Container): __slots__ = ('name', 'arch', 'epoch', 'version', 'release') @@ -39,7 +39,6 @@ Parser.__init__(self) self._containerClass = BaseContainer - self._objects = [] self._states.update({ 'package' : self._package, @@ -51,18 +50,6 @@ 'original-maintainer' : self._keyval, }) - def parse(self, fileObj): - self._objects = [] - Parser.parse(self, fileObj) - return self._objects - - @staticmethod - def _getState(key): - key = key.strip() - if key.endswith(':'): - key = key[:-1] - return key.lower() - def _package(self): if self._curObj is not None: if hasattr(self._curObj, 'finalize'): diff --git a/aptmd/parser.py b/aptmd/parser.py --- a/aptmd/parser.py +++ b/aptmd/parser.py @@ -64,7 +64,6 @@ self._curObj = None self._lineTokenizer = _QuotedLineTokenizer() - self._containerClass = None self._states = {} def parse(self, fn): @@ -105,3 +104,36 @@ key = self._getState(self._line[0]) value = ' '.join(self._line[1:]).strip() self._curObj.set(key, value) + + +class ContainerizedParser(Parser): + def __init__(self): + Parser.__init__(self) + + self._objects = [] + self._containerClass = None + self._stateFilters = { + } + + def _filter(self, filter, state): + self._stateFilters[re.compile(filter)] = state + + def _getState(key): + key = key.strip() + key = key.lower() + if key.endswith(':'): + key = key[:-1] + + if key in self._states: + return key + + for filter, state in self._stateFilters.iteritems(): + if filter.match(key): + return state + + return key + + def parse(self, fileObj): + self._objects = [] + Parser.parse(self, fileObj) + return self._objects diff --git a/pmap/__init__.py b/pmap/__init__.py new file mode 100644 --- /dev/null +++ b/pmap/__init__.py @@ -0,0 +1,19 @@ +# +# Copyright (c) 2008 rPath, Inc. +# +# This program is distributed under the terms of the Common Public License, +# version 1.0. A copy of this license should have been distributed with this +# source file in a file called LICENSE. If it is not present, the license +# is always available at http://www.rpath.com/permanent/licenses/CPL-1.0. +# +# This program is distributed in the hope that it will be useful, but +# without any warranty; without even the implied warranty of merchantability +# or fitness for a particular purpose. See the Common Public License for +# full details. +# + +""" +PMAP - PiperMail Archive Parser + +Parse advisories from pipermail archives. +""" diff --git a/pmap/common.py b/pmap/common.py new file mode 100644 --- /dev/null +++ b/pmap/common.py @@ -0,0 +1,38 @@ +# +# Copyright (c) 2008 rPath, Inc. +# +# This program is distributed under the terms of the Common Public License, +# version 1.0. A copy of this license should have been distributed with this +# source file in a file called LICENSE. If it is not present, the license +# is always available at http://www.rpath.com/permanent/licenses/CPL-1.0. +# +# This program is distributed in the hope that it will be useful, but +# without any warranty; without even the implied warranty of merchantability +# or fitness for a particular purpose. See the Common Public License for +# full details. +# + +from aptmd.container import Container +from aptmd.parser import ContainerizedParser as Parser + +class BaseContainer(Container): + pass + +class BaseParser(Parser): + def __init__(self): + Parser.__init__(self) + + self._containerClass = BaseContainer + self._objects = [] + + self._states.update({ + '--------------' : self._messagesep, + }) + + def _messagesep(self): + if self._line[1] == 'next' and self._line[2] == 'part': + if self._curObj is not None: + if hasattr(self._curObj, 'finalize'): + self._curObj.finalize() + self._objects.append(self._curObj) + self._curObj = self._containerClass() From johnsonm at rpath.com Wed Aug 19 17:41:50 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:41:50 +0000 Subject: mirrorball: refactor into a common super class Message-ID: <200908192141.n7JLfoIS021093@scc.eng.rpath.com> changeset: 638ca603e0b0 user: Elliot Peele <https://issues.rpath.com/> date: Wed, 17 Dec 2008 12:58:18 -0500 refactor into a common super class committer: Elliot Peele <https://issues.rpath.com/> diff --git a/updatebot/pkgsource/common.py b/updatebot/pkgsource/common.py new file mode 100644 --- /dev/null +++ b/updatebot/pkgsource/common.py @@ -0,0 +1,60 @@ +# +# Copyright (c) 2008 rPath, Inc. +# +# This program is distributed under the terms of the Common Public License, +# version 1.0. A copy of this license should have been distributed with this +# source file in a file called LICENSE. If it is not present, the license +# is always available at http://www.rpath.com/permanent/licenses/CPL-1.0. +# +# This program is distributed in the hope that it will be useful, but +# without any warranty; without even the implied warranty of merchantability +# or fitness for a particular purpose. See the Common Public License for +# full details. +# + +""" +Common module between all pkgSource implementations. +""" + +import logging + +log = logging.getLogger('updatebot.pkgsource') + +class BasePackageSource(object): + """ + Base class for pkgSources + """ + + def __init__(self, cfg): + self._cfg = cfg + self._excludeArch = self._cfg.excludeArch + + # {repoShortUrl: clientObj} + self._clients = dict() + + # {location: srpm} + self.locationMap = dict() + + # {srcPkg: [binPkg, ... ] } + self.srcPkgMap = dict() + + # {binPkg: srcPkg} + self.binPkgMap = dict() + + # {srcName: [srcPkg, ... ] } + self.srcNameMap = dict() + + # {binName: [binPkg, ... ] } + self.binNameMap = dict() + + def getClients(self): + """ + Get instances of repository clients. + """ + + if not self._clients: + self.load() + + return self._clients + + diff --git a/updatebot/pkgsource/debsource.py b/updatebot/pkgsource/debsource.py --- a/updatebot/pkgsource/debsource.py +++ b/updatebot/pkgsource/debsource.py @@ -16,33 +16,16 @@ import aptmd from updatebot import util +from updatebot.pkgsource.common import BasePackageSource log = logging.getLogger('updatebot.pkgsource') -class DebSource(object): +class DebSource(BasePackageSource): def __init__(self, cfg): - self._cfg = cfg - self._excludeArch = self._cfg.excludeArch + BasePackageSource.__init__(self, cfg) self._binPkgs = set() self._srcPkgs = set() - self._clients = dict() - - self.srcPkgMap = dict() - self.binPkgMap = dict() - self.srcNameMap = dict() - self.binNameMap = dict() - self.locationMap = dict() - - def getClients(self): - """ - Get repository client instances. - """ - - if not self._clients: - self.load() - - return self._clients def load(self): """ diff --git a/updatebot/pkgsource/rpmsource.py b/updatebot/pkgsource/rpmsource.py --- a/updatebot/pkgsource/rpmsource.py +++ b/updatebot/pkgsource/rpmsource.py @@ -21,17 +21,17 @@ import repomd from updatebot import util +from updatebot.pkgsource.common import BasePackageSource log = logging.getLogger('updatebot.pkgsource') -class RpmSource(object): +class RpmSource(BasePackageSource): """ Class that builds maps of packages from multiple yum repositories. """ def __init__(self, cfg): - self._cfg = cfg - self._excludeArch = self._cfg.excludeArch + BasePackageSource.__init__(self, cfg) # {srcTup: srpm} self._srcMap = dict() @@ -42,34 +42,6 @@ # set of all src pkg objects self._srcPkgs = set() - # {repoShortUrl: clientObj} - self._clients = dict() - - # {location: srpm} - self.locationMap = dict() - - # {srcPkg: [binPkg, ... ] } - self.srcPkgMap = dict() - - # {binPkg: srcPkg} - self.binPkgMap = dict() - - # {srcName: [srcPkg, ... ] } - self.srcNameMap = dict() - - # {binName: [binPkg, ... ] } - self.binNameMap = dict() - - def getClients(self): - """ - Get instances of repository clients. - """ - - if not self._clients: - self.load() - - return self._clients - def load(self): """ Load package source based on config data. From johnsonm at rpath.com Wed Aug 19 17:42:26 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:42:26 +0000 Subject: mirrorball: add suport for add a buildrequires file to the repository Message-ID: <200908192142.n7JLgQN5022507@scc.eng.rpath.com> changeset: 343aa5d65802 user: Elliot Peele <https://issues.rpath.com/> date: Sat, 14 Feb 2009 15:43:06 -0500 add suport for add a buildrequires file to the repository committer: Elliot Peele <https://issues.rpath.com/> diff --git a/updatebot/conaryhelper.py b/updatebot/conaryhelper.py --- a/updatebot/conaryhelper.py +++ b/updatebot/conaryhelper.py @@ -211,8 +211,7 @@ if not os.path.exists(manifestFileName): raise NoManifestFoundError(pkgname=pkgname, dir=recipeDir) - manifest = [ x.strip() for x in open(manifestFileName).readlines() ] - #util.rmtree(recipeDir) + manifest = [ x.strip() for x in open(manifestFileName) ] return manifest def setManifest(self, pkgname, manifest): @@ -287,6 +286,47 @@ # Make sure metadata file has been added. self._addFile(recipeDir, 'metadata.xml') + def getBuildRequires(self, pkgname): + """ + Get the build requires defined in the buildrequires file. + @param pkgname: name of the package to retrieve + @type pkgname: string + @return list of build requires + """ + + log.info('retrieving buildrequires for %s' % pkgname) + recipeDir = self._edit(pkgname) + buildRequiresFileName = util.join(recipeDir, 'buildrequires') + + if not os.path.exists(buildRequiresFileName): + return [] + + buildRequires = [ x.strip() for x in open(buildRequiresFileName) ] + return buildRequires + + def setBuildRequires(self, pkgname, buildrequires): + """ + Set the contents of the build requires file in the repository. + @param pkgname: name of hte package to edit + @type pkgname: string + @param buildrequires: list of build requires + @type buildrequires: list + """ + + log.info('setting buildrequires for %s' % pkgname) + + recipeDir = self._edit(pkgname) + buildRequiresFileName = util.join(recipeDir, 'buildrequires') + + # generate buildrequires file + buildRequiresfh = open(buildRequiresFileName, 'w') + buildRequiresfh.write('\n'.join(buildrequires)) + buildRequiresfh.write('\n') + buildRequiresfh.close() + + # add file to the source compoent + self._addFile(recipeDir, 'buildrequires') + def commit(self, pkgname, commitMessage=''): """ Commit the cached checkout of a source component. diff --git a/updatebot/config.py b/updatebot/config.py --- a/updatebot/config.py +++ b/updatebot/config.py @@ -179,6 +179,9 @@ # Satis Info satisUrl = CfgString + # Tell the updater to write build requires information + generateBuildRequires = (CfgBool, False) + class UpdateBotConfig(cfg.SectionedConfigFile): """ diff --git a/updatebot/update.py b/updatebot/update.py --- a/updatebot/update.py +++ b/updatebot/update.py @@ -354,6 +354,10 @@ metadata = self._getMetadataFromPkgSource(srcPkg) self._conaryhelper.setMetadata(nvf[0], metadata) + if self._cfg.repositoryFormat == 'yum' and self._cfg.generateBuildRequires: + buildrequires = self._getBuildRequiresFromPkgSource(srcPkg) + self._conaryhelper.setBuildRequires(nvf[0], buildrequires) + newVersion = self._conaryhelper.commit(nvf[0], commitMessage=self._cfg.commitMessage) return newVersion @@ -393,6 +397,31 @@ return self._conaryhelper.getMetadata(pkgName) + def _getBuildRequiresFromPkgSource(self, srcPkg): + """ + Get the buildrequires for a given srcPkg. + @param srcPkg: source package object + @return list of build requires + """ + + reqs = [] + for reqType in srcPkg.format: + if reqType.getName() == 'rpm:requires': + reqs.extend([ x.name for x in reqType.iterChildren() ]) + + reqs = list(set(reqs)) + return reqs + + def _getBuildRequiresFromConaryRepository(self, pkgName): + """ + Get the contents of the build requires file from the repository. + @param pkgName: name of the package + @type pkgName: string + @return list of build requires + """ + + return self._conaryhelper.getBuildRequires(pkgName) + def publish(self, trvLst, expected, targetLabel, checkPackageList=True): """ Publish a group and its contents to a target label. From johnsonm at rpath.com Wed Aug 19 17:42:43 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:42:43 +0000 Subject: mirrorball: add metadata setting scripts and methods Message-ID: <200908192142.n7JLghZM023187@scc.eng.rpath.com> changeset: e89366f0f304 user: Elliot Peele <https://issues.rpath.com/> date: Tue, 14 Apr 2009 18:35:04 -0400 add metadata setting scripts and methods committer: Elliot Peele <https://issues.rpath.com/> diff --git a/scripts/setmetadata b/scripts/setmetadata new file mode 100755 --- /dev/null +++ b/scripts/setmetadata @@ -0,0 +1,63 @@ +#!/usr/bin/python +# +# Copyright (c) 2009 rPath, Inc. +# +# This program is distributed under the terms of the Common Public License, +# version 1.0. A copy of this license should have been distributed with this +# source file in a file called LICENSE. If it is not present, the license +# is always available at http://www.rpath.com/permanent/licenses/CPL-1.0. +# +# This program is distributed in the hope that it will be useful, but +# without any warranty; without even the implied warranty of merchantability +# or fitness for a particular purpose. See the Common Public License for +# full details. +# + +import os +import sys +from conary.lib import util as cutil + +sys.excepthook = cutil.genExcepthook() + +sys.path.insert(0, os.environ['HOME'] + '/hg/mirrorball') + +cfgDir = os.environ['HOME'] + '/hg/mirrorball/config' + +platform = sys.argv[1] + +from conary import versions + +from updatebot import log +from updatebot import config +from updatebot import update +from updatebot import pkgsource +from updatebot.lib import util + +log.addRootLogger() + +import logging +slog = logging.getLogger('script') + +cfg = config.UpdateBotConfig() +cfg.read(util.join(cfgDir, platform, 'updatebotrc')) + +# set metadata on the taget label +topGroups = [ (cfg.topGroup[0], cfg.targetLabel, cfg.topGroup[2]), + (cfg.topGroup[0], versions.VersionFromString('/' + cfg.topGroup[1]), cfg.topGroup[2]), ] + +pkgSource = pkgsource.PackageSource(cfg) +updater = update.Updater(cfg, pkgSource) +helper = updater._conaryhelper + +pkgSource.load() + +for topGroup in topGroups: + for srcTroveSpec, binSet in helper.getSourceTroves(topGroup).iteritems(): + name = srcTroveSpec[0].split(':')[0] + if name not in pkgSource.srcNameMap: + slog.warn('%s not found in package source, not setting metadata' % name) + continue + if srcTroveSpec[1].getHost() != topGroup[1].getHost(): + slog.warn('not setting metadata for %s, not on same label' % name) + continue + updater.setTroveMetadata(srcTroveSpec, binSet) diff --git a/updatebot/conaryhelper.py b/updatebot/conaryhelper.py --- a/updatebot/conaryhelper.py +++ b/updatebot/conaryhelper.py @@ -659,3 +659,27 @@ log.info('mirror complete') return rc + + def setTroveMetadata(self, trvSpecs, license=None, desc=None, shortDesc=None): + """ + Set metadata on a given trove spec. + """ + + if not license and not dec and not shortDesc: + log.warn('no metadata found for %s' % trvSpecs[-1][0]) + return + + enc = 'utf-8' + mi = trove.MetadataItem() + + if license: + mi.licenses.set(license.encode(enc)) + if desc: + mi.longDesc.set(desc.encode(enc)) + if shortDesc: + mi.shortDesc.set(shortDesc.encode(enc)) + + log.info('setting metadata for %s' % trvSpecs[-1][0].split(':')[0]) + + metadata = [ (x, mi) for x in trvSpecs ] + self._repos.addMetadataItems(metadata) diff --git a/updatebot/update.py b/updatebot/update.py --- a/updatebot/update.py +++ b/updatebot/update.py @@ -468,3 +468,27 @@ """ return self._conaryhelper.mirror(fullTroveSync=fullTroveSync) + + def setTroveMetadata(self, srcTrvSpec, binTrvSet): + """ + Add metadata from a pkgsource to the specified troves. + @param srcTrvSpec: source to use to find srcPkg + @type srcTrvSpec: (name:source, conary.versions.Version, None) + @param binTrvSet: set of binaries built from the given source. + @type binTrvSet: set((n, v, f), ...) + """ + + srcName = srcTrvSpec[0].split(':')[0] + srcPkg = self._getLatestSource(srcName) + + trvSpecs = list(binTrvSet) + + # FIXME: figure out why conary does't let you set metadata on + # source troves. + #trvSpecs.append(srcTrvSpec) + + self._conaryhelper.setTroveMetadata(trvSpecs, + license=srcPkg.license, + desc=srcPkg.description, + shortDesc=srcPkg.summary, + ) From johnsonm at rpath.com Wed Aug 19 17:42:09 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:42:09 +0000 Subject: mirrorball: update to better find missing binaries Message-ID: <200908192142.n7JLg9l8021792@scc.eng.rpath.com> changeset: af859db5ade8 user: Elliot Peele <https://issues.rpath.com/> date: Thu, 22 Jan 2009 15:14:40 -0500 update to better find missing binaries committer: Elliot Peele <https://issues.rpath.com/> diff --git a/scripts/findbinaries.py b/scripts/findbinaries.py --- a/scripts/findbinaries.py +++ b/scripts/findbinaries.py @@ -1,4 +1,4 @@ -#!/usr/bin/python +#!/usr/bin/python2.6 # # Copyright (c) 2008 rPath, Inc. # @@ -16,14 +16,21 @@ import os import sys -sys.path.insert(0, os.environ['HOME'] + '/hg/rpath-xmllib') -sys.path.insert(0, os.environ['HOME'] + '/hg/conary') -sys.path.insert(0, os.environ['HOME'] + '/hg/mirrorball') +sys.path.insert(0, os.environ['HOME'] + '/hg/26/rpath-xmllib') +sys.path.insert(0, os.environ['HOME'] + '/hg/26/conary') +sys.path.insert(0, os.environ['HOME'] + '/hg/26/mirrorball') +sys.path.insert(0, os.environ['HOME'] + '/hg/26/xobj/py') +sys.path.insert(0, os.environ['HOME'] + '/hg/26/rmake') +from conary.lib import util +sys.excepthook = util.genExcepthook() + +import copy import logging from updatebot import log from updatebot import bot +from updatebot import errors from updatebot import config from updatebot import conaryhelper @@ -32,26 +39,63 @@ slog = logging.getLogger('findbinaries') cfg = config.UpdateBotConfig() -cfg.read(os.environ['HOME'] + '/hg/mirrorball/config/centos/updatebotrc') +cfg.read(os.environ['HOME'] + '/hg/26/mirrorball/config/ubuntu/updatebotrc') bot = bot.Bot(cfg) -bot._populatePkgSource() - updater = bot._updater helper = updater._conaryhelper -sources, failed = updater.create(cfg.package, buildAll=True) +def filterPkgs(collection): + def fltr(name): + if type(name) == tuple: + name = name[0] + + if (name.split(':')[0] in cfg.excludePackages or + name.startswith('info-') or + name.startswith('factory-') or + name.startswith('group-')): + return True + return False + + newCollection = copy.deepcopy(collection) + for item in collection: + if not fltr(item): + continue + if type(collection) == dict: + del newCollection[item] + else: + newCollection.remove(item) + + return newCollection + pkgs = helper._repos.getTroveLeavesByLabel({None: {helper._ccfg.buildLabel: None}}) +groupPkgs = helper.getSourceTroves(cfg.topGroup) + +pkgs = filterPkgs(pkgs) +groupPkgs = filterPkgs(groupPkgs) + pkgSet = set([ x.split(':')[0] for x in pkgs ]) -#import epdb; epdb.st() +# remove versions and flavors from groupPkgs +grpPkgs = {} +for key, value in groupPkgs.iteritems(): + key = key[0].split(':')[0] + if key not in grpPkgs: + grpPkgs[key] = set() + for item in value: + grpPkgs[key].add(item[0]) + +# Build of map of the latest versions of all src:set([binary, ...]) that are +# on the buildLabel. srcDict = {} for pkg in pkgSet: + # This is normally due to unbuilt sources. if pkg not in pkgs: - slog.warn('skipping %s, not in packages' % pkg) + slog.warn('skipping %s' % pkg) continue + version = pkgs[pkg].keys()[0] flavors = pkgs[pkg][version] if len(flavors) == 0: @@ -62,10 +106,6 @@ slog.info('getting binaries for %s' % pkg) for src, binSet in helper._getSourceTroves((pkg, version, flavor)).iteritems(): src = (src[0].split(':')[0], src[1], src[2]) - if src not in sources: - slog.warn('skipping %s, it not in sources' % src[0]) - continue - if src not in srcDict: srcDict[src] = set() @@ -73,7 +113,7 @@ name = bin[0].split(':')[0] srcDict[src].add((name, bin[1], bin[2])) -bins = set() +srcNameDict = {} for src, binSet in srcDict.iteritems(): latest = None for bin in binSet: @@ -83,9 +123,27 @@ if bin[1] > latest: latest = bin[1] + if src[0] not in srcNameDict: + srcNameDict[src[0]] = set() + for bin in binSet: if bin[1] == latest: - bins.add(bin[0]) + srcNameDict[src[0]].add(bin[0]) + + +# Now that we have a mapping of source trove name to set of binary trove names +# find the sources and binaries that are not included in the group. +newPkgs = {} +for srcName in srcNameDict: + if srcName not in grpPkgs: + newPkgs[srcName] = srcNameDict[srcName] + else: + newPkgs[srcName] = srcNameDict[srcName].difference(grpPkgs[srcName]) + +# Flatten the newPkgs dict to a list of binaries +bins = set() +for value in newPkgs.itervalues(): + bins.update(value) binLst = list(bins) binLst.sort() From johnsonm at rpath.com Wed Aug 19 17:39:56 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:39:56 +0000 Subject: mirrorball: return bin troves since we actually have them, add some import code while Message-ID: <200908192140.n7JLduUg016487@scc.eng.rpath.com> changeset: 59ebdf636aa0 user: Elliot Peele <http://issues.rpath.com/> date: Fri, 25 Jul 2008 15:21:07 -0400 return bin troves since we actually have them, add some import code while we're at it committer: Elliot Peele <http://issues.rpath.com/> diff --git a/updatebot/conaryhelper.py b/updatebot/conaryhelper.py --- a/updatebot/conaryhelper.py +++ b/updatebot/conaryhelper.py @@ -84,10 +84,15 @@ if len(latest) != 2: raise TooManyFlavorsFoundError(why=latest) - srcTrvs = set() + srcTrvs = {} for trv in latest: log.info('querying %s for source troves' % (trv, )) - srcTrvs.update(self._getSourceTroves(trv)) + srcTrvs = self._getSourceTroves(trv) + for src, binLst in srcTrvs.iteritems(): + if src in srcTrvs: + srcTrvs[src].extend(binLst) + else: + srcTrvs[src] = binLst return srcTrvs @@ -136,13 +141,16 @@ # Iterate over both strong and weak refs because msw said it was a # good idea. - srcTrvs = set() + srcTrvs = {} sources = self._repos.getTroveInfo(trove._TROVEINFO_TAG_SOURCENAME, list(topTrove.iterTroveList(weakRefs=True, strongRefs=True))) - for i, (_, v, _) in enumerate(topTrove.iterTroveList(weakRefs=True, + for i, (n, v, f) in enumerate(topTrove.iterTroveList(weakRefs=True, strongRefs=True)): - srcTrvs.add((sources[i](), v.getSourceVersion(), None)) + src = (sources[i](), v.getSourceVersion(), None) + if src not in srcTrvs: + srcTrvs[src] = [] + srcTrvs[src].append((n, v, f)) return srcTrvs diff --git a/updatebot/update.py b/updatebot/update.py --- a/updatebot/update.py +++ b/updatebot/update.py @@ -74,7 +74,8 @@ # ((name, version, flavor), srpm) troves = [] - for name, version, flavor in self._conaryhelper.getSourceTroves(group): + for name, version, flavor in \ + self._conaryhelper.getSourceTroves(group).iterkeys(): name = name.split(':')[0] # skip special packages @@ -205,25 +206,41 @@ log.info('getting existing packages') pkgs = self._getExistingPackageNames() - toBuild = set() - fail = set() + # Find all of the source to update. + toUpdate = set() for pkg in pkgNames: if pkg not in self._rpmSource.binNameMap: log.warn('no package named %s found in rpm source' % pkg) continue - if pkg not in pkgs: - log.info('importing %s' % pkg) + srcPkg = self._getPackagesToImport(pkg) - try: - srcPkg = self._getPackagesToImport(pkg) - version = self.update((pkg, None, None), srcPkg) + if srcPkg.name not in pkgs: + toUpdate.add(srcPkg) - toBuild.add((pkg, version, None)) - except Exception, e: - log.error('failed to import %s: %s' % (pkg, e)) - fail.add((pkg, e)) -# raise + # Update all of the unique sources. + fail = set() + toBuild = set() + for pkg in toUpdate: + log.info('importing %s' % pkg) + + try: + # FIXME: Remove this once opensuse has groups. + # Only import packages that haven't been imported before + version = self._conaryhelper._getVersionsByName('%s:source' % pkg.name) + if not version: + version = self.update((pkg.name, None, None), pkg) + else: + version = version[0] + +# if not self._conaryhelper._getVersionsByName(pkg.name): + toBuild.add((pkg.name, version, None)) +# else: +# log.info('not building %s' % pkg.name) + except Exception, e: + log.error('failed to import %s: %s' % (pkg, e)) + fail.add((pkg, e)) + raise return toBuild, fail @@ -237,7 +254,7 @@ try: return [ n.split(':')[0] for n, v, f in - self._conaryhelper.getSourceTroves(self._cfg.topGroup) ] + self._conaryhelper.getSourceTroves(self._cfg.topGroup).iterkeys() ] except GroupNotFound: return [] From johnsonm at rpath.com Wed Aug 19 17:41:33 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:41:33 +0000 Subject: mirrorball: branch merge Message-ID: <200908192141.n7JLfXVn020378@scc.eng.rpath.com> changeset: c8248c6b0496 user: Elliot Peele <https://issues.rpath.com/> date: Tue, 18 Nov 2008 10:22:07 -0500 branch merge committer: Elliot Peele <https://issues.rpath.com/> diff --git a/updatebot/build.py b/updatebot/build.py --- a/updatebot/build.py +++ b/updatebot/build.py @@ -201,6 +201,9 @@ # correct flavors. if name.startswith('group-'): troves.append((name, version, flavor)) + elif name == 'kernel' and self._cfg.kernelFlavors: + for context, flavor in self._cfg.kernelFlavors: + troves.append((name, version, flavor, context)) else: # Build all packages as x86 and x86_64. for context in self._cfg.archContexts: diff --git a/updatebot/conaryhelper.py b/updatebot/conaryhelper.py --- a/updatebot/conaryhelper.py +++ b/updatebot/conaryhelper.py @@ -373,7 +373,7 @@ return None def promote(self, trvLst, expected, sourceLabels, targetLabel, - checkPackageList=True): + checkPackageList=True, extraPromoteTroves=None): """ Promote a group and its contents to a target label. @param trvLst: list of troves to publish @@ -388,12 +388,19 @@ @param checkPackageList: verify the list of packages being promoted or not. @type checkPackageList: boolean + @param extraPromoteTroves: troves to promote in addition to the troves + that have been built. + @type extraPromoteTroves: list of trove specs. """ start = time.time() log.info('starting promote') log.info('creating changeset') + # make extraPromoteTroves a list if it was not specified. + if extraPromoteTroves is None: + extraPromoteTroves = [] + # Get the label that the group is on. fromLabel = trvLst[0][1].trailingLabel() @@ -403,6 +410,10 @@ assert(label is not None) labelMap[label] = targetLabel + # mix in the extra troves + for n, v, f in extraPromoteTroves: + trvLst.append((n, None, None)) + success, cs = self._client.createSiblingCloneChangeSet( labelMap, trvLst, @@ -423,9 +434,11 @@ # that we think should be available to promote. Note that all packages # in expected will not be promoted because not all packages are # included in the groups. - difference = newPkgs.difference(oldPkgs) + trvDiff = newPkgs.difference(oldPkgs) grpTrvs = set([ (x[0], x[2]) for x in trvLst if not x[0].endswith(':source') ]) - if checkPackageList and difference != grpTrvs: + grpDiff = set([ x[0] for x in trvDiff.difference(grpTrvs) ]) + extraTroves = set([ x[0] for x in extraPromoteTroves ]) + if checkPackageList and grpDiff.difference(extraTroves): raise PromoteMismatchError(expected=oldPkgs, actual=newPkgs) log.info('committing changeset') diff --git a/updatebot/config.py b/updatebot/config.py --- a/updatebot/config.py +++ b/updatebot/config.py @@ -40,6 +40,23 @@ except versions.ParseError, e: raise ParseError, e +class CfgContextFlavor(CfgFlavor): + """ + Class for representing both a flavor context name and a build flavor. + """ + + def parseString(self, val): + """ + Parse config string. + """ + + try: + context, flavorStr = val.split() + flavor = CfgFlavor.parseString(self, flavorStr) + return context, flavor + except versions.ParseError, e: + raise ParseError, e + class UpdateBotConfig(cfg.SectionedConfigFile): """ Config class for updatebot. @@ -115,6 +132,9 @@ # flavors to build the source group. groupFlavors = (CfgList(CfgFlavor), []) + # flavors to build kernels. + kernelFlavors = (CfgList(CfgContextFlavor), []) + # email information for sending advisories emailFromName = CfgString emailFrom = CfgString diff --git a/updatebot/update.py b/updatebot/update.py --- a/updatebot/update.py +++ b/updatebot/update.py @@ -365,4 +365,5 @@ return self._conaryhelper.promote(trvLst, expected, self._cfg.sourceLabel, targetLabel, - checkPackageList=checkPackageList) + checkPackageList=checkPackageList, + extraPromoteTroves=self._cfg.extraPromoteTroves) From johnsonm at rpath.com Wed Aug 19 17:41:54 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:41:54 +0000 Subject: mirrorball: initial cut at hooking up ubuntu advisories Message-ID: <200908192141.n7JLfsWp021230@scc.eng.rpath.com> changeset: 393e8be70da8 user: Elliot Peele <https://issues.rpath.com/> date: Fri, 19 Dec 2008 14:33:40 -0500 initial cut at hooking up ubuntu advisories committer: Elliot Peele <https://issues.rpath.com/> diff --git a/updatebot/advisories/__init__.py b/updatebot/advisories/__init__.py --- a/updatebot/advisories/__init__.py +++ b/updatebot/advisories/__init__.py @@ -24,7 +24,7 @@ class InvalidBackendError(Exception): pass -__supported_backends = ('sles', 'centos') +__supported_backends = ('sles', 'centos', 'ubuntu') def __getBackend(backend): if backend not in __supported_backends: diff --git a/updatebot/advisories/ubuntu.py b/updatebot/advisories/ubuntu.py new file mode 100644 --- /dev/null +++ b/updatebot/advisories/ubuntu.py @@ -0,0 +1,115 @@ +# +# Copyright (c) 2008 rPath, Inc. +# +# This program is distributed under the terms of the Common Public License, +# version 1.0. A copy of this license should have been distributed with this +# source file in a file called LICENSE. If it is not present, the license +# is always available at http://www.rpath.com/permanent/licenses/CPL-1.0. +# +# This program is distributed in the hope that it will be useful, but +# without any warranty; without even the implied warranty of merchantability +# or fitness for a particular purpose. See the Common Public License for +# full details. +# + +""" +Advisory module for Ubuntu. +""" + +import pmap +import logging + +from updatebot.errors import AdvisoryError +from updatebot.advisories.common import BaseAdvisor + +log = logging.getLogger('updatebot.advisories') + +class Advisor(BaseAdvisor): + def load(self): + """ + Parse the required data to generate a mapping of binary package + object to patch object for a given platform into self._pkgMap. + """ + + # Build data structure for looking up srcPkgs based on file path in + # the advisory. + pkgCache = {} + for pkg in self._pkgSource.binPkgMap: + # is a source package + if hasattr(pkg, 'files'): + files = pkg.files + elif hasattr(pkg, 'location'): + files = [pkg.location, ] + + for path in set(files): + if path not in pkgCache: + pkgCache[path] = set() + pkgCache[path].add(pkg) + + # Fetch all of the archives and process them. + for url in self._getArchiveUrls(): + log.info('parsing mail archive: %s' % url) + for msg in pmap.parse(url, backend=self._cfg.platformName): + self._loadOne(msg, pkgCache) + + def _loadOne(self, msg, pkgCache): + """ + Handles matching one message to any mentioned packages. + """ + + if self._filterPatch(msg): + return + + if self._cfg.upstreamProductVersion not in msg.pkgNameVersion: + return + + for path in msg.pkgs: + if path not in pkgCache: + #log.warn('found path (%s) not in cache' % path) + continue + + #import epdb; epdb.st() + log.info('found path (%s) in cache' % path) + nvMap = msg.pkgNameVersion[self._cfg.upstreamProductVersion] + for pkg in pkgCache[path]: + nv = (pkg.name, '-'.join([pkg.version, pkg.release])) + if nv[1].startswith('-') or nv[1].endswith('-'): + raise AdvisoryError + if nv in nvMap: + if pkg not in self._pkgMap: + self._pkgMap[pkg] = set() + self._pkgMap[pkg].add(msg) + + if msg.packages is None: + msg.packages = set() + msg.packages.add(pkg) + + + def _hasException(self, binPkg): + """ + Check the config for repositories with exceptions for sending + advisories. (io. repositories that we generated metadata for.) + @param binPkg: binary package object + @type binPkg: repomd.packagexml._Package + """ + + return False + + def _isUpdatesRepo(self, binPkg): + """ + Check the repository name. If this package didn't come from a updates + repository it is probably not security related. + @param binPkg: binary package object + @type binPkg: repomd.packagexml._Package + """ + + return True + + def _checkForDuplicates(self, patchSet): + """ + Check a set of "patch" objects for duplicates. If there are duplicates + combine any required information into the first object in the set and + return True, otherwise return False. + """ + + return False From johnsonm at rpath.com Wed Aug 19 17:41:47 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:41:47 +0000 Subject: mirrorball: more command line work Message-ID: <200908192141.n7JLflck020940@scc.eng.rpath.com> changeset: 27b45dd31543 user: Elliot Peele <https://issues.rpath.com/> date: Mon, 01 Dec 2008 11:15:32 -0500 more command line work committer: Elliot Peele <https://issues.rpath.com/> diff --git a/bin/bot-wrapper b/bin/bot-wrapper new file mode 100755 --- /dev/null +++ b/bin/bot-wrapper @@ -0,0 +1,20 @@ +#!/bin/sh +# +# Copyright (c) 2004-2008 rPath, Inc. +# +# This program is distributed under the terms of the Common Public License, +# version 1.0. A copy of this license should have been distributed with this +# source file in a file called LICENSE. If it is not present, the license +# is always available at http://www.rpath.com/permanent/licenses/CPL-1.0. +# +# This program is distributed in the hope that it will be useful, but +# without any warranty; without even the implied warranty of merchantability +# or fitness for a particular purpose. See the Common Public License for +# full details. +# + +bindir=$(dirname $0) +bin=$(basename $0) +importdir=$(cd $bindir/.. 2> /dev/null && pwd -P;) +export PYTHONPATH=$importdir +exec $importdir/commands/$bin "$@" diff --git a/updatebot/cmdline/clientcfg.py b/updatebot/cmdline/clientcfg.py new file mode 100644 --- /dev/null +++ b/updatebot/cmdline/clientcfg.py @@ -0,0 +1,40 @@ +# +# Copyright (c) 2008 rPath, Inc. +# +# This program is distributed under the terms of the Common Public License, +# version 1.0. A copy of this license should have been distributed with this +# source file in a file called LICENSE. If it is not present, the license +# is always available at http://www.rpath.com/permanent/licenses/CPL-1.0. +# +# This program is distributed in the hope that it will be useful, but +# without any warranty; without even the implied warranty of merchantability +# or fitness for a particular purpose. See the Common Public License for +# full details. +# + +""" +Command line client configuration. +""" + +import os + +from updatebot.config import UpdateBotConfig + +class UpdateBotClientConfig(UpdateBotConfig): + """ + Client config object. + """ + + def __init__(self, readConfigFiles=False, ignoreErrors=False): + UpdateBotConfig.__init__(self) + self._ignoreErrors = ignoreErrors + + if readConfigFiles: + self.readFiles() + + def readFiles(self): + self.read('/etc/updatebot/clientrc', exception=False) + if 'HOME' in os.environ: + self.read(os.path.join(os.environ['HOME'], '.updatebot-clientrc'), + exception=False) + self.read('updatebot-clientrc', exception=False) diff --git a/updatebot/cmdline/command.py b/updatebot/cmdline/command.py --- a/updatebot/cmdline/command.py +++ b/updatebot/cmdline/command.py @@ -27,7 +27,7 @@ _commands.append(cmd) -class _BotCommand(options.AbstractCommand): +class BotCommand(options.AbstractCommand): defaultGroup = 'Common Options' docs = {'config' : (VERBOSE_HELP, @@ -60,7 +60,7 @@ pass -class BuildPackageCommand(_BotCommand): +class BuildPackageCommand(BotCommand): """ Build a list of packages. """ diff --git a/updatebot/cmdline/main.py b/updatebot/cmdline/main.py --- a/updatebot/cmdline/main.py +++ b/updatebot/cmdline/main.py @@ -19,17 +19,17 @@ from conary.lib import options from updatebot import errors -from updatebot import config from updatebot import constants from updatebot import log as logger from updatebot.cmdline import command +from updatebot.cmdline import clientcfg class BotMain(options.MainHandler): name = 'updatebot' version = constants.version abstractCommand = command.BotCommand - configClass = config.UpdateBotConfig + configClass = clientcfg.UpdateBotClientConfig useConaryOptions = False diff --git a/updatebot/config.py b/updatebot/config.py --- a/updatebot/config.py +++ b/updatebot/config.py @@ -41,6 +41,7 @@ except versions.ParseError, e: raise ParseError, e + class CfgContextFlavor(CfgFlavor): """ Class for representing both a flavor context name and a build flavor. @@ -58,7 +59,8 @@ except versions.ParseError, e: raise ParseError, e -class UpdateBotConfig(cfg.SectionedConfigFile): + +class UpdateBotConfigSection(cfg.ConfigSection): """ Config class for updatebot. """ @@ -151,8 +153,19 @@ emailBcc = (CfgList(CfgString), []) smtpServer = CfgString - def __init__(self, *args, **kwargs): - cfg.SectionedConfigFile.__init__(self, *args, **kwargs) + +class UpdateBotConfig(cfg.SectionedConfigFile): + """ + Config object for UpdateBot. + """ + + _defaultSectionType = UpdateBotConfigSection + + def __init__(self): + cfg.SectionedConfigFile.__init__(self) + for info in self._defaultSectionType._getConfigOptions(): + if info[0] not in self: + self.addConfigOption(*info) def read(self, *args, **kwargs): """ From johnsonm at rpath.com Wed Aug 19 17:39:40 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:39:40 +0000 Subject: mirrorball: fix broken build tests Message-ID: <200908192139.n7JLdeTh015824@scc.eng.rpath.com> changeset: a813165f2fa4 user: Elliot Peele <http://issues.rpath.com/> date: Tue, 24 Jun 2008 16:10:37 -0400 fix broken build tests add test for builder.build increase coverage to 100% committer: Elliot Peele <http://issues.rpath.com/> diff --git a/test/unit_test/updatebottest/buildtest.py b/test/unit_test/updatebottest/buildtest.py --- a/test/unit_test/updatebottest/buildtest.py +++ b/test/unit_test/updatebottest/buildtest.py @@ -24,6 +24,66 @@ from updatebot import errors class BuilderTest(slehelp.Helper): + def testBuild(self): + jobId = 1 + + mockVersion1 = mock.MockObject() + mockVersion2 = mock.MockObject() + mockFlavor1 = mock.MockObject() + mockFlavor2 = mock.MockObject() + + trvSpecs = [('foo', mockVersion1, mockFlavor1), + ('group-foo', mockVersion2, mockFlavor2), + ] + + contexts = ['a', 'b'] + + self.mock(self.updateBotCfg, 'archContexts', contexts) + + troves = [('foo', mockVersion1, mockFlavor1, 'a'), + ('foo', mockVersion1, mockFlavor1, 'b'), + ('group-foo', mockVersion2, mockFlavor2), + ] + + trvMap = {('foo', mockVersion1, mockFlavor1, 'a'): + [('foo', mockVersion1, mockFlavor1), ], + ('foo', mockVersion1, mockFlavor1, 'b'): + [('foo:lib', mockVersion1, mockFlavor1), + ('foo', mockVersion1, mockFlavor1), ], + ('group-foo', mockVersion2, mockFlavor2, None): + [('group-foo-devel', mockVersion2, mockFlavor2), ], + } + + ret = {('foo', mockVersion1, None): + set([('foo', mockVersion1, mockFlavor1), + ('foo:lib', mockVersion1, mockFlavor1), + ]), + ('group-foo', mockVersion2, None): + set([('group-foo-devel', mockVersion2, mockFlavor2), + ]), + } + + mockStartJob = mock.MockObject() + mockMonitorJob = mock.MockObject() + mockSanityCheckJob = mock.MockObject() + mockCommitJob = mock.MockObject() + + mockStartJob._mock.setReturn(jobId, troves) + mockCommitJob._mock.setReturn(trvMap, jobId) + + builder = build.Builder(self.updateBotCfg) + self.mock(builder, '_startJob', mockStartJob) + self.mock(builder, '_monitorJob', mockMonitorJob) + self.mock(builder, '_sanityCheckJob', mockSanityCheckJob) + self.mock(builder, '_commitJob', mockCommitJob) + + result = builder.build(trvSpecs) + self.failUnlessEqual(result, ret) + mockStartJob._mock.assertCalled(troves) + mockMonitorJob._mock.assertCalled(jobId) + mockSanityCheckJob._mock.assertCalled(jobId) + mockCommitJob._mock.assertCalled(jobId) + def testStartJob(self): trvSpecs = (('foo', '', ''), ) @@ -42,9 +102,10 @@ def testMonitorJob(self): mockMonitor = mock.MockObject() - self.mock(monitor, 'monitorJob', mockMonitor) builder = build.Builder(self.updateBotCfg) + self.mock(build.monitor, 'monitorJob', mockMonitor) + builder._monitorJob(1) mockMonitor._mock.assertCalled(builder._helper.client, 1, @@ -109,23 +170,26 @@ mockHelper = mock.MockObject(stableReturnValues=True) mockHelper.getJob._mock.setReturn(mockJob, jobId) mockCommitJobs = mock.MockObject(stableReturnValues=True) + mockBuildCfg = mock.MockObject(stableReturnValues=True) + mockBuildCfg._mock.set(reposName='foo.example.com') self.mock(commit, 'commitJobs', mockCommitJobs) builder = build.Builder(self.updateBotCfg) builder._helper = mockHelper + builder._rmakeCfg = mockBuildCfg mockCommitJobs._mock.setDefaultReturn((True, sampleData)) trvMap = builder._commitJob(jobId) self.failUnlessEqual(trvMap, expected) mockHelper.getJob._mock.assertCalled(jobId) - mockHelper.client.startCommit._mock.assertCalled(jobId) + mockHelper.client.startCommit._mock.assertCalled([jobId, ]) mockHelper.client.commitSucceeded._mock.assertCalled(sampleData) mockCommitJobs._mock.setDefaultReturn((False, sampleData)) self.failUnlessRaises(errors.CommitFailedError, builder._commitJob, jobId) mockHelper.getJob._mock.assertCalled(jobId) - mockHelper.client.startCommit._mock.assertCalled(jobId) + mockHelper.client.startCommit._mock.assertCalled([jobId, ]) mockHelper.client.commitFailed._mock.assertCalled([jobId, ], sampleData) mockHelper.client.commitSucceeded._mock.assertNotCalled() From johnsonm at rpath.com Wed Aug 19 17:38:46 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:38:46 +0000 Subject: mirrorball: add basic makefiles, not adapted yet Message-ID: <200908192138.n7JLckFN013592@scc.eng.rpath.com> changeset: 8ca8532c7610 user: Elliot Peele <http://issues.rpath.com/> date: Tue, 27 May 2008 18:02:35 -0400 add basic makefiles, not adapted yet committer: Elliot Peele <http://issues.rpath.com/> diff --git a/Make.defs b/Make.defs new file mode 100644 --- /dev/null +++ b/Make.defs @@ -0,0 +1,31 @@ +# +# Copyright (c) 2006-2008 rPath, Inc. +# +# This program is distributed under the terms of the Common Public License, +# version 1.0. A copy of this license should have been distributed with this +# source file in a file called LICENSE. If it is not present, the license +# is always available at http://www.rpath.com/permanent/licenses/CPL-1.0. +# +# This program is distributed in the hope that it will be useful, but +# without any warranty; without even the implied warranty of merchantability +# or fitness for a particular purpose. See the Common Public License for +# full details. +# + +VERSION=0.1 +# XVERSION can be overridden by snapshot builds to force incompatible versions + +export prefix = /usr +export bindir = $(prefix)/bin +export sbindir = $(prefix)/sbin +export libdir = $(prefix)/lib +export libexecdir = $(prefix)/libexec +export datadir = $(prefix)/share +export mandir = $(datadir)/man +export sitedir = $(libdir)/python$(PYVERSION)/site-packages/ +export rbuilddir = $(sitedir)/rbuild +export plugindir = $(datadir)/rbuild/plugins/ +export initdir = /etc/init.d +export sysconfdir = /etc/sysconfig + +export $(VERSION) diff --git a/Make.rules b/Make.rules new file mode 100644 --- /dev/null +++ b/Make.rules @@ -0,0 +1,58 @@ +# +# Copyright (c) 2006-2008 rPath, Inc. +# +# This program is distributed under the terms of the Common Public License, +# version 1.0. A copy of this license should have been distributed with this +# source file in a file called LICENSE. If it is not present, the license +# is always available at http://www.rpath.com/permanent/licenses/CPL-1.0. +# +# This program is distributed in the hope that it will be useful, but +# without any warranty; without even the implied warranty of merchantability +# or fitness for a particular purpose. See the Common Public License for +# full details. +# + +PYTHON = $(shell [ -x /usr/bin/python2.4 ] && echo /usr/bin/python2.4) +PYVERSION = $(shell $(PYTHON) -c 'import os, sys; print sys.version[:3]') +PYINCLUDE = $(shell $(PYTHON) -c 'import os, sys; print os.sep.join((sys.prefix, "include", "python" + sys.version[:3]))') + +pyfiles-install: + mkdir -p $(DESTDIR)$(rbuilddir) + for f in $(python_files); do \ + mkdir -p `dirname $(DESTDIR)/$(sitedir)/$(DIR)/$$f`; \ + cp -a $$f $(DESTDIR)/$(sitedir)/$(DIR)/$$f; \ + done + +default-dist: dist-subdirs + for f in $(dist_files); do \ + mkdir -p $(DISTDIR)/$(DIR)/`dirname $$f`; \ + cp -a $$f $(DISTDIR)/$(DIR)/$$f; \ + done + +default-install: + +default-all: + +default-clean: clean-subdirs + rm -f *~ .??*~ .#* *.pyo *.pyc *,cover $(generated_files) *.orig *.ccs + +default-test: + $(TESTSUITE) *.py + +default-subdirs: + for d in $(SUBDIRS); do make -C $$d DIR=$$d || exit 1; done + +clean-subdirs: +ifdef SUBDIRS + for d in $(SUBDIRS); do make -C $$d DIR=$(DIR)/$$d clean || exit 1; done +endif + +install-subdirs: +ifdef SUBDIRS + for d in $(SUBDIRS); do make -C $$d DIR=$(DIR)/$$d install || exit 1; done +endif + +dist-subdirs: +ifdef SUBDIRS + for d in $(SUBDIRS); do make -C $$d DIR=$(DIR)/$$d dist || exit 1; done +endif diff --git a/Makefile b/Makefile new file mode 100644 --- /dev/null +++ b/Makefile @@ -0,0 +1,65 @@ +# +# Copyright (c) 2006-2008 rPath, Inc. +# +# This program is distributed under the terms of the Common Public License, +# version 1.0. A copy of this license should have been distributed with this +# source file in a file called LICENSE. If it is not present, the license +# is always available at http://www.rpath.com/permanent/licenses/CPL-1.0. +# +# This program is distributed in the hope that it will be useful, but +# without any warranty; without even the implied warranty of merchantability +# or fitness for a particular purpose. See the Common Public License for +# full details. +# + +all: default-subdirs default-all + +export TOPDIR = $(shell pwd) +export DISTDIR = $(TOPDIR)/mirrorball-$(VERSION) + +SUBDIRS=updateBot repomd rpmimport pylint test + +extra_files = \ + Make.rules \ + Makefile \ + Make.defs \ + +dist_files = $(extra_files) + +.PHONY: clean dist install subdirs + +subdirs: default-subdirs + +install: install-subdirs + +clean: clean-subdirs default-clean + +dist: + if ! grep "^Changes in $(VERSION)" NEWS > /dev/null 2>&1; then \ + echo "no NEWS entry"; \ + exit 1; \ + fi + $(MAKE) forcedist + + +archive: $(dist_files) + rm -rf $(DISTDIR) + mkdir $(DISTDIR) + for d in $(SUBDIRS); do make -C $$d DIR=$$d dist || exit 1; done + for f in $(dist_files); do \ + mkdir -p $(DISTDIR)/`dirname $$f`; \ + cp -a $$f $(DISTDIR)/$$f; \ + done; \ + tar cjf $(DISTDIR).tar.bz2 `basename $(DISTDIR)` + +forcedist: archive + +tag: + hg tag -f rbuild-$(VERSION) + +clean: clean-subdirs default-clean + +include Make.rules +include Make.defs + +# vim: set sts=8 sw=8 noexpandtab : From johnsonm at rpath.com Wed Aug 19 17:41:59 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:41:59 +0000 Subject: mirrorball: use py 2.6 Message-ID: <200908192141.n7JLfxL7021434@scc.eng.rpath.com> changeset: 53a502e08671 user: Elliot Peele <https://issues.rpath.com/> date: Mon, 12 Jan 2009 16:56:50 -0500 use py 2.6 committer: Elliot Peele <https://issues.rpath.com/> diff --git a/scripts/buildgroups b/scripts/buildgroups --- a/scripts/buildgroups +++ b/scripts/buildgroups @@ -1,4 +1,4 @@ -#!/usr/bin/python +#!/usr/bin/python2.6 # # Copryright (c) 2008 rPath, Inc. # diff --git a/scripts/buildpackages b/scripts/buildpackages --- a/scripts/buildpackages +++ b/scripts/buildpackages @@ -1,4 +1,4 @@ -#!/usr/bin/python +#!/usr/bin/python2.6 # # Copryright (c) 2008 rPath, Inc. # diff --git a/scripts/genmanifest.py b/scripts/genmanifest.py --- a/scripts/genmanifest.py +++ b/scripts/genmanifest.py @@ -1,4 +1,4 @@ -#!/usr/bin/python +#!/usr/bin/python2.6 # # Copyright (c) 2008 rPath, Inc. # @@ -16,9 +16,11 @@ import os import sys -sys.path.insert(0, os.environ['HOME'] + '/hg/rpath-xmllib') -sys.path.insert(0, os.environ['HOME'] + '/hg/conary') -sys.path.insert(0, os.environ['HOME'] + '/hg/mirrorball') +sys.path.insert(0, os.environ['HOME'] + '/hg/26/rpath-xmllib') +sys.path.insert(0, os.environ['HOME'] + '/hg/26/conary') +sys.path.insert(0, os.environ['HOME'] + '/hg/26/mirrorball') +sys.path.insert(0, os.environ['HOME'] + '/hg/26/rmake') +sys.path.insert(0, os.environ['HOME'] + '/hg/26/epdb') from conary.lib import util sys.excepthook = util.genExcepthook() @@ -27,10 +29,10 @@ log.addRootLogger() cfg = config.UpdateBotConfig() -cfg.read(os.environ['HOME'] + '/hg/mirrorball/config/centos/updatebotrc') +cfg.read(os.environ['HOME'] + '/hg/mirrorball/config/ubuntu/updatebotrc') obj = bot.Bot(cfg) -obj._populatePkgSource() +obj._pkgSource.load() pkgName = sys.argv[1] diff --git a/scripts/header.py b/scripts/header.py --- a/scripts/header.py +++ b/scripts/header.py @@ -15,9 +15,15 @@ import os import sys -sys.path.insert(0, os.environ['HOME'] + '/hg/rpath-xmllib') -sys.path.insert(0, os.environ['HOME'] + '/hg/conary') -sys.path.insert(0, os.environ['HOME'] + '/hg/mirrorball') +sys.path.insert(0, os.environ['HOME'] + '/hg/26/rpath-xmllib') +sys.path.insert(0, os.environ['HOME'] + '/hg/26/conary') +sys.path.insert(0, os.environ['HOME'] + '/hg/26/mirrorball') +sys.path.insert(0, os.environ['HOME'] + '/hg/26/epdb') +sys.path.insert(0, os.environ['HOME'] + '/hg/26/rmake') +sys.path.insert(0, os.environ['HOME'] + '/hg/26/xobj/py') + +from conary.lib import util +sys.excepthook = util.genExcepthook() from updatebot import log from updatebot import build @@ -32,7 +38,7 @@ log.addRootLogger() cfg = config.UpdateBotConfig() -cfg.read(os.environ['HOME'] + '/hg/mirrorball/config/%s/updatebotrc' % sys.argv[1]) +cfg.read(os.environ['HOME'] + '/hg/26/mirrorball/config/%s/updatebotrc' % sys.argv[1]) builder = build.Builder(cfg) diff --git a/scripts/import.py b/scripts/import.py --- a/scripts/import.py +++ b/scripts/import.py @@ -1,4 +1,4 @@ -#!/usr/bin/python +#!/usr/bin/python2.6 # # Copyright (c) 2008 rPath, Inc. # @@ -16,9 +16,11 @@ import os import sys -sys.path.insert(0, os.environ['HOME'] + '/hg/rpath-xmllib') -sys.path.insert(0, os.environ['HOME'] + '/hg/conary') -sys.path.insert(0, os.environ['HOME'] + '/hg/mirrorball') +sys.path.insert(0, os.environ['HOME'] + '/hg/26/rpath-xmllib') +sys.path.insert(0, os.environ['HOME'] + '/hg/26/conary') +sys.path.insert(0, os.environ['HOME'] + '/hg/26/mirrorball') +sys.path.insert(0, os.environ['HOME'] + '/hg/26/rmake') +sys.path.insert(0, os.environ['HOME'] + '/hg/26/epdb') from conary.lib import util sys.excepthook = util.genExcepthook() @@ -27,7 +29,7 @@ log.addRootLogger() cfg = config.UpdateBotConfig() -cfg.read(os.environ['HOME'] + '/hg/mirrorball/config/centos/updatebotrc') +cfg.read(os.environ['HOME'] + '/hg/26/mirrorball/config/sles/updatebotrc') obj = bot.Bot(cfg) trvMap = obj.create() diff --git a/scripts/pkgsource.py b/scripts/pkgsource.py --- a/scripts/pkgsource.py +++ b/scripts/pkgsource.py @@ -1,4 +1,4 @@ -#!/usr/bin/python +#!/usr/bin/python2.6 import os import sys diff --git a/scripts/promote.py b/scripts/promote.py --- a/scripts/promote.py +++ b/scripts/promote.py @@ -1,4 +1,4 @@ -#!/usr/bin/python +#!/usr/bin/python2.6 # # Copyright (c) 2008 rPath, Inc. # diff --git a/scripts/update.py b/scripts/update.py --- a/scripts/update.py +++ b/scripts/update.py @@ -1,19 +1,24 @@ -#!/usr/bin/python +#!/usr/bin/python2.6 import os import sys -sys.path.insert(0, os.environ['HOME'] + '/hg/rpath-xmllib') -sys.path.insert(0, os.environ['HOME'] + '/hg/conary') -sys.path.insert(0, os.environ['HOME'] + '/hg/mirrorball') +sys.path.insert(0, os.environ['HOME'] + '/hg/26/rpath-xmllib') +sys.path.insert(0, os.environ['HOME'] + '/hg/26/conary') +sys.path.insert(0, os.environ['HOME'] + '/hg/26/mirrorball') +sys.path.insert(0, os.environ['HOME'] + '/hg/26/rmake') +sys.path.insert(0, os.environ['HOME'] + '/hg/26/epdb') +sys.path.insert(0, os.environ['HOME'] + '/hg/26/xobj/py') + +from updatebot import log +log.addRootLogger() from conary.lib import util sys.excepthook = util.genExcepthook() -from updatebot import bot, config, log +from updatebot import bot, config -log.addRootLogger() cfg = config.UpdateBotConfig() -cfg.read(os.environ['HOME'] + '/hg/mirrorball/config/sles/updatebotrc') +cfg.read(os.environ['HOME'] + '/hg/26/mirrorball/config/sles/updatebotrc') obj = bot.Bot(cfg) obj.update() From johnsonm at rpath.com Wed Aug 19 17:39:48 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:39:48 +0000 Subject: mirrorball: rename some scripts Message-ID: <200908192139.n7JLdmCV016147@scc.eng.rpath.com> changeset: d25b3325668d user: Elliot Peele <http://issues.rpath.com/> date: Wed, 09 Jul 2008 12:06:00 -0400 rename some scripts committer: Elliot Peele <http://issues.rpath.com/> diff --git a/scripts/buildgroups b/scripts/buildgroups new file mode 100755 --- /dev/null +++ b/scripts/buildgroups @@ -0,0 +1,44 @@ +#!/usr/bin/python +# +# Copryright (c) 2008 rPath, Inc. +# + +""" +Script for cooking groups defined in the updatebot config. +""" + +import os +import sys + +sys.path.insert(0, os.environ['HOME'] + '/hg/rpath-xmllib') +sys.path.insert(0, os.environ['HOME'] + '/hg/conary') +sys.path.insert(0, os.environ['HOME'] + '/hg/mirrorball') + +from updatebot import log +from updatebot import build +from updatebot import config + +log.addRootLogger() +cfg = config.UpdateBotConfig() +cfg.read(os.environ['HOME'] + '/hg/mirrorball/config/updatebotrc') + +builder = build.Builder(cfg) + +grpTrvs = set() +for flavor in cfg.groupFlavors: + grpTrvs.add((cfg.topSourceGroup[0], cfg.topSourceGroup[1], flavor)) +grpTrvMap = builder.build(grpTrvs) + +print "built:\n" + +def displayTrove(nvf): + flavor = '' + if nvf[2] is not None: + flavor = '[%s]' % nvf[2] + + return '%s=%s%s' % (nvf[0], nvf[1], flavor) + +for srcTrv in grpTrvMap.iterkeys(): + print displayTrove(srcTrv) + for binTrv in grpTrvMap[srcTrv]: + print "\t", displayTrove(binTrv) diff --git a/scripts/buildpackages b/scripts/buildpackages new file mode 100755 --- /dev/null +++ b/scripts/buildpackages @@ -0,0 +1,46 @@ +#!/usr/bin/python +# +# Copryright (c) 2008 rPath, Inc. +# + +""" +Script for cooking groups defined in the updatebot config. +""" + +import os +import sys + +sys.path.insert(0, os.environ['HOME'] + '/hg/rpath-xmllib') +sys.path.insert(0, os.environ['HOME'] + '/hg/conary') +sys.path.insert(0, os.environ['HOME'] + '/hg/mirrorball') + +from updatebot import log +from updatebot import build +from updatebot import config + +log.addRootLogger() +cfg = config.UpdateBotConfig() +cfg.read(os.environ['HOME'] + '/hg/mirrorball/config/updatebotrc') + +builder = build.Builder(cfg) + + +trvs = set() +label = cfg.topSourceGroup[1] +for pkg in sys.argv[1:]: + trvs.add((pkg, label, None)) +trvMap = builder.build(trvs) + +print "built:\n" + +def displayTrove(nvf): + flavor = '' + if nvf[2] is not None: + flavor = '[%s]' % nvf[2] + + return '%s=%s%s' % (nvf[0], nvf[1], flavor) + +for srcTrv in trvMap.iterkeys(): + print displayTrove(srcTrv) + for binTrv in trvMap[srcTrv]: + print "\t", displayTrove(binTrv) diff --git a/scripts/cookgroups.py b/scripts/cookgroups.py deleted file mode 100755 --- a/scripts/cookgroups.py +++ /dev/null @@ -1,44 +0,0 @@ -#!/usr/bin/python -# -# Copryright (c) 2008 rPath, Inc. -# - -""" -Script for cooking groups defined in the updatebot config. -""" - -import os -import sys - -sys.path.insert(0, os.environ['HOME'] + '/hg/rpath-xmllib') -sys.path.insert(0, os.environ['HOME'] + '/hg/conary') -sys.path.insert(0, os.environ['HOME'] + '/hg/mirrorball') - -from updatebot import log -from updatebot import build -from updatebot import config - -log.addRootLogger() -cfg = config.UpdateBotConfig() -cfg.read(os.environ['HOME'] + '/hg/mirrorball/config/updatebotrc') - -builder = build.Builder(cfg) - -grpTrvs = set() -for flavor in cfg.groupFlavors: - grpTrvs.add((cfg.topSourceGroup[0], cfg.topSourceGroup[1], flavor)) -grpTrvMap = builder.build(grpTrvs) - -print "built:\n" - -def displayTrove(nvf): - flavor = '' - if nvf[2] is not None: - flavor = '[%s]' % nvf[2] - - return '%s=%s%s' % (nvf[0], nvf[1], flavor) - -for srcTrv in grpTrvMap.iterkeys(): - print displayTrove(srcTrv) - for binTrv in grpTrvMap[srcTrv]: - print "\t", displayTrove(binTrv) diff --git a/scripts/cookpackages.py b/scripts/cookpackages.py deleted file mode 100755 --- a/scripts/cookpackages.py +++ /dev/null @@ -1,46 +0,0 @@ -#!/usr/bin/python -# -# Copryright (c) 2008 rPath, Inc. -# - -""" -Script for cooking groups defined in the updatebot config. -""" - -import os -import sys - -sys.path.insert(0, os.environ['HOME'] + '/hg/rpath-xmllib') -sys.path.insert(0, os.environ['HOME'] + '/hg/conary') -sys.path.insert(0, os.environ['HOME'] + '/hg/mirrorball') - -from updatebot import log -from updatebot import build -from updatebot import config - -log.addRootLogger() -cfg = config.UpdateBotConfig() -cfg.read(os.environ['HOME'] + '/hg/mirrorball/config/updatebotrc') - -builder = build.Builder(cfg) - - -trvs = set() -label = cfg.topSourceGroup[1] -for pkg in sys.argv[1:]: - trvs.add((pkg, label, None)) -trvMap = builder.build(trvs) - -print "built:\n" - -def displayTrove(nvf): - flavor = '' - if nvf[2] is not None: - flavor = '[%s]' % nvf[2] - - return '%s=%s%s' % (nvf[0], nvf[1], flavor) - -for srcTrv in trvMap.iterkeys(): - print displayTrove(srcTrv) - for binTrv in trvMap[srcTrv]: - print "\t", displayTrove(binTrv) From johnsonm at rpath.com Wed Aug 19 17:42:25 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:42:25 +0000 Subject: mirrorball: branch merge Message-ID: <200908192142.n7JLgP43022472@scc.eng.rpath.com> changeset: 807681838a72 user: Elliot Peele <https://issues.rpath.com/> date: Sat, 14 Feb 2009 19:31:18 +0000 branch merge committer: Elliot Peele <https://issues.rpath.com/> diff --git a/updatebot/build.py b/updatebot/build.py --- a/updatebot/build.py +++ b/updatebot/build.py @@ -34,6 +34,32 @@ log = logging.getLogger('updateBot.build') +def jobInfoExceptionHandler(func): + def deco(self, *args, **kwargs): + retry = kwargs.pop('retry', True) + + exception = None + while retry: + try: + ret = func(self, *args, **kwargs) + return ret + except xml.parsers.expat.ExpatError, e: + exception = e + except Exception, e: + exception = e + + if type(retry) == int: + retry -= 1 + + # sleep between each retry + time.sleep(5) + + if exception is not None: + raise exception + + return deco + + class Builder(object): """ Class for wrapping the rMake api until we can switch to using rBuild. @@ -140,16 +166,13 @@ for trv in jobkeys: jobId = jobs[trv] - job = self._helper.getJob(jobId) - while not job.isFinished() and not job.isFailed(): - log.info('waiting for %s' % jobId) - time.sleep(1) - job = self._helper.getJob(jobId) + job = self._getJob(jobId) + self._wait(jobId) failed = set() results = {} for trv, jobId in jobs.iteritems(): - job = self._helper.getJob(jobId) + job = self._getJob(jobId) if job.isFailed(): failed.add((trv, jobId)) elif job.isFinished(): @@ -196,10 +219,7 @@ # Wait for the jobs to finish. log.info('Waiting for jobs to complete') for jobId in jobIds.itervalues(): - job = self._helper.getJob(jobId) - while not job.isFinished() and not job.isFailed(): - time.sleep(1) - job = self._helper.getJob(jobId) + self._wait(jobId) # Sanity check all jobs. for jobId in jobIds.itervalues(): @@ -274,6 +294,22 @@ return troves + @jobInfoExceptionHandler + def _getJob(self, jobId, retry=None): + """ + Get a job instance from the rMake helper, catching several common + exceptions. + @param jobId: id of an rMake job + @type jobId: integer + @param retry: information about retrying the get job, if retry is None + then retry forever, if retry is an integer retry n times. + @type retry: None + @type retry: integet + @return rmake job instance + """ + + return self._helper.getJob(jobId) + def _startJob(self, troveSpecs): """ Create and start a rMake build. @@ -290,6 +326,20 @@ return jobId + def _wait(self, jobId): + """ + Wait for a job to complete. + @param jobId: rMake job ID + @type jobId: integer + """ + + log.info('waiting for job [%s] to complete' % jobId) + job = self._getJob(jobId) + while not job.isFinished() and not job.isFailed(): + time.sleep(5) + job = self._getJob(jobId) + + @jobInfoExceptionHandler def _monitorJob(self, jobId): """ Monitor job status, block until complete. @@ -309,7 +359,7 @@ """ # Check for errors - job = self._helper.getJob(jobId) + job = self._getJob(jobId) if job.isFailed(): log.error('Job %d failed', jobId) raise JobFailedError(jobId=jobId, why=job.status) @@ -337,7 +387,7 @@ # Do the commit startTime = time.time() - jobs = [ self._helper.getJob(x) for x in jobIds ] + jobs = [ self._getJob(x) for x in jobIds ] log.info('Starting commit of job %s', jobIdsStr) self._helper.client.startCommit(jobIds) @@ -489,10 +539,9 @@ def _doBuild(self): self.jobId = self.builder.start([self.trv, ]) - done, job = self._watch() - while not done: - done, job = self._watch() + self.builder._wait(self.jobId) + job = self.builder._getJob(self.jobId) if job.isFailed(): self.error('job failed') @@ -503,20 +552,6 @@ except JobFailedError: self.error('job failed') - def _watch(self): - try: - job = self.builder._helper.getJob(self.jobId) - while not job.isFinished() and not job.isFailed(): - time.sleep(20 + self.offset) - job = self.builder._helper.getJob(self.jobId) - except xml.parsers.expat.ExpatError, e: - self.log('bad xml from server, retrying') - return False, None - except Exception, e: - self.log('unknown error querying server: %s' % e) - return False, None - return True, job - def _status(self, msg, type=0): msg = StatusMessage(self.name, self.trv, self.jobId, msg, type) self.status.put(msg) From johnsonm at rpath.com Wed Aug 19 17:39:00 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:39:00 +0000 Subject: mirrorball: Don't use class variables for instance data Message-ID: <200908192139.n7JLd0wd014194@scc.eng.rpath.com> changeset: b904c5adc227 user: Matt Wilson <https://issues.rpath.com/> date: Mon, 02 Jun 2008 15:31:49 -0400 Don't use class variables for instance data committer: Matt Wilson <https://issues.rpath.com/> diff --git a/repomd/packagexml.py b/repomd/packagexml.py --- a/repomd/packagexml.py +++ b/repomd/packagexml.py @@ -21,42 +21,20 @@ from rpath_common.xmllib import api1 as xmllib from repomd.errors import UnknownElementError, UnknownAttributeError +from repomd.xmlcommon import SlotNode -class _Package(xmllib.BaseNode): +class _Package(SlotNode): ''' Python representation of package section of xml files from the repository metadata. ''' - - # R0902 - Too many instance attributes - # pylint: disable-msg=R0902 - - name = None - arch = None - epoch = None - version = None - release = None - checksum = None - checksumType = None - summary = None - description = None - packager = None - url = None - fileTimestamp = None - buildTimestamp = None - packageSize = None - installedSize = None - archiveSize = None - location = None - format = None - license = None - vendor = None - group = None - buildhost = None - sourcerpm = None - headerStart = None - headerEnd = None - licenseToConfirm = None + __slots__ = ('name', 'arch', 'epoch', 'version', 'release', + 'checksum', 'checksumType', 'summary', 'description', + 'fileTimestamp', 'buildTimestamp', 'packageSize', + 'installedSize', 'archiveSize', 'location', 'format', + 'license', 'vendor', 'group', 'buildhost', + 'sourcerpm', 'headerStart', 'headerEnd', + 'licenseToConfirm') def addChild(self, child): ''' diff --git a/repomd/patchesxml.py b/repomd/patchesxml.py --- a/repomd/patchesxml.py +++ b/repomd/patchesxml.py @@ -22,7 +22,7 @@ from rpath_common.xmllib import api1 as xmllib from repomd.patchxml import PatchXml -from repomd.xmlcommon import XmlFileParser +from repomd.xmlcommon import XmlFileParser, SlotNode from repomd.errors import UnknownElementError class _Patches(xmllib.BaseNode): @@ -55,15 +55,11 @@ return self.getChildren('patch') -class _PatchElement(xmllib.BaseNode): +class _PatchElement(SlotNode): ''' Parser for patch element of patches.xml. ''' - - id = None - checksum = '' - checksumType = 'sha' - location = '' + __slots__ = ('id', 'checksum', 'checksumType', 'location') def addChild(self, child): ''' diff --git a/repomd/patchxml.py b/repomd/patchxml.py --- a/repomd/patchxml.py +++ b/repomd/patchxml.py @@ -10,30 +10,18 @@ from rpath_common.xmllib import api1 as xmllib -from repomd.xmlcommon import XmlFileParser +from repomd.xmlcommon import XmlFileParser, SlotNode from repomd.packagexml import PackageXmlMixIn from repomd.errors import UnknownElementError -class _Patch(xmllib.BaseNode): +class _Patch(SlotNode): ''' Python representation of patch-*.xml from the repository metadata. ''' - - # R0902 - Too many instance attributes - # pylint: disable-msg=R0902 - - name = None - summary = None - description = None - version = None - release = None - requires = None - recommends = None - rebootNeeded = False - licenseToConfirm = None - packageManager = False - category = None - packages = None + __slots__ = ('name', 'summary', 'description', 'version', + 'release', 'requires', 'recommends', 'rebootNeeded', + 'licenseToConfirm', 'packageManager', 'category', + 'packages') def addChild(self, child): ''' diff --git a/repomd/repomdxml.py b/repomd/repomdxml.py --- a/repomd/repomdxml.py +++ b/repomd/repomdxml.py @@ -23,7 +23,7 @@ from repomd.primaryxml import PrimaryXml from repomd.patchesxml import PatchesXml -from repomd.xmlcommon import XmlFileParser +from repomd.xmlcommon import XmlFileParser, SlotNode from repomd.errors import UnknownElementError class _RepoMd(xmllib.BaseNode): @@ -71,17 +71,12 @@ return None -class _RepoMdDataElement(xmllib.BaseNode): +class _RepoMdDataElement(SlotNode): ''' Parser for repomd.xml data elements. ''' - - location = '' - checksum = '' - checksumType = 'sha' - timestamp = '' - openChecksum = '' - openChecksumType = 'sha' + __slots__ = ('location', 'checksum', 'checksumType', 'timestamp', + 'openChecksum', 'openChecksumType') def addChild(self, child): ''' diff --git a/repomd/xmlcommon.py b/repomd/xmlcommon.py --- a/repomd/xmlcommon.py +++ b/repomd/xmlcommon.py @@ -62,3 +62,12 @@ child._parser._repository = self._repository return self._data + + +class SlotNode(xmllib.BaseNode): + __slots__ = () + + def __init__(self, *args, **kw): + for attr in self.__slots__: + setattr(self, attr, None) + xmllib.BaseNode.__init__(self, *args, **kw) From johnsonm at rpath.com Wed Aug 19 17:39:03 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:39:03 +0000 Subject: mirrorball: add conaryhelper module Message-ID: <200908192139.n7JLd3vl014343@scc.eng.rpath.com> changeset: 1cc5441d0d85 user: Elliot Peele <http://issues.rpath.com/> date: Mon, 02 Jun 2008 21:24:36 -0400 add conaryhelper module committer: Elliot Peele <http://issues.rpath.com/> diff --git a/updatebot/conaryhelper.py b/updatebot/conaryhelper.py new file mode 100644 --- /dev/null +++ b/updatebot/conaryhelper.py @@ -0,0 +1,142 @@ +# +# Copyright (c) 2008 rPath, Inc. +# +# This program is distributed under the terms of the Common Public License, +# version 1.0. A copy of this license should have been distributed with this +# source file in a file called LICENSE. If it is not present, the license +# is always available at http://www.rpath.com/permanent/licenses/CPL-1.0. +# +# This program is distributed in the hope that it will be useful, but +# without any warranty; without even the implied warranty of merchantability +# or fitness for a particular purpose. See the Common Public License for +# full details. +# + +""" +Module to wrap around conary api. Maybe this could be replaced by rbuild at +some point. +""" + +import logging + +from conary.deps import deps +from conary import conaryclient, conarycfg, trove + +from updatebot import util + +log = logging.getLogger('updatebot.conaryhelper') + +class ConaryHelper(object): + """ + Wrapper object for conary api. + """ + + def __init__(self, cfg): + self._cfg = cfg + + self._ccfg = conarycfg.ConaryConfiguration(readConfigFiles=False) + self._ccfg.read(util.join(self._cfg.configPath, 'conaryrc')) + self._ccfg.flavor = deps.parseFlavor('') + # FIXME: do we need to initialize flavors or not? + #self._ccfg.initializeFlavors() + + self._client = conaryclient.ConaryClient(self._ccfg) + self._repos = self._client.getRepos() + + def getSourceTroves(self, group=None): + """ + Find all of the source troves included in group. If group is None use + the top level group config option. + @param group: optional argument to specify the group to query + @type group: None or troveTuple (name, versionStr, flavorStr) + @return set of source trove specs + """ + + if not group: + group = self._cfg.topGroup + + trvlst = self._repos.findTrove(self._ccfg.buildLabel, group) + + srcTrvs = set() + for trv in self._findLatest(trvlst): + srcTrvs.update(self._getSourceTroves(trv)) + + return srcTrvs + + def _findLatest(self, trvlst): + """ + Given a list of trove specs, find the most recent versions. + @param trvlst: list of trove specs + @type trvlst: [(name, versionObj, flavorObj), ...] + @return [(name, versionObj, flavorObj), ...] + """ + + latest = [] + + trvlst.sort() + trvlst.reverse() + while len(trvlst) > 0: + trv = trvlst.pop(0) + if len(latest) == 0 or latest[-1][1] == trv[1]: + latest.append(trv) + else: + break + + return latest + + def _getSourceTroves(self, troveSpec): + """ + Iterate over the contents of a trv to find all of source troves + refrenced by that trove. + @param troveSpec: trove to walk. + @type troveSpec: (name, versionObj, flavorObj) + @return set([(trvSpec, trvSpec, ...]) + """ + + name, version, flavor = troveSpec + cl = [ (name, (None, None), (version, flavor), True) ] + cs = self._client.createChangeSet(cl, withFiles=False, + withFileContents=False, + skipNotByDefault=False) + + topTrove = self._getTrove(cs, name, version, flavor) + + srcTrvs = set() + for n, v, f in topTrove.iterTroveList(weakRefs=True): + trv = self._getTrove(cs, n, v, f) + srcTrvs.add((trv.getSourceName(), v.getSourceVersion(), None)) + + return srcTrvs + + def _getTrove(self, cs, name, version, flavor): + """ + Get a trove object for a given name, version, flavor from a changeset. + @param cs: conary changeset object + @type cs: changeset + @param name: name of a trove + @type name: string + @param version: conary version object + @type version: conary.versions.Version + @param flavor: conary flavor object + @type flavor: conary.deps.Flavor + @return conary.trove.Trove object + """ + + troveCs = cs.getNewTroveVersion(name, version, flavor) + trv = trove.Trove(troveCs, skipIntegrityChecks=True) + return trv + +if __name__ == '__main__': + import sys + from conary.lib import util as cnyutil + sys.excepthook = cnyutil.genExcepthook() + + import config + cfg = config.UpdateBotConfig() + cfg.topGroup = ('group-dist', 'sle.rpath.com at rpath:sle-devel', None) + cfg.configPath = '../' + + obj = ConaryHelper(cfg) + srcTrvs = obj.getSourceTrovesInTopLevel() + + import epdb ; epdb.st() From johnsonm at rpath.com Wed Aug 19 17:42:22 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:42:22 +0000 Subject: mirrorball: add extra error handling Message-ID: <200908192142.n7JLgMQj022353@scc.eng.rpath.com> changeset: 1108ffb45810 user: Elliot Peele <https://issues.rpath.com/> date: Sat, 14 Feb 2009 19:28:05 +0000 add extra error handling committer: Elliot Peele <https://issues.rpath.com/> diff --git a/updatebot/build.py b/updatebot/build.py --- a/updatebot/build.py +++ b/updatebot/build.py @@ -34,6 +34,32 @@ log = logging.getLogger('updateBot.build') +def jobInfoExceptionHandler(func): + def deco(self, *args, **kwargs): + retry = kwargs.pop('retry', True) + + exception = None + while retry: + try: + ret = func(self, *args, **kwargs) + return ret + except xml.parsers.expat.ExpatError, e: + exception = e + except Exception, e: + exception = e + + if type(retry) == int: + retry -= 1 + + # sleep between each retry + time.sleep(5) + + if exception is not None: + raise exception + + return deco + + class Builder(object): """ Class for wrapping the rMake api until we can switch to using rBuild. @@ -140,16 +166,13 @@ for trv in jobkeys: jobId = jobs[trv] - job = self._helper.getJob(jobId) - while not job.isFinished() and not job.isFailed(): - log.info('waiting for %s' % jobId) - time.sleep(1) - job = self._helper.getJob(jobId) + job = self._getJob(jobId) + self._wait(jobId) failed = set() results = {} for trv, jobId in jobs.iteritems(): - job = self._helper.getJob(jobId) + job = self._getJob(jobId) if job.isFailed(): failed.add((trv, jobId)) elif job.isFinished(): @@ -196,10 +219,7 @@ # Wait for the jobs to finish. log.info('Waiting for jobs to complete') for jobId in jobIds.itervalues(): - job = self._helper.getJob(jobId) - while not job.isFinished() and not job.isFailed(): - time.sleep(1) - job = self._helper.getJob(jobId) + self._wait(jobId) # Sanity check all jobs. for jobId in jobIds.itervalues(): @@ -274,6 +294,22 @@ return troves + @jobInfoExceptionHandler + def _getJob(self, jobId, retry=None): + """ + Get a job instance from the rMake helper, catching several common + exceptions. + @param jobId: id of an rMake job + @type jobId: integer + @param retry: information about retrying the get job, if retry is None + then retry forever, if retry is an integer retry n times. + @type retry: None + @type retry: integet + @return rmake job instance + """ + + return self._helper.getJob(jobId) + def _startJob(self, troveSpecs): """ Create and start a rMake build. @@ -290,6 +326,20 @@ return jobId + def _wait(self, jobId): + """ + Wait for a job to complete. + @param jobId: rMake job ID + @type jobId: integer + """ + + log.info('waiting for job [%s] to complete' % jobId) + job = self._getJob(jobId) + while not job.isFinished() and not job.isFailed(): + time.sleep(5) + job = self._getJob(jobId) + + @jobInfoExceptionHandler def _monitorJob(self, jobId): """ Monitor job status, block until complete. @@ -309,7 +359,7 @@ """ # Check for errors - job = self._helper.getJob(jobId) + job = self._getJob(jobId) if job.isFailed(): log.error('Job %d failed', jobId) raise JobFailedError(jobId=jobId, why=job.status) @@ -337,7 +387,7 @@ # Do the commit startTime = time.time() - jobs = [ self._helper.getJob(x) for x in jobIds ] + jobs = [ self._getJob(x) for x in jobIds ] log.info('Starting commit of job %s', jobIdsStr) self._helper.client.startCommit(jobIds) @@ -488,10 +538,9 @@ def _doBuild(self): self.jobId = self.builder.start([self.trv, ]) - done, job = self._watch() - while not done: - done, job = self._watch() + self.builder._wait(self.jobId) + job = self.builder._getJob(self.jobId) if job.isFailed(): self.error('job failed') @@ -502,18 +551,6 @@ except JobFailedError: self.error('job failed') - def _watch(self): - try: - job = self.builder._helper.getJob(self.jobId) - while not job.isFinished() and not job.isFailed(): - time.sleep(20 + self.offset) - job = self.builder._helper.getJob(self.jobId) - except xml.parsers.expat.ExpatError, e: - return False, None - except Exception, e: - return False, None - return True, job - def _status(self, msg, type=0): msg = StatusMessage(self.name, self.trv, self.jobId, msg, type) self.status.put(msg) From johnsonm at rpath.com Wed Aug 19 17:42:05 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:42:05 +0000 Subject: mirrorball: add satis and jira client Message-ID: <200908192142.n7JLg56Y021673@scc.eng.rpath.com> changeset: 7e06d5ea66f7 user: Elliot Peele <https://issues.rpath.com/> date: Fri, 16 Jan 2009 23:26:51 -0500 add satis and jira client committer: Elliot Peele <https://issues.rpath.com/> diff --git a/updatebot/lib/jira.py b/updatebot/lib/jira.py new file mode 100644 --- /dev/null +++ b/updatebot/lib/jira.py @@ -0,0 +1,36 @@ +# +# Copyright (c) 2009 rPath, Inc. +# +# This program is distributed under the terms of the Common Public License, +# version 1.0. A copy of this license should have been distributed with this +# source file in a file called LICENSE. If it is not present, the license +# is always available at http://www.rpath.com/permanent/licenses/CPL-1.0. +# +# This program is distributed in the hope that it will be useful, but +# without any warranty; without even the implied warranty of merchantability +# or fitness for a particular purpose. See the Common Public License for +# full details. +# + +import pyjira + +class JiraClient(pyjira.JiraClient): + """ + Basic SOAP based Jira client. + """ + + def __init__(self, cfg): + self._cfg = cfg + self._wsdl = '%s/rpc/soap/jirasoapservice-v2?wsdl' % cfg.jiraUrl + self._login = (cfg.jiraUser, cfg.jiraPassword) + + pyjira.JiraClient.__init__(self, self._wsdl, self._login) + + def addComment(self, issueKey, body): + """ + Add a comment to an issue, using the security level specified in the + config object. + """ + + pyjira.JiraClient.addComment(self, issueKey, body, + level=cfg.jiraSecurityGroup) diff --git a/updatebot/lib/satisclient.py b/updatebot/lib/satisclient.py new file mode 100644 --- /dev/null +++ b/updatebot/lib/satisclient.py @@ -0,0 +1,107 @@ +# +# Copyright (c) 2009 rPath, Inc. +# +# This program is distributed under the terms of the Common Public License, +# version 1.0. A copy of this license should have been distributed with this +# source file in a file called LICENSE. If it is not present, the license +# is always available at http://www.rpath.com/permanent/licenses/CPL-1.0. +# +# This program is distributed in the hope that it will be useful, but +# without any warranty; without even the implied warranty of merchantability +# or fitness for a particular purpose. See the Common Public License for +# full details. +# + +""" +Module for interacting with satis. +""" + +from satis import types +from satis import util as satisutil +from satis.client.http import HTTPClient +from satis.types import Or, And, Equals, NewerThan + +class SatisClient(object): + """ + Base class for interacting with satis. + """ + + def __init__(self, url): + self._url = url + self._client = HTTPClient(self._url) + + +class AutoBuild(SatisClient): + """ + Class for managing autobuild related metadata in satis. + """ + + def __init__(self, url, productName): + SatisClient.__init__(self, url) + self._productName = productName + self._type = 'com.rpath.autobuild.checkdata' + + def setUUID(self, label, uuid): + """ + Set autobuild checkdata. + """ + + symb = types.Symbol() + symb['type'] = self._type + symb['autobuild-symbol-version'] = 0 + symb['autobuild-product-name'] = self._productName + symb['autobuild-label'] = label.asString() + symb['autobuild-check-uuid'] = uuid + self._client.add(symb) + + def getUUID(self, label): + """ + Get the last checkdata for a given label. + """ + + matcher = And(Equals('type', self._type), + Equals('autobuild-label', label.asString()), + Equals('autobuild-product-name', self._productName)) + fltr = types.Filter(matcher, order=types.ORDER_DESC, limit=1) + sub = types.Subscription(fltr) + req = self._client.subscribe(sub) + + if req: + return req[0].symbol['autobuild-check-uuid'] + return satisutil.makeUUID() + + +class ConaryCommits(SatisClient): + """ + Interface for searching satis for commits to a given label. + """ + + def __init__(self, url, productName): + SatisClient.__init__(self, url) + self._rules = ( Equals('type', 'com.rpath.commit.conary.trove'), + Equals('trove-type', 'source') ) + self._autobuild = AutoBuild(self._url, productName) + + def search(self, label, getMarked=False): + """ + Query satis for commits to the given label. + @param label: conary label + @type label: conary.versions.Label + @param getMarked: to return all commits or just the commits since the + last check. + @type getMarked: boolean + """ + + uuid = self._autobuild.getUUID(label) + + rules = list(self._rules) + rules.append(Equals('trove-label', label.asString())) + matcher = And(*rules) + + fltr = types.Filter(matcher, order=types.ORDER_ASC, token=uuid, + getMarked=getMarked) + sub = types.Subscription(fltr, token=uuid, markOnSend=True) + req = self._client.subscribe(sub) + + self._autobuild.setUUID(label, uuid) + return req From johnsonm at rpath.com Wed Aug 19 17:40:05 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:40:05 +0000 Subject: mirrorball: branch merge Message-ID: <200908192140.n7JLe53t016830@scc.eng.rpath.com> changeset: 3e6a4a173ad0 user: Elliot Peele <http://issues.rpath.com/> date: Tue, 29 Jul 2008 22:12:20 -0400 branch merge committer: Elliot Peele <http://issues.rpath.com/> diff --git a/scripts/report.py b/scripts/report.py new file mode 100755 --- /dev/null +++ b/scripts/report.py @@ -0,0 +1,111 @@ +#!/usr/bin/python +# +# Copyright (c) 2008 rPath, Inc. +# +# This program is distributed under the terms of the Common Public License, +# version 1.0. A copy of this license should have been distributed with this +# source file in a file called LICENSE. If it is not present, the license +# is always available at http://www.rpath.com/permanent/licenses/CPL-1.0. +# +# This program is distributed in the hope that it will be useful, but +# without any warranty; without even the implied warranty of merchantability +# or fitness for a particular purpose. See the Common Public License for +# full details. +# + +import os +import sys +import operator + +sys.path.insert(0, os.environ['HOME'] + '/hg/rpath-xmllib') +sys.path.insert(0, os.environ['HOME'] + '/hg/conary') +sys.path.insert(0, os.environ['HOME'] + '/hg/mirrorball') + +from conary.lib import util +from conary.lib.sha1helper import md5String, md5ToString +from conary import rpmhelper, files, updatecmd +sys.excepthook = util.genExcepthook() + +from updatebot import bot, conaryhelper, config, log +from rpmutils import readHeader + +outfile = open(sys.argv[1], 'w') +cb = updatecmd.UpdateCallback() + +#import cProfile +#prof = cProfile.Profile() +#prof.enable() + +log.addRootLogger() +cfg = config.UpdateBotConfig() +cfg.read(os.environ['HOME'] + '/hg/mirrorball/config/sles/updatebotrc') +obj = bot.Bot(cfg) + +#prof.disable() +#prof.dump_stats('report.lsprof') + +conaryhelper = obj._updater._conaryhelper +conaryclient = conaryhelper._client + +pkgMap = conaryhelper.getSourceTroves(cfg.topGroup) +sources = sorted(pkgMap.keys()) + +for src in sources: + srcName = src[0].split(':')[0] + if srcName in cfg.excludePackages: + continue + if srcName.startswith('group-') or srcName.startswith('info-'): + continue + manifest = conaryhelper.getManifest(srcName) + + pathMap = {} + # collect up a path -> rpm package map + for rpm in manifest: + if 'src.rpm' in rpm: + continue + rpmloc = '%s/%s' %(cfg.repositoryUrl, rpm) + h = readHeader(rpmloc) + arch = h[rpmhelper.ARCH] + rpmname = os.path.basename(rpm) + d = dict.fromkeys(((util.normpath(x), arch) for x in h.paths()), rpmname) + pathMap.update(d) + binaries = sorted(pkgMap[src]) + flist = [] + for binary in binaries: + name, version, flavor = binary + flavorStr = str(flavor) + if ':' not in name: + continue + if 'debuginfo' in name: + continue + if 'is: x86_64' in str(flavorStr): + arch = 'x86_64' + elif 'is: x86(' in str(flavorStr): + arch = 'i586' + else: + arch = 'noarch' + print name, version, flavor + cs = conaryclient.createChangeSet([(name, + (None, None), + (version, flavor), True)], + withFiles=True, + withFileContents=True, + callback=cb) + tcs = [x for x in cs.iterNewTroveList()][0] + for (pathId, path, fileId, version) in sorted(tcs.newFiles): + f = files.ThawFile(cs.getFileChange(None, fileId), pathId) + if hasattr(f, 'contents') and f.contents: + cont = cs.getFileContents(pathId, fileId)[1] + md5 = md5ToString(md5String(cont.get().read())) + try: + rpm = pathMap[(path, arch)] + except KeyError: + # some file we added, like a tag handler + continue + flist.append((binary[0], rpm, path, md5)) + # sort based on path + flist.sort(key=operator.itemgetter(2)) + for info in flist: + outfile.write('\t'.join(info)) + outfile.write('\n') + outfile.flush() diff --git a/updatebot/conaryhelper.py b/updatebot/conaryhelper.py --- a/updatebot/conaryhelper.py +++ b/updatebot/conaryhelper.py @@ -84,17 +84,18 @@ if len(latest) != 2: raise TooManyFlavorsFoundError(why=latest) - srcTrvs = {} + d = {} for trv in latest: log.info('querying %s for source troves' % (trv, )) srcTrvs = self._getSourceTroves(trv) for src, binLst in srcTrvs.iteritems(): - if src in srcTrvs: - srcTrvs[src].extend(binLst) + s = set(binLst) + if src in d: + d[src].update(s) else: - srcTrvs[src] = binLst + d[src] = s - return srcTrvs + return d @staticmethod def _findLatest(trvlst): @@ -149,8 +150,8 @@ strongRefs=True)): src = (sources[i](), v.getSourceVersion(), None) if src not in srcTrvs: - srcTrvs[src] = [] - srcTrvs[src].append((n, v, f)) + srcTrvs[src] = set() + srcTrvs[src].add((n, v, f)) return srcTrvs From johnsonm at rpath.com Wed Aug 19 17:39:01 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:39:01 +0000 Subject: mirrorball: 1) remove account creation relics Message-ID: <200908192139.n7JLd1Th014248@scc.eng.rpath.com> changeset: 658c2ae98659 user: Matt Wilson <https://issues.rpath.com/> date: Mon, 02 Jun 2008 15:44:01 -0400 1) remove account creation relics 2) add SLES 10 SP2 repositories to default set 3) add logic to determine if a package needs to be updated 4) adjust command line syntax since there is only one operation mode now committer: Matt Wilson <https://issues.rpath.com/> diff --git a/pkgwiz.py b/pkgwiz.py --- a/pkgwiz.py +++ b/pkgwiz.py @@ -17,36 +17,29 @@ from conary import cvc import conary.lib.util import os +import tempfile from rpmimport import infomaker, recipemaker, rpmsource, rpmhelper +import rpmvercmp import shutil import sys DEFAULT_URL = 'http://install.rdu.rpath.com/sle/' DEFAULT_BASE_PATHS = [ - 'SLES10-SP1/sles-10-i586', - 'SLES10-SP1/sles-10-x86_64', - 'SLE10-Debuginfo-Updates/sles-10-i586', - 'SLE10-Debuginfo-Updates/sles-10-x86_64', - 'SLES10-SP1-Online/sles-10-i586', - 'SLES10-SP1-Online/sles-10-x86_64', - 'SLES10-Updates/sles-10-i586', - 'SLES10-Updates/sles-10-x86_64', + 'SLES10-SP2/sles-10-i586', + 'SLES10-SP2/sles-10-x86_64', + 'SLES10-SP2-Online/sles-10-i586', + 'SLES10-SP2-Online/sles-10-x86_64', + 'SLES10-SP2-Updates/sles-10-i586', + 'SLES10-SP2-Updates/sles-10-x86_64', ] HELP_TEXT = """ -pkgs <dir>|<pkg> [<dir>|<pkg> ...] -accounts <user>|<group> [<user>|<group> ...] - -pkgs can be given individual RPMs or the root of a directory tree to walk. +[<base url>] [[repo path 1] [repo path 2] ... [repo path N]] When packages are imported, they will be checked against what's in the repository. If newer, then check new one in. - -accounts will create user and/or group info- packages, using -information from the build system. """ -sles10sp1prefix = '/srv/www/html/sle/' -sles10sp1pkgs = ( +sles10pkgs = ( 'aaa_base', 'acl', 'apache2', @@ -254,70 +247,60 @@ self._setupRepo() for basePath in basePaths: self.rpmSource.load(url, basePath) - # {foo:source: {cfg.buildLabel: None}} srccomps = {} # {foo:source: foo-1.0-1.1.src.rpm} srcmap = {} - for src in set(self.rpmSource.getSrpms(sles10sp1pkgs)): + for src in set(self.rpmSource.getSrpms(sles10pkgs)): name = self.rpmSource.srcPath[src].name srccomp = name + ':source' srcmap[srccomp] = src srccomps[srccomp] = {self.cfg.buildLabel: None} - d = self.repos.getTroveVersionsByLabel(srccomps) + d = self.repos.getTroveLeavesByLabel(srccomps) # Iterate over foo:source. - for srccomp in set(srccomps.iterkeys()): + for srccomp in sorted(list(set(srccomps.iterkeys()))): srpm = srcmap[srccomp] pkgname = srccomp.split(':')[0] if srccomp not in d: - self.recipeMaker.createManifest(pkgname, srpm, sles10sp1prefix) + self.recipeMaker.createManifest(pkgname, srpm) else: - continue - self.recipeMaker.updateManifest(pkgname, srpm, sles10sp1prefix) - - def createUsers(self, users): - self._setupRepo() - # Get all users and groups used in this run. - users = set() - groups = set() - for src in self.rpmSource.getSrpms(slessp1pkgs): - for rpm in self.rpmSource.rpmMap[src].values(): - header = rpmhelper.readHeader(rpm.location) - users = users.union(header[FILEUSERNAME]) - groups = groups.union(header[FILEGROUPNAME]) - - infoMaker = infomaker.InfoMaker(cfg, repos, self.recipeMaker) - infoMaker.makeInfo(users, groups) + # aaa_base-10-12.46.src.rpm -> 10_12.46 + ver = '_'.join(srpm.rsplit('.', 2)[0].rsplit('-', 2)[1:3]) + curVer = str(d[srccomp].keys()[0].trailingRevision().version) + if rpmvercmp.rpmvercmp(ver, curVer) == 1: + self.recipeMaker.updateManifest(pkgname, srpm) def main(self, argv): if '--debug' in argv: argv.remove('--debug') sys.excepthook = conary.lib.util.genExcepthook(debug=True) - if len(argv) < 2: + if '--help' in argv: self.help() - return - category = argv[1] - if 'pkgs' == category: - url = None - basePaths = None - if len(argv) >= 3: - url = argv[2] - if len(argv) >= 4: - basePaths = argv[3:] - if not url: - url = DEFAULT_URL - if not basePaths: - basePaths = DEFAULT_BASE_PATHS + return 0 + + url = None + basePaths = None + if len(argv) >= 2: + url = argv[1] + if len(argv) >= 3: + basePaths = argv[2:] + if not url: + url = DEFAULT_URL + if not basePaths: + basePaths = DEFAULT_BASE_PATHS + cwd = os.getcwd() + tmpdir = tempfile.mkdtemp() + print 'workdir is', tmpdir + os.chdir(tmpdir) + try: self.createPkgs(url, basePaths) - elif 'accounts' == category: - users = argv[2:] - self.createUsers(users) - else: - self.help() - return + finally: + os.chdir(cwd) + + return 0 if __name__ == '__main__': pkgWiz = PkgWiz() From johnsonm at rpath.com Wed Aug 19 17:42:13 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:42:13 +0000 Subject: mirrorball: script changes Message-ID: <200908192142.n7JLgDTP021962@scc.eng.rpath.com> changeset: 3b7369400560 user: Elliot Peele <https://issues.rpath.com/> date: Thu, 05 Feb 2009 18:14:38 -0500 script changes committer: Elliot Peele <https://issues.rpath.com/> diff --git a/scripts/findbinaries.py b/scripts/findbinaries.py --- a/scripts/findbinaries.py +++ b/scripts/findbinaries.py @@ -39,7 +39,7 @@ slog = logging.getLogger('findbinaries') cfg = config.UpdateBotConfig() -cfg.read(os.environ['HOME'] + '/hg/26/mirrorball/config/ubuntu/updatebotrc') +cfg.read(os.environ['HOME'] + '/hg/26/mirrorball/config/sles/updatebotrc') bot = bot.Bot(cfg) updater = bot._updater diff --git a/scripts/import.py b/scripts/import.py --- a/scripts/import.py +++ b/scripts/import.py @@ -21,6 +21,7 @@ sys.path.insert(0, os.environ['HOME'] + '/hg/26/mirrorball') sys.path.insert(0, os.environ['HOME'] + '/hg/26/rmake') sys.path.insert(0, os.environ['HOME'] + '/hg/26/epdb') +sys.path.insert(0, os.environ['HOME'] + '/hg/26/xobj/py') from conary.lib import util sys.excepthook = util.genExcepthook() @@ -29,7 +30,7 @@ log.addRootLogger() cfg = config.UpdateBotConfig() -cfg.read(os.environ['HOME'] + '/hg/26/mirrorball/config/sles/updatebotrc') +cfg.read(os.environ['HOME'] + '/hg/mirrorball/config/sles/updatebotrc') obj = bot.Bot(cfg) trvMap = obj.create() diff --git a/scripts/mirror.py b/scripts/mirror.py --- a/scripts/mirror.py +++ b/scripts/mirror.py @@ -1,4 +1,4 @@ -#!/usr/bin/python +#!/usr/bin/python2.6 # # Copyright (c) 2008 rPath, Inc. # diff --git a/scripts/pkgsource.py b/scripts/pkgsource.py --- a/scripts/pkgsource.py +++ b/scripts/pkgsource.py @@ -24,7 +24,7 @@ from updatebot import pkgsource cfg = config.UpdateBotConfig() -cfg.read(os.environ['HOME'] + '/hg/mirrorball/config/sles/updatebotrc') +cfg.read(os.environ['HOME'] + '/hg/26/mirrorball/config/sles/updatebotrc') pkgSource = pkgsource.PackageSource(cfg) pkgSource.load() diff --git a/scripts/rebuild.py b/scripts/rebuild.py new file mode 100755 --- /dev/null +++ b/scripts/rebuild.py @@ -0,0 +1,41 @@ +#!/usr/bin/python2.6 +# +# Copyright (c) 2008 rPath, Inc. +# +# This program is distributed under the terms of the Common Public License, +# version 1.0. A copy of this license should have been distributed with this +# source file in a file called LICENSE. If it is not present, the license +# is always available at http://www.rpath.com/permanent/licenses/CPL-1.0. +# +# This program is distributed in the hope that it will be useful, but +# without any warranty; without even the implied warranty of merchantability +# or fitness for a particular purpose. See the Common Public License for +# full details. +# + +import os +import sys + +sys.path.insert(0, os.environ['HOME'] + '/hg/26/rpath-xmllib') +sys.path.insert(0, os.environ['HOME'] + '/hg/26/conary') +sys.path.insert(0, os.environ['HOME'] + '/hg/26/mirrorball') +sys.path.insert(0, os.environ['HOME'] + '/hg/26/rmake') +sys.path.insert(0, os.environ['HOME'] + '/hg/26/epdb') +sys.path.insert(0, os.environ['HOME'] + '/hg/26/xobj/py') + +from conary.lib import util +sys.excepthook = util.genExcepthook() + +from updatebot import bot, config, log + +log.addRootLogger() +cfg = config.UpdateBotConfig() +cfg.read(os.environ['HOME'] + '/hg/26/mirrorball/config/%s/updatebotrc' % sys.argv[1]) +obj = bot.Bot(cfg) +trvMap = obj.create(rebuild=True) + +for source in trvMap: + for bin in trvMap[source]: + print '%s=%s[%s]' % bin + +import epdb ; epdb.st() diff --git a/scripts/update.py b/scripts/update.py --- a/scripts/update.py +++ b/scripts/update.py @@ -19,6 +19,6 @@ from updatebot import bot, config cfg = config.UpdateBotConfig() -cfg.read(os.environ['HOME'] + '/hg/26/mirrorball/config/sles/updatebotrc') +cfg.read(os.environ['HOME'] + '/hg/26/mirrorball/config/ubuntu/updatebotrc') obj = bot.Bot(cfg) obj.update() diff --git a/scripts/validatemanifests.py b/scripts/validatemanifests.py --- a/scripts/validatemanifests.py +++ b/scripts/validatemanifests.py @@ -73,27 +73,28 @@ changed[pkg] = map[key] -#import epdb; epdb.st() +#toBuild = set() +#for pkg in changed: +# srcPkg = changed[pkg] +# manifest = b._updater._getManifestFromPkgSource(srcPkg) +# helper.setManifest(pkg, manifest) +# metadata = b._updater._getMetadataFromPkgSource(srcPkg) +# helper.setMetadata(pkg, metadata) + +# new = helper.getMetadata(pkg) +# assert metadata == new + +# newManifest = helper.getManifest(pkg) +# assert manifest == newManifest + +# helper.commit(pkg, commitMessage=cfg.commitMessage) +# toBuild.add((pkg, cfg.topSourceGroup[1], None)) + toBuild = set() -for pkg in changed: - srcPkg = changed[pkg] - manifest = b._updater._getManifestFromPkgSource(srcPkg) - helper.setManifest(pkg, manifest) - metadata = b._updater._getMetadataFromPkgSource(srcPkg) - helper.setMetadata(pkg, metadata) - - new = helper.getMetadata(pkg) - assert metadata == new - - newManifest = helper.getManifest(pkg) - assert manifest == newManifest - - helper.commit(pkg, commitMessage=cfg.commitMessage) +for pkg in changed.iterkeys(): toBuild.add((pkg, cfg.topSourceGroup[1], None)) -import epdb; epdb.st() - -trvMap = b._builder.buildmany(toBuild) +trvMap = b._builder.buildsplitarch(toBuild) def displayTrove(nvf): flavor = '' From johnsonm at rpath.com Wed Aug 19 17:39:42 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:39:42 +0000 Subject: mirrorball: add ability to add more frumptuus Message-ID: <200908192139.n7JLdgw3015892@scc.eng.rpath.com> changeset: 4f46a015b0f4 user: Elliot Peele <http://issues.rpath.com/> date: Wed, 25 Jun 2008 19:21:58 -0400 add ability to add more frumptuus committer: Elliot Peele <http://issues.rpath.com/> diff --git a/updatebot/conaryhelper.py b/updatebot/conaryhelper.py --- a/updatebot/conaryhelper.py +++ b/updatebot/conaryhelper.py @@ -18,6 +18,7 @@ """ import os +import time import logging import tempfile @@ -38,10 +39,8 @@ """ def __init__(self, cfg): - self._cfg = cfg - self._ccfg = conarycfg.ConaryConfiguration(readConfigFiles=False) - self._ccfg.read(util.join(self._cfg.configPath, 'conaryrc')) + self._ccfg.read(util.join(cfg.configPath, 'conaryrc')) # Have to initialize flavors to commit to the repository. self._ccfg.initializeFlavors() @@ -214,8 +213,10 @@ self._commit(recipeDir, commitMessage) util.rmtree(recipeDir) - # Get new version of source trove. - return self._getVersionsByName('%s:source' % pkgname) + # Get new version of the source trove. + versions = self._getVersionsByName('%s:source' % pkgname) + assert len(versions) == 1 + return versions[0] def _checkout(self, pkgname): """ @@ -288,24 +289,39 @@ versions = verMap.keys() return versions - def promote(self, trvLst, expected, targetLabel): + def promote(self, trvLst, expected, sourceLabels, targetLabel): """ Promote a group and its contents to a target label. @param trvLst: list of troves to publish @type trvLst: [(name, version, flavor), ... ] @param expected: list of troves that are expected to be published. @type expected: [(name, version, flavor), ...] + @param sourceLabels: list of labels that should be flattened onto the + targetLabel. + @type sourceLabels: [labelObject, ... ] @param targetLabel: table to publish to @type targetLabel: conary Label object """ - # Assume that all troves are on the same label. + start = time.time() + log.info('starting promote') + log.info('creating changeset') + + # Get the label that the group is on. fromLabel = trvLst[0][1].trailingLabel() + + # Build the label map. + labelMap = {fromLabel: targetLabel} + for label in sourceLabels: + labelMap[label] = targetLabel + success, cs = self._client.createSiblingCloneChangeSet( - {fromLabel:targetLabel}, + labelMap, trvLst, cloneSources=True) + log.info('changeset created in %s' % (time.time() - start, )) + if not success: raise PromoteFailedError(what=trvLst) @@ -315,10 +331,19 @@ oldPkgs = set([ (x[0], x[2]) for x in expected ]) newPkgs = set([ (x[0], x[2]) for x in packageList ]) - difference = oldPkgs.difference(newPkgs) + # Make sure that all packages being promoted are in the set of packages + # that we think should be available to promote. Note that all packages + # in expected will not be promoted because not all packages are + # included in the groups. + difference = newPkgs.difference(oldPkgs) if difference != set(): raise PromoteMismatchError(expected=oldPkgs, actual=newPkgs) + log.info('committing changeset') + self._repos.commitChangeSet(cs) + log.info('changeset committed') + log.info('promote complete, elapsed time %s' % (time.time() - start, )) + return packageList diff --git a/updatebot/config.py b/updatebot/config.py --- a/updatebot/config.py +++ b/updatebot/config.py @@ -51,6 +51,10 @@ # The top level source group. topSourceGroup = CfgTroveSpec + # Other labels that are referenced in the group that need to be flattend + # onto the targetLabel. + sourceLabel = (CfgList(CfgLabel), []) + # Label to promote to targetLabel = CfgLabel diff --git a/updatebot/update.py b/updatebot/update.py --- a/updatebot/update.py +++ b/updatebot/update.py @@ -197,11 +197,13 @@ @type nvf: tuple(name, versionObj, flavorObj) @param srcPkg: package object for source rpm @type srcPkg: repomd.packagexml._Package + @return version of the updated source trove """ manifest = self._getManifestFromRpmSource(srcPkg) - self._conaryhelper.setManifest(nvf[0], manifest, - commitMessage=self._cfg.commitMessage) + newVersion = self._conaryhelper.setManifest(nvf[0], manifest, + commitMessage=self._cfg.commitMessage) + return newVersion def _getManifestFromRpmSource(self, srcPkg): """ @@ -225,4 +227,5 @@ @type targetLabel: conary Label object """ - return self._conaryhelper.promote(trvLst, expected, targetLabel) + return self._conaryhelper.promote(trvLst, expected, + self._cfg.sourceLabel, targetLabel) From johnsonm at rpath.com Wed Aug 19 17:41:20 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:41:20 +0000 Subject: mirrorball: refactor package metadata loading into pkgsource backend Message-ID: <200908192141.n7JLfKMx019881@scc.eng.rpath.com> changeset: dd2fb82a2c6e user: Elliot Peele <https://issues.rpath.com/> date: Fri, 14 Nov 2008 16:31:01 -0500 refactor package metadata loading into pkgsource backend committer: Elliot Peele <https://issues.rpath.com/> diff --git a/updatebot/bot.py b/updatebot/bot.py --- a/updatebot/bot.py +++ b/updatebot/bot.py @@ -38,7 +38,6 @@ def __init__(self, cfg): self._cfg = cfg - self._pkgSourcePopulated = False self._patchSourcePopulated = False self._clients = {} @@ -49,29 +48,6 @@ self._patchSource) self._builder = build.Builder(self._cfg) - def _populatePkgSource(self): - """ - Populate the rpm source data structures. - """ - - if self._pkgSourcePopulated: - return - - if self._cfg.repositoryFormat == 'apt': - client = aptmd.Client(self._cfg.repositoryUrl) - - for repo in self._cfg.repositoryPaths: - log.info('loading repository data %s' % repo) - - if self._cfg.repositoryFormat == 'yum': - client = repomd.Client(self._cfg.repositoryUrl + '/' + repo) - - self._pkgSource.loadFromClient(client, repo) - self._clients[repo] = client - - self._pkgSource.finalize() - self._pkgSourcePopulated = True - def _populatePatchSource(self): """ Populate the patch source data structures. @@ -109,7 +85,7 @@ log.info('starting import') # Populate rpm source object from yum metadata. - self._populatePkgSource() + self.pkgSource.load() # Import sources into repository. toBuild, fail = self._updater.create(self._cfg.package, buildAll=False) @@ -168,7 +144,7 @@ log.info('starting update') # Populate rpm source object from yum metadata. - self._populatePkgSource() + self.pkgSource.load() # Get troves to update and send advisories. toAdvise, toUpdate = self._updater.getUpdates() @@ -184,6 +160,8 @@ # Check to see if advisories exist for all required packages. self._advisor.check(toAdvise) + import epdb; epdb.st() + # Update source for nvf, srcPkg in toUpdate: toAdvise.remove((nvf, srcPkg)) diff --git a/updatebot/pkgsource/debsource.py b/updatebot/pkgsource/debsource.py --- a/updatebot/pkgsource/debsource.py +++ b/updatebot/pkgsource/debsource.py @@ -25,6 +25,7 @@ self._binPkgs = set() self._srcPkgs = set() + self._clients = dict() self.srcPkgMap = dict() self.binPkgMap = dict() @@ -32,6 +33,29 @@ self.binNameMap = dict() self.locationMap = dict() + def getClients(self): + """ + Get repository client instances. + """ + + if not self._clients: + self.load() + + return self._clients + + def load(self): + """ + Load repository metadata from a config object. + """ + + client = repomd.Client(self._cfg.repositoryUrl) + for repo in self._cfg.repositoryPaths: + log.info('loading repository data %s' % repo) + self._pkgSource.loadFromClient(client, repo) + self._clients[repo] = client + + self.finalize() + def loadFromClient(self, client, path): for pkg in client.parse(path): if pkg.arch in self._excludeArch: diff --git a/updatebot/pkgsource/rpmsource.py b/updatebot/pkgsource/rpmsource.py --- a/updatebot/pkgsource/rpmsource.py +++ b/updatebot/pkgsource/rpmsource.py @@ -41,6 +41,9 @@ # set of all src pkg objects self._srcPkgs = set() + # {repoShortUrl: clientObj} + self._clients = dict() + # {location: srpm} self.locationMap = dict() @@ -56,7 +59,30 @@ # {binName: [binPkg, ... ] } self.binNameMap = dict() - def load(self, url, basePath=''): + def getClients(self): + """ + Get instances of repository clients. + """ + + if not self._clients: + self.load() + + return self._clients + + def load(self): + """ + Load package source based on config data. + """ + + for repo in self._cfg.repositoryPaths: + log.info('loading repository data %s' % repo) + client = repomd.Client(self._cfg.repositoryUrl + '/' + repo) + self._pkgSource.loadFromClient(client, repo) + self._clients[repo] = client + + self.finalize() + + def loadFromUrl(self, url, basePath=''): """ Walk the yum repository rooted at url/basePath and collect information about rpms found. From johnsonm at rpath.com Wed Aug 19 17:42:31 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:42:31 +0000 Subject: mirrorball: add several more experimental builders Message-ID: <200908192142.n7JLgVpH022728@scc.eng.rpath.com> changeset: 80ddd7d0bfc5 user: Elliot Peele <https://issues.rpath.com/> date: Mon, 16 Mar 2009 11:23:31 -0400 add several more experimental builders committer: Elliot Peele <https://issues.rpath.com/> diff --git a/updatebot/build.py b/updatebot/build.py --- a/updatebot/build.py +++ b/updatebot/build.py @@ -36,7 +36,7 @@ def jobInfoExceptionHandler(func): def deco(self, *args, **kwargs): - retry = kwargs.pop('retry', True) + retry = kwargs.pop('retry', 100) exception = None while retry: @@ -140,7 +140,7 @@ jobs[index].append(trv) - if i % 50 == 0: + if i % 20 == 0: index += 1 failed = set() @@ -187,7 +187,15 @@ return results, failed def buildmany2(self, troveSpecs): - dispatcher = Dispatcher(self._cfg, 20) + dispatcher = Dispatcher(self._cfg, 100) + return dispatcher.buildmany(troveSpecs) + + def buildmany3(self, troveSpecs): + dispatcher = Dispatcher2(self._cfg, 50) + return dispatcher.buildmany(troveSpecs) + + def buildmany4(self, troveSpecs): + dispatcher = Dispatcher3(self._cfg, 50) return dispatcher.buildmany(troveSpecs) def buildsplitarch(self, troveSpecs): @@ -644,3 +652,133 @@ elif msg.type == MESSAGE_TYPES['results']: results.add(msg.message) return results, errors + + +class Dispatcher2(object): + builderClass = Builder + + def __init__(self, cfg, workerCount): + self._cfg = cfg + self._workerCount = workerCount + + self._builder = self.builderClass(self._cfg) + + self._activeJobs = [] + self._commitJobs = [] + self._failedJobs = set() + self._completedJobs = [] + + self._results = {} + + def buildmany(self, troveSpecs): + troveSpecs = list(troveSpecs) + + while len(troveSpecs): + # Wait for some amount of jobs to complete. + while len(self._activeJobs) >= self._workerCount: + time.sleep(5) + self._checkStatus() + + # Populate build queue. + while len(self._activeJobs) < self._workerCount: + trvSpec = troveSpecs.pop() + self._start(trvSpec) + + # Commit completed jobs. + self._commit() + + return self._results, self._failedJobs + + @jobInfoExceptionHandler + def _start(self, troveSpec): + jobId = self._builder.start([troveSpec, ]) + self._activeJobs.append((troveSpec, jobId)) + + def _checkStatus(self): + for troveSpec, jobId in self._activeJobs: + log.info('Checking status of %s' % jobId) + job = self._builder._getJob(jobId, retry=10) + if job is None: + log.warn('Failed to retrieve job information for %s' % jobId) + import epdb; epdb.st() + elif job.isFailed(): + self._failedJobs.add((troveSpec, jobId)) + self._activeJobs.remove((troveSpec, jobId)) + elif job.isFinished(): + self._commitJobs.append((troveSpec, jobId)) + self._activeJobs.remove((troveSpec, jobId)) + + # Wait between each status check. + #time.sleep(1) + + def _commit(self): + for troveSpec, jobId in self._commitJobs: + try: + res = self._builder.commit(jobId) + self._results.update(res) + self._completedJobs.append(jobId) + except JobFailedError: + self._failedJobs.add((troveSpec, jobId)) + self._commitJobs.remove((troveSpec, jobId)) + + +class CommitWorker(Thread): + BuilderClass = Builder + + def __init__(self, cfg, toCommit, results, name=None): + Thread.__init__(self, name=name) + + self.name = name + self.toCommit = toCommit + self.results = results + self.builder = self.BuilderClass(cfg) + + self.setDaemon(True) + + def run(self): + while True: + trvSpec, jobId = self.toCommit.get() + try: + res = self.builder.commit(jobId) + self.msg(0, trvSpec, jobId, res) + except JobFailedError: + self.msg(1, trvSpec, jobId) + except Exception, e: + log.critical('%s received exception: %s' % (self.name, e)) + + def msg(self, rc, trvSpec, jobId, data=None): + self.results.put((rc, ((trvSpec, jobId), data))) + + +class Dispatcher3(Dispatcher2): + workerClass = CommitWorker + + def __init__(self, cfg, workerCount): + Dispatcher2.__init__(self, cfg, workerCount) + + self._commitQueue = Queue() + self._resultQueue = Queue() + + self._workers = [] + for i in range(10): + worker = self.workerClass(self._cfg, self._commitQueue, + self._resultQueue, name='Commit Worker %s' % i) + worker.start() + self._workers.append(worker) + + def _commit(self): + for trvSpec, jobId in self._commitJobs: + self._commitQueue.put((trvSpec, jobId)) + + try: + msg = self._resultQueue.get(False) + while msg: + rc, ((trvSpec, jobId), data) = msg + if rc == 0: + self._results.update(data) + self._completedJobs.append(jobId) + elif rc == 1: + self._failedJobs.add((trvSpec, jobId)) + msg = self._resultQueue.get(False) + except Empty: + pass From johnsonm at rpath.com Wed Aug 19 17:40:32 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:40:32 +0000 Subject: mirrorball: branch merge Message-ID: <200908192140.n7JLeWuD017920@scc.eng.rpath.com> changeset: b140fd60b576 user: Elliot Peele <http://bugs.rpath.com/> date: Thu, 28 Aug 2008 10:30:39 -0400 branch merge committer: Elliot Peele <http://bugs.rpath.com/> diff --git a/commands/bot b/commands/bot new file mode 100755 --- /dev/null +++ b/commands/bot @@ -0,0 +1,9 @@ +#!/usr/bin/python +# +# Copyright (c) 2008 rPath, Inc. +# + +if __name__ == '__main__': + import sys + from updatebot.cmdline import main + sys.exit(main.main(sys.argv)) diff --git a/scripts/findbinaries.py b/scripts/findbinaries.py new file mode 100644 --- /dev/null +++ b/scripts/findbinaries.py @@ -0,0 +1,94 @@ +#!/usr/bin/python +# +# Copyright (c) 2008 rPath, Inc. +# +# This program is distributed under the terms of the Common Public License, +# version 1.0. A copy of this license should have been distributed with this +# source file in a file called LICENSE. If it is not present, the license +# is always available at http://www.rpath.com/permanent/licenses/CPL-1.0. +# +# This program is distributed in the hope that it will be useful, but +# without any warranty; without even the implied warranty of merchantability +# or fitness for a particular purpose. See the Common Public License for +# full details. +# + +import os +import sys + +sys.path.insert(0, os.environ['HOME'] + '/hg/rpath-xmllib') +sys.path.insert(0, os.environ['HOME'] + '/hg/conary') +sys.path.insert(0, os.environ['HOME'] + '/hg/mirrorball') + +import logging + +from updatebot import log +from updatebot import bot +from updatebot import config +from updatebot import conaryhelper + +log.addRootLogger() + +slog = logging.getLogger('findbinaries') + +cfg = config.UpdateBotConfig() +cfg.read(os.environ['HOME'] + '/hg/mirrorball/config/ubuntu/updatebotrc') + +bot = bot.Bot(cfg) +bot._populatePkgSource() + +updater = bot._updater +helper = updater._conaryhelper + +sources, failed = updater.create(cfg.package, buildAll=True) + +pkgs = helper._repos.getTroveLeavesByLabel({None: {helper._ccfg.buildLabel: None}}) +pkgSet = set([ x.split(':')[0] for x in pkgs ]) + +#import epdb; epdb.st() + +srcDict = {} +for pkg in pkgSet: + if pkg not in pkgs: + slog.warn('skipping %s, not in packages' % pkg) + continue + version = pkgs[pkg].keys()[0] + flavors = pkgs[pkg][version] + if len(flavors) == 0: + flavor = None + else: + flavor = flavors[0] + + slog.info('getting binaries for %s' % pkg) + for src, binSet in helper._getSourceTroves((pkg, version, flavor)).iteritems(): + src = (src[0].split(':')[0], src[1], src[2]) + if src not in sources: + slog.warn('skipping %s, it not in souces' % src[0]) + continue + + if src not in srcDict: + srcDict[src] = set() + + for bin in binSet: + name = bin[0].split(':')[0] + srcDict[src].add((name, bin[1], bin[2])) + +bins = set() +for src, binSet in srcDict.iteritems(): + latest = None + for bin in binSet: + if not latest: + latest = bin[1] + + if bin[1] > latest: + latest = bin[1] + + for bin in binSet: + if bin[1] == latest: + bins.add(bin[0]) + +binLst = list(bins) +binLst.sort() + +for item in binLst: + print ' ' * 12, '\'%s\',' % item diff --git a/updatebot/cmdline/__init__.py b/updatebot/cmdline/__init__.py new file mode 100644 --- /dev/null +++ b/updatebot/cmdline/__init__.py @@ -0,0 +1,13 @@ +# +# Copyright (c) 2008 rPath, Inc. +# +# This program is distributed under the terms of the Common Public License, +# version 1.0. A copy of this license should have been distributed with this +# source file in a file called LICENSE. If it is not present, the license +# is always available at http://www.rpath.com/permanent/licenses/CPL-1.0. +# +# This program is distributed in the hope that it will be useful, but +# without any warranty; without even the implied warranty of merchantability +# or fitness for a particular purpose. See the Common Public License for +# full details. +# diff --git a/updatebot/cmdline/command.py b/updatebot/cmdline/command.py new file mode 100644 --- /dev/null +++ b/updatebot/cmdline/command.py @@ -0,0 +1,60 @@ +# +# Copyright (c) 2008 rPath, Inc. +# +# This program is distributed under the terms of the Common Public License, +# version 1.0. A copy of this license should have been distributed with this +# source file in a file called LICENSE. If it is not present, the license +# is always available at http://www.rpath.com/permanent/licenses/CPL-1.0. +# +# This program is distributed in the hope that it will be useful, but +# without any warranty; without even the implied warranty of merchantability +# or fitness for a particular purpose. See the Common Public License for +# full details. +# + +from conary.lib import options + +from conary.lib.options import NO_PARAM +from conary.lib.options import ONE_PARAM +from conary.lib.options import OPT_PARAM +from conary.lib.options import MULT_PARAM +from conary.lib.options import NORMAL_HELP +from conary.lib.options import VERBOSE_HELP + +_commands = [] +def register(cmd): + _commands.append(cmd) + + +class BotCommand(options.AbstractCommand): + defaultGroup = 'Common Options' + commandGroup = CG_MISC + + docs = {'config' : (VERBOSE_HELP, + "Set config KEY to VALUE", "'KEY VALUE'"), + 'config-file' : (VERBOSE_HELP, + "Read PATH config file", "PATH"), + 'context' : (VERBOSE_HELP, + "Set the configuration context to use"), + 'conary-config-file' : (VERBOSE_HELP, + "Read PATH conary config file", "PATH"), + 'rmake-config-file' : (VERBOSE_HELP, + "Read PATH config file", "PATH"), + 'skip-default-config': (VERBOSE_HELP, + "Don't read default configs"), + 'verbose' : (VERBOSE_HELP, + "Display more detailed information where available") } + + def addParameters(self, argDef): + d = {} + d["config"] = MULT_PARAM + d["config-file"] = MULT_PARAM + d["context"] = ONE_PARAM + d["conary-config-file"] = MULT_PARAM + d["rmake-config-file"] = MULT_PARAM + d["skip-default-config"] = NO_PARAM + d["verbose"] = NO_PARAM + argDef[self.defaultGroup] = d + + def processConfigOptions(self, cfg, cfgMap, argSet): + pass diff --git a/updatebot/cmdline/main.py b/updatebot/cmdline/main.py new file mode 100644 --- /dev/null +++ b/updatebot/cmdline/main.py @@ -0,0 +1,68 @@ +# +# Copyright (c) 2008 rPath, Inc. +# +# This program is distributed under the terms of the Common Public License, +# version 1.0. A copy of this license should have been distributed with this +# source file in a file called LICENSE. If it is not present, the license +# is always available at http://www.rpath.com/permanent/licenses/CPL-1.0. +# +# This program is distributed in the hope that it will be useful, but +# without any warranty; without even the implied warranty of merchantability +# or fitness for a particular purpose. See the Common Public License for +# full details. +# + +import sys +import logging + +from conary.lib import options + +from updatebot import config +from updatebot import constants +from updatebot import log as logger +from updatebot.cmdline import command + +class BotMain(options.MainHandler): + name = 'updatebot' + version = constants.version + + abstractCommand = command.BotCommand + configClass = config.UpdateBotConfiguration + + useConaryOptions = False + + commandList = command._commands + + def usage(self, rc=1, showAll=False): + if not showAll: + print ('Common Commands (use "%s help" for the full list)' + % self.name) + return options.MainHandler.usage(self, rc, showAll=showAll) + + def runCommand(self, thisCommand, *args, **kw): + return options.MainHandler.runCommand(self, thisCommand, *args, **kw) + + +def main(argv): + rootLogger = logger.addRootLogger() + log = logging.getLogger('updatebot.main') + try: + argv = list(argv) + debugAll = '--debug-all' in argv + if debugAll: + debuggerException = Exception + argv.remove('--debug-all') + else: + debuggerException = errors.UpdateBotError + sys.excepthook = errors.genExcepthook(debug=debugAll, + debugCtrlC=debugAll) + return BotMain().main(argv, debuggerException=debuggerException) + except debuggerException, err: + raise + except IOError, e: + # allow broken pipe to exit + if e.errno != errno.EPIPE: + raise + except KeyboardInterrupt: + return 1 + return 0 diff --git a/updatebot/constants.py b/updatebot/constants.py new file mode 100644 --- /dev/null +++ b/updatebot/constants.py @@ -0,0 +1,15 @@ +# +# Copyright (c) 2008 rPath, Inc. +# +# This program is distributed under the terms of the Common Public License, +# version 1.0. A copy of this license should have been distributed with this +# source file in a file called LICENSE. If it is not present, the license +# is always available at http://www.rpath.com/permanent/licenses/CPL-1.0. +# +# This program is distributed in the hope that it will be useful, but +# without any warranty; without even the implied warranty of merchantability +# or fitness for a particular purpose. See the Common Public License for +# full details. +# + +version = '0.1' diff --git a/updatebot/log.py b/updatebot/log.py --- a/updatebot/log.py +++ b/updatebot/log.py @@ -37,3 +37,5 @@ conaryLog = logging.getLogger('conary') for handler in conaryLog.handlers: conaryLog.removeHandler(handler) + + return rootLog From johnsonm at rpath.com Wed Aug 19 17:39:30 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:39:30 +0000 Subject: mirrorball: setup flavors correctly for committing to the repository Message-ID: <200908192139.n7JLdUoB015465@scc.eng.rpath.com> changeset: a4f75ca93d2c user: Elliot Peele <http://issues.rpath.com/> date: Thu, 19 Jun 2008 21:31:35 -0400 setup flavors correctly for committing to the repository add a newline to the end of the manifest, otherwise throws off super class add promote implementation committer: Elliot Peele <http://issues.rpath.com/> diff --git a/updatebot/conaryhelper.py b/updatebot/conaryhelper.py --- a/updatebot/conaryhelper.py +++ b/updatebot/conaryhelper.py @@ -22,6 +22,7 @@ import tempfile from conary.deps import deps +from conary.build import use from conary import conaryclient, conarycfg, trove, checkin from updatebot import util @@ -39,9 +40,11 @@ self._ccfg = conarycfg.ConaryConfiguration(readConfigFiles=False) self._ccfg.read(util.join(self._cfg.configPath, 'conaryrc')) - self._ccfg.flavor = deps.parseFlavor('') - # FIXME: do we need to initialize flavors or not? - #self._ccfg.initializeFlavors() + # Have to initialize flavors to commit to the repository. + self._ccfg.initializeFlavors() + + # Setup flavor objects + use.setBuildFlagsFromFlavor(pkgname, self._ccfg.buildFlavor, error=False) self._client = conaryclient.ConaryClient(self._ccfg) self._repos = self._client.getRepos() @@ -194,6 +197,7 @@ manifestFileName = util.join(recipeDir, 'manifest') manifestfh = open(manifestFileName, 'w') manifestfh.write('\n'.join(manifest)) + manifestfh.write('\n') manifestfh.close() # Commit to repository. @@ -270,3 +274,36 @@ verMap = trvMap.get(pkgname, {}) versions = verMap.keys() return versions + + def promote(self, trvLst, expected, targetLabel): + """ + Promote a group and its contents to a target label. + @param trvLst: list of troves to publish + @type trvLst: [(name, version, flavor), ... ] + @param expected: list of troves that are expected to be published. + @type expected: [(name, version, flavor), ...] + @param targetLabel: table to publish to + @type targetLabel: conary Label object + """ + + fromLabel = trvLst[0][1].getTrailingRevision().label + success, cs = client.createSiblingCloneChangeSet({fromLabel:targetLabel}, + trvLst, + cloneSources=True) + if not success: + raise PromoteFailedError(what=trvLst) + + packageList = [ x.getNewNameVersionFlavor() + for x in cs.iterNewTroveList() ] + + oldPkgs = set([ (x[0], x[2]) for x in expected ]) + newPkgs = set([ (x[0], x[2]) for x in packageList ]) + + difference = oldPkgs.difference(newPkgs) + if difference != set(): + raise PromoteMismatchError(expected=oldPkgs, actual=newPkgs) + + self._repos.commitChangeSet(cs) + + return packageList + From johnsonm at rpath.com Wed Aug 19 17:39:47 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:39:47 +0000 Subject: mirrorball: move common rpm utilities into rpmutils module Message-ID: <200908192139.n7JLdlTb016096@scc.eng.rpath.com> changeset: 2326b748efb8 user: Elliot Peele <http://issues.rpath.com/> date: Wed, 09 Jul 2008 11:57:37 -0400 move common rpm utilities into rpmutils module committer: Elliot Peele <http://issues.rpath.com/> diff --git a/rpmimport/rpmhelper.py b/rpmimport/rpmhelper.py deleted file mode 100644 --- a/rpmimport/rpmhelper.py +++ /dev/null @@ -1,81 +0,0 @@ -# -# Copyright (c) 2008 rPath, Inc. -# -# This program is distributed under the terms of the Common Public License, -# version 1.0. A copy of this license should have been distributed with this -# source file in a file called LICENSE. If it is not present, the license -# is always available at http://www.rpath.com/permanent/licenses/CPL-1.0. -# -# This program is distributed in the hope that it will be useful, but -# without any warranty; without even the implied warranty of merchantability -# or fitness for a particular purpose. See the Common Public License for -# full details. -# - -""" -Wrappers around conary.rpmhelper. -""" - -import urllib2 - -from conary import rpmhelper - -class _SeekableStream(object): - """ - File like object that can be seeked forward. - """ - - def __init__(self, url): - self._url = url - - self._fh = urllib2.urlopen(url) - self._pos = 0 - - def read(self, *args): - """ - Wrapper around urllib's file object's read method that keeps track - of position. - """ - - buf = self._fh.read(*args) - self._pos += len(buf) - return buf - - def seek(self, amount, sense): - """ - Simple seek implementation that only goes forward. - @param amount: amount to seek into file. - @type amount: integer - @param sense: direction to seek into file (only valid value is 1) - @type sense: integer - """ - - assert(sense == 1) - self.read(amount) - - def tell(self): - """ - Report the current position in the file. - @return current position in the file - """ - - return self._pos - - -def readHeader(url): - """ - Read an RPM header (and only the RPM header) from a remotely hosted RPM. - @param url: url to RPM file. - @type url: string - @return conary.rpmhelper.RpmHeader object - """ - - fh = _SeekableStream(url) - - # Have to read into the file a bit to get to the begining of the header - # that we care about. - fh.read(96) - rpmhelper.RpmHeader(fh) - - header = rpmhelper.RpmHeader(fh) - return header diff --git a/rpmutils/__init__.py b/rpmutils/__init__.py new file mode 100644 --- /dev/null +++ b/rpmutils/__init__.py @@ -0,0 +1,18 @@ +# +# Copyright (c) 2008 rPath, Inc. +# +# This program is distributed under the terms of the Common Public License, +# version 1.0. A copy of this license should have been distributed with this +# source file in a file called LICENSE. If it is not present, the license +# is always available at http://www.rpath.com/permanent/licenses/CPL-1.0. +# +# This program is distributed in the hope that it will be useful, but +# without any warranty; without even the implied warranty of merchantability +# or fitness for a particular purpose. See the Common Public License for +# full details. +# + +__ALL__ = ('rpmvercmp', 'readHeader', ) + +from rpmutils.vercmp import rpmvercmp +from rpmutils.header import readHeader diff --git a/rpmutils/header.py b/rpmutils/header.py new file mode 100644 --- /dev/null +++ b/rpmutils/header.py @@ -0,0 +1,81 @@ +# +# Copyright (c) 2008 rPath, Inc. +# +# This program is distributed under the terms of the Common Public License, +# version 1.0. A copy of this license should have been distributed with this +# source file in a file called LICENSE. If it is not present, the license +# is always available at http://www.rpath.com/permanent/licenses/CPL-1.0. +# +# This program is distributed in the hope that it will be useful, but +# without any warranty; without even the implied warranty of merchantability +# or fitness for a particular purpose. See the Common Public License for +# full details. +# + +""" +Wrappers around conary.rpmhelper. +""" + +import urllib2 + +from conary import rpmhelper + +class _SeekableStream(object): + """ + File like object that can be seeked forward. + """ + + def __init__(self, url): + self._url = url + + self._fh = urllib2.urlopen(url) + self._pos = 0 + + def read(self, *args): + """ + Wrapper around urllib's file object's read method that keeps track + of position. + """ + + buf = self._fh.read(*args) + self._pos += len(buf) + return buf + + def seek(self, amount, sense): + """ + Simple seek implementation that only goes forward. + @param amount: amount to seek into file. + @type amount: integer + @param sense: direction to seek into file (only valid value is 1) + @type sense: integer + """ + + assert(sense == 1) + self.read(amount) + + def tell(self): + """ + Report the current position in the file. + @return current position in the file + """ + + return self._pos + + +def readHeader(url): + """ + Read an RPM header (and only the RPM header) from a remotely hosted RPM. + @param url: url to RPM file. + @type url: string + @return conary.rpmhelper.RpmHeader object + """ + + fh = _SeekableStream(url) + + # Have to read into the file a bit to get to the begining of the header + # that we care about. + fh.read(96) + rpmhelper.RpmHeader(fh) + + header = rpmhelper.RpmHeader(fh) + return header diff --git a/rpmutils/vercmp.py b/rpmutils/vercmp.py new file mode 100644 --- /dev/null +++ b/rpmutils/vercmp.py @@ -0,0 +1,90 @@ +#!/usr/bin/python + +def _rpmversplit(s): + l = [] + isNumericHunk = s[0].isdigit() + + i = 1 + start = 0 + + + while i < len(s): + if not s[i].isalnum(): + l.append(s[start:i]) + start = i + 1 + elif isNumericHunk != s[i].isdigit(): + l.append(s[start:i]) + start = i + + i += 1 + + l.append(s[start:i]) + + # filter out empty strings + return [ x for x in l if x ] + +def rpmvercmp(ver1string, ver2string): + + ver1list = _rpmversplit(ver1string) + ver2list = _rpmversplit(ver2string) + + while ver1list or ver2list: + if not ver1list: + return -1 + elif not ver2list: + return 1 + + v1 = ver1list.pop(0) + v2 = ver2list.pop(0) + + if v1.isdigit() and v2.isdigit(): + v1 = int(v1) + v2 = int(v2) + elif v1.isdigit() and not v2.isdigit(): + # numbers are newer than letters + return 1 + elif not v1.isdigit() and v2.isdigit(): + return -1 + + if v1 < v2: + return -1 + elif v1 > v2: + return 1 + + return 0 + +if __name__ == '__main__': + def _test(a, b, expected): + result = rpmvercmp(a, b) + if result == -1: + print a, '<', b + elif result == 1: + print a, '>', b + else: + print a, '==', b + assert(result == expected) + + _test('1', '1', 0) + _test('1', '2', -1) + _test('2', '1', 1) + _test('a', 'a', 0) + _test('a', 'b', -1) + _test('b', 'a', 1) + _test('1.2', '1.3', -1) + _test('1.3', '1.1', 1) + _test('1.a', '1.a', 0) + _test('1.a', '1.b', -1) + _test('1.b', '1.a', 1) + _test('1.2+', '1.2', 0) + _test('1.0010', '1.0', 1) + _test('1.05', '1.5', 0) + _test('1.0', '1', 1) + _test('2.50', '2.5', 1) + _test('fc4', 'fc.4', 0) + _test('FC5', 'fc5', -1) + _test('2a', '2.0', -1) + _test('1.0', '1.fc4', 1) + _test('3.0.0_fc', '3.0.0.fc', 0) + _test('1++', '1_', 0) + _test('+', '_', -1) + _test('_', '+', -1) diff --git a/rpmvercmp.py b/rpmvercmp.py deleted file mode 100644 --- a/rpmvercmp.py +++ /dev/null @@ -1,90 +0,0 @@ -#!/usr/bin/python - -def _rpmversplit(s): - l = [] - isNumericHunk = s[0].isdigit() - - i = 1 - start = 0 - - - while i < len(s): - if not s[i].isalnum(): - l.append(s[start:i]) - start = i + 1 - elif isNumericHunk != s[i].isdigit(): - l.append(s[start:i]) - start = i - - i += 1 - - l.append(s[start:i]) - - # filter out empty strings - return [ x for x in l if x ] - -def rpmvercmp(ver1string, ver2string): - - ver1list = _rpmversplit(ver1string) - ver2list = _rpmversplit(ver2string) - - while ver1list or ver2list: - if not ver1list: - return -1 - elif not ver2list: - return 1 - - v1 = ver1list.pop(0) - v2 = ver2list.pop(0) - - if v1.isdigit() and v2.isdigit(): - v1 = int(v1) - v2 = int(v2) - elif v1.isdigit() and not v2.isdigit(): - # numbers are newer than letters - return 1 - elif not v1.isdigit() and v2.isdigit(): - return -1 - - if v1 < v2: - return -1 - elif v1 > v2: - return 1 - - return 0 - -if __name__ == '__main__': - def _test(a, b, expected): - result = rpmvercmp(a, b) - if result == -1: - print a, '<', b - elif result == 1: - print a, '>', b - else: - print a, '==', b - assert(result == expected) - - _test('1', '1', 0) - _test('1', '2', -1) - _test('2', '1', 1) - _test('a', 'a', 0) - _test('a', 'b', -1) - _test('b', 'a', 1) - _test('1.2', '1.3', -1) - _test('1.3', '1.1', 1) - _test('1.a', '1.a', 0) - _test('1.a', '1.b', -1) - _test('1.b', '1.a', 1) - _test('1.2+', '1.2', 0) - _test('1.0010', '1.0', 1) - _test('1.05', '1.5', 0) - _test('1.0', '1', 1) - _test('2.50', '2.5', 1) - _test('fc4', 'fc.4', 0) - _test('FC5', 'fc5', -1) - _test('2a', '2.0', -1) - _test('1.0', '1.fc4', 1) - _test('3.0.0_fc', '3.0.0.fc', 0) - _test('1++', '1_', 0) - _test('+', '_', -1) - _test('_', '+', -1) From johnsonm at rpath.com Wed Aug 19 17:39:54 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:39:54 +0000 Subject: mirrorball: add importing Message-ID: <200908192139.n7JLdsCd016402@scc.eng.rpath.com> changeset: fbd2d568661a user: Elliot Peele <http://issues.rpath.com/> date: Fri, 11 Jul 2008 18:47:21 -0400 add importing committer: Elliot Peele <http://issues.rpath.com/> diff --git a/updatebot/bot.py b/updatebot/bot.py --- a/updatebot/bot.py +++ b/updatebot/bot.py @@ -41,7 +41,7 @@ self._patchSourcePopulated = False self._clients = {} - self._rpmSource = rpmsource.RpmSource() + self._rpmSource = rpmsource.RpmSource(self._cfg) self._patchSource = patchsource.PatchSource(self._cfg) self._updater = update.Updater(self._cfg, self._rpmSource) self._advisor = advise.Advisor(self._cfg, self._rpmSource, @@ -100,6 +100,29 @@ Do initial imports. """ + start = time.time() + log.info('starting import') + + # Populate rpm source object from yum metadata. + self._populateRpmSource() + + import epdb; epdb.st() + + # Import sources into repository. + toBuild, fail = self._updater.create(self._cfg.package) + + import epdb; epdb.st() + + # Build all newly imported packages. + trvMap = self._builder.build(toBuild) + + for trv in self._flattenSetDict(trvMap): + log.info('built: %s' % trv) + + log.info('import completed successfully') + log.info('imported %s source packages' % (len(toBuild), )) + log.info('elapsed time %s' % (time.time() - start, )) + def update(self): """ Update the conary repository from the yum repositories. diff --git a/updatebot/conaryhelper.py b/updatebot/conaryhelper.py --- a/updatebot/conaryhelper.py +++ b/updatebot/conaryhelper.py @@ -22,10 +22,12 @@ import logging import tempfile +import conary from conary.build import use from conary import conaryclient, conarycfg, trove, checkin from updatebot import util +from updatebot.errors import GroupNotFound from updatebot.errors import TooManyFlavorsFoundError from updatebot.errors import NoManifestFoundError from updatebot.errors import PromoteFailedError @@ -47,6 +49,8 @@ self._client = conaryclient.ConaryClient(self._ccfg) self._repos = self._client.getRepos() + self._newPkgFactory = cfg.newPackageFactory + def getConaryConfig(self): """ Get a conary config instance. @@ -67,7 +71,10 @@ # E1101 - Instance of 'ConaryConfiguration' has no 'buildLabel' member # pylint: disable-msg=E1101 - trvlst = self._repos.findTrove(self._ccfg.buildLabel, group) + try: + trvlst = self._repos.findTrove(self._ccfg.buildLabel, group) + except conary.errors.TroveNotFound: + raise GroupNotFound(group=group, label=self._ccfg.buildLabel) latest = self._findLatest(trvlst) @@ -205,6 +212,9 @@ manifestfh.write('\n') manifestfh.close() + # Make sure manifest file has been added. + self._addFile(recipeDir, 'manifest') + # Setup flavor objects use.setBuildFlagsFromFlavor(pkgname, self._ccfg.buildFlavor, error=False) @@ -266,12 +276,32 @@ cwd = os.getcwd() try: os.chdir(recipeDir) - checkin.newTrove(self._repos, self._ccfg, pkgname) + checkin.newTrove(self._repos, self._ccfg, pkgname, + factory=self._newPkgFactory) finally: os.chdir(cwd) return util.join(recipeDir, pkgname) + @staticmethod + def _addFile(pkgDir, fileName): + """ + Add a file to a source component. + @param pkgDir: directory where package is checked out to. + @type pkgDir: string + @param fileName: file name to add. + @type fileName: string + """ + + log.info('adding file: %s' % fileName) + + cwd = os.getcwd() + try: + os.chdir(pkgDir) + checkin.addFiles([fileName, ], ignoreExisting=True, text=True) + finally: + os.chdir(cwd) + def _getVersionsByName(self, pkgname): """ Figure out if a trove exists in the repository. diff --git a/updatebot/config.py b/updatebot/config.py --- a/updatebot/config.py +++ b/updatebot/config.py @@ -58,11 +58,20 @@ # Label to promote to targetLabel = CfgLabel + # Packages to import + package = (CfgList(CfgString), []) + + # Factory to use for importing + newPackageFactory = (CfgString, None) + # Package to exclude from all updates, these are normally packages that # are not managed as part of this distro (ie. in sles we pull some # packages from rpl:1). excludePackages = (CfgList(CfgString), []) + # Exclude these archs from the rpm source. + excludeArch = (CfgList(CfgString), []) + # Packages for which there might not reasonably be advisories. Define a # default advisory message to send with these packages. advisoryException = (CfgList(CfgList(CfgString)), []) diff --git a/updatebot/errors.py b/updatebot/errors.py --- a/updatebot/errors.py +++ b/updatebot/errors.py @@ -87,6 +87,15 @@ the manifest. """ +class GroupNotFound(UnhandledUpdateError): + """ + GroupNotFound, raised when the bot can't find the top level group as + configured. + """ + + _params = ['group', 'label'] + _template = 'Could not find %(group)s on label %(label)s' + class TooManyFlavorsFoundError(UnhandledUpdateError): """ TooManFlavorsFoundError, raised when the bot finds more flavors of the top diff --git a/updatebot/log.py b/updatebot/log.py --- a/updatebot/log.py +++ b/updatebot/log.py @@ -30,7 +30,7 @@ '%(name)s %(message)s') handler.setFormatter(formatter) rootLog.addHandler(handler) - rootLog.setLevel(logging.DEBUG) + rootLog.setLevel(logging.INFO) # Delete conary's log handler since it puts things on stderr and without # any timestamps. diff --git a/updatebot/rpmsource.py b/updatebot/rpmsource.py --- a/updatebot/rpmsource.py +++ b/updatebot/rpmsource.py @@ -28,7 +28,9 @@ Class that builds maps of packages from multiple yum repositories. """ - def __init__(self): + def __init__(self, cfg): + self._excludeArch = cfg.excludeArch + # {srpm: {rpm: path} self._rpmMap = dict() @@ -79,6 +81,10 @@ if '32bit' in pkg.name: continue + # Don't use all arches. + if pkg.arch in self._excludeArch: + continue + assert '-' not in pkg.version assert '-' not in pkg.release diff --git a/updatebot/update.py b/updatebot/update.py --- a/updatebot/update.py +++ b/updatebot/update.py @@ -22,7 +22,9 @@ from updatebot import util from updatebot import conaryhelper -from updatebot.errors import UpdateGoesBackwardsError, UpdateRemovesPackageError +from updatebot.errors import GroupNotFound +from updatebot.errors import UpdateGoesBackwardsError +from updatebot.errors import UpdateRemovesPackageError log = logging.getLogger('updatebot.update') @@ -192,6 +194,90 @@ return ret + def create(self, pkgNames): + """ + Import a new package into the repository. + @param pkgNames: list of packages to import + @type pkgNames: list + @return new source [(name, version, flavor), ... ] + """ + + log.info('getting existing packages') + pkgs = self._getExistingPackageNames() + + toBuild = set() + fail = set() + for pkg in pkgNames: + if pkg not in self._rpmSource.binNameMap: + log.warn('no package named %s found in rpm source' % pkg) + continue + + if pkg not in pkgs: + log.info('importing %s' % pkg) + + try: + srcPkg = self._getPackagesToImport(pkg) + version = self.update((pkg, None, None), srcPkg) + + toBuild.add((pkg, version, None)) + except Exception, e: + log.error('failed to import %s: %s' % (pkg, e)) + fail.add((pkg, e)) +# raise + + return toBuild, fail + + def _getExistingPackageNames(self): + """ + Returns a list of names of all sources included in the top level group. + """ + + # W0612 - Unused variable + # pylint: disable-msg=W0612 + + try: + return [ n.split(':')[0] for n, v, f in + self._conaryhelper.getSourceTroves(self._cfg.topGroup) ] + except GroupNotFound: + return [] + + def _getPackagesToImport(self, name): + """ + Add any missing packages to the srcPkgMap entry for this package. + @param name: name of the srpm to look for. + @type name: string + @return latest source package for the given name + """ + + latestRpm = self._getLatestBinary(name) + latestSrpm = self._rpmSource.binPkgMap[latestRpm] + + pkgs = {} + for pkg in self._rpmSource.srcPkgMap[latestSrpm]: + pkgs[(pkg.name, pkg.arch)] = pkg + + for srpm in self._rpmSource.srcNameMap[latestSrpm.name]: + if latestSrpm.epoch == srpm.epoch and \ + latestSrpm.version == srpm.version: + for pkg in self._rpmSource.srcPkgMap[srpm]: + if (pkg.name, pkg.arch) not in pkgs: + pkgs[(pkg.name, pkg.arch)] = pkg + + self._rpmSource.srcPkgMap[latestSrpm] = set(pkgs.values()) + + return latestSrpm + + def _getLatestBinary(self, name): + """ + Find the latest version of a given binary package. + @param name: name of the package to look for + @type name: string + """ + + rpms = list(self._rpmSource.binNameMap[name]) + rpms.sort(util.packagevercmp) + return rpms[-1] + def update(self, nvf, srcPkg): """ Update rpm manifest in source trove. From johnsonm at rpath.com Wed Aug 19 17:38:59 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:38:59 +0000 Subject: mirrorball: branch merge Message-ID: <200908192139.n7JLcxVo014140@scc.eng.rpath.com> changeset: c464481b7ca7 user: Matt Wilson <https://issues.rpath.com/> date: Thu, 29 May 2008 10:09:53 -0400 branch merge committer: Matt Wilson <https://issues.rpath.com/> diff --git a/repomd/patchxml.py b/repomd/patchxml.py --- a/repomd/patchxml.py +++ b/repomd/patchxml.py @@ -22,12 +22,6 @@ # R0902 - Too many instance attributes # pylint: disable-msg=R0902 - # W0232 - Class has no __init__ method (Yes, really it does) - # pylint: disable-msg=W0232 - - # R0903 - Too few public methods - # pylint: disable-msg=R0903 - name = None summary = None description = None @@ -96,12 +90,6 @@ Parser for the atoms element of a path-*.xml file. ''' - # W0232 - Class has no __init__ method (Yes, really it does) - # pylint: disable-msg=W0232 - - # R0903 - Too few public methods - # pylint: disable-msg=R0903 - def addChild(self, child): ''' Parse children of atoms element. diff --git a/test/slehelp.py b/test/slehelp.py --- a/test/slehelp.py +++ b/test/slehelp.py @@ -17,12 +17,13 @@ import rmakehelp -from updateBot import config +from updatebot import config class Helper(rmakehelp.RmakeHelper): def setUp(self): rmakehelp.RmakeHelper.setUp(self) - self.updateBotCfg = config.UpdateBotConfig(False) + self.updateBotCfg = config.UpdateBotConfig() + self.updateBotCfg.configPath = self.cfg.root self.cfg.user = ('test', 'test') self.writeFile(self.cfg.root + '/conaryrc', '') diff --git a/test/testsuite.py b/test/testsuite.py --- a/test/testsuite.py +++ b/test/testsuite.py @@ -56,6 +56,9 @@ sys.path.insert(0, thisPath) return thisPath + # set default COVERAGE_PATH, if it was not set. + coveragePath = setPathFromEnv('COVERAGE_PATH', 'utils') + # set default CONARY_PATH, if it was not set. conaryPath = setPathFromEnv('CONARY_PATH', 'conary') @@ -136,11 +139,11 @@ def getCoverageDirs(handler, environ): basePath = os.environ['SLEESTACK_PATH'] - coverageDirs = [ 'updateBot', 'rpmimport', 'repomd', ] + coverageDirs = [ 'updatebot', 'rpmimport', 'repomd', ] coveragePath = [] for path in coverageDirs: - covaregePath.append(os.path.normpath(os.path.join(basePath, path))) + coveragePath.append(os.path.normpath(os.path.join(basePath, path))) return coveragePath diff --git a/test/unit_test/updatebottest/buildtest.py b/test/unit_test/updatebottest/buildtest.py new file mode 100644 --- /dev/null +++ b/test/unit_test/updatebottest/buildtest.py @@ -0,0 +1,133 @@ +#!/usr/bin/python +# +# Copyright (c) 2008 rPath, Inc. +# +# This program is distributed under the terms of the Common Public License, +# version 1.0. A copy of this license should have been distributed with this +# source file in a file called LICENSE. If it is not present, the license +# is always available at http://www.rpath.com/permanent/licenses/CPL-1.0. +# +# This program is distributed in the hope that it will be useful, but +# without any warranty; without even the implied warranty of merchantability +# or fitness for a particular purpose. See the Common Public License for +# full details. +# + +import testsetup + +import mock +import slehelp + +from rmake.cmdline import monitor, commit + +from updatebot import build +from updatebot import errors + +class BuilderTest(slehelp.Helper): + def testStartJob(self): + trvSpecs = (('foo', '', ''), ) + + mockJob = mock.MockObject() + mockHelper = mock.MockObject(stableReturnValues=True) + mockHelper.createBuildJob._mock.setReturn(mockJob, trvSpecs) + mockHelper.buildJob._mock.setReturn(1, mockJob) + + builder = build.Builder(self.updateBotCfg) + builder._helper = mockHelper + jobId = builder._startJob(trvSpecs) + + mockHelper.createBuildJob._mock.assertCalled(trvSpecs) + mockHelper.buildJob._mock.assertCalled(mockJob) + self.failUnlessEqual(jobId, 1) + + def testMonitorJob(self): + mockMonitor = mock.MockObject() + self.mock(monitor, 'monitorJob', mockMonitor) + + builder = build.Builder(self.updateBotCfg) + builder._monitorJob(1) + + mockMonitor._mock.assertCalled(builder._helper.client, 1, + exitOnFinish=True, displayClass=build._StatusOnlyDisplay) + + def testSanityCheckJob(self): + jobId = 1 + + mockJob = mock.MockObject(stableReturnValues=True) + mockHelper = mock.MockObject(stableReturnValues=True) + mockHelper.getJob._mock.setReturn(mockJob, jobId) + + builder = build.Builder(self.updateBotCfg) + builder._helper = mockHelper + + mockJob.isFailed._mock.setReturn(True) + self.failUnlessRaises(errors.JobFailedError, builder._sanityCheckJob, jobId) + mockHelper.getJob._mock.assertCalled(jobId) + mockJob.isFailed._mock.assertCalled() + mockJob.isFinished._mock.assertNotCalled() + mockJob.iterBuiltTroves._mock.assertNotCalled() + mockJob.isFailed._mock.setReturn(False) + + mockJob.isFinished._mock.setReturn(False) + self.failUnlessRaises(errors.JobFailedError, builder._sanityCheckJob, jobId) + mockHelper.getJob._mock.assertCalled(jobId) + mockJob.isFailed._mock.assertCalled() + mockJob.isFinished._mock.assertCalled() + mockJob.iterBuiltTroves._mock.assertNotCalled() + mockJob.isFinished._mock.setReturn(True) + + mockJob.iterBuiltTroves._mock.setReturn([]) + self.failUnlessRaises(errors.JobFailedError, builder._sanityCheckJob, jobId) + mockHelper.getJob._mock.assertCalled(jobId) + mockJob.isFailed._mock.assertCalled() + mockJob.isFinished._mock.assertCalled() + mockJob.iterBuiltTroves._mock.assertCalled() + mockJob.iterBuiltTroves._mock.setReturn([1, 2, 3]) + + def testCommitJob(self): + jobId = 1 + + sampleData = {'foo': { + ('foo', '1.0', 'is:x86'): [ + ('foo:source', '1.0-1', ''), + ('foo:config', '1.0-1-1', 'is:x86'), + ('foo:runtime', '1.0-1-1', 'is:x86'), ] + }, + 'bar': { + ('bar', '1.0', 'is:x86_64'): [ + ('bar:source', '1.0-1', ''), + ('bar:runtime', '1.0-1-1', 'is:x86_64'), ] + }, + } + + expected = {} + for value in sampleData.itervalues(): + for buildTrove, committedTrove in value.iteritems(): + expected[buildTrove] = committedTrove + + mockJob = mock.MockObject(stableReturnValues=True) + mockHelper = mock.MockObject(stableReturnValues=True) + mockHelper.getJob._mock.setReturn(mockJob, jobId) + mockCommitJobs = mock.MockObject(stableReturnValues=True) + + self.mock(commit, 'commitJobs', mockCommitJobs) + + builder = build.Builder(self.updateBotCfg) + builder._helper = mockHelper + + mockCommitJobs._mock.setDefaultReturn((True, sampleData)) + trvMap = builder._commitJob(jobId) + self.failUnlessEqual(trvMap, expected) + mockHelper.getJob._mock.assertCalled(jobId) + mockHelper.client.startCommit._mock.assertCalled(jobId) + mockHelper.client.commitSucceeded._mock.assertCalled(sampleData) + + mockCommitJobs._mock.setDefaultReturn((False, sampleData)) + self.failUnlessRaises(errors.CommitFailedError, builder._commitJob, jobId) + mockHelper.getJob._mock.assertCalled(jobId) + mockHelper.client.startCommit._mock.assertCalled(jobId) + mockHelper.client.commitFailed._mock.assertCalled([jobId, ], sampleData) + mockHelper.client.commitSucceeded._mock.assertNotCalled() + + +testsetup.main() diff --git a/updatebot/build.py b/updatebot/build.py --- a/updatebot/build.py +++ b/updatebot/build.py @@ -41,7 +41,6 @@ self._helper = helper.rMakeHelper(root=self._cfg.configPath) - def build(self, troveSpecs): ''' Build a list of troves. @@ -50,16 +49,47 @@ @return troveMap: dictionary of troveSpecs to built troves ''' + jobId = self._startJob(troveSpecs) + self._monitorJob(jobId) + self._sanityCheckJob(jobId) + trvMap = self._commitJob(jobId) + + return trvMap + + def _startJob(self, troveSpecs): + ''' + Create and start a rMake build. + @param troveSpecs: list of trove specs + @type troveSpecs: [(name, versionObj, flavorObj), ...] + @return integer jobId + ''' + # Create rMake job log.info('Creating build job: %s' % (troveSpecs, )) job = self._helper.createBuildJob(troveSpecs) jobId = self._helper.buildJob(job) log.info('Started jobId: %s' % jobId) + return jobId + + def _monitorJob(self, jobId): + ''' + Monitor job status, block until complete. + @param jobId: rMake job ID + @type jobId: integer + ''' + # Watch build, wait for completion monitor.monitorJob(self._helper.client, jobId, exitOnFinish=True, displayClass=_StatusOnlyDisplay) + def _sanityCheckJob(self, jobId): + ''' + Verify the status of a job. + @param jobId: rMake job ID + @type jobId: integer + ''' + # Check for errors job = self._helper.getJob(jobId) if job.isFailed(): @@ -72,8 +102,17 @@ log.error('Job %d has no built troves', jobId) raise JobFailedError(jobId=jobId, why='Job built no troves') + def _commitJob(self, jobId): + ''' + Commit completed job. + @param jobId: rMake job ID + @type jobId: integer + @return troveMap: dictionary of troveSpecs to built troves + ''' + # Do the commit startTime = time.time() + job = self._helper.getJob(jobId) log.info('Starting commit of job %d', jobId) self._helper.client.startCommit(jobId) succeeded, data = commit.commitJobs(self._helper.getConaryClient(), @@ -88,7 +127,7 @@ jobId, time.time() - startTime) troveMap = {} - for _, troveTupleDict in data.iteritems(): + for troveTupleDict in data.itervalues(): for buildTroveTuple, committedList in troveTupleDict.iteritems(): troveMap[buildTroveTuple] = committedList @@ -109,8 +148,6 @@ def _troveLogUpdated(self, (jobId, troveTuple), state, status): '''Don't care about trove logs''' - pass def _trovePreparingChroot(self, (jobId, troveTuple), host, path): '''Don't care about resolving/installing chroot''' - pass From johnsonm at rpath.com Wed Aug 19 17:42:33 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:42:33 +0000 Subject: mirrorball: remove old mirror and promote scripts, rename newer scripts to more reasonable Message-ID: <200908192142.n7JLgXrk022813@scc.eng.rpath.com> changeset: 03914858c931 user: Elliot Peele <https://issues.rpath.com/> date: Mon, 16 Mar 2009 12:01:39 -0400 remove old mirror and promote scripts, rename newer scripts to more reasonable names committer: Elliot Peele <https://issues.rpath.com/> diff --git a/scripts/mirror b/scripts/mirror new file mode 100755 --- /dev/null +++ b/scripts/mirror @@ -0,0 +1,20 @@ +#!/usr/bin/python +# +# Copyright (c) 2008-2009 rPath, Inc. +# +# This program is distributed under the terms of the Common Public License, +# version 1.0. A copy of this license should have been distributed with this +# source file in a file called LICENSE. If it is not present, the license +# is always available at http://www.rpath.com/permanent/licenses/CPL-1.0. +# +# This program is distributed in the hope that it will be useful, but +# without any warranty; without even the implied warranty of merchantability +# or fitness for a particular purpose. See the Common Public License for +# full details. +# + +from header import * + +from updatebot import conaryhelper +helper = conaryhelper.ConaryHelper(cfg) +helper.mirror(fullTroveSync=False) diff --git a/scripts/mirror-centos.sh b/scripts/mirror-centos.sh deleted file mode 100755 --- a/scripts/mirror-centos.sh +++ /dev/null @@ -1,16 +0,0 @@ -#!/bin/sh -# -# Copyright (c) 2008 rPath, Inc. -# -# This program is distributed under the terms of the Common Public License, -# version 1.0. A copy of this license should have been distributed with this -# source file in a file called LICENSE. If it is not present, the license -# is always available at http://www.rpath.com/permanent/licenses/CPL-1.0. -# -# This program is distributed in the hope that it will be useful, but -# without any warranty; without even the implied warranty of merchantability -# or fitness for a particular purpose. See the Common Public License for -# full details. -# - -~/hg/conary/scripts/mirror --config-file=/home/elliot/hg/mirrorball/config/centos/mirror.conf -v diff --git a/scripts/mirror-ubuntu.sh b/scripts/mirror-ubuntu.sh deleted file mode 100755 --- a/scripts/mirror-ubuntu.sh +++ /dev/null @@ -1,16 +0,0 @@ -#!/bin/sh -# -# Copyright (c) 2008 rPath, Inc. -# -# This program is distributed under the terms of the Common Public License, -# version 1.0. A copy of this license should have been distributed with this -# source file in a file called LICENSE. If it is not present, the license -# is always available at http://www.rpath.com/permanent/licenses/CPL-1.0. -# -# This program is distributed in the hope that it will be useful, but -# without any warranty; without even the implied warranty of merchantability -# or fitness for a particular purpose. See the Common Public License for -# full details. -# - -~/hg/conary/scripts/mirror --config-file=/home/elliot/hg/mirrorball/config/ubuntu/mirror.conf -v diff --git a/scripts/mirror.py b/scripts/mirror.py deleted file mode 100755 --- a/scripts/mirror.py +++ /dev/null @@ -1,20 +0,0 @@ -#!/usr/bin/python -# -# Copyright (c) 2008-2009 rPath, Inc. -# -# This program is distributed under the terms of the Common Public License, -# version 1.0. A copy of this license should have been distributed with this -# source file in a file called LICENSE. If it is not present, the license -# is always available at http://www.rpath.com/permanent/licenses/CPL-1.0. -# -# This program is distributed in the hope that it will be useful, but -# without any warranty; without even the implied warranty of merchantability -# or fitness for a particular purpose. See the Common Public License for -# full details. -# - -from header import * - -from updatebot import conaryhelper -helper = conaryhelper.ConaryHelper(cfg) -helper.mirror(fullTroveSync=False) diff --git a/scripts/promote b/scripts/promote new file mode 100755 --- /dev/null +++ b/scripts/promote @@ -0,0 +1,63 @@ +#!/usr/bin/python +# +# Copyright (c) 2008-2009 rPath, Inc. +# +# This program is distributed under the terms of the Common Public License, +# version 1.0. A copy of this license should have been distributed with this +# source file in a file called LICENSE. If it is not present, the license +# is always available at http://www.rpath.com/permanent/licenses/CPL-1.0. +# +# This program is distributed in the hope that it will be useful, but +# without any warranty; without even the implied warranty of merchantability +# or fitness for a particular purpose. See the Common Public License for +# full details. +# + +from header import * + +from updatebot import conaryhelper +helper = conaryhelper.ConaryHelper(cfg) + +from conary import versions +from conary.deps import deps + +import logging +slog = logging.getLogger('script') + +trvLst = helper._repos.findTrove(helper._ccfg.buildLabel, cfg.topGroup) +trvLst = helper._findLatest(trvLst) + +cs, packages = helper.promote( + trvLst, + [], + cfg.sourceLabel, + cfg.targetLabel, + checkPackageList=False, + extraPromoteTroves=cfg.extraPromoteTroves, + commit=False +) + +newPkgs = set([ (x[0].split(':')[0], x[1].getSourceVersion(), x[2]) for x in packages]) + +pkgMap = {} +for n, v, f in newPkgs: + if (n, v) not in pkgMap: + pkgMap[(n, v)] = set() + pkgMap[(n, v)].add(f) + +pkgs = pkgMap.keys() +pkgs.sort() + +for pkg in pkgs: + if pkg[0].startswith('group-'): + continue + for flv in pkgMap[pkg]: + if len(pkgMap[pkg]) > 1 and flv == deps.Flavor(): + continue + slog.info('promoting %s=%s[%s]' % (pkg[0], pkg[1], flv)) + +slog.info('committing') + +helper._repos.commitChangeSet(cs) + +slog.info('done') diff --git a/scripts/promote-centos.sh b/scripts/promote-centos.sh deleted file mode 100755 --- a/scripts/promote-centos.sh +++ /dev/null @@ -1,29 +0,0 @@ -#!/bin/bash -# -# Copyright (c) 2008 rPath, Inc. -# -# This program is distributed under the terms of the Common Public License, -# version 1.0. A copy of this license should have been distributed with this -# source file in a file called LICENSE. If it is not present, the license -# is always available at http://www.rpath.com/permanent/licenses/CPL-1.0. -# -# This program is distributed in the hope that it will be useful, but -# without any warranty; without even the implied warranty of merchantability -# or fitness for a particular purpose. See the Common Public License for -# full details. -# - -cfg="--config-file $HOME/hg/mirrorball/config/centos/conaryrc -m promote --interactive" - -date -time cvc promote $cfg \ - group-appliance:source=centos.rpath.com at rpath:centos-5-devel \ - platform-definition:source=centos.rpath.com at rpath:centos-5-devel \ - kernel=centos.rpath.com at rpath:centos-5-devel \ - anaconda-templates=centos.rpath.com at rpath:centos-5-devel \ - group-os=centos.rpath.com at rpath:centos-5-devel \ - /centos.rpath.com at rpath:centos-5-devel--/centos.rpath.com at rpath:centos-5 \ - /centos.rpath.com at rpath:centos-devel//centos-5-devel--/centos.rpath.com at rpath:centos-5 \ - /conary.rpath.com at rpl:devel//2--/centos.rpath.com at rpath:centos-5 \ - /conary.rpath.com at rpl:devel//2//centos.rpath.com at rpath:centos-5-devel--/centos.rpath.com at rpath:centos-5 \ - /conary.rpath.com at rpl:devel//centos.rpath.com at rpath:centos-5-devel--/centos.rpath.com at rpath:centos-5 diff --git a/scripts/promote-ubuntu.sh b/scripts/promote-ubuntu.sh deleted file mode 100755 --- a/scripts/promote-ubuntu.sh +++ /dev/null @@ -1,39 +0,0 @@ -#!/bin/bash -# -# Copyright (c) 2008 rPath, Inc. -# -# This program is distributed under the terms of the Common Public License, -# version 1.0. A copy of this license should have been distributed with this -# source file in a file called LICENSE. If it is not present, the license -# is always available at http://www.rpath.com/permanent/licenses/CPL-1.0. -# -# This program is distributed in the hope that it will be useful, but -# without any warranty; without even the implied warranty of merchantability -# or fitness for a particular purpose. See the Common Public License for -# full details. -# - -cfg="--config-file $HOME/hg/mirrorball/config/ubuntu/conaryrc -m promote --interactive" - -#date -#time cvc promote $cfg \ -#{{auto,build,c,}package,deb-import,factory-deb}:source=ubuntu.rpath.org at rpath:ubuntu-devel \ -# /ubuntu.rpath.org at rpath:ubuntu-devel--/ubuntu.rpath.com at rpath:ubuntu-devel - -#date -#time cvc promote $cfg \ -# group-appliance:source=ubuntu.rpath.org at rpath:ubuntu-hardy-devel \ -# platform-definition:source=ubuntu.rpath.org at rpath:ubuntu-hardy-devel \ -# group-os=ubuntu.rpath.org at rpath:ubuntu-hardy-devel \ -# ubuntu.rpath.org at rpath:ubuntu-hardy-devel--ubuntu.rpath.com at rpath:ubuntu-hardy-devel \ - -date -time cvc promote $cfg \ - group-appliance:source=ubuntu.rpath.com at rpath:ubuntu-hardy-devel \ - platform-definition:source=ubuntu.rpath.com at rpath:ubuntu-hardy-devel \ - group-os=ubuntu.rpath.com at rpath:ubuntu-hardy-devel \ - /ubuntu.rpath.com at rpath:ubuntu-hardy-devel--/ubuntu.rpath.com at rpath:ubuntu-hardy \ - /ubuntu.rpath.com at rpath:ubuntu-devel//ubuntu-hardy-devel--/ubuntu.rpath.com at rpath:ubuntu-devel//ubuntu-hardy \ - /conary.rpath.com at rpl:devel//conary.rpath.com at rpl:2--/ubuntu.rpath.com at rpath:ubuntu-hardy \ - /conary.rpath.com at rpl:devel//conary.rpath.com at rpl:2//ubuntu.rpath.com at rpath:ubuntu-hardy-devel--/ubuntu.rpath.com at rpath:ubuntu-hardy \ - /conary.rpath.com at rpl:devel//ubuntu.rpath.com at rpath:ubuntu-hardy-devel--/ubuntu.rpath.com at rpath:ubuntu-hardy \ diff --git a/scripts/promote.py b/scripts/promote.py deleted file mode 100755 --- a/scripts/promote.py +++ /dev/null @@ -1,63 +0,0 @@ -#!/usr/bin/python -# -# Copyright (c) 2008-2009 rPath, Inc. -# -# This program is distributed under the terms of the Common Public License, -# version 1.0. A copy of this license should have been distributed with this -# source file in a file called LICENSE. If it is not present, the license -# is always available at http://www.rpath.com/permanent/licenses/CPL-1.0. -# -# This program is distributed in the hope that it will be useful, but -# without any warranty; without even the implied warranty of merchantability -# or fitness for a particular purpose. See the Common Public License for -# full details. -# - -from header import * - -from updatebot import conaryhelper -helper = conaryhelper.ConaryHelper(cfg) - -from conary import versions -from conary.deps import deps - -import logging -slog = logging.getLogger('script') - -trvLst = helper._repos.findTrove(helper._ccfg.buildLabel, cfg.topGroup) -trvLst = helper._findLatest(trvLst) - -cs, packages = helper.promote( - trvLst, - [], - cfg.sourceLabel, - cfg.targetLabel, - checkPackageList=False, - extraPromoteTroves=cfg.extraPromoteTroves, - commit=False -) - -newPkgs = set([ (x[0].split(':')[0], x[1].getSourceVersion(), x[2]) for x in packages]) - -pkgMap = {} -for n, v, f in newPkgs: - if (n, v) not in pkgMap: - pkgMap[(n, v)] = set() - pkgMap[(n, v)].add(f) - -pkgs = pkgMap.keys() -pkgs.sort() - -for pkg in pkgs: - if pkg[0].startswith('group-'): - continue - for flv in pkgMap[pkg]: - if len(pkgMap[pkg]) > 1 and flv == deps.Flavor(): - continue - slog.info('promoting %s=%s[%s]' % (pkg[0], pkg[1], flv)) - -slog.info('committing') - -helper._repos.commitChangeSet(cs) - -slog.info('done') From johnsonm at rpath.com Wed Aug 19 17:39:28 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:39:28 +0000 Subject: mirrorball: add methods and tests for getting and setting manifests Message-ID: <200908192139.n7JLdStw015396@scc.eng.rpath.com> changeset: c1803c21c49a user: Elliot Peele <http://issues.rpath.com/> date: Tue, 17 Jun 2008 16:55:08 -0400 add methods and tests for getting and setting manifests committer: Elliot Peele <http://issues.rpath.com/> diff --git a/test/unit_test/updatebottest/conaryhelpertest.py b/test/unit_test/updatebottest/conaryhelpertest.py --- a/test/unit_test/updatebottest/conaryhelpertest.py +++ b/test/unit_test/updatebottest/conaryhelpertest.py @@ -15,6 +15,7 @@ import testsetup +import os import mock import slehelp @@ -22,6 +23,7 @@ from conary import versions from conary.deps import deps +from updatebot import util from updatebot import errors from updatebot import conaryhelper @@ -212,5 +214,149 @@ mockTroveTrove._mock.assertCalled(mockTroveCs, skipIntegrityChecks=True) + def testGetManifest(self): + trvName = 'foo' + mockCheckout = mock.MockObject(stableReturnValues=True) + mockCheckout._mock.setReturn('foo', trvName) + + expected = ['bar', 'baz'] + + os.mkdir('foo') + fh = open('foo/manifest', 'w') + fh.write('\n'.join(expected)) + fh.close() + + helper = conaryhelper.ConaryHelper(self.updateBotCfg) + self.mock(helper, '_checkout', mockCheckout) + + result = helper.getManifest(trvName) + self.failUnlessEqual(expected, result) + self.failUnless(not os.path.exists('foo')) + mockCheckout._mock.assertCalled(trvName) + + self.failUnlessRaises(errors.NoManifestFoundError, + helper.getManifest, trvName) + mockCheckout._mock.assertCalled(trvName) + + def testSetManifest(self): + trvName = 'foo' + manifestLst = ['bar', 'baz'] + commitMessage = 'foobar' + + mockTrove = mock.MockObject(stableReturnValues=True) + mockRmTree = mock.MockObject(stableReturnValues=True) + mockCommit = mock.MockObject(stableReturnValues=True) + mockNewPkg = mock.MockObject(stableReturnValues=True) + mockCheckout = mock.MockObject(stableReturnValues=True) + mockGetVersionsByName = mock.MockObject(stableReturnValues=False) + + helper = conaryhelper.ConaryHelper(self.updateBotCfg) + self.mock(helper, '_commit', mockCommit) + self.mock(helper, '_newpkg', mockNewPkg) + self.mock(helper, '_checkout', mockCheckout) + self.mock(helper, '_getVersionsByName', mockGetVersionsByName) + self.mock(util, 'rmtree', mockRmTree) + + # new pkg case + os.mkdir('foo') + mockGetVersionsByName._mock.setReturn([], '%s:source' % trvName) + mockNewPkg._mock.setReturn('foo', trvName) + + result = helper.setManifest(trvName, manifestLst, commitMessage) + self.failUnlessEqual(result, []) + self.failUnless(os.path.exists('foo/manifest')) + self.failUnlessEqual(open('foo/manifest').read(), 'bar\nbaz') + mockGetVersionsByName._mock.assertCalled('%s:source' % trvName) + mockNewPkg._mock.assertCalled(trvName) + mockCheckout._mock.assertNotCalled() + mockCommit._mock.assertCalled('foo', commitMessage) + mockRmTree._mock.assertCalled('foo') + + # checkout case + os.mkdir('bar') + mockGetVersionsByName._mock.setReturn(['bar', ], '%s:source' % trvName) + mockCheckout._mock.setReturn('bar', trvName) + + result = helper.setManifest(trvName, manifestLst, commitMessage) + self.failUnlessEqual(result, ['bar', ]) + self.failUnless(os.path.exists('bar/manifest')) + self.failUnlessEqual(open('bar/manifest').read(), 'bar\nbaz') + mockGetVersionsByName._mock.assertCalled('%s:source' % trvName) + mockNewPkg._mock.assertNotCalled() + mockCheckout._mock.assertCalled(trvName) + mockCommit._mock.assertCalled('bar', commitMessage) + mockRmTree._mock.assertCalled('bar') + + def testCheckout(self): + mockTempFile = mock.MockObject(stableReturnValues=True) + mockTempFile.mkdtemp._mock.setReturn('foodir', prefix='conaryhelper-') + + mockCheckin = mock.MockObject(stableReturnValues=True) + self.mock(conaryhelper, 'checkin', mockCheckin) + self.mock(conaryhelper, 'tempfile', mockTempFile) + + helper = conaryhelper.ConaryHelper(self.updateBotCfg) + result = helper._checkout('foo') + self.failUnlessEqual(result, 'foodir') + mockCheckin.checkout._mock.assertCalled(helper._repos, helper._ccfg, + 'foodir', ['foo', ]) + mockTempFile.mkdtemp._mock.assertCalled(prefix='conaryhelper-') + + def testCommit(self): + mockCheckin = mock.MockObject() + self.mock(conaryhelper, 'checkin', mockCheckin) + + os.mkdir('foo') + helper = conaryhelper.ConaryHelper(self.updateBotCfg) + result = helper._commit('foo', 'foocommit') + self.failUnlessEqual(result, None) + mockCheckin.commit._mock.assertCalled(helper._repos, helper._ccfg, + 'foocommit') + + def testNewPkg(self): + mockTempFile = mock.MockObject(stableReturnValues=True) + mockTempFile.mkdtemp._mock.setReturn('foodir', prefix='conaryhelper-') + mockCheckin = mock.MockObject() + self.mock(conaryhelper, 'checkin', mockCheckin) + self.mock(conaryhelper, 'tempfile', mockTempFile) + + os.mkdir('foodir') + helper = conaryhelper.ConaryHelper(self.updateBotCfg) + result = helper._newpkg('foo') + self.failUnlessEqual(result, 'foodir/foo') + mockTempFile.mkdtemp._mock.assertCalled(prefix='conaryhelper-') + mockCheckin.newTrove._mock.assertCalled(helper._repos, helper._ccfg, + 'foo') + + def testGetVersionByName(self): + pkgName = 'foo' + mockCfg = mock.MockObject() + mockLabel = mock.MockObject() + mockRepos = mock.MockObject() + mockVersion = mock.MockObject() + + mockCfg._mock.set(buildLabel=mockLabel) + mockRepos.getTroveLeavesByLabel._mock.setReturn( + {pkgName: {mockVersion: None}}, + {pkgName: {mockLabel: None}}) + + helper = conaryhelper.ConaryHelper(self.updateBotCfg) + helper._repos = mockRepos + helper._ccfg = mockCfg + + result = helper._getVersionsByName(pkgName) + self.failUnlessEqual(result, [mockVersion, ]) + mockRepos.getTroveLeavesByLabel._mock.assertCalled( + {pkgName: {mockLabel: None}}) + + mockRepos.getTroveLeavesByLabel._mock.setReturn( + {}, + {pkgName: {mockLabel: None}}) + + result = helper._getVersionsByName(pkgName) + self.failUnlessEqual(result, []) + mockRepos.getTroveLeavesByLabel._mock.assertCalled( + {pkgName: {mockLabel: None}}) + testsetup.main() diff --git a/updatebot/conaryhelper.py b/updatebot/conaryhelper.py --- a/updatebot/conaryhelper.py +++ b/updatebot/conaryhelper.py @@ -17,13 +17,15 @@ some point. """ +import os import logging +import tempfile from conary.deps import deps -from conary import conaryclient, conarycfg, trove +from conary import conaryclient, conarycfg, trove, checkin from updatebot import util -from updatebot.errors import TooManyFlavorsFoundError +from updatebot.errors import TooManyFlavorsFoundError, NoManifestFoundError log = logging.getLogger('updatebot.conaryhelper') @@ -147,3 +149,124 @@ troveCs = cs.getNewTroveVersion(name, version, flavor) trv = trove.Trove(troveCs, skipIntegrityChecks=True) return trv + + def getManifest(self, pkgname): + """ + Get the contents of the manifest file from the source component for a + given package. + @param pkgname: name of the package to retrieve + @type pkgname: string + @return manifest for pkgname + """ + + log.info('retrieving manifest for %s' % pkgname) + recipeDir = self._checkout(pkgname) + manifestFileName = util.join(recipeDir, 'manifest') + + if not os.path.exists(manifestFileName): + raise NoManifestFoundError(pkgname=pkgname, dir=recipeDir) + + manifest = [ x.strip() for x in open(manifestFileName).readlines() ] + util.rmtree(recipeDir) + return manifest + + def setManifest(self, pkgname, manifest, commitMessage=''): + """ + Create/Update a manifest file. + @param pkgname: name of the package + @type pkgname: string + @param manifest: list of files to go in the manifest file + @type manifest: list(string, string, ...) + @param commitMessage: optional argument for setting the commit message + to use when committing to the repository. + @type commitMessage: string + """ + + log.info('setting manifest for %s' % pkgname) + + # Figure out if we should create or update. + if not self._getVersionsByName('%s:source' % pkgname): + recipeDir = self._newpkg(pkgname) + else: + recipeDir = self._checkout(pkgname) + + # Update manifest file. + manifestFileName = util.join(recipeDir, 'manifest') + manifestfh = open(manifestFileName, 'w') + manifestfh.write('\n'.join(manifest)) + manifestfh.close() + + # Commit to repository. + self._commit(recipeDir, commitMessage) + util.rmtree(recipeDir) + + # Get new version of source trove. + return self._getVersionsByName('%s:source' % pkgname) + + def _checkout(self, pkgname): + """ + Checkout a source component from the repository. + @param pkgname: name of the package to checkout + @type pkgname: string + @return checkout directory + """ + + log.info('checking out %s' % pkgname) + + recipeDir = tempfile.mkdtemp(prefix='conaryhelper-') + checkin.checkout(self._repos, self._ccfg, recipeDir, [pkgname, ]) + + return recipeDir + + def _commit(self, pkgDir, commitMessage): + """ + Commit a source trove to the repository. + @param pkgDir: directory returned by checkout + @type pkgDir: string + @param commitMessage: commit message to use. + @type commitMessage: string + """ + + log.info('committing %s' % os.path.basename(pkgDir)) + + cwd = os.getcwd() + try: + os.chdir(pkgDir) + checkin.commit(self._repos, self._ccfg, commitMessage) + finally: + os.chdir(cwd) + + def _newpkg(self, pkgname): + """ + Create a new source component. + @param pkgname: name of the package to create. + @type pkgname: string + @return checkout directory + """ + + log.info('creating new package %s' % pkgname) + + recipeDir = tempfile.mkdtemp(prefix='conaryhelper-') + + cwd = os.getcwd() + try: + os.chdir(recipeDir) + checkin.newTrove(self._repos, self._ccfg, pkgname) + finally: + os.chdir(cwd) + + return util.join(recipeDir, pkgname) + + def _getVersionsByName(self, pkgname): + """ + Figure out if a trove exists in the repository. + @param pkgname: name of the package to look for. + @type pkgname: string + """ + + label = self._ccfg.buildLabel + + trvMap = self._repos.getTroveLeavesByLabel({pkgname: {label: None }}) + verMap = trvMap.get(pkgname, {}) + versions = verMap.keys() + return versions From johnsonm at rpath.com Wed Aug 19 17:39:22 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:39:22 +0000 Subject: mirrorball: add module for handling advisories and related tests Message-ID: <200908192139.n7JLdMLZ015157@scc.eng.rpath.com> changeset: 38ed1355f1e3 user: Elliot Peele <http://issues.rpath.com/> date: Mon, 16 Jun 2008 17:26:30 -0400 add module for handling advisories and related tests committer: Elliot Peele <http://issues.rpath.com/> diff --git a/test/unit_test/updatebottest/advisetest.py b/test/unit_test/updatebottest/advisetest.py new file mode 100644 --- /dev/null +++ b/test/unit_test/updatebottest/advisetest.py @@ -0,0 +1,106 @@ +# +# Copyright (c) 2008 rPath, Inc. +# +# This program is distributed under the terms of the Common Public License, +# version 1.0. A copy of this license should have been distributed with this +# source file in a file called LICENSE. If it is not present, the license +# is always available at http://www.rpath.com/permanent/licenses/CPL-1.0. +# +# This program is distributed in the hope that it will be useful, but +# without any warranty; without even the implied warranty of merchantability +# or fitness for a particular purpose. See the Common Public License for +# full details. +# + +import testsetup + +import mock +import slehelp + +from updatebot import advise +from updatebot import errors + +class AdviseTest(slehelp.Helper): + def testCheck(self): + mockRpmSource = mock.MockObject(stableReturnValues=True) + mockPatchSource = mock.MockObject(stableReturnValues=True) + mockHasException = mock.MockObject(stableReturnValues=True) + mockIsSecurity = mock.MockObject(stableReturnValues=True) + + mockSrcPkg1 = mock.MockObject() + mockSrcPkg2 = mock.MockObject() + mockSrcPkg3 = mock.MockObject() + + mockBinPkg1 = mock.MockObject() + mockBinPkg2 = mock.MockObject() + mockBinPkg3 = mock.MockObject() + + mockPatch1 = mock.MockObject() + mockPatch2 = mock.MockObject() + + srcPkgMap = {mockSrcPkg1: [mockSrcPkg1, mockBinPkg1], + mockSrcPkg2: [mockSrcPkg2, mockBinPkg2], + mockSrcPkg3: [mockSrcPkg3, mockBinPkg3]} + mockRpmSource._mock.set(srcPkgMap=srcPkgMap) + + pkgMap = {mockBinPkg1: set([mockPatch1, ]), } + mockPatchSource._mock.set(pkgMap=pkgMap) + + input = [(None, mockSrcPkg1), + (None, mockSrcPkg2), + (None, mockSrcPkg3)] + + advisor = advise.Advisor(self.updateBotCfg, mockRpmSource, + mockPatchSource) + self.mock(advisor, '_hasException', mockHasException) + self.mock(advisor, '_isSecurity', mockIsSecurity) + + mockHasException._mock.setReturn(True, mockBinPkg2) + mockHasException._mock.setReturn(False, mockBinPkg3) + mockIsSecurity._mock.setReturn(False, mockBinPkg3) + + expected = {(None, mockSrcPkg1): set([mockPatch1, ]), + (None, mockSrcPkg2): set(), + (None, mockSrcPkg3): set()} + + # test normal case + result = advisor.check(input) + self.failUnlessEqual(result, None) + self.failUnlessEqual(advisor._cache, expected) + mockHasException._mock.assertCalled(mockBinPkg2) + mockHasException._mock.assertCalled(mockBinPkg3) + mockIsSecurity._mock.assertCalled(mockBinPkg3) + + # test exception case + mockHasException._mock.setReturn(False, mockBinPkg2) + mockIsSecurity._mock.setReturn(True, mockBinPkg2) + self.failUnlessRaises(errors.NoAdvisoryFoundError, advisor.check, input) + mockHasException._mock.assertCalled(mockBinPkg2) + mockIsSecurity._mock.assertCalled(mockBinPkg2) + + def testHasException(self): + binPkg = mock.MockObject(stableReturnValues=True) + mockCfg = mock.MockObject(stableReturnValues=True) + mockCfg._mock.set(advisoryException=[['foo', None]]) + + advisor = advise.Advisor(mockCfg, None, None) + + binPkg._mock.set(location='foo/bar') + result = advisor._hasException(binPkg) + self.failUnlessEqual(result, True) + + binPkg._mock.set(location='bar/foo') + result = advisor._hasException(binPkg) + self.failUnlessEqual(result, False) + + def testIsSecurity(self): + advisor = advise.Advisor(self.updateBotCfg, None, None) + binPkg = mock.MockObject(stableReturnValues=True) + binPkg._mock.set(location='foo-Updates/bar') + result = advisor._isSecurity(binPkg) + self.failUnlessEqual(result, True) + binPkg._mock.set(location='foo-Online/bar') + result = advisor._isSecurity(binPkg) + self.failUnlessEqual(result, False) + +testsetup.main() diff --git a/test/unit_test/updatebottest/patchsourcetest.py b/test/unit_test/updatebottest/patchsourcetest.py new file mode 100644 --- /dev/null +++ b/test/unit_test/updatebottest/patchsourcetest.py @@ -0,0 +1,55 @@ +# +# Copyright (c) 2008 rPath, Inc. +# +# This program is distributed under the terms of the Common Public License, +# version 1.0. A copy of this license should have been distributed with this +# source file in a file called LICENSE. If it is not present, the license +# is always available at http://www.rpath.com/permanent/licenses/CPL-1.0. +# +# This program is distributed in the hope that it will be useful, but +# without any warranty; without even the implied warranty of merchantability +# or fitness for a particular purpose. See the Common Public License for +# full details. +# + +import testsetup + +import mock +import slehelp + +from updatebot import patchsource + +class PatchSourceTest(slehelp.Helper): + def testLoadFromClient(self): + mockPatch = mock.MockObject() + mockClient = mock.MockObject(stableReturnValues=True) + mockClient.getPatchDetail._mock.setReturn([mockPatch, ]) + + mockLoadOne = mock.MockObject(stableReturnValues=True) + mockLoadOne._mock.setReturn(None, mockPatch, '/foo') + + patchSource = patchsource.PatchSource() + self.mock(patchSource, '_loadOne', mockLoadOne) + + result = patchSource.loadFromClient(mockClient, '/foo') + self.failUnlessEqual(result, None) + mockClient.getPatchDetail._mock.assertCalled() + mockLoadOne._mock.assertCalled(mockPatch, '/foo') + + def testLoadOne(self): + mockPackage = mock.MockObject(stableReturnValues=True) + mockPackage._mock.set(location='foo/bar') + + mockPatch = mock.MockObject(stableReturnValues=True) + mockPatch._mock.set(packages=[mockPackage, ]) + + expectedResult = {mockPackage: set([mockPatch, ])} + + patchSource = patchsource.PatchSource() + result = patchSource._loadOne(mockPatch, 'baz') + self.failUnlessEqual(result, None) + self.failUnlessEqual(patchSource.pkgMap, expectedResult) + self.failUnlessEqual(mockPackage.location, 'baz/foo/bar') + + +testsetup.main() diff --git a/updatebot/advise.py b/updatebot/advise.py --- a/updatebot/advise.py +++ b/updatebot/advise.py @@ -16,13 +16,85 @@ Module for managing/manipulating advisories. """ -from updatebot.errors import * +import logging + +from updatebot.errors import NoAdvisoryFoundError + +log = logging.getLogger('updatebot.advisor') class Advisor(object): """ Class for managing, manipulating, and distributing advisories. """ - def __init__(self, cfg, rpmSource): + def __init__(self, cfg, rpmSource, patchSource): self._cfg = cfg self._rpmSource = rpmSource + self._patchSource = patchSource + + # { ((name, version, flavor), srcPkg): [ patchObj, ... ] + self._cache = dict() + + def check(self, trvLst): + """ + Check to see if there are advisories for troves in trvLst. + @param trvLst: list of troves and srpms. + @type trvLst: [((name, version, flavor), srcPkg), ... ] + """ + + for nvf, srcPkg in trvLst: + patches = set() + for binPkg in self._rpmSource.srcPkgMap[srcPkg]: + # Don't check srpms. + if binPkg is srcPkg: + continue + + if binPkg in self._patchSource.pkgMap: + patches.update(self._patchSource.pkgMap[binPkg]) + elif self._hasException(binPkg): + log.info('found advisory exception for %s' % binPkg) + log.debug(binPkg.location) + elif not self._isSecurity(binPkg): + log.info('package not in updates repository %s' % binPkg) + log.debug(binPkg.location) + else: + log.error('could not find patch for %s' % binPkg) + raise NoAdvisoryFoundError(why=binPkg) + + if (nvf, srcPkg) not in self._cache: + self._cache[(nvf, srcPkg)] = patches + + + def _hasException(self, binPkg): + """ + Check the config for repositories with exceptions for sending + advisories. (io. repositories that we generated metadata for.) + @param binPkg: binary package object + @type binPkg: repomd.packagexml._Package + """ + + shortPath = binPkg.location.split('/')[0] + + for advisoryException in self._cfg.advisoryException: + path = advisoryException[0].split('/')[0] + if path == shortPath: + return True + return False + + @classmethod + def _isSecurity(self, binPkg): + """ + Check the repository name. If this package didn't come from a updates + repository it is probably not security related. + @param binPkg: binary package object + @type binPkg: repomd.packagexml._Package + """ + + # FIXME: Make sure this is a sane check. + + shortPath = binPkg.location.split('/')[0] + + if shortPath.endswith('Updates'): + return True + else: + return False diff --git a/updatebot/patchsource.py b/updatebot/patchsource.py new file mode 100644 --- /dev/null +++ b/updatebot/patchsource.py @@ -0,0 +1,57 @@ +# +# Copyright (c) 2008 rPath, Inc. +# +# This program is distributed under the terms of the Common Public License, +# version 1.0. A copy of this license should have been distributed with this +# source file in a file called LICENSE. If it is not present, the license +# is always available at http://www.rpath.com/permanent/licenses/CPL-1.0. +# +# This program is distributed in the hope that it will be useful, but +# without any warranty; without even the implied warranty of merchantability +# or fitness for a particular purpose. See the Common Public License for +# full details. +# + +""" +Module for building datastructures for packages to patches. +""" + +import logging + +log = logging.getLogger('updatebot.patchsource') + +class PatchSource(object): + """ + Store patch related mappings. + """ + + def __init__(self): + # {binPkg: patchObj} + self.pkgMap = dict() + + def loadFromClient(self, client, path): + """ + Load patch information from a repomd client object. + @param client: repomd client object + @type client: repomd.Client + @param path: base path to repository + @type path: string + """ + + for patch in client.getPatchDetail(): + self._loadOne(patch, path) + + def _loadOne(self, patch, path): + """ + Load one patch into mappings. + @param patch: repomd patch object + @type patch: repomd.patchxml._Patch + @param path: base path to repository + @type path: string + """ + + for package in patch.packages: + package.location = path + '/' + package.location + if package not in self.pkgMap: + self.pkgMap[package] = set() + self.pkgMap[package].add(patch) From johnsonm at rpath.com Wed Aug 19 17:41:43 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:41:43 +0000 Subject: mirrorball: pylint updates Message-ID: <200908192141.n7JLfhM3020804@scc.eng.rpath.com> changeset: da3876a9b0ce user: Elliot Peele <https://issues.rpath.com/> date: Wed, 19 Nov 2008 22:47:15 -0500 pylint updates committer: Elliot Peele <https://issues.rpath.com/> diff --git a/aptmd/__init__.py b/aptmd/__init__.py --- a/aptmd/__init__.py +++ b/aptmd/__init__.py @@ -12,6 +12,11 @@ # full details. # +""" +Module for pasring Apt repository metadata. Also includes a handy FSM for +parsing text files in general. +""" + import os from repomd.repository import Repository @@ -21,6 +26,10 @@ from aptmd.errors import UnsupportedFileError class Client(object): + """ + Class for interacting with apt repositories. + """ + def __init__(self, repoUrl): self._repoUrl = repoUrl @@ -29,6 +38,10 @@ self._sources = SourcesParser() def parse(self, path): + """ + Parse repository metadata. + """ + fh = self._repo.get(path) basename = os.path.basename(path) if basename.startswith('Packages'): diff --git a/aptmd/common.py b/aptmd/common.py --- a/aptmd/common.py +++ b/aptmd/common.py @@ -12,21 +12,36 @@ # full details. # +""" +Common module for apt metadata parsers to inherit from. +""" + from updatebot import util from aptmd.container import Container from aptmd.parser import ContainerizedParser as Parser class BaseContainer(Container): + """ + Base class that implements a data container for entries in apt repository + metadata. + """ + __slots__ = ('name', 'arch', 'epoch', 'version', 'release') def __repr__(self): + # Instance of 'BaseContainer' has no 'name' member + # pylint: disable-msg=E1101 + klass = self.__class__.__name__.strip('_') return '<%s(%s, %s, %s, %s, %s)>' % (klass, self.name, self.epoch, self.version, self.release, self.arch) def __hash__(self): + # Instance of 'BaseContainer' has no 'name' member + # pylint: disable-msg=E1101 + return hash((self.name, self.epoch, self.version, self.release, self.arch)) @@ -35,6 +50,10 @@ class BaseParser(Parser): + """ + Base parser class to be used in parsing apt metadata. + """ + def __init__(self): Parser.__init__(self) @@ -51,6 +70,10 @@ }) def _package(self): + """ + Parse package info. + """ + if self._curObj is not None: if hasattr(self._curObj, 'finalize'): self._curObj.finalize() @@ -59,11 +82,25 @@ self._curObj.name = self._getLine() def _architecture(self): + """ + Parse architectures. + """ + + # Attribute 'arch' defined outside __init__ + # pylint: disable-msg=W0201 + arch = self._getLine() assert arch in ('all', 'i386', 'amd64') self._curObj.arch = arch def _version(self): + """ + Parse versions. + """ + + # Attribute 'release' defined outside __init__ + # pylint: disable-msg=W0201 + debVer = self._getLine() epoch = '0' diff --git a/aptmd/container.py b/aptmd/container.py --- a/aptmd/container.py +++ b/aptmd/container.py @@ -12,7 +12,15 @@ # full details. # +""" +Base container class to be used with a ContainerizedParser. +""" + class Container(object): + """ + Base container class. + """ + __slots__ = ('_data', ) def __init__(self): @@ -24,15 +32,32 @@ self._data = {} def set(self, key, value): + """ + Set data in a local dictionary, also used for __setitem__. + @param key - key for dictionary + @type key hashable object + @param value - value for dictionary + @type value object + """ + if key not in self._data: self._data[key] = value __setitem__ = set def get(self, key): + """ + Get information from the data dict. + @param key - item to retrieve from the dict + @type key hashable object + """ + return self._data[key] __getitem__ = get def finalize(self): - pass + """ + Method to be implemented by subclasses for computing any data based + on parsed information. + """ diff --git a/aptmd/errors.py b/aptmd/errors.py --- a/aptmd/errors.py +++ b/aptmd/errors.py @@ -12,11 +12,22 @@ # full details. # +""" +AptMD Error Module. +""" + class AptMDError(Exception): - pass + """ + Base error class. + """ class UnsupportedFileError(AptMDError): + """ + Raised for files that the parser doesn't know how to handle. + """ + def __init__(self, path): + AptMDError.__init__(self) self.path = path def __str__(self): diff --git a/aptmd/packages.py b/aptmd/packages.py --- a/aptmd/packages.py +++ b/aptmd/packages.py @@ -12,14 +12,25 @@ # full details. # +""" +Module for parsing package metadata files. +""" + from aptmd.common import BaseContainer, BaseParser class _Package(BaseContainer): + """ + Package container class. + """ + __slots__ = ('source', 'sourceVersion', 'location', 'summary', 'description') class PackagesParser(BaseParser): + """ + Package MD Parser class. + """ def __init__(self): BaseParser.__init__(self) @@ -44,6 +55,15 @@ }) def parse(self, fn): + """ + Parse a given file or file like object line by line. + @param fn: filename or file like object to parse. + @type fn: string or file like object. + """ + + # Attribute 'description' defined outside __init__ + # pylint: disable-msg=W0201 + ret = BaseParser.parse(self, fn) # If there is any text left, collect it in the description if self._text: @@ -53,6 +73,13 @@ return ret def _source(self): + """ + Parse the source line. + """ + + # Attribute 'sourceVersion' defined outside __init__ + # pylint: disable-msg=W0201 + source = self._getLine() assert source != '' @@ -69,12 +96,30 @@ self._curObj.source = source def _filename(self): + """ + Parse the filename line. + """ + + # Attribute 'location' defined outside __init__ + # pylint: disable-msg=W0201 + self._curObj.location = self._getLine() def _description(self): + """ + Parse the description line. + """ + + # Attribute 'summary' defined outside __init__ + # pylint: disable-msg=W0201 + self._curObj.summary = self._getLine() def _bugs(self): + """ + Parse the bugs line. + """ + self._curObj.description = self._text self._text = '' self._keyval() diff --git a/aptmd/parser.py b/aptmd/parser.py --- a/aptmd/parser.py +++ b/aptmd/parser.py @@ -12,9 +12,17 @@ # full details. # +""" +Base parsing module, defines all low level parser classes. +""" + import re class _QuotedLineTokenizer(object): + """ + Class for breaking up a line into quoted chunks. + """ + def __init__(self): self._cur = None self._list = None @@ -27,6 +35,12 @@ } def tokenize(self, line): + """ + Break apart the line based on quotes. + @param line: line from a file. + @type line string + """ + self._singleQuotedString = False self._doubleQuotedString = False @@ -44,22 +58,42 @@ return [ ''.join(x) for x in self._list ] def _space(self): + """ + Handle spaces. + """ + if not self._singleQuotedString and not self._doubleQuotedString: self._list.append([]) def _singleQuote(self): + """ + Handle single quotes. + """ + self._add() self._singleQuotedString = not self._singleQuotedString def _doubleQuote(self): + """ + Handle double quotes. + """ + self._add() self._doubleQuotedString = not self._doubleQuotedString def _add(self): + """ + Handle all other text. + """ + self._list[-1].append(self._cur) class Parser(object): + """ + Base parser class. + """ + def __init__(self): self._text = '' self._line = None @@ -69,6 +103,12 @@ self._states = {} def parse(self, fn): + """ + Parse file or file line object. + @param fn: name of file or file like object to parse. + @type fn: string or file like object. + """ + if isinstance(fn, str): fileObj = open(fn) else: @@ -78,6 +118,12 @@ self._parseLine(line) def _parseLine(self, cfgline): + """ + Process a single line. + @param cfgline: single line of text config file. + @type cfgline: string + """ + self._line = self._lineTokenizer.tokenize(cfgline) if len(self._line) > 0: state = self._getState(self._line[0]) @@ -91,27 +137,57 @@ else: self._text += ' '.join(self._line) - @staticmethod - def _getState(key): + def _getState(self, key): + """ + Translate the first word of a line to a key of the state dict. This + method is meant to be overridden by subclasses. + """ + + # Method could be a function + # pylint: disable-msg=R0201 + return key def _getLine(self): + """ + Get the original line after the first word. + """ + return ' '.join(self._line[1:]).strip() def _getFullLine(self): + """ + Get the entire line including the first word. + """ + return ' '.join(self._line).strip() def _checkLength(self, length, gt=False): - if gt: assert(len(self._line) > length) - else: assert(len(self._line) == length) + """ + Validate the length of a line. + """ + + if gt: + assert(len(self._line) > length) + else: + assert(len(self._line) == length) def _keyval(self): + """ + Parse a line as a key/value pair. + """ + key = self._getState(self._line[0]) value = ' '.join(self._line[1:]).strip() self._curObj.set(key, value) class ContainerizedParser(Parser): + """ + Parser for files that can be split into containers. Generates a list of + container objects. + """ + def __init__(self): Parser.__init__(self) @@ -120,10 +196,18 @@ self._stateFilters = { } - def _filter(self, filter, state): - self._stateFilters[re.compile(filter)] = state + def _filter(self, fltr, state): + """ + Build a state based on a filter. + """ + + self._stateFilters[re.compile(fltr)] = state def _getState(self, key): + """ + Filter states based on filter map. + """ + key = key.strip() key = key.lower() if key.endswith(':'): @@ -132,13 +216,20 @@ if key in self._states: return key - for filter, state in self._stateFilters.iteritems(): - if filter.match(key): + for fltr, state in self._stateFilters.iteritems(): + if fltr.match(key): return state return key def _newContainer(self): + """ + Create a new container object and store the current one. + """ + + # self._containerClass is not callable + # pylint: disable-msg=E1102 + if self._curObj is not None: if hasattr(self._curObj, 'finalize'): self._curObj.finalize() @@ -146,6 +237,10 @@ self._curObj = self._containerClass() def parse(self, fileObj): + """ + Parse a file or file line object. + """ + self._objects = [] Parser.parse(self, fileObj) return self._objects diff --git a/aptmd/sources.py b/aptmd/sources.py --- a/aptmd/sources.py +++ b/aptmd/sources.py @@ -12,15 +12,27 @@ # full details. # +""" +Module for parsing Sources file from an Apt repository. +""" + import os from aptmd.common import BaseContainer, BaseParser class _SourcePackage(BaseContainer): + """ + Container for source data. + """ + __slots__ = ('binaries', 'directory', 'files') class SourcesParser(BaseParser): + """ + Class for parsing Source metadata. + """ + def __init__(self): BaseParser.__init__(self) @@ -38,19 +50,51 @@ }) def _architecture(self): + """ + Parse architecture. + """ + + # Attribute 'arch' defined outside __init__ + # pylint: disable-msg=W0201 + self._curObj.arch = 'src' def _binary(self): + """ + Parse binary info. + """ + + # Attribute 'binary' defined outside __init__ + # pylint: disable-msg=W0201 + self._line[-1] = self._line[-1].strip() self._curObj.binaries = [ x.strip(',') for x in self._line[1:] ] def _directory(self): + """ + Parse directory info. + """ + + # Attribute 'directory' defined outside __init__ + # pylint: disable-msg=W0201 + self._curObj.directory = self._getLine() def _files(self): + """ + Parse files info. + """ + + # Attribute 'files' defined outside __init__ + # pylint: disable-msg=W0201 + self._curObj.files = [] def _file(self): + """ + Parse file info. + """ + if len(self._line) != 4: return From johnsonm at rpath.com Wed Aug 19 17:41:22 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:41:22 +0000 Subject: mirrorball: new advisory model Message-ID: <200908192141.n7JLfMXW019984@scc.eng.rpath.com> changeset: dd23ad4d2e16 user: Elliot Peele <https://issues.rpath.com/> date: Fri, 14 Nov 2008 17:41:14 -0500 new advisory model committer: Elliot Peele <https://issues.rpath.com/> diff --git a/updatebot/advise.py b/updatebot/advise.py deleted file mode 100644 --- a/updatebot/advise.py +++ /dev/null @@ -1,348 +0,0 @@ -# -# Copyright (c) 2008 rPath, Inc. -# -# This program is distributed under the terms of the Common Public License, -# version 1.0. A copy of this license should have been distributed with this -# source file in a file called LICENSE. If it is not present, the license -# is always available at http://www.rpath.com/permanent/licenses/CPL-1.0. -# -# This program is distributed in the hope that it will be useful, but -# without any warranty; without even the implied warranty of merchantability -# or fitness for a particular purpose. See the Common Public License for -# full details. -# - -""" -Module for managing/manipulating advisories. -""" - -import time -import smtplib -import logging -from email.MIMEText import MIMEText -from smtplib import SMTPRecipientsRefused, SMTPHeloError -from smtplib import SMTPSenderRefused, SMTPDataError - -from updatebot.errors import AdvisoryRecipientRefusedError -from updatebot.errors import FailedToSendAdvisoryError -from updatebot.errors import MultipleAdvisoriesFoundError -from updatebot.errors import NoAdvisoryFoundError -from updatebot.errors import NoPackagesFoundForAdvisory -from updatebot.errors import NoRecipientsFoundError -from updatebot.errors import NoSenderFoundError -from updatebot.errors import ProductNameNotDefinedError - -log = logging.getLogger('updatebot.advisor') - -class Advisor(object): - """ - Class for managing, manipulating, and distributing advisories. - """ - - def __init__(self, cfg, pkgSource, patchSource): - self._cfg = cfg - self._pkgSource = pkgSource - self._patchSource = patchSource - - # { patchObj: set([srcPkg, ...] } - self._cache = dict() - - # { ((name, version, flavor), srcPkg): [ advisoryObj, ...] } - self._advisories = dict() - - def check(self, trvLst): - """ - Check to see if there are advisories for troves in trvLst. - @param trvLst: list of troves and srpms. - @type trvLst: [((name, version, flavor), srcPkg), ... ] - """ - - # FIXME: Maybe we should check to see if all binary rpms listed in - # the advisory are in the set of packages to be updated. - - for nvf, srcPkg in trvLst: - patches = set() - for binPkg in self._pkgSource.srcPkgMap[srcPkg]: - if binPkg in self._patchSource.pkgMap: - patches.update(self._patchSource.pkgMap[binPkg]) - - for binPkg in self._pkgSource.srcPkgMap[srcPkg]: - # Don't check srpms. - if binPkg is srcPkg or binPkg in self._patchSource.pkgMap: - continue - elif self._hasException(binPkg): - log.info('found advisory exception for %s' % binPkg) - log.debug(binPkg.location) - elif not self._isSecurity(binPkg): - log.info('package not in updates repository %s' % binPkg) - log.debug(binPkg.location) - elif len(patches) > 0: - log.info('found package not mentioned in advisory %s' - % binPkg) - log.debug(binPkg.location) - else: - log.error('could not find advisory for %s' % binPkg) - raise NoAdvisoryFoundError(why=binPkg) - - if len(patches) > 1: - raise MultipleAdvisoriesFoundError(what=srcPkg, - advisories=patches) - elif len(patches) == 0: - continue - - patch = list(patches)[0] - if patch not in self._cache: - self._cache[patch] = set() - - self._cache[patch].add(srcPkg) - self._advisories[patch] = self._mkAdvisory(patch) - - def _hasException(self, binPkg): - """ - Check the config for repositories with exceptions for sending - advisories. (io. repositories that we generated metadata for.) - @param binPkg: binary package object - @type binPkg: repomd.packagexml._Package - """ - - shortPath = binPkg.location.split('/')[0] - - for advisoryException in self._cfg.advisoryException: - path = advisoryException[0].split('/')[0] - if path == shortPath: - return True - return False - - @staticmethod - def _isSecurity(binPkg): - """ - Check the repository name. If this package didn't come from a updates - repository it is probably not security related. - @param binPkg: binary package object - @type binPkg: repomd.packagexml._Package - """ - - # msw agrees that this seems to be a sane check. - - shortPath = binPkg.location.split('/')[0] - - if shortPath.endswith('Updates'): - return True - else: - return False - - def _mkAdvisory(self, patch): - """ - Create and populate advisory object for a given package. - """ - - advisory = Advisory(self._cfg) - advisory.setSubject(patch.summary) - advisory.setDescription(patch.description) - return advisory - - def send(self, trvLst, newTroves): - """ - Send advisories for all troves in the trvLst. - @param trvLst: list of packages that have been updated. - @type trvLst: [((name, version, flavor), srcPkg), ...] - @param newTroves: list of new troves after the promote - @type newTroves: [(name, version, flavor), ...] - """ - - newTroveMap = self._mkNewTroveMap(trvLst, newTroves) - - toSend = set() - for patch, advisory in self._advisories.iteritems(): - binNames = [ x.name for x in patch.packages ] - for srpm in self._cache[patch]: - if srpm not in newTroveMap: - log.warn('%s not in newTroveMap' % srpm) - continue - - # Make sure advisory applies to a package that was promoted. - toAdvise = [ x for x in newTroveMap[srpm] if x[0] in binNames ] - if not toAdvise: - log.warn('%s does not apply to any published packages of %s' - % (advisory, srpm)) - continue - - advisory.setUpdateTroves(newTroveMap[srpm]) - toSend.add(advisory) - - for advisory in toSend: - log.info('sending advisory: %s' % advisory) - advisory.send() - - def _mkNewTroveMap(self, trvLst, newTroves): - """ - Create a mapping of the elements in trvLst to newTroves. - @param trvLst: list of packages that have been updated. - @type trvLst: [((name, version, flavor), srcPkg), ...] - @param newTroves: list of new troves after the promote - @type newTroves: [(name, version, flavor), ...] - @return {trvLstElement: [(n, v, f), ...]} - """ - - res = dict() - for nvf, srcPkg in trvLst: - res[srcPkg] = [] - binNames = [ x.name for x in self._pkgSource.srcPkgMap[srcPkg] ] - for n, v, f in newTroves: - if n in binNames: - res[srcPkg].append((n, v, f)) - if not res[srcPkg]: - raise NoPackagesFoundForAdvisory(what=(nvf, srcPkg)) - - return res - - -class Advisory(object): - """ - Module for representing an advisory message. - """ - - template = """\ -Published: %(date)s -Products: - %(product)s - -Updated Versions: -%(updateTroves)s - -Description: -%(description)s -""" - - def __init__(self, cfg): - self._cfg = cfg - - self._fromName = self._cfg.emailFromName - self._from = self._cfg.emailFrom - self._to = self._cfg.emailTo - self._bcc = self._cfg.emailBcc - - self._subject = None - - if not self._cfg.productName: - raise ProductNameNotDefinedError - - if not self._from or not self._fromName: - raise NoSenderFoundError(why='cfg.emailFrom or cfg.emailFromName not defined') - - if not self._to: - raise NoRecipientsFoundError(why='cfg.emailTo not defined') - - self._data = {'product': self._cfg.productName, - 'date': time.strftime('%Y-%m-%d', time.localtime()), - 'updateTroves': ''} - - def __str__(self): - return self._subject - - def send(self): - """ - Send an advisory email. - """ - - message = self._getMessage() - smtp = self._smtpConnect() - - try: - results = smtp.sendmail(self._from, self._to, message.as_string()) - except (SMTPRecipientsRefused, SMTPHeloError, SMTPSenderRefused, - SMTPDataError), e: - raise FailedToSendAdvisoryError(error=e) - - smtp.quit() - - if results is not None and results != {}: - raise AdvisoryRecipientRefusedError(data=results) - - return results - - def _getMessage(self): - """ - Get the message to send. - """ - - msgText = self.template % self._data - email = MIMEText(msgText) - email['Subject'] = self._subject - email['From'] = '%s <%s>' % (self._fromName, self._from) - email['To'] = self._formatList(self._to) - email['Bcc'] = self._formatList(self._bcc) - return email - - @staticmethod - def _formatList(lst): - """ - Format a list to be comma separated. - """ - - return ', '.join(lst) - - def _smtpConnect(self): - """ - Get a smtp connection object. - """ - - server = smtplib.SMTP(self._cfg.smtpServer) - - rootLogger = logging.getLogger('') - if rootLogger.level == logging.DEBUG: - server.set_debuglevel(1) - - return server - - def setUpdateTroves(self, troves): - """ - Set the list of updated troves to add to the advisory. - @param troves: list of troves - @type troves: [(name, version, flavor), ... ] - """ - - trvs = set() - for n, v, f in troves: - trvs.add(self._formatTrove(n, v, f)) - - self._data['updateTroves'] += self._indentFormatList(trvs) - - @staticmethod - def _indentFormatList(lst, indent=1): - """ - Format a list into a string with tab indention. - """ - - tab = ' ' * indent - joinString = '\n' + tab - result = tab + joinString.join(lst) - return result - - @staticmethod - def _formatTrove(n, v, f): - """ - Format a trove spec into a string. - """ - - # W0613 - Unused argument 'f' - # pylint: disable-msg=W0613 - - label = v.trailingLabel().asString() - revision = v.trailingRevision().asString() - - return '%s=%s/%s' % (n, label, revision) - - def setSubject(self, subject): - """ - Set the subject of the advisory email. - """ - - self._subject = subject - - def setDescription(self, desc): - """ - Set the description of the advisory. - """ - - self._data['description'] = desc diff --git a/updatebot/patchsource.py b/updatebot/patchsource.py deleted file mode 100644 --- a/updatebot/patchsource.py +++ /dev/null @@ -1,77 +0,0 @@ -# -# Copyright (c) 2008 rPath, Inc. -# -# This program is distributed under the terms of the Common Public License, -# version 1.0. A copy of this license should have been distributed with this -# source file in a file called LICENSE. If it is not present, the license -# is always available at http://www.rpath.com/permanent/licenses/CPL-1.0. -# -# This program is distributed in the hope that it will be useful, but -# without any warranty; without even the implied warranty of merchantability -# or fitness for a particular purpose. See the Common Public License for -# full details. -# - -""" -Module for building datastructures for packages to patches. -""" - -import logging - -log = logging.getLogger('updatebot.patchsource') - -class PatchSource(object): - """ - Store patch related mappings. - """ - - def __init__(self, cfg): - self._cfg = cfg - - # {binPkg: patchObj} - self.pkgMap = dict() - - def loadFromClient(self, client, path): - """ - Load patch information from a repomd client object. - @param client: repomd client object - @type client: repomd.Client - @param path: base path to repository - @type path: string - """ - - for patch in client.getPatchDetail(): - self._loadOne(patch, path) - - def _loadOne(self, patch, path): - """ - Load one patch into mappings. - @param patch: repomd patch object - @type patch: repomd.patchxml._Patch - @param path: base path to repository - @type path: string - """ - - if self._filterPatch(patch): - return - - for package in patch.packages: - package.location = path + '/' + package.location - if package not in self.pkgMap: - self.pkgMap[package] = set() - self.pkgMap[package].add(patch) - - def _filterPatch(self, patch): - """ - Filter out patches that match filters in config. - @param patch: repomd patch object - @type patch: repomd.patchxml._Patch - """ - - for _, regex in self._cfg.patchFilter: - if regex.match(patch.summary): - return True - if regex.match(patch.description): - return True - - return False From johnsonm at rpath.com Wed Aug 19 17:40:10 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:40:10 +0000 Subject: mirrorball: add module for parsing apt repository metadata Message-ID: <200908192140.n7JLeB4D017035@scc.eng.rpath.com> changeset: f7db75e905cd user: Elliot Peele <http://issues.rpath.com/> date: Thu, 14 Aug 2008 16:23:15 -0400 add module for parsing apt repository metadata committer: Elliot Peele <http://issues.rpath.com/> diff --git a/aptmd/__init__.py b/aptmd/__init__.py new file mode 100644 --- /dev/null +++ b/aptmd/__init__.py @@ -0,0 +1,39 @@ +# +# Copyright (c) 2008 rPath, Inc. +# +# This program is distributed under the terms of the Common Public License, +# version 1.0. A copy of this license should have been distributed with this +# source file in a file called LICENSE. If it is not present, the license +# is always available at http://www.rpath.com/permanent/licenses/CPL-1.0. +# +# This program is distributed in the hope that it will be useful, but +# without any warranty; without even the implied warranty of merchantability +# or fitness for a particular purpose. See the Common Public License for +# full details. +# + +import os + +from repomd.repository import Repository + +from aptmd.sources import SourcesParser +from aptmd.packages import PackagesParser +from aptmd.errors import UnsupportedFileError + +class Client(object): + def __init__(self, repoUrl): + self._repoUrl = repoUrl + + self._repo = Repository(self._repoUrl) + self._packages = PackagesParser() + self._sources = SourcesParser() + + def parse(self, path): + fh = self._repo.get(path) + basename = os.path.basename(path) + if basename.startswith('Packages'): + return self._packages.parse(fh) + elif basename.startswith('Sources'): + return self._sources.parse(fh) + else: + raise UnsupportedFileError(path) diff --git a/aptmd/common.py b/aptmd/common.py new file mode 100644 --- /dev/null +++ b/aptmd/common.py @@ -0,0 +1,76 @@ +# +# Copyright (c) 2008 rPath, Inc. +# +# This program is distributed under the terms of the Common Public License, +# version 1.0. A copy of this license should have been distributed with this +# source file in a file called LICENSE. If it is not present, the license +# is always available at http://www.rpath.com/permanent/licenses/CPL-1.0. +# +# This program is distributed in the hope that it will be useful, but +# without any warranty; without even the implied warranty of merchantability +# or fitness for a particular purpose. See the Common Public License for +# full details. +# + +from aptmd.parser import Parser +from aptmd.container import Container + +class BaseContainer(Container): + __slots__ = ('name', 'arch', 'version', 'release') + + def __hash__(self): + return hash((self.name, self.arch, self.version, self.release)) + + +class BaseParser(Parser): + def __init__(self): + Parser.__init__(self) + + self._containerClass = BaseContainer + self._objects = [] + + self._states.update({ + 'package' : self._package, + 'architecture' : self._architecture, + 'version' : self._version, + 'priority' : self._keyval, + 'section' : self._keyval, + 'maintainer' : self._keyval, + 'original-maintainer' : self._keyval, + }) + + def parse(self, fileObj): + self._objects = [] + Parser.parse(self, fileObj) + return self._objects + + @staticmethod + def _getState(key): + if key.endswith(':'): + key = key[:-1] + return key.lower() + + def _package(self): + if self._curObj is not None: + if hasattr(self._curObj, 'finalize'): + self._curObj.finalize() + self._objects.append(self._curObj) + self._curObj = self._containerClass() + self._curObj.name = self._getLine() + + def _architecture(self): + arch = self._getLine() + assert arch in ('all', 'i386', 'x86_64') + self._curObj.arch = arch + + def _version(self): + debVer = self._getLine() + if '-' in debVer: + sdebVer = debVer.split('-') + version = sdebVer[0] + release = '-'.join(sdebVer[1:]) + else: + version = debVer + release = '' + self._curObj.version = version + self._curObj.release = release diff --git a/aptmd/container.py b/aptmd/container.py new file mode 100644 --- /dev/null +++ b/aptmd/container.py @@ -0,0 +1,36 @@ +# +# Copyright (c) 2008 rPath, Inc. +# +# This program is distributed under the terms of the Common Public License, +# version 1.0. A copy of this license should have been distributed with this +# source file in a file called LICENSE. If it is not present, the license +# is always available at http://www.rpath.com/permanent/licenses/CPL-1.0. +# +# This program is distributed in the hope that it will be useful, but +# without any warranty; without even the implied warranty of merchantability +# or fitness for a particular purpose. See the Common Public License for +# full details. +# + +class Container(object): + __slots__ = ('_data', ) + + def __init__(self): + for item in self.__slots__: + setattr(self, item, None) + + self._data = {} + + def set(self, key, value): + if key not in self._data: + self._data[key] = value + + __setitem__ = set + + def get(self, key): + return self._data[key] + + __getitem__ = get + + def finalize(self): + pass diff --git a/aptmd/errors.py b/aptmd/errors.py new file mode 100644 --- /dev/null +++ b/aptmd/errors.py @@ -0,0 +1,23 @@ +# +# Copyright (c) 2008 rPath, Inc. +# +# This program is distributed under the terms of the Common Public License, +# version 1.0. A copy of this license should have been distributed with this +# source file in a file called LICENSE. If it is not present, the license +# is always available at http://www.rpath.com/permanent/licenses/CPL-1.0. +# +# This program is distributed in the hope that it will be useful, but +# without any warranty; without even the implied warranty of merchantability +# or fitness for a particular purpose. See the Common Public License for +# full details. +# + +class AptMDError(Exception): + pass + +class UnsupportedFileError(AptMDError): + def __init__(self, path): + self.path = path + + def __str__(self): + return 'No parser available for: %s' % self.path diff --git a/aptmd/packages.py b/aptmd/packages.py new file mode 100644 --- /dev/null +++ b/aptmd/packages.py @@ -0,0 +1,61 @@ +# +# Copyright (c) 2008 rPath, Inc. +# +# This program is distributed under the terms of the Common Public License, +# version 1.0. A copy of this license should have been distributed with this +# source file in a file called LICENSE. If it is not present, the license +# is always available at http://www.rpath.com/permanent/licenses/CPL-1.0. +# +# This program is distributed in the hope that it will be useful, but +# without any warranty; without even the implied warranty of merchantability +# or fitness for a particular purpose. See the Common Public License for +# full details. +# + +from aptmd.common import BaseContainer, BaseParser + +class _Package(BaseContainer): + __slots__ = ('source', 'location', 'summary', 'description') + + def __repr__(self): + return '<Package(%s)>' % self.location + + +class PackagesParser(BaseParser): + + def __init__(self): + BaseParser.__init__(self) + + self._containerClass = _Package + self._states.update({ + 'installed-size' : self._keyval, + 'source' : self._source, + 'replaces' : self._keyval, + 'depends' : self._keyval, + 'recommends' : self._keyval, + 'conflicts' : self._keyval, + 'filename' : self._filename, + 'size' : self._keyval, + 'md5sum' : self._keyval, + 'sha1' : self._keyval, + 'sha256' : self._keyval, + 'description' : self._description, + 'bugs' : self._bugs, + 'origin' : self._keyval, + 'task' : self._keyval, + }) + + def _source(self): + source = self._getLine() + assert source != '' + self._curObj.source = source + + def _filename(self): + self._curObj.location = self._getLine() + + def _description(self): + self._curObj.summary = self._getLine() + + def _bugs(self): + self._curObj.description = self._text + self._keyval() diff --git a/aptmd/parser.py b/aptmd/parser.py new file mode 100644 --- /dev/null +++ b/aptmd/parser.py @@ -0,0 +1,104 @@ +# +# Copyright (c) 2008 rPath, Inc. +# +# This program is distributed under the terms of the Common Public License, +# version 1.0. A copy of this license should have been distributed with this +# source file in a file called LICENSE. If it is not present, the license +# is always available at http://www.rpath.com/permanent/licenses/CPL-1.0. +# +# This program is distributed in the hope that it will be useful, but +# without any warranty; without even the implied warranty of merchantability +# or fitness for a particular purpose. See the Common Public License for +# full details. +# + +class _QuotedLineTokenizer(object): + def __init__(self): + self._cur = None + self._list = None + self._singleQuotedString = False + self._doubleQuotedString = False + self._states = {' ': self._space, + '\'': self._singleQuote, + '"': self._doubleQuote, + 'other': self._add, + } + + def tokenize(self, line): + self._list = [''] + for char in line: + self._cur = char + if char in self._states: + self._states[char]() + else: + self._states['other']() + + if self._list[-1] == '': + self._list = self._list[:-1] + + return self._list + + def _space(self): + if not self._singleQuotedString and not self._doubleQuotedString: + self._list.append('') + + def _singleQuote(self): + self._add() + self._singleQuotedString = not self._singleQuotedString + + def _doubleQuote(self): + self._add() + self._doubleQuotedString = not self._doubleQuotedString + + def _add(self): + self._list[-1] += self._cur + + +class Parser(object): + def __init__(self): + self._text = '' + self._line = None + self._curObj = None + self._lineTokenizer = _QuotedLineTokenizer() + + self._containerClass = None + self._states = {} + + def parse(self, fn): + if isinstance(fn, str): + fileObj = open(fn) + else: + fileObj = fn + + for line in fileObj: + self._parseLine(line) + + def _parseLine(self, cfgline): + self._line = self._lineTokenizer.tokenize(cfgline) + if len(self._line) > 0: + state = self._getState(self._line[0]) + if state in self._states: + func = self._states[state] + if func is None: + # ignore this line + return + func() + self._text = '' + else: + self._text += ' '.join(self._line) + + @staticmethod + def _getState(key): + return key + + def _getLine(self): + return ' '.join(self._line[1:]).strip() + + def _checkLength(self, length, gt=False): + if gt: assert(len(self._line) > length) + else: assert(len(self._line) == length) + + def _keyval(self): + key = self._getState(self._line[0]) + value = ' '.join(self._line[1:]).strip() + self._curObj.set(key, value) diff --git a/aptmd/sources.py b/aptmd/sources.py new file mode 100644 --- /dev/null +++ b/aptmd/sources.py @@ -0,0 +1,70 @@ +# +# Copyright (c) 2008 rPath, Inc. +# +# This program is distributed under the terms of the Common Public License, +# version 1.0. A copy of this license should have been distributed with this +# source file in a file called LICENSE. If it is not present, the license +# is always available at http://www.rpath.com/permanent/licenses/CPL-1.0. +# +# This program is distributed in the hope that it will be useful, but +# without any warranty; without even the implied warranty of merchantability +# or fitness for a particular purpose. See the Common Public License for +# full details. +# + +import os + +from aptmd.common import BaseContainer, BaseParser + +class _SourcePackage(BaseContainer): + __slots__ = ('binaries', 'directory', 'files') + + +class SourcesParser(BaseParser): + + def __init__(self): + BaseParser.__init__(self) + + self._containerClass = _SourcePackage + self._inFiles = False + self._states.update({ + 'architecture' : self._architecture, + 'binary' : self._binary, + 'build-depends' : self._keyval, + 'standards-version' : self._keyval, + 'format' : self._keyval, + 'directory' : self._directory, + 'files' : self._files, + 'homepage' : self._keyval, + 'uploaders' : self._keyval, + }) + + def _parseLine(self, line): + BaseParser._parseLine(self, line) + + state = self._getState(self._line[0]) + if state != 'files' and state in self._states: + self._inFiles = False + + if self._inFiles: + self._file() + + def _architecture(self): + self._curObj.arch = 'src' + + def _binary(self): + self._line[-1] = self._line[-1].strip() + self._curObj.binaries = [ x.strip(',') for x in self._line[1:] ] + + def _directory(self): + self._curObj.directory = self._getLine() + + def _files(self): + self._inFiles = True + + def _file(self): + if self._curObj.files is None: + self._curObj.files = [] + + path = os.path.join(self._curObj.directory, self._line[2]) + self._curObj.files.append(path) From johnsonm at rpath.com Wed Aug 19 17:39:55 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:39:55 +0000 Subject: mirrorball: now that updatebot has import functionallity remove the old import code Message-ID: <200908192139.n7JLdtdl016419@scc.eng.rpath.com> changeset: 0bc10fda7e1f user: Elliot Peele <http://issues.rpath.com/> date: Fri, 11 Jul 2008 23:38:38 -0400 now that updatebot has import functionallity remove the old import code committer: Elliot Peele <http://issues.rpath.com/> diff --git a/extra/pkgwiz.py b/extra/pkgwiz.py deleted file mode 100755 --- a/extra/pkgwiz.py +++ /dev/null @@ -1,312 +0,0 @@ -#!/usr/bin/python -# -# Copyright (c) 2006,2008 rPath, Inc. -# -# -# This program is distributed under the terms of the Common Public License, -# version 1.0. A copy of this license should have been distributed with this -# source file in a file called LICENSE. If it is not present, the license -# is always available at http://www.rpath.com/permanent/licenses/CPL-1.0. -# -# This program is distributed in the hope that it will be useful, but -# without any warranty; without even the implied warranty of merchantability -# or fitness for a particular purpose. See the Common Public License for -# full details. -# - -from conary import cvc -import conary.lib.util -import os -import tempfile -from rpmimport import recipemaker, rpmsource, rpmhelper -import rpmvercmp -import shutil -import sys - -DEFAULT_URL = 'http://install.rdu.rpath.com/sle/' -DEFAULT_BASE_PATHS = [ - 'SLES10-SP2/sles-10-i586', - 'SLES10-SP2/sles-10-x86_64', - 'SLES10-SP2-Online/sles-10-i586', - 'SLES10-SP2-Online/sles-10-x86_64', - 'SLES10-SP2-Updates/sles-10-i586', - 'SLES10-SP2-Updates/sles-10-x86_64', -] - -HELP_TEXT = """ -[<base url>] [[repo path 1] [repo path 2] ... [repo path N]] - -When packages are imported, they will be checked against what's in the -repository. If newer, then check new one in. -""" -sles10pkgs = ( - 'aaa_base', - 'acl', - 'apache2', - 'apache2-mod_python', - 'ash', - 'attr', - 'audit', - 'bash', - 'binutils', - 'busybox', - 'bzip2', - 'compat-libstdc++', - 'compat-openssl097g', - 'coreutils', - 'cpio', - 'cpp', - 'cracklib', - 'cron', - 'curl', - 'cyrus-sasl', - 'device-mapper', - 'db', - 'dbus-1', - 'dhcpcd', - 'diffutils', - 'e2fsprogs', - 'expat', - 'expect', - 'file', - 'filesystem', - 'fillup', - 'findutils', - 'findutils-locate', - 'fontconfig', - 'freetype', - 'freetype2', - 'gawk', - 'gcc', - 'gdbm', - 'glib2', - 'glibc', - 'gmp', - 'gnutls', - 'gpm', - 'grep', - 'grub', - 'gzip', - 'hal', - 'hwinfo', - 'insserv', - 'iproute2', - 'iptables', - 'iputils', - #'java-1_4_2-sun', - 'jpeg', - 'kbd', - 'klogd', - 'krb5', - 'ksh', - 'less', - 'libaio', - 'libapr-util1', - 'libapr1', - 'libattr', - 'libcap', - 'libcom_err', - 'libelf', - 'libevent', - 'libgcc', - 'libgpg-error', - 'libgcrypt', - 'libgssapi', - 'libidn', - 'libiniparser', - 'libjpeg', - 'libnscd', - 'libopencdk', - 'libpcap', - 'libpng', - 'librpcsecgss', - 'libstdc++', - 'libtool', - 'libusb', - 'libxcrypt', - 'libxml2', - 'libxml2-python', - 'libxslt', - 'logrotate', - 'lsof', - 'lvm2', - 'lzo', - 'm4', - 'make', - 'mdadm', - 'mingetty', - 'mkinitrd', - 'mktemp', - 'mm', - 'module-init-tools', - 'ncurses', - 'net-tools', - 'netcfg', - 'nfs-utils', - 'nfsidmap', - 'openct', - 'openldap2', - 'openldap2-client', - 'opensc', - 'openslp', - 'openssh', - 'openssl', - 'pam', - 'pam-modules', - 'patch', - 'pciutils', - 'pcre', - 'perl', - 'perl-Bootloader', - 'perl-Compress-Zlib', - 'perl-DBD-SQLite', - 'perl-DBI', - 'perl-Digest-SHA1', - 'perl-Net-Daemon', - 'perl-PlRPC', - 'perl-TermReadKey', - 'perl-URI', - 'perl-gettext', - 'php5', - 'pkgconfig', - 'popt', - 'portmap', - 'procps', - 'psmisc', - 'pwdutils', - 'python', - 'python-xml', - 'resmgr', - 'samba', - 'sed', - 'sendmail', - 'slang', - 'sles-release', - 'strace', - 'sysconfig', - 'sysfsutils', - 'syslog-ng', - 'sysstat', - 'sysvinit', - 'tar', - 'tcl', - 'tcpd', - 'tcsh', - 'tcpdump', - 'termcap', - 'timezone', - 'udev', - 'unixODBC', - 'unzip', - 'util-linux', - 'vim', - 'wget', - 'wireless-tools', - 'xntp', - #'xorg-x11', - 'zlib', - 'zip', - ) - -class PkgWiz: - def __init__(self): - self.cfg = None - self.client = None - self.repos = None - self.rpmSource = rpmsource.RpmSource() - self.recipeMaker = None - - def help(self): - print HELP_TEXT - - def _setupRepo(self): - if self.cfg: - return - from conary import conaryclient, conarycfg, versions, errors - from conary import deps - from conary.build import use - - self.cfg = conarycfg.ConaryConfiguration(readConfigFiles=True) - self.cfg.read(os.path.dirname(__file__) + '/conaryrc') - self.cfg.initializeFlavors() - cvcCommand = cvc.CvcCommand() - cvcCommand.setContext(self.cfg, dict()) - if not self.cfg.buildLabel and self.cfg.installLabelPath: - self.cfg.buildLabel = self.cfg.installLabelPath[0] - - buildFlavor = deps.deps.parseFlavor('is:x86(i586,!i686)') - self.cfg.buildFlavor = deps.deps.overrideFlavor( - self.cfg.buildFlavor, buildFlavor) - use.setBuildFlagsFromFlavor(None, self.cfg.buildFlavor, error=False) - - self.client = conaryclient.ConaryClient(self.cfg) - self.repos = self.client.getRepos() - self.recipeMaker = recipemaker.RecipeMaker(self.cfg, self.repos, self.rpmSource) - - def createPkgs(self, url, basePaths): - self._setupRepo() - for basePath in basePaths: - self.rpmSource.load(url, basePath) - # {foo:source: {cfg.buildLabel: None}} - srccomps = {} - - # {foo:source: foo-1.0-1.1.src.rpm} - srcmap = {} - for src in set(self.rpmSource.getSrpms(sles10pkgs)): - name = self.rpmSource.srcPath[src].name - srccomp = name + ':source' - srcmap[srccomp] = src - srccomps[srccomp] = {self.cfg.buildLabel: None} - d = self.repos.getTroveLeavesByLabel(srccomps) - - # Iterate over foo:source. - for srccomp in sorted(list(set(srccomps.iterkeys()))): - srpm = srcmap[srccomp] - pkgname = srccomp.split(':')[0] - if srccomp not in d: - self.recipeMaker.createManifest(pkgname, srpm) - else: - # aaa_base-10-12.46.src.rpm -> 10_12.46 - ver = '_'.join(srpm.rsplit('.', 2)[0].rsplit('-', 2)[1:3]) - curVer = str(d[srccomp].keys()[0].trailingRevision().version) - if rpmvercmp.rpmvercmp(ver, curVer) == 1: - self.recipeMaker.updateManifest(pkgname, srpm) - - def main(self, argv): - if '--debug' in argv: - argv.remove('--debug') - sys.excepthook = conary.lib.util.genExcepthook(debug=True) - - if '--help' in argv: - self.help() - return 0 - - url = None - basePaths = None - if len(argv) >= 2: - url = argv[1] - if len(argv) >= 3: - basePaths = argv[2:] - if not url: - url = DEFAULT_URL - if not basePaths: - basePaths = DEFAULT_BASE_PATHS - - if not url.startswith('http'): - self.help() - return 1 - - cwd = os.getcwd() - tmpdir = tempfile.mkdtemp() - print 'workdir is', tmpdir - os.chdir(tmpdir) - try: - self.createPkgs(url, basePaths) - finally: - os.chdir(cwd) - - return 0 - -if __name__ == '__main__': - pkgWiz = PkgWiz() - pkgWiz.main(sys.argv) diff --git a/extra/rpmimport/__init__.py b/extra/rpmimport/__init__.py deleted file mode 100644 --- a/extra/rpmimport/__init__.py +++ /dev/null @@ -1,18 +0,0 @@ -# -# Copyright (c) 2008 rPath, Inc. -# -# This program is distributed under the terms of the Common Public License, -# version 1.0. A copy of this license should have been distributed with this -# source file in a file called LICENSE. If it is not present, the license -# is always available at http://www.rpath.com/permanent/licenses/CPL-1.0. -# -# This program is distributed in the hope that it will be useful, but -# without any warranty; without even the implied warranty of merchantability -# or fitness for a particular purpose. See the Common Public License for -# full details. -# - -""" -Module for managing (importing, creating, updating) RPMs in a conary -repository. -""" diff --git a/extra/rpmimport/rpmsource.py b/extra/rpmimport/rpmsource.py deleted file mode 100755 --- a/extra/rpmimport/rpmsource.py +++ /dev/null @@ -1,234 +0,0 @@ -# -# Copyright (c) 2006,2008 rPath, Inc. -# -# -# This program is distributed under the terms of the Common Public License, -# version 1.0. A copy of this license should have been distributed with this -# source file in a file called LICENSE. If it is not present, the license -# is always available at http://www.rpath.com/permanent/licenses/CPL-1.0. -# -# This program is distributed in the hope that it will be useful, but -# without any warranty; without even the implied warranty of merchantability -# or fitness for a particular purpose. See the Common Public License for -# full details. -# - -""" -Module for interacting with packages in multiple yum repositories. -""" - -import os -import logging - -import repomd - -log = logging.getLogger('rpmimport.rpmsource') - -class RpmSource(object): - """ - Class that builds maps of packages from multiple yum repositories. - """ - - def __init__(self): - # {srpm: {rpm: path} - self.rpmMap = dict() - - # {name: srpm} - self.revMap = dict() - - # {srpm: path} - self.srcPath = dict() - - # {location: srpm} - self.locationMap = dict() - - # {srcPkg: srpmname} - self.srcPkgNameMap = dict() - - # {srcPkg: [binPkg, ... ] } - self.srcPkgMap = dict() - - # {binPkg: srcPkg} - self.binPkgMap = dict() - - # {srcName: [srcPkg, ... ] } - self.srcNameMap = dict() - - # {binName: [binPkg, ... ] } - self.binNameMap = dict() - - def _procSrc(self, basePath, package): - """ - Process source rpms. - @param basePath: path to yum repository. - @type basePath: string - @param package: package object - @type package: repomd.packagexml._Package - """ - shortSrpm = os.path.basename(package.location) - longLoc = basePath + '/' + package.location - package.location = longLoc - if shortSrpm not in self.srcPath: - self.srcPath[shortSrpm] = package - - self.srcPkgNameMap[package] = shortSrpm - - if package.name not in self.srcNameMap: - self.srcNameMap[package.name] = [] - self.srcNameMap[package.name].append(package) - self.locationMap[package.location] = package - - def _procBin(self, basePath, package): - """ - Process binary rpms. - @param basePath: path to yum repository. - @type basePath: string - @param package: package object - @type package: repomd.packagexml._Package - """ - srpm = package.sourcerpm - longLoc = basePath + '/' + package.location - package.location = longLoc - - if srpm not in self.rpmMap: - self.rpmMap[srpm] = {} - self.rpmMap[srpm][longLoc] = package - self.revMap[package.name] = srpm - - if package.name not in self.binNameMap: - self.binNameMap[package.name] = [] - self.binNameMap[package.name].append(package) - self.locationMap[package.location] = package - - def load(self, url, basePath=''): - """ - Walk the yum repository rooted at url/basePath and collect information - about rpms found. - @param url: url to common directory on host where all repositories resides. - @type url: string - @param basePath: directory where repository resides. - @type basePath: string - """ - - client = repomd.Client(url + '/' + basePath) - self.loadFromClient(client, basePath=basePath) - - def loadFromClient(self, client, basePath=''): - """ - Walk the yum repository rooted at url/basePath and collect information - about rpms found. - @param client: client object for extracting data from the repo metadata - @type client: repomd.Client - @param basePath: path to prefix location metadata with - @type basePath: string - """ - - for pkg in client.getPackageDetail(): - # ignore the 32-bit compatibility libs - we will - # simply use the 32-bit components from the repository - if '32bit' in pkg.name: - continue - - if pkg.sourcerpm == '': - self._procSrc(basePath, pkg) - else: - self._procBin(basePath, pkg) - - def finalize(self): - """ - Make some final datastructures now that we are done populating object. - """ - - # Now that we have processed all of the rpms, build some more data - # structures. - count = 0 - for pkg, shortSrpm in self.srcPkgNameMap.iteritems(): - if pkg in self.srcPkgMap: - continue - - if shortSrpm not in self.rpmMap: - count += 1 - #log.warn('found source without binary rpms: %s' % pkg) - if pkg in self.srcNameMap[pkg.name]: - self.srcNameMap[pkg.name].remove(pkg) - continue - - self.srcPkgMap[pkg] = self.rpmMap[shortSrpm].values() - self.srcPkgMap[pkg].append(pkg) - - for binPkg in self.srcPkgMap[pkg]: - self.binPkgMap[binPkg] = pkg - - log.warn('found %s source rpms without matching binary rpms' % count) - - def getNames(self, src): - """ - @return list that goes into the rpms line in the recipe. - """ - - names = set([ x.name for x in self.rpmMap[src].itervalues() ]) - return names - - def getRPMS(self, src): - """ - @return list of binary RPMS built from this source. - """ - return [ x for x in self.rpmMap[src].itervalues() ] - - def getSrpms(self, pkglist): - """ - Get source RPMS for a given list of binary RPM package names - """ - srpms = list() - for p in pkglist: - srpms.append(self.revMap[p]) - return srpms - - def getArchs(self, src): - """ - @return list that goes into the archs line in the recipe. - """ - - archs = set([ x.arch for x in self.rpmMap[src].itervalues() ]) - if 'i586' in archs and 'i686' in archs: - # remove the base arch if we have an extra arch - arch = self.getExtraArchs(src)[0] - if arch == 'i686': - archs.remove('i586') - return archs - - def getExtraArchs(self, src): - """ - For the special case of RPMs that have components optimized for the - i686 architecture while other components are at i586, then return - ('i686', set(rpms that are i686 only)), otherwise return (None, None). - """ - - hdrs = [ (x.arch, x.name) for x in self.rpmMap[src].itervalues() ] - archMap = {} - for arch, name in hdrs: - if arch in archMap: - archMap[arch].add(name) - else: - archMap[arch] = set((name,)) - if 'i586' in archMap and 'i686' in archMap: - if archMap['i586'] != archMap['i686']: - return 'i686', archMap['i686'] - return None, None - - def createManifest(self, srpm): - """ - @return the text for the manifest file. - """ - l = [] - l.append(self.srcPath[srpm].location) - - locs = [] - for loc in self.getRPMS(srpm): - baseLoc = os.path.basename(loc) - if baseLoc not in locs: - locs.append(baseLoc) - l.append(loc) - - # add a trailing newline - return '\n'.join(sorted(l) + ['']) From johnsonm at rpath.com Wed Aug 19 17:40:38 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:40:38 +0000 Subject: mirrorball: branch merge Message-ID: <200908192140.n7JLecx0018175@scc.eng.rpath.com> changeset: 5a1019595c48 user: Elliot Peele <https://issues.rpath.com/> date: Mon, 08 Sep 2008 01:32:50 -0400 branch merge committer: Elliot Peele <https://issues.rpath.com/> diff --git a/extra/logparse.py b/extra/logparse.py new file mode 100755 --- /dev/null +++ b/extra/logparse.py @@ -0,0 +1,579 @@ +#!/usr/bin/python +# +# Copyright (c) 2008 rPath, Inc. +# +# This program is distributed under the terms of the Common Public License, +# version 1.0. A copy of this license should have been distributed with this +# source file in a file called LICENSE. If it is not present, the license +# is always available at http://www.rpath.com/permanent/licenses/CPL-1.0. +# +# This program is distributed in the hope that it will be useful, but +# without any warranty; without even the implied warranty of merchantability +# or fitness for a particular purpose. See the Common Public License for +# full details. +# + +import os +import sys +import cmd +import logging + +sys.path.insert(0, os.environ['HOME'] + '/hg/rpath-xmllib') +sys.path.insert(0, os.environ['HOME'] + '/hg/conary') +sys.path.insert(0, os.environ['HOME'] + '/hg/mirrorball') + +from conary.conaryclient.cmdline import askYn + +from rpath_common.xmllib import api1 as xmllib + +from repomd import xmlcommon +from repomd import repository + +from updatebot import config +from updatebot import conaryhelper +from updatebot import log as logger + +logger.addRootLogger() +log = logging.getLogger('logparse') + +class _LogXml(xmlcommon.SlotNode): + pass + +class _RecordXml(xmlcommon.SlotNode): + + __slots__ = ('descriptor', 'level', 'message', 'messageId', 'pid', 'time') + + def addChild(self, child): + n = child.getName() + if n in ('descriptor', 'level', 'message', 'messageId', 'pid', 'time'): + setattr(self, n, child.finalize()) + + def __repr__(self): + return '%s\t%s: %s' % (self.level, self.descriptor, self.message) + + def __cmp__(self, other): + return cmp(self.messageId, other.messageId) + + def __hash__(self): + return hash((self.descriptor, self.messageId)) + + +class _BuildLogXml(xmlcommon.XmlFileParser): + def _registerTypes(self): + self._databinder.registerType(_LogXml, name='log') + self._databinder.registerType(_RecordXml, name='record') + self._databinder.registerType(xmllib.StringNode, name='descriptor') + self._databinder.registerType(xmllib.StringNode, name='level') + self._databinder.registerType(xmllib.StringNode, name='message') + self._databinder.registerType(xmllib.IntegerNode, name='messageId') + self._databinder.registerType(xmllib.IntegerNode, name='pid') + self._databinder.registerType(xmllib.StringNode, name='time') + + def parse(self): + data = xmlcommon.XmlFileParser.parse(self) + return [ x for x in data.iterChildren() ] + + +class _BaseRecord(object): + def __init__(self, log, record): + self.log = log + self.log._control.append(self) + for var in _RecordXml.__slots__: + setattr(self, var, getattr(record, var)) + + def __repr__(self): + return '%s\t%s: %s' % (self.level, self.descriptor, self.message) + + def isParsable(self): + if '$' in self.message: + return False + return True + + def isSupported(self): + return False + + def getCommand(self): + return None + + def parseCommand(self): + return None + + +class _ScriptActionRecord(_BaseRecord): + def __str__(self): + return self.cmd + + def parseCommand(self): + if not self.getCommand(): + return None + + cmd = [] + for e in self.cmd.split(): + if e == '2>' or e == '>' or '/dev/null' in e: + break + cmd.append(e) + + return ' '.join(cmd) + + +class OwnershipRecord(_ScriptActionRecord): + def isSupported(self): + if 'root:root' in self.cmd: + return False + elif '--reference' in self.cmd: + return False + elif not self.cmd.startswith('chown'): + return False + else: + return True + + def getCommand(self): + if not self.isSupported(): + return None + + args = [] + kwargs = {} + + recursive = False + for e in self.cmd.split(): + if e == 'chown': + continue + elif e == '-R': + recursive = True + elif ':' in e and len(e.split(':')) == 2: + owner, group = e.split(':') + args.insert(0, group) + args.insert(0, owner) + elif e == '2>' or '/dev/null' in e: + break + else: + if recursive: + e = e + '.*' + + args.append(e) + + return args, kwargs + + + +class PermissionRecord(_ScriptActionRecord): + def isSupported(self): + if not self.cmd.startswith('chmod'): + return False + elif not self.cmd.split()[1].isdigit(): + return False + elif '--refrence' in self.cmd: + return False + elif 'dpkg' in self.cmd: + return False + else: + return True + + def getCommand(self): + if not self.isSupported(): + return None + + args = [] + kwargs = {} + + mode = None + for e in self.cmd.split(): + if e == 'chmod': + continue + elif e.isdigit(): + mode = int(e) + elif e == '2>' or '/dev/null' in e: + break + else: + args.append(e) + + if not mode: + return None + + args.append(mode) + return args, kwargs + + +class InitscriptRecord(_ScriptActionRecord): + def isSupported(self): + if not self.cmd.startswith('update-rc.d'): + return False + elif 'remove' in self.cmd: + return False + else: + return True + + def getCommand(self): + if not self.isSupported(): + return None + + args = [] + kwargs = {} + + for e in self.cmd.split(): + if e == 'update-rc.d': + continue + elif e == '>' or '/dev/null' in e: + break + else: + args.append(e) + + return args, kwargs + + +class CreateDirectoryRecord(_ScriptActionRecord): + def getDirectory(self): + cmd = self.cmd.strip(',') + index = cmd.find('/') + return cmd[index:] + + def isSupported(self): + index = self.cmd.find('mkdir') + self.cmd = self.cmd[index:] + + if not self.cmd.startswith('mkdir'): + return False + else: + return True + + def getCommand(self): + if not self.isSupported(): + return None + + args = [] + kwargs = {} + + mode = False + for e in self.cmd.split(): + if e == 'mkdir': + continue + elif e == '-p': + continue + elif e == '-m': + mode = True + elif mode and e.isdigit(): + kwargs['mode'] = int(mode) + elif e == '2>' or e == '>' or '/dev/null' in e: + break + else: + args.append(e) + + return args, kwargs + + +class AlternativesRecord(_ScriptActionRecord): + def isSupported(self): + if not self.cmd.startswith('update-alternatives'): + return False + elif '--remove' in self.cmd: + return False + else: + return True + + def getCommand(self): + if not self.isSupported(): + return None + + args = [] + kwargs = {} + + for e in self.cmd.split(): + if e == '>' or e == '2>' or '/dev/null' in e: + break + else: + args.append(e) + + return args, kwargs + + +class UserInfoRecord(_ScriptActionRecord): + def isSupported(self): + cmds = ('adduser', 'useradd', 'groupadd', 'addgroup') + if not self.cmd.split()[0] in cmds: + return False + else: + return True + + +class DeviceNodeRecord(_ScriptActionRecord): + pass + + +class UnhandledRecord(_ScriptActionRecord): + pass + + +def ScriptActionRecord(log, record): + assert record.message.startswith('warning: ReportScriptActions: ') + m = record.message[len('warning: ReportScriptActions: '):] + if '$' in m: + klass = UnhandledRecord + elif 'chown' in m: + klass = OwnershipRecord + elif 'chmod' in m: + klass = PermissionRecord + elif 'update-rc.d' in m: + klass = InitscriptRecord + elif 'mkdir' in m: + klass = CreateDirectoryRecord + elif 'update-alternatives' in m: + klass = AlternativesRecord + elif ('useradd' in m or + 'adduser' in m or + 'groupadd' in m or + 'addgroup' in m): + klass = UserInfoRecord + elif 'mknod' in m: + klass = DeviceNodeRecord + else: + klass = UnhandledRecord + + obj = klass(log, record) + obj.cmd = m + return obj + + +class ExcludeDirectoriesRecord(_BaseRecord): + def getDirectory(self): + index = self.message.find('/') + return self.message[index:] + + def isSupported(self): + if not self.message.startswith('+ ExcludeDirectories: excluding '): + return False + + message = self.message[len('+ ExcludeDirectories: excluding '):] + if not message.startswith('empty') and not message.startswith('directory'): + return False + + self.short = message + return True + + def getCommand(self): + if not self.isSupported(): + return None + + blackList = ( + '/dev', + '/etc/apparmor.d', + '/lib', + '/usr/lib', + '/sbin', + '/usr/sbin', + '/bin', + '/usr/bin', + '/usr/share/man', + '/usr/share/consolefonts', + '/var/lib/pycentral', + '/usr/lib/locale', + '/usr/share/perl5', + ) + + args = [] + kwargs = {} + + mode = False + for e in self.short.split(): + if e == 'empty': + continue + elif e == 'directory': + continue + elif e == 'with': + continue + elif e == 'mode': + mode = True + elif mode and e.isdigit(): + kwargs['mode'] = int(e) + elif e.startswith('/'): + matches = [ x for x in blackList if e.startswith(x) ] + if not matches: + args.append(e) + + if len(args) == 0: + return None + + return args, kwargs + + def __str__(self): + return self.short + + def parseCommand(self): + cmd = self.getCommand() + if not cmd: + return None + + args, kwargs = cmd + + new = 'mkdir ' + if 'mode' in kwargs: + new += '-m %s' % kwargs['mode'] + + for arg in args: + new += ' %s' % arg + + return new + + +class BuildLog(object): + def __init__(self, path, helper): + assert path.endswith('-xml') + + self._path = path + self._helper = helper + self._name = os.path.basename(path) + self._repository = repository.Repository('file:/') + self._parser = _BuildLogXml(self._repository, self._path) + + self._data = self._parser.parse() + + self._control = [] + + def auditPolicy(self): + recordFilter = ( + 'cook.build.policy.ENFORCEMENT.ReportScriptActions', + 'cook.build.policy.PACKAGE_CREATION.ExcludeDirectories' + ) + + negativeFilter = ( + 'Running policy: ReportScriptActions', + 'Running policy: ExcludeDirectories', + '+ ExcludeDirectories: excluding empty directory /lib', + '+ ExcludeDirectories: excluding empty directory /usr/lib', + ) + + records = [] + for record in self._data: + if (record.descriptor in recordFilter and + record.message not in negativeFilter): + obj = None + if record.descriptor == \ + 'cook.build.policy.ENFORCEMENT.ReportScriptActions': + obj = ScriptActionRecord(self, record) + elif record.descriptor == \ + 'cook.build.policy.PACKAGE_CREATION.ExcludeDirectories': + if record.message.startswith('+ ExcludeDirectories: excluding'): + obj = ExcludeDirectoriesRecord(self, record) + if obj: + records.append(obj) + return records + + def getControl(self): + valid = [ x.parseCommand() for x in self._control if x.getCommand() ] + + if not valid: + return '' + + ret = '\n'.join(valid) + ret += '\n' + return ret + + def _getPkgName(self): + slice = self._name.split('-') + return '-'.join(slice[:-2]) + + def writeControl(self): + pkgName = self._getPkgName() + log.info('writing control file for %s' % pkgName) + + recipeDir = self.helper._checkout(pkgName) + fd = open(os.path.join(recipeDir, 'control'), 'w') + fd.write(self.getControl()) + fd.close() + self.helper._addFile(recipeDir, 'control') + self.helper._commit(recipeDir, 'add/update control file') + + def __repr__(self): + return self._name + + def __cmp__(self, other): + return cmp(self._name, other._name) + + +class BuildLogAnalyzer(object): + def __init__(self, directory, helper): + self._logDir = os.path.abspath(directory) + self._helper = helper + + self._logFiles = [] + for logfile in os.listdir(self._logDir): + if logfile.endswith('-xml'): + path = os.path.join(self._logDir, logfile) + self._logFiles.append(BuildLog(path, helper)) + self._logFiles.sort() + + self._buckets = {} + + def auditPolicy(self): + new = True + + numPkgs = [ x for x in self._logFiles if x.auditPolicy() ] + print ("Found %s of %s packages with policy errors" + % (len(numPkgs), len(self._logFiles))) + + prompt = 80 * '=' + '\n' + 'Repeat Log Data? (y/N): ' + + for logObj in self._logFiles: + while new or askYn(prompt): + new = False + + records = logObj.auditPolicy() + if not records: + break + + print 'Package: ', logObj + for rec in records: + print rec + + new = True + + def makeBuckets(self): + for logObj in self._logFiles: + for record in logObj.auditPolicy(): + key = record.__class__.__name__ + if key not in self._buckets: + self._buckets[key] = [] + self._buckets[key].append(record) + + def findPathConflicts(self): + d = {} + for record in (self._buckets['ExcludeDirectoriesRecord'] + + self._buckets['CreateDirectoryRecord']): + if not record.isParsable(): + continue + key = record.getDirectory() + if key not in d: + d[key] = set() + d[key].add(record.log) + + for key, value in d.iteritems(): + if len(value) > 1: + print ('Found path conflict (%s) between the following ' + 'packages: %s' % (key, ', '.join(map(str, value)))) + + def writeControl(self): + for logObj in self._logFiles: + lobObj.writeControl() + + +if __name__ == '__main__': + import sys + from conary.lib import util + sys.excepthook = util.genExcepthook() + + cfg = config.UpdateBotConfig() + cfg.read(os.environ['HOME'] + '/hg/mirrorball/config/ubuntu/updatebotrc') + + helper = conaryhelper.ConaryHelper(cfg) + + obj = BuildLogAnalyzer(sys.argv[1], helper) +# obj.auditPolicy() + obj.makeBuckets() +# obj.findPathConflicts() + + for logObj in obj._logFiles: + print 80 * '=' + print 'Package:', logObj + print logObj.getControl() + logObj.writeControl() + + import epdb; epdb.st() From johnsonm at rpath.com Wed Aug 19 17:40:37 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:40:37 +0000 Subject: mirrorball: parse log files conver to control files Message-ID: <200908192140.n7JLebVi018141@scc.eng.rpath.com> changeset: d2729dd7c01d user: Elliot Peele <https://issues.rpath.com/> date: Mon, 08 Sep 2008 01:32:16 -0400 parse log files conver to control files committer: Elliot Peele <https://issues.rpath.com/> diff --git a/extra/logparse.py b/extra/logparse.py new file mode 100755 --- /dev/null +++ b/extra/logparse.py @@ -0,0 +1,579 @@ +#!/usr/bin/python +# +# Copyright (c) 2008 rPath, Inc. +# +# This program is distributed under the terms of the Common Public License, +# version 1.0. A copy of this license should have been distributed with this +# source file in a file called LICENSE. If it is not present, the license +# is always available at http://www.rpath.com/permanent/licenses/CPL-1.0. +# +# This program is distributed in the hope that it will be useful, but +# without any warranty; without even the implied warranty of merchantability +# or fitness for a particular purpose. See the Common Public License for +# full details. +# + +import os +import sys +import cmd +import logging + +sys.path.insert(0, os.environ['HOME'] + '/hg/rpath-xmllib') +sys.path.insert(0, os.environ['HOME'] + '/hg/conary') +sys.path.insert(0, os.environ['HOME'] + '/hg/mirrorball') + +from conary.conaryclient.cmdline import askYn + +from rpath_common.xmllib import api1 as xmllib + +from repomd import xmlcommon +from repomd import repository + +from updatebot import config +from updatebot import conaryhelper +from updatebot import log as logger + +logger.addRootLogger() +log = logging.getLogger('logparse') + +class _LogXml(xmlcommon.SlotNode): + pass + +class _RecordXml(xmlcommon.SlotNode): + + __slots__ = ('descriptor', 'level', 'message', 'messageId', 'pid', 'time') + + def addChild(self, child): + n = child.getName() + if n in ('descriptor', 'level', 'message', 'messageId', 'pid', 'time'): + setattr(self, n, child.finalize()) + + def __repr__(self): + return '%s\t%s: %s' % (self.level, self.descriptor, self.message) + + def __cmp__(self, other): + return cmp(self.messageId, other.messageId) + + def __hash__(self): + return hash((self.descriptor, self.messageId)) + + +class _BuildLogXml(xmlcommon.XmlFileParser): + def _registerTypes(self): + self._databinder.registerType(_LogXml, name='log') + self._databinder.registerType(_RecordXml, name='record') + self._databinder.registerType(xmllib.StringNode, name='descriptor') + self._databinder.registerType(xmllib.StringNode, name='level') + self._databinder.registerType(xmllib.StringNode, name='message') + self._databinder.registerType(xmllib.IntegerNode, name='messageId') + self._databinder.registerType(xmllib.IntegerNode, name='pid') + self._databinder.registerType(xmllib.StringNode, name='time') + + def parse(self): + data = xmlcommon.XmlFileParser.parse(self) + return [ x for x in data.iterChildren() ] + + +class _BaseRecord(object): + def __init__(self, log, record): + self.log = log + self.log._control.append(self) + for var in _RecordXml.__slots__: + setattr(self, var, getattr(record, var)) + + def __repr__(self): + return '%s\t%s: %s' % (self.level, self.descriptor, self.message) + + def isParsable(self): + if '$' in self.message: + return False + return True + + def isSupported(self): + return False + + def getCommand(self): + return None + + def parseCommand(self): + return None + + +class _ScriptActionRecord(_BaseRecord): + def __str__(self): + return self.cmd + + def parseCommand(self): + if not self.getCommand(): + return None + + cmd = [] + for e in self.cmd.split(): + if e == '2>' or e == '>' or '/dev/null' in e: + break + cmd.append(e) + + return ' '.join(cmd) + + +class OwnershipRecord(_ScriptActionRecord): + def isSupported(self): + if 'root:root' in self.cmd: + return False + elif '--reference' in self.cmd: + return False + elif not self.cmd.startswith('chown'): + return False + else: + return True + + def getCommand(self): + if not self.isSupported(): + return None + + args = [] + kwargs = {} + + recursive = False + for e in self.cmd.split(): + if e == 'chown': + continue + elif e == '-R': + recursive = True + elif ':' in e and len(e.split(':')) == 2: + owner, group = e.split(':') + args.insert(0, group) + args.insert(0, owner) + elif e == '2>' or '/dev/null' in e: + break + else: + if recursive: + e = e + '.*' + + args.append(e) + + return args, kwargs + + + +class PermissionRecord(_ScriptActionRecord): + def isSupported(self): + if not self.cmd.startswith('chmod'): + return False + elif not self.cmd.split()[1].isdigit(): + return False + elif '--refrence' in self.cmd: + return False + elif 'dpkg' in self.cmd: + return False + else: + return True + + def getCommand(self): + if not self.isSupported(): + return None + + args = [] + kwargs = {} + + mode = None + for e in self.cmd.split(): + if e == 'chmod': + continue + elif e.isdigit(): + mode = int(e) + elif e == '2>' or '/dev/null' in e: + break + else: + args.append(e) + + if not mode: + return None + + args.append(mode) + return args, kwargs + + +class InitscriptRecord(_ScriptActionRecord): + def isSupported(self): + if not self.cmd.startswith('update-rc.d'): + return False + elif 'remove' in self.cmd: + return False + else: + return True + + def getCommand(self): + if not self.isSupported(): + return None + + args = [] + kwargs = {} + + for e in self.cmd.split(): + if e == 'update-rc.d': + continue + elif e == '>' or '/dev/null' in e: + break + else: + args.append(e) + + return args, kwargs + + +class CreateDirectoryRecord(_ScriptActionRecord): + def getDirectory(self): + cmd = self.cmd.strip(',') + index = cmd.find('/') + return cmd[index:] + + def isSupported(self): + index = self.cmd.find('mkdir') + self.cmd = self.cmd[index:] + + if not self.cmd.startswith('mkdir'): + return False + else: + return True + + def getCommand(self): + if not self.isSupported(): + return None + + args = [] + kwargs = {} + + mode = False + for e in self.cmd.split(): + if e == 'mkdir': + continue + elif e == '-p': + continue + elif e == '-m': + mode = True + elif mode and e.isdigit(): + kwargs['mode'] = int(mode) + elif e == '2>' or e == '>' or '/dev/null' in e: + break + else: + args.append(e) + + return args, kwargs + + +class AlternativesRecord(_ScriptActionRecord): + def isSupported(self): + if not self.cmd.startswith('update-alternatives'): + return False + elif '--remove' in self.cmd: + return False + else: + return True + + def getCommand(self): + if not self.isSupported(): + return None + + args = [] + kwargs = {} + + for e in self.cmd.split(): + if e == '>' or e == '2>' or '/dev/null' in e: + break + else: + args.append(e) + + return args, kwargs + + +class UserInfoRecord(_ScriptActionRecord): + def isSupported(self): + cmds = ('adduser', 'useradd', 'groupadd', 'addgroup') + if not self.cmd.split()[0] in cmds: + return False + else: + return True + + +class DeviceNodeRecord(_ScriptActionRecord): + pass + + +class UnhandledRecord(_ScriptActionRecord): + pass + + +def ScriptActionRecord(log, record): + assert record.message.startswith('warning: ReportScriptActions: ') + m = record.message[len('warning: ReportScriptActions: '):] + if '$' in m: + klass = UnhandledRecord + elif 'chown' in m: + klass = OwnershipRecord + elif 'chmod' in m: + klass = PermissionRecord + elif 'update-rc.d' in m: + klass = InitscriptRecord + elif 'mkdir' in m: + klass = CreateDirectoryRecord + elif 'update-alternatives' in m: + klass = AlternativesRecord + elif ('useradd' in m or + 'adduser' in m or + 'groupadd' in m or + 'addgroup' in m): + klass = UserInfoRecord + elif 'mknod' in m: + klass = DeviceNodeRecord + else: + klass = UnhandledRecord + + obj = klass(log, record) + obj.cmd = m + return obj + + +class ExcludeDirectoriesRecord(_BaseRecord): + def getDirectory(self): + index = self.message.find('/') + return self.message[index:] + + def isSupported(self): + if not self.message.startswith('+ ExcludeDirectories: excluding '): + return False + + message = self.message[len('+ ExcludeDirectories: excluding '):] + if not message.startswith('empty') and not message.startswith('directory'): + return False + + self.short = message + return True + + def getCommand(self): + if not self.isSupported(): + return None + + blackList = ( + '/dev', + '/etc/apparmor.d', + '/lib', + '/usr/lib', + '/sbin', + '/usr/sbin', + '/bin', + '/usr/bin', + '/usr/share/man', + '/usr/share/consolefonts', + '/var/lib/pycentral', + '/usr/lib/locale', + '/usr/share/perl5', + ) + + args = [] + kwargs = {} + + mode = False + for e in self.short.split(): + if e == 'empty': + continue + elif e == 'directory': + continue + elif e == 'with': + continue + elif e == 'mode': + mode = True + elif mode and e.isdigit(): + kwargs['mode'] = int(e) + elif e.startswith('/'): + matches = [ x for x in blackList if e.startswith(x) ] + if not matches: + args.append(e) + + if len(args) == 0: + return None + + return args, kwargs + + def __str__(self): + return self.short + + def parseCommand(self): + cmd = self.getCommand() + if not cmd: + return None + + args, kwargs = cmd + + new = 'mkdir ' + if 'mode' in kwargs: + new += '-m %s' % kwargs['mode'] + + for arg in args: + new += ' %s' % arg + + return new + + +class BuildLog(object): + def __init__(self, path, helper): + assert path.endswith('-xml') + + self._path = path + self._helper = helper + self._name = os.path.basename(path) + self._repository = repository.Repository('file:/') + self._parser = _BuildLogXml(self._repository, self._path) + + self._data = self._parser.parse() + + self._control = [] + + def auditPolicy(self): + recordFilter = ( + 'cook.build.policy.ENFORCEMENT.ReportScriptActions', + 'cook.build.policy.PACKAGE_CREATION.ExcludeDirectories' + ) + + negativeFilter = ( + 'Running policy: ReportScriptActions', + 'Running policy: ExcludeDirectories', + '+ ExcludeDirectories: excluding empty directory /lib', + '+ ExcludeDirectories: excluding empty directory /usr/lib', + ) + + records = [] + for record in self._data: + if (record.descriptor in recordFilter and + record.message not in negativeFilter): + obj = None + if record.descriptor == \ + 'cook.build.policy.ENFORCEMENT.ReportScriptActions': + obj = ScriptActionRecord(self, record) + elif record.descriptor == \ + 'cook.build.policy.PACKAGE_CREATION.ExcludeDirectories': + if record.message.startswith('+ ExcludeDirectories: excluding'): + obj = ExcludeDirectoriesRecord(self, record) + if obj: + records.append(obj) + return records + + def getControl(self): + valid = [ x.parseCommand() for x in self._control if x.getCommand() ] + + if not valid: + return '' + + ret = '\n'.join(valid) + ret += '\n' + return ret + + def _getPkgName(self): + slice = self._name.split('-') + return '-'.join(slice[:-2]) + + def writeControl(self): + pkgName = self._getPkgName() + log.info('writing control file for %s' % pkgName) + + recipeDir = self.helper._checkout(pkgName) + fd = open(os.path.join(recipeDir, 'control'), 'w') + fd.write(self.getControl()) + fd.close() + self.helper._addFile(recipeDir, 'control') + self.helper._commit(recipeDir, 'add/update control file') + + def __repr__(self): + return self._name + + def __cmp__(self, other): + return cmp(self._name, other._name) + + +class BuildLogAnalyzer(object): + def __init__(self, directory, helper): + self._logDir = os.path.abspath(directory) + self._helper = helper + + self._logFiles = [] + for logfile in os.listdir(self._logDir): + if logfile.endswith('-xml'): + path = os.path.join(self._logDir, logfile) + self._logFiles.append(BuildLog(path, helper)) + self._logFiles.sort() + + self._buckets = {} + + def auditPolicy(self): + new = True + + numPkgs = [ x for x in self._logFiles if x.auditPolicy() ] + print ("Found %s of %s packages with policy errors" + % (len(numPkgs), len(self._logFiles))) + + prompt = 80 * '=' + '\n' + 'Repeat Log Data? (y/N): ' + + for logObj in self._logFiles: + while new or askYn(prompt): + new = False + + records = logObj.auditPolicy() + if not records: + break + + print 'Package: ', logObj + for rec in records: + print rec + + new = True + + def makeBuckets(self): + for logObj in self._logFiles: + for record in logObj.auditPolicy(): + key = record.__class__.__name__ + if key not in self._buckets: + self._buckets[key] = [] + self._buckets[key].append(record) + + def findPathConflicts(self): + d = {} + for record in (self._buckets['ExcludeDirectoriesRecord'] + + self._buckets['CreateDirectoryRecord']): + if not record.isParsable(): + continue + key = record.getDirectory() + if key not in d: + d[key] = set() + d[key].add(record.log) + + for key, value in d.iteritems(): + if len(value) > 1: + print ('Found path conflict (%s) between the following ' + 'packages: %s' % (key, ', '.join(map(str, value)))) + + def writeControl(self): + for logObj in self._logFiles: + lobObj.writeControl() + + +if __name__ == '__main__': + import sys + from conary.lib import util + sys.excepthook = util.genExcepthook() + + cfg = config.UpdateBotConfig() + cfg.read(os.environ['HOME'] + '/hg/mirrorball/config/ubuntu/updatebotrc') + + helper = conaryhelper.ConaryHelper(cfg) + + obj = BuildLogAnalyzer(sys.argv[1], helper) +# obj.auditPolicy() + obj.makeBuckets() +# obj.findPathConflicts() + + for logObj in obj._logFiles: + print 80 * '=' + print 'Package:', logObj + print logObj.getControl() + logObj.writeControl() + + import epdb; epdb.st() From johnsonm at rpath.com Wed Aug 19 17:42:33 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:42:33 +0000 Subject: mirrorball: remove 26 references Message-ID: <200908192142.n7JLgXXk022796@scc.eng.rpath.com> changeset: f6b3b88f75ee user: Elliot Peele <https://issues.rpath.com/> date: Mon, 16 Mar 2009 11:39:49 -0400 remove 26 references committer: Elliot Peele <https://issues.rpath.com/> diff --git a/scripts/buildgroups b/scripts/buildgroups --- a/scripts/buildgroups +++ b/scripts/buildgroups @@ -1,6 +1,6 @@ -#!/usr/bin/python2.6 +#!/usr/bin/python # -# Copryright (c) 2008 rPath, Inc. +# Copryright (c) 2008-2009 rPath, Inc. # """ diff --git a/scripts/buildpackages b/scripts/buildpackages --- a/scripts/buildpackages +++ b/scripts/buildpackages @@ -1,6 +1,6 @@ -#!/usr/bin/python2.6 +#!/usr/bin/python # -# Copryright (c) 2008 rPath, Inc. +# Copryright (c) 2008-2009 rPath, Inc. # """ diff --git a/scripts/findbinaries.py b/scripts/findbinaries.py --- a/scripts/findbinaries.py +++ b/scripts/findbinaries.py @@ -1,6 +1,6 @@ -#!/usr/bin/python2.6 +#!/usr/bin/python # -# Copyright (c) 2008 rPath, Inc. +# Copyright (c) 2008-2009 rPath, Inc. # # This program is distributed under the terms of the Common Public License, # version 1.0. A copy of this license should have been distributed with this @@ -16,11 +16,11 @@ import os import sys -sys.path.insert(0, os.environ['HOME'] + '/hg/26/rpath-xmllib') -sys.path.insert(0, os.environ['HOME'] + '/hg/26/conary') -sys.path.insert(0, os.environ['HOME'] + '/hg/26/mirrorball') -sys.path.insert(0, os.environ['HOME'] + '/hg/26/xobj/py') -sys.path.insert(0, os.environ['HOME'] + '/hg/26/rmake') +sys.path.insert(0, os.environ['HOME'] + '/hg/rpath-xmllib') +sys.path.insert(0, os.environ['HOME'] + '/hg/conary') +sys.path.insert(0, os.environ['HOME'] + '/hg/mirrorball') +sys.path.insert(0, os.environ['HOME'] + '/hg/xobj/py') +sys.path.insert(0, os.environ['HOME'] + '/hg/rmake') from conary.lib import util sys.excepthook = util.genExcepthook() @@ -39,7 +39,7 @@ slog = logging.getLogger('findbinaries') cfg = config.UpdateBotConfig() -cfg.read(os.environ['HOME'] + '/hg/26/mirrorball/config/sles/updatebotrc') +cfg.read(os.environ['HOME'] + '/hg/mirrorball/config/sles/updatebotrc') bot = bot.Bot(cfg) updater = bot._updater diff --git a/scripts/forceupdate.py b/scripts/forceupdate.py --- a/scripts/forceupdate.py +++ b/scripts/forceupdate.py @@ -1,6 +1,6 @@ -#!/usr/bin/python2.6 +#!/usr/bin/python # -# Copyright (c) 2008 rPath, Inc. +# Copyright (c) 2008-2009 rPath, Inc. # # This program is distributed under the terms of the Common Public License, # version 1.0. A copy of this license should have been distributed with this diff --git a/scripts/genmanifest.py b/scripts/genmanifest.py --- a/scripts/genmanifest.py +++ b/scripts/genmanifest.py @@ -1,6 +1,6 @@ -#!/usr/bin/python2.6 +#!/usr/bin/python # -# Copyright (c) 2008 rPath, Inc. +# Copyright (c) 2008-2009 rPath, Inc. # # This program is distributed under the terms of the Common Public License, # version 1.0. A copy of this license should have been distributed with this @@ -16,11 +16,11 @@ import os import sys -sys.path.insert(0, os.environ['HOME'] + '/hg/26/rpath-xmllib') -sys.path.insert(0, os.environ['HOME'] + '/hg/26/conary') -sys.path.insert(0, os.environ['HOME'] + '/hg/26/mirrorball') -sys.path.insert(0, os.environ['HOME'] + '/hg/26/rmake') -sys.path.insert(0, os.environ['HOME'] + '/hg/26/epdb') +sys.path.insert(0, os.environ['HOME'] + '/hg/rpath-xmllib') +sys.path.insert(0, os.environ['HOME'] + '/hg/conary') +sys.path.insert(0, os.environ['HOME'] + '/hg/mirrorball') +sys.path.insert(0, os.environ['HOME'] + '/hg/rmake') +sys.path.insert(0, os.environ['HOME'] + '/hg/epdb') from conary.lib import util sys.excepthook = util.genExcepthook() diff --git a/scripts/header.py b/scripts/header.py --- a/scripts/header.py +++ b/scripts/header.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2008 rPath, Inc. +# Copyright (c) 2008-2009 rPath, Inc. # # This program is distributed under the terms of the Common Public License, # version 1.0. A copy of this license should have been distributed with this @@ -15,12 +15,11 @@ import os import sys -sys.path.insert(0, os.environ['HOME'] + '/hg/26/rpath-xmllib') -sys.path.insert(0, os.environ['HOME'] + '/hg/26/conary') -sys.path.insert(0, os.environ['HOME'] + '/hg/26/mirrorball') -sys.path.insert(0, os.environ['HOME'] + '/hg/26/epdb') -sys.path.insert(0, os.environ['HOME'] + '/hg/26/rmake') -sys.path.insert(0, os.environ['HOME'] + '/hg/26/xobj/py') +sys.path.insert(0, os.environ['HOME'] + '/hg/rpath-xmllib') +sys.path.insert(0, os.environ['HOME'] + '/hg/conary') +sys.path.insert(0, os.environ['HOME'] + '/hg/mirrorball') +sys.path.insert(0, os.environ['HOME'] + '/hg/rmake') +sys.path.insert(0, os.environ['HOME'] + '/hg/xobj/py') from conary.lib import util sys.excepthook = util.genExcepthook() @@ -38,7 +37,7 @@ log.addRootLogger() cfg = config.UpdateBotConfig() -cfg.read(os.environ['HOME'] + '/hg/26/mirrorball/config/%s/updatebotrc' % sys.argv[1]) +cfg.read(os.environ['HOME'] + '/hg/mirrorball/config/%s/updatebotrc' % sys.argv[1]) builder = build.Builder(cfg) diff --git a/scripts/import.py b/scripts/import.py --- a/scripts/import.py +++ b/scripts/import.py @@ -1,6 +1,6 @@ #!/usr/bin/python # -# Copyright (c) 2008 rPath, Inc. +# Copyright (c) 2008-2009 rPath, Inc. # # This program is distributed under the terms of the Common Public License, # version 1.0. A copy of this license should have been distributed with this @@ -17,13 +17,10 @@ import sys sys.path.insert(0, os.environ['HOME'] + '/hg/xobj/py') - -#sys.path.insert(0, os.environ['HOME'] + '/hg/26/rpath-xmllib') -#sys.path.insert(0, os.environ['HOME'] + '/hg/26/conary') -sys.path.insert(0, os.environ['HOME'] + '/hg/26/mirrorball') -#sys.path.insert(0, os.environ['HOME'] + '/hg/26/rmake') -#sys.path.insert(0, os.environ['HOME'] + '/hg/26/epdb') -#sys.path.insert(0, os.environ['HOME'] + '/hg/26/xobj/py') +sys.path.insert(0, os.environ['HOME'] + '/hg/rpath-xmllib') +sys.path.insert(0, os.environ['HOME'] + '/hg/conary') +sys.path.insert(0, os.environ['HOME'] + '/hg/mirrorball') +sys.path.insert(0, os.environ['HOME'] + '/hg/rmake') from conary.lib import util sys.excepthook = util.genExcepthook() @@ -32,7 +29,7 @@ log.addRootLogger() cfg = config.UpdateBotConfig() -cfg.read(os.environ['HOME'] + '/hg/26/mirrorball/config/%s/updatebotrc' % sys.argv[1]) +cfg.read(os.environ['HOME'] + '/hg/mirrorball/config/%s/updatebotrc' % sys.argv[1]) obj = bot.Bot(cfg) trvMap = obj.create() diff --git a/scripts/mirror.py b/scripts/mirror.py --- a/scripts/mirror.py +++ b/scripts/mirror.py @@ -1,6 +1,6 @@ -#!/usr/bin/python2.6 +#!/usr/bin/python # -# Copyright (c) 2008 rPath, Inc. +# Copyright (c) 2008-2009 rPath, Inc. # # This program is distributed under the terms of the Common Public License, # version 1.0. A copy of this license should have been distributed with this diff --git a/scripts/pcheck.py b/scripts/pcheck.py --- a/scripts/pcheck.py +++ b/scripts/pcheck.py @@ -1,4 +1,21 @@ #!/usr/bin/python +# +# Copyright 2008-2009 rPath, Inc. +# +# This program is distributed under the terms of the Common Public License, +# version 1.0. A copy of this license should have been distributed with this +# source file in a file called LICENSE. If it is not present, the license +# is always available at http://www.rpath.com/permanent/licenses/CPL-1.0. +# +# This program is distributed in the hope that it will be useful, but +# without any warranty; without even the implied warranty of merchantability +# or fitness for a particular purpose. See the Common Public License for +# full details. +# + +""" +Check sanity of the promote from rpl:1 -> rpl:1-qa. +""" import os import sys diff --git a/scripts/pkgsource.py b/scripts/pkgsource.py --- a/scripts/pkgsource.py +++ b/scripts/pkgsource.py @@ -1,13 +1,25 @@ -#!/usr/bin/python2.6 +#!/usr/bin/python +# +# Copyright (c) 2008-2009 rPath, Inc. +# +# This program is distributed under the terms of the Common Public License, +# version 1.0. A copy of this license should have been distributed with this +# source file in a file called LICENSE. If it is not present, the license +# is always available at http://www.rpath.com/permanent/licenses/CPL-1.0. +# +# This program is distributed in the hope that it will be useful, but +# without any warranty; without even the implied warranty of merchantability +# or fitness for a particular purpose. See the Common Public License for +# full details. +# import os import sys -sys.path.insert(0, os.environ['HOME'] + '/hg/26/rpath-xmllib') -sys.path.insert(0, os.environ['HOME'] + '/hg/26/conary') -sys.path.insert(0, os.environ['HOME'] + '/hg/26/mirrorball') -sys.path.insert(0, os.environ['HOME'] + '/hg/26/rmake') -sys.path.insert(0, os.environ['HOME'] + '/hg/26/epdb') +sys.path.insert(0, os.environ['HOME'] + '/hg/rpath-xmllib') +sys.path.insert(0, os.environ['HOME'] + '/hg/conary') +sys.path.insert(0, os.environ['HOME'] + '/hg/mirrorball') +sys.path.insert(0, os.environ['HOME'] + '/hg/rmake') from conary.lib import util sys.excepthook = util.genExcepthook() @@ -22,7 +34,7 @@ from updatebot import pkgsource cfg = config.UpdateBotConfig() -cfg.read(os.environ['HOME'] + '/hg/26/mirrorball/config/%s/updatebotrc' % sys.argv[1] ) +cfg.read(os.environ['HOME'] + '/hg/mirrorball/config/%s/updatebotrc' % sys.argv[1] ) pkgSource = pkgsource.PackageSource(cfg) pkgSource.load() diff --git a/scripts/promote.py b/scripts/promote.py --- a/scripts/promote.py +++ b/scripts/promote.py @@ -1,6 +1,6 @@ -#!/usr/bin/python2.6 +#!/usr/bin/python # -# Copyright (c) 2008 rPath, Inc. +# Copyright (c) 2008-2009 rPath, Inc. # # This program is distributed under the terms of the Common Public License, # version 1.0. A copy of this license should have been distributed with this diff --git a/scripts/rebuild.py b/scripts/rebuild.py --- a/scripts/rebuild.py +++ b/scripts/rebuild.py @@ -1,6 +1,6 @@ -#!/usr/bin/python2.6 +#!/usr/bin/python # -# Copyright (c) 2008 rPath, Inc. +# Copyright (c) 2008-2009 rPath, Inc. # # This program is distributed under the terms of the Common Public License, # version 1.0. A copy of this license should have been distributed with this @@ -16,12 +16,11 @@ import os import sys -sys.path.insert(0, os.environ['HOME'] + '/hg/26/rpath-xmllib') -sys.path.insert(0, os.environ['HOME'] + '/hg/26/conary') -sys.path.insert(0, os.environ['HOME'] + '/hg/26/mirrorball') -sys.path.insert(0, os.environ['HOME'] + '/hg/26/rmake') -sys.path.insert(0, os.environ['HOME'] + '/hg/26/epdb') -sys.path.insert(0, os.environ['HOME'] + '/hg/26/xobj/py') +sys.path.insert(0, os.environ['HOME'] + '/hg/rpath-xmllib') +sys.path.insert(0, os.environ['HOME'] + '/hg/conary') +sys.path.insert(0, os.environ['HOME'] + '/hg/mirrorball') +sys.path.insert(0, os.environ['HOME'] + '/hg/rmake') +sys.path.insert(0, os.environ['HOME'] + '/hg/xobj/py') from conary.lib import util sys.excepthook = util.genExcepthook() @@ -30,7 +29,7 @@ log.addRootLogger() cfg = config.UpdateBotConfig() -cfg.read(os.environ['HOME'] + '/hg/26/mirrorball/config/%s/updatebotrc' % sys.argv[1]) +cfg.read(os.environ['HOME'] + '/hg/mirrorball/config/%s/updatebotrc' % sys.argv[1]) obj = bot.Bot(cfg) trvMap = obj.create(rebuild=True) diff --git a/scripts/testimport.py b/scripts/testimport.py --- a/scripts/testimport.py +++ b/scripts/testimport.py @@ -1,6 +1,6 @@ -#!/usr/bin/python2.6 +#!/usr/bin/python # -# Copyright (c) 2008 rPath, Inc. +# Copyright (c) 2008-2009 rPath, Inc. # # This program is distributed under the terms of the Common Public License, # version 1.0. A copy of this license should have been distributed with this @@ -16,11 +16,10 @@ import os import sys -sys.path.insert(0, os.environ['HOME'] + '/hg/26/rpath-xmllib') -sys.path.insert(0, os.environ['HOME'] + '/hg/26/conary') -sys.path.insert(0, os.environ['HOME'] + '/hg/26/mirrorball') -sys.path.insert(0, os.environ['HOME'] + '/hg/26/epdb') -sys.path.insert(0, os.environ['HOME'] + '/hg/26/rmake') +sys.path.insert(0, os.environ['HOME'] + '/hg/rpath-xmllib') +sys.path.insert(0, os.environ['HOME'] + '/hg/conary') +sys.path.insert(0, os.environ['HOME'] + '/hg/mirrorball') +sys.path.insert(0, os.environ['HOME'] + '/hg/rmake') from conary.lib import util sys.excethook = util.genExcepthook() @@ -36,7 +35,7 @@ log.addRootLogger() cfg = config.UpdateBotConfig() -cfg.read(os.environ['HOME'] + '/hg/26/mirrorball/config/%s/updatebotrc' % sys.argv[1]) +cfg.read(os.environ['HOME'] + '/hg/mirrorball/config/%s/updatebotrc' % sys.argv[1]) import logging slog = logging.getLogger('script') diff --git a/scripts/update.py b/scripts/update.py --- a/scripts/update.py +++ b/scripts/update.py @@ -1,14 +1,13 @@ -#!/usr/bin/python2.6 +#!/usr/bin/python import os import sys -sys.path.insert(0, os.environ['HOME'] + '/hg/26/rpath-xmllib') -sys.path.insert(0, os.environ['HOME'] + '/hg/26/conary') -sys.path.insert(0, os.environ['HOME'] + '/hg/26/mirrorball') -sys.path.insert(0, os.environ['HOME'] + '/hg/26/rmake') -sys.path.insert(0, os.environ['HOME'] + '/hg/26/epdb') -sys.path.insert(0, os.environ['HOME'] + '/hg/26/xobj/py') +sys.path.insert(0, os.environ['HOME'] + '/hg/rpath-xmllib') +sys.path.insert(0, os.environ['HOME'] + '/hg/conary') +sys.path.insert(0, os.environ['HOME'] + '/hg/mirrorball') +sys.path.insert(0, os.environ['HOME'] + '/hg/rmake') +sys.path.insert(0, os.environ['HOME'] + '/hg/xobj/py') from updatebot import log log.addRootLogger() @@ -19,6 +18,6 @@ from updatebot import bot, config cfg = config.UpdateBotConfig() -cfg.read(os.environ['HOME'] + '/hg/26/mirrorball/config/ubuntu/updatebotrc') +cfg.read(os.environ['HOME'] + '/hg/mirrorball/config/ubuntu/updatebotrc') obj = bot.Bot(cfg) obj.update() diff --git a/scripts/validatemanifests.py b/scripts/validatemanifests.py --- a/scripts/validatemanifests.py +++ b/scripts/validatemanifests.py @@ -1,6 +1,6 @@ -#!/usr/bin/python2.6 +#!/usr/bin/python # -# Copyright (c) 2008 rPath, Inc. +# Copyright (c) 2008-2009 rPath, Inc. # # This program is distributed under the terms of the Common Public License, # version 1.0. A copy of this license should have been distributed with this @@ -17,11 +17,10 @@ import sys import logging -sys.path.insert(0, os.environ['HOME'] + '/hg/26/xobj/py') -sys.path.insert(0, os.environ['HOME'] + '/hg/26/mirrorball') -sys.path.insert(0, os.environ['HOME'] + '/hg/26/conary') -sys.path.insert(0, os.environ['HOME'] + '/hg/26/rmake') -sys.path.insert(0, os.environ['HOME'] + '/hg/26/epdb') +sys.path.insert(0, os.environ['HOME'] + '/hg/xobj/py') +sys.path.insert(0, os.environ['HOME'] + '/hg/mirrorball') +sys.path.insert(0, os.environ['HOME'] + '/hg/conary') +sys.path.insert(0, os.environ['HOME'] + '/hg/rmake') from conary.lib import util sys.excepthook = util.genExcepthook() @@ -36,7 +35,7 @@ log = logging.getLogger('verifymanifest') cfg = config.UpdateBotConfig() -cfg.read(os.environ['HOME'] + '/hg/26/mirrorball/config/ubuntu/updatebotrc') +cfg.read(os.environ['HOME'] + '/hg/mirrorball/config/ubuntu/updatebotrc') b = bot.Bot(cfg) b._pkgSource.load() diff --git a/scripts/virtdepfinder.py b/scripts/virtdepfinder.py --- a/scripts/virtdepfinder.py +++ b/scripts/virtdepfinder.py @@ -1,14 +1,26 @@ -#!/usr/bin/python2.6 +#!/usr/bin/python +# +# Copyright (c) 2009 rPath, Inc. +# +# This program is distributed under the terms of the Common Public License, +# version 1.0. A copy of this license should have been distributed with this +# source file in a file called LICENSE. If it is not present, the license +# is always available at http://www.rpath.com/permanent/licenses/CPL-1.0. +# +# This program is distributed in the hope that it will be useful, but +# without any warranty; without even the implied warranty of merchantability +# or fitness for a particular purpose. See the Common Public License for +# full details. +# import os import sys -sys.path.insert(0, os.environ['HOME'] + '/hg/26/rpath-xmllib') -sys.path.insert(0, os.environ['HOME'] + '/hg/26/conary') -sys.path.insert(0, os.environ['HOME'] + '/hg/26/mirrorball') -sys.path.insert(0, os.environ['HOME'] + '/hg/26/rmake') -sys.path.insert(0, os.environ['HOME'] + '/hg/26/epdb') -sys.path.insert(0, os.environ['HOME'] + '/hg/26/xobj/py') +sys.path.insert(0, os.environ['HOME'] + '/hg/rpath-xmllib') +sys.path.insert(0, os.environ['HOME'] + '/hg/conary') +sys.path.insert(0, os.environ['HOME'] + '/hg/mirrorball') +sys.path.insert(0, os.environ['HOME'] + '/hg/rmake') +sys.path.insert(0, os.environ['HOME'] + '/hg/xobj/py') from conary.lib import util sys.excepthook = util.genExcepthook() @@ -24,7 +36,7 @@ from updatebot import pkgsource cfg = config.UpdateBotConfig() -cfg.read(os.environ['HOME'] + '/hg/26/mirrorball/config/%s/updatebotrc' % sys.argv[1] ) +cfg.read(os.environ['HOME'] + '/hg/mirrorball/config/%s/updatebotrc' % sys.argv[1] ) pkgSource = pkgsource.PackageSource(cfg) pkgSource.load() From johnsonm at rpath.com Wed Aug 19 17:38:58 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:38:58 +0000 Subject: mirrorball: update to using repomd Message-ID: <200908192139.n7JLcwen014100@scc.eng.rpath.com> changeset: c29544563ef6 user: Elliot Peele <http://issues.rpath.com/> date: Fri, 30 May 2008 15:51:19 -0400 update to using repomd committer: Elliot Peele <http://issues.rpath.com/> diff --git a/pkgwiz.py b/pkgwiz.py --- a/pkgwiz.py +++ b/pkgwiz.py @@ -14,13 +14,25 @@ # full details. # -from conary import cvc, rpmhelper +from conary import cvc import conary.lib.util import os -from rpmimport import infomaker, recipemaker, rpmsource +from rpmimport import infomaker, recipemaker, rpmsource, rpmhelper import shutil import sys +DEFAULT_URL = 'http://install.rdu.rpath.com/sle/' +DEFAULT_BASE_PATHS = [ + 'SLES10-SP1/sles-10-i586', + 'SLES10-SP1/sles-10-x86_64', + 'SLE10-Debuginfo-Updates/sles-10-i586', + 'SLE10-Debuginfo-Updates/sles-10-x86_64', + 'SLES10-SP1-Online/sles-10-i586', + 'SLES10-SP1-Online/sles-10-x86_64', + 'SLES10-Updates/sles-10-i586', + 'SLES10-Updates/sles-10-x86_64', +] + HELP_TEXT = """ pkgs <dir>|<pkg> [<dir>|<pkg> ...] accounts <user>|<group> [<user>|<group> ...] @@ -237,10 +249,10 @@ self.repos = self.client.getRepos() self.recipeMaker = recipemaker.RecipeMaker(self.cfg, self.repos, self.rpmSource) - def createPkgs(self, dirs): + def createPkgs(self, url, basePaths): self._setupRepo() - for dir in dirs: - self.rpmSource.walk(dir) + for basePath in basePaths: + self.rpmSource.load(url, basePath) # {foo:source: {cfg.buildLabel: None}} srccomps = {} @@ -248,8 +260,8 @@ # {foo:source: foo-1.0-1.1.src.rpm} srcmap = {} for src in set(self.rpmSource.getSrpms(sles10sp1pkgs)): - h = self.rpmSource.getHeader(self.rpmSource.srcPath[src]) - srccomp = h[rpmhelper.NAME] + ':source' + name = self.rpmSource.srcPath[src].name + srccomp = name + ':source' srcmap[srccomp] = src srccomps[srccomp] = {self.cfg.buildLabel: None} d = self.repos.getTroveVersionsByLabel(srccomps) @@ -271,7 +283,7 @@ groups = set() for src in self.rpmSource.getSrpms(slessp1pkgs): for rpm in self.rpmSource.rpmMap[src].values(): - header = self.rpmSource.getHeader(rpm) + header = rpmhelper.readHeader(rpm.location) users = users.union(header[FILEUSERNAME]) groups = groups.union(header[FILEGROUPNAME]) @@ -288,10 +300,17 @@ return category = argv[1] if 'pkgs' == category: - dirs = argv[2:] - if not dirs: - dirs = ['.'] - self.createPkgs(dirs) + url = None + basePaths = None + if len(argv) >= 3: + url = argv[2] + if len(argv) >= 4: + basePaths = argv[3:] + if not url: + url = DEFAULT_URL + if not basePaths: + basePaths = DEFAULT_BASE_PATHS + self.createPkgs(url, basePaths) elif 'accounts' == category: users = argv[2:] self.createUsers(users) diff --git a/rpmimport/__init__.py b/rpmimport/__init__.py --- a/rpmimport/__init__.py +++ b/rpmimport/__init__.py @@ -0,0 +1,18 @@ +# +# Copyright (c) 2008 rPath, Inc. +# +# This program is distributed under the terms of the Common Public License, +# version 1.0. A copy of this license should have been distributed with this +# source file in a file called LICENSE. If it is not present, the license +# is always available at http://www.rpath.com/permanent/licenses/CPL-1.0. +# +# This program is distributed in the hope that it will be useful, but +# without any warranty; without even the implied warranty of merchantability +# or fitness for a particular purpose. See the Common Public License for +# full details. +# + +''' +Module for managing (importing, creating, updating) RPMs in a conary +repository. +''' diff --git a/rpmimport/recipemaker.py b/rpmimport/recipemaker.py --- a/rpmimport/recipemaker.py +++ b/rpmimport/recipemaker.py @@ -14,12 +14,19 @@ # full details. # -import copy +''' +Module for creating package recipes and managing source components. +''' + import os import shutil -from conary import cvc, deps +from conary import cvc -class RecipeMaker: +class RecipeMaker(object): + ''' + Class for creating and managing rpm factory based source components. + ''' + def __init__(self, cfg, repos, rpmSource): self.cfg = cfg self.repos = repos @@ -59,7 +66,8 @@ cvc.sourceCommand(self.cfg, [ 'commit' ], { 'message': - 'Automated initial commit of %s:source' %pkgname}) + 'Automated initial commit of %s:source' + % pkgname}) #cvc.sourceCommand(self.cfg, ['cook', pkgname], {'no-deps': None}) #cfg = copy.copy(self.cfg) #buildFlavor = deps.deps.parseFlavor('is:x86_64') diff --git a/rpmimport/rpmhelper.py b/rpmimport/rpmhelper.py new file mode 100644 --- /dev/null +++ b/rpmimport/rpmhelper.py @@ -0,0 +1,81 @@ +# +# Copyright (c) 2008 rPath, Inc. +# +# This program is distributed under the terms of the Common Public License, +# version 1.0. A copy of this license should have been distributed with this +# source file in a file called LICENSE. If it is not present, the license +# is always available at http://www.rpath.com/permanent/licenses/CPL-1.0. +# +# This program is distributed in the hope that it will be useful, but +# without any warranty; without even the implied warranty of merchantability +# or fitness for a particular purpose. See the Common Public License for +# full details. +# + +''' +Wrappers around conary.rpmhelper. +''' + +import urllib2 + +from conary import rpmhelper + +class _SeekableStream(object): + ''' + File like object that can be seeked forward. + ''' + + def __init__(self, url): + self._url = url + + self._fh = urllib2.urlopen(url) + self._pos = 0 + + def read(self, *args): + ''' + Wrapper around urllib's file object's read method that keeps track + of position. + ''' + + buf = self._fh.read(*args) + self._pos += len(buf) + return buf + + def seek(self, amount, sense): + ''' + Simple seek implementation that only goes forward. + @param amount: amount to seek into file. + @type amount: integer + @param sense: direction to seek into file (only valid value is 1) + @type sense: integer + ''' + + assert(sense == 1) + self.read(amount) + + def tell(self): + ''' + Report the current position in the file. + @return current position in the file + ''' + + return self._pos + + +def readHeader(url): + ''' + Read an RPM header (and only the RPM header) from a remotely hosted RPM. + @param url: url to RPM file. + @type url: string + @return conary.rpmhelper.RpmHeader object + ''' + + fh = _SeekableStream(url) + + # Have to read into the file a bit to get to the begining of the header + # that we care about. + fh.read(96) + rpmhelper.RpmHeader(fh) + + header = rpmhelper.RpmHeader(fh) + return header diff --git a/rpmimport/rpmsource.py b/rpmimport/rpmsource.py --- a/rpmimport/rpmsource.py +++ b/rpmimport/rpmsource.py @@ -13,20 +13,35 @@ # full details. # +''' +Module for interacting with packages in multiple yum repositories. +''' import os -import sys -import shutil -from conary import rpmhelper +import repomd -# make local copies of tags for convenience -for tag in ('NAME', 'VERSION', 'RELEASE', 'SOURCERPM', 'FILEUSERNAME', - 'FILEGROUPNAME'): - sys.modules[__name__].__dict__[tag] = getattr(rpmhelper, tag) -ARCH = 1022 +class PackageChecksumMismatchError(Exception): + ''' + Exception for packages that have checksums that don't match. + ''' -class RpmSource: + def __init__(self, pkg1, pkg2): + Exception.__init__(self) + self.pkg1 = pkg1 + self.pkg2 = pkg2 + + def __str___(self): + return ('The checksums of %s (%s) and %s (%s) do not match.' + % (self.pkg1, self.pkg1.checksum, self.pkg2, + self.pkg2.checksum)) + + +class RpmSource(object): + ''' + Class that builds maps of packages from multiple yum repositories. + ''' + def __init__(self): # {srpm: {rpm: path} self.rpmMap = dict() @@ -37,29 +52,90 @@ # {srpm: path} self.srcPath = dict() - # {rpmfile: header} - self.headers = dict() + @classmethod + def transformName(cls, name): + """ + In name, - => _, + => _plus. + """ - def getHeader(self, f): - if f in self.headers: - return self.headers[f] - header = rpmhelper.readHeader(file(f)) - self.headers[f] = header - return header + return name.replace('-', '_').replace('+', '_plus') - def procBin(self, f, rpm): - header = self.getHeader(f) - self.headers[f] = header - if SOURCERPM in header: - srpm = header[SOURCERPM] + @classmethod + def quoteSequence(cls, seq): + """ + [a, b] => 'a', 'b' + """ + + return ', '.join("'%s'" % x for x in sorted(seq)) + + def _procSrc(self, basePath, package): + ''' + Process source rpms. + @param basePath: path to yum repository. + @type basePath: string + @param package: package object + @type package: repomd.packagexml._Package + ''' + shortSrpm = os.path.basename(package.location) + longLoc = basePath + '/' + package.location + if shortSrpm in self.srcPath: + #if package.checksum != self.srcPath[shortSrpm].checksum: + # raise PackageChecksumMismatchError(package, + # self.srcPath[shortSrpm]) + #else: + # return + pass + else: + package.location = longLoc + self.srcPath[shortSrpm] = package + + def _procBin(self, basePath, package): + ''' + Process binary rpms. + @param basePath: path to yum repository. + @type basePath: string + @param package: package object + @type package: repomd.packagexml._Package + ''' + srpm = package.sourcerpm + longLoc = basePath + '/' + package.location + package.location = longLoc if self.rpmMap.has_key(srpm): - self.rpmMap[srpm][rpm] = f + self.rpmMap[srpm][longLoc] = package else: - self.rpmMap[srpm] = {rpm: f} - self.revMap[header[NAME]] = srpm + self.rpmMap[srpm] = {longLoc: package} + self.revMap[package.name] = srpm - def procSrc(self, f, rpm): - self.srcPath[rpm] = f + def load(self, url, basePath): + """ + Walk the yum repository rooted at url/basePath and collect information + about rpms found. + @param url: url to common directory on host where all repositories resides. + @type url: string + @param basePath: directory where repository resides. + @type basePath: string + """ + + client = repomd.Client(url + '/' + basePath) + + for pkg in client.getPackageDetail(): + # ignore the 32-bit compatibility libs - we will + # simply use the 32-bit components from the repository + if '32bit' in pkg.name: + continue + + if pkg.sourcerpm == '': + self._procSrc(basePath, pkg) + else: + self._procBin(basePath, pkg) + + def getNames(self, src): + """ + @return list that goes into the rpms line in the recipe. + """ + + names = set([ x.name for x in self.rpmMap[src].itervalues() ]) + return names def getRPMS(self, src): """ @@ -67,43 +143,6 @@ """ return [ x for x in self.rpmMap[src].itervalues() ] - def walk(self, root): - """ - Walk the tree rooted at root and collect information about rpms found. - """ - ignored = ('patch.rpm', 'delta.rpm') - for dirpath, dirnames, filenames in os.walk(root): - for f in filenames: - # ignore the 32-bit compatibility libs - we will - # simply use the 32-bit components from the repository - skip = False - if '32bit' in f: - skip = True - for suffix in ignored: - if f.endswith(suffix): - skip = True - break - continue - if not f.endswith(".rpm"): - skip = True - if skip: - continue - fullpath = os.path.join(dirpath, f) - try: - h = self.getHeader(fullpath) - except IOError: - print 'bad rpm:', fullpath - os.unlink(fullpath) - continue - if h[VERSION] not in f: - # ignore files like aaa_base.rpm. We only want - # one version, like aaa_base-10-0.8.x86_64.rpm - continue - if f.endswith(".src.rpm") or f.endswith('.nosrc.rpm'): - self.procSrc(fullpath, f) - else: - self.procBin(fullpath, f) - def getSrpms(self, pkglist): """ Get source RPMS for a given list of binary RPM package names @@ -113,43 +152,19 @@ srpms.append(self.revMap[p]) return srpms - def transformName(self, name): - """ - In name, - => _, + => _plus. - """ - - return name.replace('-', '_').replace('+', '_plus') - - def quoteSequence(self, seq): - """ - [a, b] => 'a', 'b' - """ - - return ', '.join("'%s'" % x for x in sorted(seq)) - def getArchs(self, src): """ @return list that goes into the archs line in the recipe. """ - hdrs = [ self.getHeader(x) for x in self.rpmMap[src].itervalues() ] - archs = set(h[ARCH] for h in hdrs) + archs = set([ x.arch for x in self.rpmMap[src].itervalues() ]) if 'i586' in archs and 'i686' in archs: # remove the base arch if we have an extra arch - arch, extra = self.getExtraArchs(src) + arch = self.getExtraArchs(src)[0] if arch == 'i686': archs.remove('i586') return archs - def getNames(self, src): - """ - @return list that goes into the rpms line in the recipe. - """ - - hdrs = [ self.getHeader(x) for x in self.rpmMap[src].itervalues() ] - names = set(h[NAME] for h in hdrs) - return names - def getExtraArchs(self, src): """ For the special case of RPMs that have components optimized for the @@ -157,11 +172,9 @@ ('i686', set(rpms that are i686 only)), otherwise return (None, None). """ - hdrs = [ self.getHeader(x) for x in self.rpmMap[src].itervalues() ] + hdrs = [ (x.arch, x.name) for x in self.rpmMap[src].itervalues() ] archMap = {} - for h in hdrs: - arch = h[ARCH] - name = h[NAME] + for arch, name in hdrs: if arch in archMap: archMap[arch].add(name) else: @@ -171,35 +184,12 @@ return 'i686', archMap['i686'] return None, None - def createTemplate(self, src): - """ - @return the content of the new recipe. - """ - - srchdr = self.getHeader(self.srcPath[src]) - l = [] - a = l.append - a("loadSuperClass('rpmimport.recipe')") - a('class %s(RPMImportRecipe):' %(self.transformName(srchdr[NAME]))) - a(" name = '%s'" %(srchdr[NAME])) - a(" version = '%s_%s'" %(srchdr[VERSION], srchdr[RELEASE])) - archs = self.getArchs(src) - names = self.getNames(src) - extras = self.getExtraArchs(src)[1] - a(' rpms = [ %s ]' % self.quoteSequence(names)) - a(' archs = [ %s ]' % self.quoteSequence(archs)) - if extras: - a(" extraArch = { 'i686': [ %s ] }" %self.quoteSequence(extras)) - # add a trailing newline - a('') - return '\n'.join(l) - def createManifest(self, srpm, prefix): """ @return the text for the manifest file. """ l = [] - l.append(self.srcPath[srpm]) + l.append(self.srcPath[srpm].location) l.extend([x for x in self.getRPMS(srpm)]) if prefix: l = [ x[len(prefix):] for x in l] From johnsonm at rpath.com Wed Aug 19 17:41:22 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:41:22 +0000 Subject: mirrorball: add new advisory model Message-ID: <200908192141.n7JLfM0a019967@scc.eng.rpath.com> changeset: 5332f8dd8452 user: Elliot Peele <https://issues.rpath.com/> date: Fri, 14 Nov 2008 17:40:51 -0500 add new advisory model committer: Elliot Peele <https://issues.rpath.com/> diff --git a/updatebot/advisories/__init__.py b/updatebot/advisories/__init__.py new file mode 100644 --- /dev/null +++ b/updatebot/advisories/__init__.py @@ -0,0 +1,40 @@ +# +# Copyright (c) 2008 rPath, Inc. +# +# This program is distributed under the terms of the Common Public License, +# version 1.0. A copy of this license should have been distributed with this +# source file in a file called LICENSE. If it is not present, the license +# is always available at http://www.rpath.com/permanent/licenses/CPL-1.0. +# +# This program is distributed in the hope that it will be useful, but +# without any warranty; without even the implied warranty of merchantability +# or fitness for a particular purpose. See the Common Public License for +# full details. +# + +import logging +from imputil import imp + +log = logging.getLogger('updatebot.advisories') + +class InvalidBackendError(Exception): + pass + +def __getBackend(backend): + if backend not in __supported_backends: + raise InvalidBackendError('%s is not a supported backend, please ' + 'choose from %s' % (backend, ','.join(__supported_backends))) + + try: + path = [imp.find_module('updatebot.advisories')[1], ] + mod = imp.find_module(backend, path) + loaded = imp.load_module(backend, mod[0], mod[1], mod[2]) + return loaded + except ImportError, e: + raise InvalidBackendError('Could not load %s backend: %s' + % (backend, e)) + +def Advisor(cfg, pkgSource, backend='centos'): + klass = __getBackend(backend) + obj = klass(cfg, pkgSource) + return obj.load() diff --git a/updatebot/advisories/common.py b/updatebot/advisories/common.py new file mode 100644 --- /dev/null +++ b/updatebot/advisories/common.py @@ -0,0 +1,373 @@ +# +# Copyright (c) 2008 rPath, Inc. +# +# This program is distributed under the terms of the Common Public License, +# version 1.0. A copy of this license should have been distributed with this +# source file in a file called LICENSE. If it is not present, the license +# is always available at http://www.rpath.com/permanent/licenses/CPL-1.0. +# +# This program is distributed in the hope that it will be useful, but +# without any warranty; without even the implied warranty of merchantability +# or fitness for a particular purpose. See the Common Public License for +# full details. +# + +import time +import smtplib +import logging +from email.MIMEText import MIMEText +from smtplib import SMTPRecipientsRefused, SMTPHeloError +from smtplib import SMTPSenderRefused, SMTPDataError + +log = logging.getLogger('updatebot.advisories') + +from updatebot.errors import AdvisoryRecipientRefusedError +from updatebot.errors import FailedToSendAdvisoryError +from updatebot.errors import MultipleAdvisoriesFoundError +from updatebot.errors import NoAdvisoryFoundError +from updatebot.errors import NoPackagesFoundForAdvisory +from updatebot.errors import NoRecipientsFoundError +from updatebot.errors import NoSenderFoundError +from updatebot.errors import ProductNameNotDefinedError + +class BaseAdvisor(object): + """ + Class for managing, manipulating, and distributing advisories. + """ + + _advisoryClass = BaseAdvisory + + def __init__(self, cfg, pkgSource): + self._cfg = cfg + self._pkgSource = pkgSource + + # { binPkg: patchObj } + self._pkgMap = dict() + + # { patchObj: set([srcPkg, ...] } + self._cache = dict() + + # { ((name, version, flavor), srcPkg): [ advisoryObj, ...] } + self._advisories = dict() + + def check(self, trvLst): + """ + Check to see if there are advisories for troves in trvLst. + @param trvLst: list of troves and srpms. + @type trvLst: [((name, version, flavor), srcPkg), ... ] + """ + + """ + 1. figure out what advisory source to use + 2. pass pkgsource, cfg, and trvLst to advisory source + 3. run .check on advisory source to populate self._advisories + + + Advisory sources should be able to sanity check themselves. + """ + + # FIXME: Maybe we should check to see if all binary rpms listed in + # the advisory are in the set of packages to be updated. + + for nvf, srcPkg in trvLst: + patches = set() + for binPkg in self._pkgSource.srcPkgMap[srcPkg]: + if binPkg in self._pkgMap: + patches.update(self._pkgMap[binPkg]) + + # Make sure we have advisories for all packages we expect to have + # advisories for. + for binPkg in self._pkgSource.srcPkgMap[srcPkg]: + # Don't check srpms. + if binPkg is srcPkg or binPkg in self._pkgMap: + continue + elif self._hasException(binPkg): + log.info('found advisory exception for %s' % binPkg) + log.debug(binPkg.location) + elif not self._isUpdatesRepo(binPkg): + log.info('package not in updates repository %s' % binPkg) + log.debug(binPkg.location) + elif len(patches) > 0: + log.info('found package not mentioned in advisory %s' + % binPkg) + log.debug(binPkg.location) + else: + log.error('could not find advisory for %s' % binPkg) + raise NoAdvisoryFoundError(why=binPkg) + + if len(patches) > 1: + raise MultipleAdvisoriesFoundError(what=srcPkg, + advisories=patches) + + # len(patches) will only be 0 if there is an exception or the new + # pkg is not in an updates repsitory. + elif len(patches) == 0: + continue + + patch = patches.pop() + if patch not in self._cache: + self._cache[patch] = set() + + self._cache[patch].add(srcPkg) + self._advisories[patch] = self._mkAdvisory(patch) + + def load(self): + """ + Parse the required data to generate a mapping of binary package + object to patch object for a given platform into self._pkgMap. + + Note: This method is meant to be implemented by the backend module + that inherits from this class. + """ + + log.error('The %s backend does not implement %s' + % (self.__class__.__name__, 'load')) + raise NotImplemntedError + + def _hasException(self, binPkg): + """ + Check the config for repositories with exceptions for sending + advisories. (io. repositories that we generated metadata for.) + @param binPkg: binary package object + @type binPkg: repomd.packagexml._Package + + Note: This method is meant to be implemented by the backend module + that inherits from this class. + """ + + log.error('The %s backend does not implement %s' + % (self.__class__.__name__, '_hasException')) + raise NotImplementedError + + def _isUpdatesRepo(self, binPkg): + """ + Check the repository name. If this package didn't come from a updates + repository it is probably not security related. + @param binPkg: binary package object + @type binPkg: repomd.packagexml._Package + + Note: This method is meant to be implemented by the backend module + that inherits from this class. + """ + + log.error('The %s backend does not implement %s' + % (self.__class__.__name__, '_isUpdateRepo')) + raise NotImplmentedError + + def _mkAdvisory(self, patch): + """ + Create and populate advisory object for a given package. + """ + + advisory = self._advisoryClass(self._cfg) + advisory.setSubject(patch.summary) + advisory.setDescription(patch.description) + return advisory + + def send(self, trvLst, newTroves): + """ + Send advisories for all troves in the trvLst. + @param trvLst: list of packages that have been updated. + @type trvLst: [((name, version, flavor), srcPkg), ...] + @param newTroves: list of new troves after the promote + @type newTroves: [(name, version, flavor), ...] + """ + + newTroveMap = self._mkNewTroveMap(trvLst, newTroves) + + toSend = set() + for patch, advisory in self._advisories.iteritems(): + binNames = [ x.name for x in patch.packages ] + for srpm in self._cache[patch]: + if srpm not in newTroveMap: + log.warn('%s not in newTroveMap' % srpm) + continue + + # Make sure advisory applies to a package that was promoted. + toAdvise = [ x for x in newTroveMap[srpm] if x[0] in binNames ] + if not toAdvise: + log.warn('%s does not apply to any published packages of %s' + % (advisory, srpm)) + continue + + advisory.setUpdateTroves(newTroveMap[srpm]) + toSend.add(advisory) + + for advisory in toSend: + log.info('sending advisory: %s' % advisory) + advisory.send() + + def _mkNewTroveMap(self, trvLst, newTroves): + """ + Create a mapping of the elements in trvLst to newTroves. + @param trvLst: list of packages that have been updated. + @type trvLst: [((name, version, flavor), srcPkg), ...] + @param newTroves: list of new troves after the promote + @type newTroves: [(name, version, flavor), ...] + @return {trvLstElement: [(n, v, f), ...]} + """ + + res = dict() + for nvf, srcPkg in trvLst: + res[srcPkg] = [] + binNames = [ x.name for x in self._pkgSource.srcPkgMap[srcPkg] ] + for n, v, f in newTroves: + if n in binNames: + res[srcPkg].append((n, v, f)) + if not res[srcPkg]: + raise NoPackagesFoundForAdvisory(what=(nvf, srcPkg)) + + return res + + +class BaseAdvisory(object): + """ + Module for representing an advisory message. + """ + + template = """\ +Published: %(date)s +Products: + %(product)s + +Updated Versions: +%(updateTroves)s + +Description: +%(description)s +""" + + def __init__(self, cfg): + self._cfg = cfg + + self._fromName = self._cfg.emailFromName + self._from = self._cfg.emailFrom + self._to = self._cfg.emailTo + self._bcc = self._cfg.emailBcc + + self._subject = None + + if not self._cfg.productName: + raise ProductNameNotDefinedError + + if not self._from or not self._fromName: + raise NoSenderFoundError(why='Configuration error, emailFrom or ' + 'emailFromName not defined') + + if not self._to: + raise NoRecipientsFoundError(why='Configuration error, emailTo not ' + 'defined') + + self._data = {'product': self._cfg.productName, + 'date': time.strftime('%Y-%m-%d', time.localtime()), + 'updateTroves': ''} + + def __str__(self): + return self._subject + + def send(self): + """ + Send an advisory email. + """ + + message = self._getMessage() + smtp = self._smtpConnect() + + try: + results = smtp.sendmail(self._from, self._to, message.as_string()) + except (SMTPRecipientsRefused, SMTPHeloError, SMTPSenderRefused, + SMTPDataError), e: + raise FailedToSendAdvisoryError(error=e) + + smtp.quit() + + if results is not None and results != {}: + raise AdvisoryRecipientRefusedError(data=results) + + return results + + def _getMessage(self): + """ + Get the message to send. + """ + + msgText = self.template % self._data + email = MIMEText(msgText) + email['Subject'] = self._subject + email['From'] = '%s <%s>' % (self._fromName, self._from) + email['To'] = self._formatList(self._to) + email['Bcc'] = self._formatList(self._bcc) + return email + + @staticmethod + def _formatList(lst): + """ + Format a list to be comma separated. + """ + + return ', '.join(lst) + + def _smtpConnect(self): + """ + Get a smtp connection object. + """ + + server = smtplib.SMTP(self._cfg.smtpServer) + + rootLogger = logging.getLogger('') + if rootLogger.level == logging.DEBUG: + server.set_debuglevel(1) + + return server + + def setUpdateTroves(self, troves): + """ + Set the list of updated troves to add to the advisory. + @param troves: list of troves + @type troves: [(name, version, flavor), ... ] + """ + + trvs = set() + for n, v, f in troves: + trvs.add(self._formatTrove(n, v, f)) + + self._data['updateTroves'] += self._indentFormatList(trvs) + + @staticmethod + def _indentFormatList(lst, indent=1): + """ + Format a list into a string with tab indention. + """ + + tab = ' ' * indent + joinString = '\n' + tab + result = tab + joinString.join(lst) + return result + + @staticmethod + def _formatTrove(n, v, f): + """ + Format a trove spec into a string. + """ + + # W0613 - Unused argument 'f' + # pylint: disable-msg=W0613 + + label = v.trailingLabel().asString() + revision = v.trailingRevision().asString() + + return '%s=%s/%s' % (n, label, revision) + + def setSubject(self, subject): + """ + Set the subject of the advisory email. + """ + + self._subject = subject + + def setDescription(self, desc): + """ + Set the description of the advisory. + """ + + self._data['description'] = desc diff --git a/updatebot/advisories/sles.py b/updatebot/advisories/sles.py new file mode 100644 --- /dev/null +++ b/updatebot/advisories/sles.py @@ -0,0 +1,93 @@ +# +# Copyright (c) 2008 rPath, Inc. +# +# This program is distributed under the terms of the Common Public License, +# version 1.0. A copy of this license should have been distributed with this +# source file in a file called LICENSE. If it is not present, the license +# is always available at http://www.rpath.com/permanent/licenses/CPL-1.0. +# +# This program is distributed in the hope that it will be useful, but +# without any warranty; without even the implied warranty of merchantability +# or fitness for a particular purpose. See the Common Public License for +# full details. +# + +from updatebot.advisories.common import BaseAdvisor, log + +class Advisor(BaseAdvisor): + def load(self): + """ + Parse the required data to generate a mapping of binary package + object to patch object for a given platform into self._pkgMap. + """ + + for path, client in self._pkgSource.getClients().iteritems(): + log.info('loading patch information %s' % path) + for patch in client.getPatchDetail(): + self._lineOne(patch, path) + + def _loadOne(self, patch, path): + """ + Load one patch into mappings. + @param patch: repomd patch object + @type patch: repomd.patchxml._Patch + @param path: base path to repository + @type path: string + """ + + if self._filterPatch(patch): + return + + for package in patch.packages: + package.location = path + '/' + package.location + if package not in self.pkgMap: + self.pkgMap[package] = set() + self.pkgMap[package].add(patch) + + def _filterPatch(self, patch): + """ + Filter out patches that match filters in config. + @param patch: repomd patch object + @type patch: repomd.patchxml._Patch + """ + + for _, regex in self._cfg.patchFilter: + if regex.match(patch.summary): + return True + if regex.match(patch.description): + return True + + return False + + def _hasException(self, binPkg): + """ + Check the config for repositories with exceptions for sending + advisories. (io. repositories that we generated metadata for.) + @param binPkg: binary package object + @type binPkg: repomd.packagexml._Package + """ + + shortPath = binPkg.location.split('/')[0] + + for advisoryException in self._cfg.advisoryException: + path = advisoryException[0].split('/')[0] + if path == shortPath: + return True + return False + + def _isUpdatesRepo(self, binPkg): + """ + Check the repository name. If this package didn't come from a updates + repository it is probably not security related. + @param binPkg: binary package object + @type binPkg: repomd.packagexml._Package + """ + + # msw agrees that this seems to be a sane check. + + shortPath = binPkg.location.split('/')[0] + + if shortPath.endswith('Updates'): + return True + else: + return False From johnsonm at rpath.com Wed Aug 19 17:38:47 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:38:47 +0000 Subject: mirrorball: rename module to match pylint Message-ID: <200908192138.n7JLcmZJ013649@scc.eng.rpath.com> changeset: bee762e9758a user: Elliot Peele <http://issues.rpath.com/> date: Tue, 27 May 2008 19:32:57 -0400 rename module to match pylint committer: Elliot Peele <http://issues.rpath.com/> diff --git a/Makefile b/Makefile --- a/Makefile +++ b/Makefile @@ -17,7 +17,7 @@ export TOPDIR = $(shell pwd) export DISTDIR = $(TOPDIR)/mirrorball-$(VERSION) -SUBDIRS=updateBot repomd rpmimport pylint test +SUBDIRS=updatebot repomd rpmimport pylint test extra_files = \ Make.rules \ diff --git a/pylint/run_pylint b/pylint/run_pylint --- a/pylint/run_pylint +++ b/pylint/run_pylint @@ -35,7 +35,7 @@ done if [ -z "$files" ] ; then - files="updateBot repomd rpmimport" + files="updatebot repomd " # "rpmimport" fi pylint --init-hook='import sys; sys.path.append("."); import init_pylint' --rcfile='../pylintrc' $pylintArgs $files diff --git a/updateBot/__init__.py b/updateBot/__init__.py deleted file mode 100644 --- a/updateBot/__init__.py +++ /dev/null @@ -1,18 +0,0 @@ -# -# Copyright (c) 2008 rPath, Inc. -# -# This program is distributed under the terms of the Common Public License, -# version 1.0. A copy of this license should have been distributed with this -# source file in a file called LICENSE. If it is not present, the license -# is always available at http://www.rpath.com/permanent/licenses/CPL-1.0. -# -# This program is distributed in the hope that it will be useful, but -# without any warranty; without even the implied warranty of merchantability -# or fitness for a particular purpose. See the Common Public License for -# full details. -# - -''' -UpdateBot is a module for automated updating of a conary repository from a -SLES yum/rpm repository. -''' diff --git a/updateBot/config.py b/updateBot/config.py deleted file mode 100644 --- a/updateBot/config.py +++ /dev/null @@ -1,23 +0,0 @@ -# -# Copyright (c) 2008 rPath, Inc. -# -# This program is distributed under the terms of the Common Public License, -# version 1.0. A copy of this license should have been distributed with this -# source file in a file called LICENSE. If it is not present, the license -# is always available at http://www.rpath.com/permanent/licenses/CPL-1.0. -# -# This program is distributed in the hope that it will be useful, but -# without any warranty; without even the implied warranty of merchantability -# or fitness for a particular purpose. See the Common Public License for -# full details. -# - -from conary.lib import cfg -from rmake.build.buildcfg import CfgTroveSpec -from conary.lib.cfgtypes import CfgList, CfgString, CfgInt, CfgDict -from conary.lib.cfgtypes import CfgQuotedLineList, CfgBool, ParseError -from conary.conarycfg import CfgFlavor, CfgLabel, CfgInstallLabelPath - -class UpdateBotConfig(cfg.SectionedConfigFile): - configPath = CfgString # path to configuration files (conaryrc, rmakerc) - commitMessage = (CfgString, 'Automated commit by updateBot') diff --git a/updateBot/errors.py b/updateBot/errors.py deleted file mode 100644 --- a/updateBot/errors.py +++ /dev/null @@ -1,59 +0,0 @@ -# -# Copyright (c) 2008 rPath, Inc. -# -# This program is distributed under the terms of the Common Public License, -# version 1.0. A copy of this license should have been distributed with this -# source file in a file called LICENSE. If it is not present, the license -# is always available at http://www.rpath.com/permanent/licenses/CPL-1.0. -# -# This program is distributed in the hope that it will be useful, but -# without any warranty; without even the implied warranty of merchantability -# or fitness for a particular purpose. See the Common Public License for -# full details. -# - -''' -UpdateBot specific errors. -''' - -class UpdateBotError(Exception): - ''' - Base UpdateBot Error for all other errors to inherit from. - ''' - - _params = [] - _template = 'An unknown error has occured.' - - def __init__(self, **kwargs): - RuntimeError.__init__(self) - - self._kwargs = kwargs - - # Copy kwargs to attributes - for key in self._params: - setattr(self, key, kwargs[key]) - - def __str__(self): - return self._template % self.__dict__ - - def __repr__(self): - params = ', '.join('%s=%r' % x for x in self._kwargs.iteritems()) - return '%s(%s)' % (self.__class__, params) - - -class CommitFailedError(UpdateBotError): - ''' - CommitFailedError, raised when failing to commit to a repository. - ''' - - _params = ['jobId', 'why'] - _template = 'rMake job %(jobId)d failed to commit: %(why)s' - - -class JobFailedError(UpdateBotError): - ''' - JobFailedError, raised when an rMake job fails. - ''' - - _params = ['jobId', 'why'] - _templates = 'rMake job %(jobId)s failed: %(why)s' diff --git a/updateBot/log.py b/updateBot/log.py deleted file mode 100644 --- a/updateBot/log.py +++ /dev/null @@ -1,31 +0,0 @@ -# -# Copyright (c) 2008 rPath, Inc. -# -# This program is distributed under the terms of the Common Public License, -# version 1.0. A copy of this license should have been distributed with this -# source file in a file called LICENSE. If it is not present, the license -# is always available at http://www.rpath.com/permanent/licenses/CPL-1.0. -# -# This program is distributed in the hope that it will be useful, but -# without any warranty; without even the implied warranty of merchantability -# or fitness for a particular purpose. See the Common Public License for -# full details. -# - -import sys -import logging - -def addRootLogger(): - root_log = logging.getLogger('') - handler = logging.StreamHandler(sys.stdout) - formatter = logging.Formatter('%(asctime)s %(levelname)s ' - '%(name)s %(message)s') - handler.setFormatter(formatter) - root_log.addHandler(handler) - root_log.setLevel(logging.INFO) - - # Delete conary's log handler since it puts things on stderr and without - # any timestamps. - conary_log = logging.getLogger('conary') - for handler in conary_log.handlers: - conary_log.removeHandler(handler) diff --git a/updateBot/rmake.py b/updateBot/rmake.py deleted file mode 100644 --- a/updateBot/rmake.py +++ /dev/null @@ -1,102 +0,0 @@ -# -# Copyright (c) 2008 rPath, Inc. -# -# This program is distributed under the terms of the Common Public License, -# version 1.0. A copy of this license should have been distributed with this -# source file in a file called LICENSE. If it is not present, the license -# is always available at http://www.rpath.com/permanent/licenses/CPL-1.0. -# -# This program is distributed in the hope that it will be useful, but -# without any warranty; without even the implied warranty of merchantability -# or fitness for a particular purpose. See the Common Public License for -# full details. -# - -import time -import logging - -from rmake.cmdline import helper, monitor, commit - -from errors import JobFailedError, CommitFailedError - -log = logging.getLogger('updateBot.build') - -class Builder(object): - ''' - Class for wrapping the rMake api until we can switch to using rBuild. - - @param cfg: updateBot configuration object - @type cfg: config.UpdateBotConfig - ''' - - def __init__(self, cfg): - self._cfg = cfg - - self._helper = helper.rMakeHelper(root=self._cfg.configPath) - - - def build(self, trvSpecs): - ''' - Build a list of troves. - @param trvSpecs: list of trove specs - @type trvSpecs: [(name, versionObj, flavorObj), ...] - ''' - - # Create rMake job - log.info('Creating build job: %s' % (troveSpecs, )) - job = self._helper.creatBuild(troveSpecs) - jobId = self._helper.buildJob(job) - log.info('Started jobId: %s' % jobId) - - # Watch build, wait for completion - monitor.monitorJob(self._helper.client, jobId, - exitOnFinish=True, displayClass=_StatusOnlyDisplay) - - # Check for errors - job = self._helper.getJob(jobId) - if job.isFailed(): - log.error('Job %d failed', jobId) - raise JobFailedError(jobId=jobId, why='Job failed') - elif not job.isFinished(): - log.error('Job %d is not done, yet watch returned early!', jobId) - raise JobFailedError(jobId=jobId, why='Job not done') - elif not list(job.iterBuiltTroves()): - log.error('Job %d has no built troves', jobId) - raise JobFailedError(jobId=jobId, why='Job built no troves') - - # Do the commit - startTime = time.time() - log.info('Starting commit of job %d', jobId) - self._helper.startCommit(jobId) - succeeded, data = commit.commitJobs(self._helper.getConaryClient(), - [job, ], - self._helper.buildConfig.reposName, - self._cfg.commitMessage) - if not succeeded: - raise CommitFailedError(jobId=job.jobId, why=data) - - log.info('Commit of job %d completed in %.02f seconds', - jobId, time.time() - startTime) - - troveMap = {} - for _, troveTupleDict in data.iteritems(): - for buildTroveTuple, committedList in troveTupleDict.iteritems(): - troveMap[buildTroveTuple] = committedList - - return troveMap - - -class _StatusOnlyDisplay(monitor.JobLogDisplay): - ''' - Display only job and trove status. No log output. - - Copied from bob3 - ''' - - def _troveLogUpdated(self, (jobId, troveTuple), state, status): - '''Don't care about trove logs''' - pass - - def _trovePreparingChroot(self, (jobId, troveTuple), host, path): - '''Don't care about resolving/installing chroot''' - pass diff --git a/updatebot/__init__.py b/updatebot/__init__.py new file mode 100644 --- /dev/null +++ b/updatebot/__init__.py @@ -0,0 +1,18 @@ +# +# Copyright (c) 2008 rPath, Inc. +# +# This program is distributed under the terms of the Common Public License, +# version 1.0. A copy of this license should have been distributed with this +# source file in a file called LICENSE. If it is not present, the license +# is always available at http://www.rpath.com/permanent/licenses/CPL-1.0. +# +# This program is distributed in the hope that it will be useful, but +# without any warranty; without even the implied warranty of merchantability +# or fitness for a particular purpose. See the Common Public License for +# full details. +# + +''' +UpdateBot is a module for automated updating of a conary repository from a +SLES yum/rpm repository. +''' diff --git a/updatebot/config.py b/updatebot/config.py new file mode 100644 --- /dev/null +++ b/updatebot/config.py @@ -0,0 +1,23 @@ +# +# Copyright (c) 2008 rPath, Inc. +# +# This program is distributed under the terms of the Common Public License, +# version 1.0. A copy of this license should have been distributed with this +# source file in a file called LICENSE. If it is not present, the license +# is always available at http://www.rpath.com/permanent/licenses/CPL-1.0. +# +# This program is distributed in the hope that it will be useful, but +# without any warranty; without even the implied warranty of merchantability +# or fitness for a particular purpose. See the Common Public License for +# full details. +# + +from conary.lib import cfg +from rmake.build.buildcfg import CfgTroveSpec +from conary.lib.cfgtypes import CfgList, CfgString, CfgInt, CfgDict +from conary.lib.cfgtypes import CfgQuotedLineList, CfgBool, ParseError +from conary.conarycfg import CfgFlavor, CfgLabel, CfgInstallLabelPath + +class UpdateBotConfig(cfg.SectionedConfigFile): + configPath = CfgString # path to configuration files (conaryrc, rmakerc) + commitMessage = (CfgString, 'Automated commit by updateBot') diff --git a/updatebot/errors.py b/updatebot/errors.py new file mode 100644 --- /dev/null +++ b/updatebot/errors.py @@ -0,0 +1,59 @@ +# +# Copyright (c) 2008 rPath, Inc. +# +# This program is distributed under the terms of the Common Public License, +# version 1.0. A copy of this license should have been distributed with this +# source file in a file called LICENSE. If it is not present, the license +# is always available at http://www.rpath.com/permanent/licenses/CPL-1.0. +# +# This program is distributed in the hope that it will be useful, but +# without any warranty; without even the implied warranty of merchantability +# or fitness for a particular purpose. See the Common Public License for +# full details. +# + +''' +UpdateBot specific errors. +''' + +class UpdateBotError(Exception): + ''' + Base UpdateBot Error for all other errors to inherit from. + ''' + + _params = [] + _template = 'An unknown error has occured.' + + def __init__(self, **kwargs): + RuntimeError.__init__(self) + + self._kwargs = kwargs + + # Copy kwargs to attributes + for key in self._params: + setattr(self, key, kwargs[key]) + + def __str__(self): + return self._template % self.__dict__ + + def __repr__(self): + params = ', '.join('%s=%r' % x for x in self._kwargs.iteritems()) + return '%s(%s)' % (self.__class__, params) + + +class CommitFailedError(UpdateBotError): + ''' + CommitFailedError, raised when failing to commit to a repository. + ''' + + _params = ['jobId', 'why'] + _template = 'rMake job %(jobId)d failed to commit: %(why)s' + + +class JobFailedError(UpdateBotError): + ''' + JobFailedError, raised when an rMake job fails. + ''' + + _params = ['jobId', 'why'] + _templates = 'rMake job %(jobId)s failed: %(why)s' diff --git a/updatebot/log.py b/updatebot/log.py new file mode 100644 --- /dev/null +++ b/updatebot/log.py @@ -0,0 +1,31 @@ +# +# Copyright (c) 2008 rPath, Inc. +# +# This program is distributed under the terms of the Common Public License, +# version 1.0. A copy of this license should have been distributed with this +# source file in a file called LICENSE. If it is not present, the license +# is always available at http://www.rpath.com/permanent/licenses/CPL-1.0. +# +# This program is distributed in the hope that it will be useful, but +# without any warranty; without even the implied warranty of merchantability +# or fitness for a particular purpose. See the Common Public License for +# full details. +# + +import sys +import logging + +def addRootLogger(): + root_log = logging.getLogger('') + handler = logging.StreamHandler(sys.stdout) + formatter = logging.Formatter('%(asctime)s %(levelname)s ' + '%(name)s %(message)s') + handler.setFormatter(formatter) + root_log.addHandler(handler) + root_log.setLevel(logging.INFO) + + # Delete conary's log handler since it puts things on stderr and without + # any timestamps. + conary_log = logging.getLogger('conary') + for handler in conary_log.handlers: + conary_log.removeHandler(handler) diff --git a/updatebot/rmake.py b/updatebot/rmake.py new file mode 100644 --- /dev/null +++ b/updatebot/rmake.py @@ -0,0 +1,102 @@ +# +# Copyright (c) 2008 rPath, Inc. +# +# This program is distributed under the terms of the Common Public License, +# version 1.0. A copy of this license should have been distributed with this +# source file in a file called LICENSE. If it is not present, the license +# is always available at http://www.rpath.com/permanent/licenses/CPL-1.0. +# +# This program is distributed in the hope that it will be useful, but +# without any warranty; without even the implied warranty of merchantability +# or fitness for a particular purpose. See the Common Public License for +# full details. +# + +import time +import logging + +from rmake.cmdline import helper, monitor, commit + +from errors import JobFailedError, CommitFailedError + +log = logging.getLogger('updateBot.build') + +class Builder(object): + ''' + Class for wrapping the rMake api until we can switch to using rBuild. + + @param cfg: updateBot configuration object + @type cfg: config.UpdateBotConfig + ''' + + def __init__(self, cfg): + self._cfg = cfg + + self._helper = helper.rMakeHelper(root=self._cfg.configPath) + + + def build(self, trvSpecs): + ''' + Build a list of troves. + @param trvSpecs: list of trove specs + @type trvSpecs: [(name, versionObj, flavorObj), ...] + ''' + + # Create rMake job + log.info('Creating build job: %s' % (troveSpecs, )) + job = self._helper.creatBuild(troveSpecs) + jobId = self._helper.buildJob(job) + log.info('Started jobId: %s' % jobId) + + # Watch build, wait for completion + monitor.monitorJob(self._helper.client, jobId, + exitOnFinish=True, displayClass=_StatusOnlyDisplay) + + # Check for errors + job = self._helper.getJob(jobId) + if job.isFailed(): + log.error('Job %d failed', jobId) + raise JobFailedError(jobId=jobId, why='Job failed') + elif not job.isFinished(): + log.error('Job %d is not done, yet watch returned early!', jobId) + raise JobFailedError(jobId=jobId, why='Job not done') + elif not list(job.iterBuiltTroves()): + log.error('Job %d has no built troves', jobId) + raise JobFailedError(jobId=jobId, why='Job built no troves') + + # Do the commit + startTime = time.time() + log.info('Starting commit of job %d', jobId) + self._helper.startCommit(jobId) + succeeded, data = commit.commitJobs(self._helper.getConaryClient(), + [job, ], + self._helper.buildConfig.reposName, + self._cfg.commitMessage) + if not succeeded: + raise CommitFailedError(jobId=job.jobId, why=data) + + log.info('Commit of job %d completed in %.02f seconds', + jobId, time.time() - startTime) + + troveMap = {} + for _, troveTupleDict in data.iteritems(): + for buildTroveTuple, committedList in troveTupleDict.iteritems(): + troveMap[buildTroveTuple] = committedList + + return troveMap + + +class _StatusOnlyDisplay(monitor.JobLogDisplay): + ''' + Display only job and trove status. No log output. + + Copied from bob3 + ''' + + def _troveLogUpdated(self, (jobId, troveTuple), state, status): + '''Don't care about trove logs''' + pass + + def _trovePreparingChroot(self, (jobId, troveTuple), host, path): + '''Don't care about resolving/installing chroot''' + pass From johnsonm at rpath.com Wed Aug 19 17:40:12 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:40:12 +0000 Subject: mirrorball: move rpmsource and debsource into generic pkgsource module Message-ID: <200908192140.n7JLeCki017120@scc.eng.rpath.com> changeset: ff7e47a5396c user: Elliot Peele <http://issues.rpath.com/> date: Thu, 14 Aug 2008 17:58:43 -0400 move rpmsource and debsource into generic pkgsource module committer: Elliot Peele <http://issues.rpath.com/> diff --git a/updatebot/debsource.py b/updatebot/debsource.py deleted file mode 100644 --- a/updatebot/debsource.py +++ /dev/null @@ -1,75 +0,0 @@ -# -# Copyright (c) 2008 rPath, Inc. -# -# This program is distributed under the terms of the Common Public License, -# version 1.0. A copy of this license should have been distributed with this -# source file in a file called LICENSE. If it is not present, the license -# is always available at http://www.rpath.com/permanent/licenses/CPL-1.0. -# -# This program is distributed in the hope that it will be useful, but -# without any warranty; without even the implied warranty of merchantability -# or fitness for a particular purpose. See the Common Public License for -# full details. -# - -import logging - -import aptmd -from updatebot import util - -class DebSource(object): - def __init__(self, cfg): - self._excludeArch = cfg.excludeArch - - self._binPkgs = set() - self._srcPkgs = set() - - self.srcPkgMap = dict() - self.binPkgMap = dict() - self.srcNameMap = dict() - self.binNameMap = dict() - self.locationMap = dict() - - def loadFromClient(self, client, path): - for pkg in self.client.parse(path): - if pkg.arch in self._excludeArch: - continue - - if pkg.arch == 'src': - self._procSrc(pkg) - else: - self._procBin(pkg) - - def _procSrc(self, pkg): - if pkg.name not in self.srcNameMap: - self.srcNameMap[pkg.name] = set() - self.srcNameMap[pkg.name].add(pkg) - - for location in pkg.files: - self.locationMap[location] = pkg - - self._srcPkgs.add(pkg) - - def _procBin(self, pkg): - if pkg.name not in self.binNameMap: - self.binNameMap[pkg.name] = set() - self.binNameMap[pkg.name].add(pkg) - - self.locationMap[pkg.location] = pkg - - self._binPkgs.add(pkg) - - def finalize(self): - for srcPkg in self._srcPkgs: - if srcPkg in self.srcPkgMap: - continue - - self.srcPkgMap[srcPkg] = set() - for binPkgName in srcPkg.binaries: - for binPkg in self.binNameMap[binPkgName]: - if binPkg.version == srcPkg.version and binPkg.release = srcPkg.release: - self.srcPkgMap[srcPkg].add(binPkg) - self.srcPkgMap[srcPkg].add(srcPkg) - - for pkg in self.srcPkgMap[srcPkg]: - self.binPkgMap[pkg] = srcPkg diff --git a/updatebot/pkgsource/__init__.py b/updatebot/pkgsource/__init__.py new file mode 100644 --- /dev/null +++ b/updatebot/pkgsource/__init__.py @@ -0,0 +1,26 @@ +# +# Copyright (c) 2008 rPath, Inc. +# +# This program is distributed under the terms of the Common Public License, +# version 1.0. A copy of this license should have been distributed with this +# source file in a file called LICENSE. If it is not present, the license +# is always available at http://www.rpath.com/permanent/licenses/CPL-1.0. +# +# This program is distributed in the hope that it will be useful, but +# without any warranty; without even the implied warranty of merchantability +# or fitness for a particular purpose. See the Common Public License for +# full details. +# + +from updatebot.pkgsource.rpmsource import RpmSource +from updatebot.pkgsource.debsource import DebSource +from updatebot.pkgsource.errors import UnsupportedRepositoryError + +def PackageSource(cfg): + if cfg.repositoryFormat == 'apt': + return DebSource(cfg) + elif cfg.repositoryFormat == 'yum': + return RpmSource(cfg) + else: + raise UnsupportedRepositoryError(repo=cfg.repositoryFormat, + supported=['apt', 'yum']) diff --git a/updatebot/pkgsource/debsource.py b/updatebot/pkgsource/debsource.py new file mode 100644 --- /dev/null +++ b/updatebot/pkgsource/debsource.py @@ -0,0 +1,75 @@ +# +# Copyright (c) 2008 rPath, Inc. +# +# This program is distributed under the terms of the Common Public License, +# version 1.0. A copy of this license should have been distributed with this +# source file in a file called LICENSE. If it is not present, the license +# is always available at http://www.rpath.com/permanent/licenses/CPL-1.0. +# +# This program is distributed in the hope that it will be useful, but +# without any warranty; without even the implied warranty of merchantability +# or fitness for a particular purpose. See the Common Public License for +# full details. +# + +import logging + +import aptmd +from updatebot import util + +class DebSource(object): + def __init__(self, cfg): + self._excludeArch = cfg.excludeArch + + self._binPkgs = set() + self._srcPkgs = set() + + self.srcPkgMap = dict() + self.binPkgMap = dict() + self.srcNameMap = dict() + self.binNameMap = dict() + self.locationMap = dict() + + def loadFromClient(self, client, path): + for pkg in self.client.parse(path): + if pkg.arch in self._excludeArch: + continue + + if pkg.arch == 'src': + self._procSrc(pkg) + else: + self._procBin(pkg) + + def _procSrc(self, pkg): + if pkg.name not in self.srcNameMap: + self.srcNameMap[pkg.name] = set() + self.srcNameMap[pkg.name].add(pkg) + + for location in pkg.files: + self.locationMap[location] = pkg + + self._srcPkgs.add(pkg) + + def _procBin(self, pkg): + if pkg.name not in self.binNameMap: + self.binNameMap[pkg.name] = set() + self.binNameMap[pkg.name].add(pkg) + + self.locationMap[pkg.location] = pkg + + self._binPkgs.add(pkg) + + def finalize(self): + for srcPkg in self._srcPkgs: + if srcPkg in self.srcPkgMap: + continue + + self.srcPkgMap[srcPkg] = set() + for binPkgName in srcPkg.binaries: + for binPkg in self.binNameMap[binPkgName]: + if binPkg.version == srcPkg.version and binPkg.release = srcPkg.release: + self.srcPkgMap[srcPkg].add(binPkg) + self.srcPkgMap[srcPkg].add(srcPkg) + + for pkg in self.srcPkgMap[srcPkg]: + self.binPkgMap[pkg] = srcPkg diff --git a/updatebot/pkgsource/errors.py b/updatebot/pkgsource/errors.py new file mode 100644 --- /dev/null +++ b/updatebot/pkgsource/errors.py @@ -0,0 +1,23 @@ +# +# Copyright (c) 2008 rPath, Inc. +# +# This program is distributed under the terms of the Common Public License, +# version 1.0. A copy of this license should have been distributed with this +# source file in a file called LICENSE. If it is not present, the license +# is always available at http://www.rpath.com/permanent/licenses/CPL-1.0. +# +# This program is distributed in the hope that it will be useful, but +# without any warranty; without even the implied warranty of merchantability +# or fitness for a particular purpose. See the Common Public License for +# full details. +# + +from updatebot.errors import UpdateBotError + +class PackageSourceError(UpdateBotError): + pass + +class UnsupportedRepositoryError(PackageSourceError): + _params = ['repo', 'supported'] + _template = ('%(repo)s is not a supported repository format, please ' + 'choose one of the following %(supported)s') diff --git a/updatebot/pkgsource/rpmsource.py b/updatebot/pkgsource/rpmsource.py new file mode 100644 --- /dev/null +++ b/updatebot/pkgsource/rpmsource.py @@ -0,0 +1,177 @@ +# +# Copyright (c) 2006,2008 rPath, Inc. +# +# +# This program is distributed under the terms of the Common Public License, +# version 1.0. A copy of this license should have been distributed with this +# source file in a file called LICENSE. If it is not present, the license +# is always available at http://www.rpath.com/permanent/licenses/CPL-1.0. +# +# This program is distributed in the hope that it will be useful, but +# without any warranty; without even the implied warranty of merchantability +# or fitness for a particular purpose. See the Common Public License for +# full details. +# + +""" +Module for interacting with packages in multiple yum repositories. +""" + +import logging + +import repomd +from updatebot import util + +log = logging.getLogger('rpmimport.rpmsource') + +class RpmSource(object): + """ + Class that builds maps of packages from multiple yum repositories. + """ + + def __init__(self, cfg): + self._excludeArch = cfg.excludeArch + + # {srpm: {rpm: path} + self._rpmMap = dict() + + # set of all src pkg objects + self._srcPkgs = set() + + # {location: srpm} + self.locationMap = dict() + + # {srcPkg: [binPkg, ... ] } + self.srcPkgMap = dict() + + # {binPkg: srcPkg} + self.binPkgMap = dict() + + # {srcName: [srcPkg, ... ] } + self.srcNameMap = dict() + + # {binName: [binPkg, ... ] } + self.binNameMap = dict() + + def load(self, url, basePath=''): + """ + Walk the yum repository rooted at url/basePath and collect information + about rpms found. + @param url: url to common directory on host where all repositories resides. + @type url: string + @param basePath: directory where repository resides. + @type basePath: string + """ + + client = repomd.Client(url + '/' + basePath) + self.loadFromClient(client, basePath=basePath) + + def loadFromClient(self, client, basePath=''): + """ + Walk the yum repository rooted at url/basePath and collect information + about rpms found. + @param client: client object for extracting data from the repo metadata + @type client: repomd.Client + @param basePath: path to prefix location metadata with + @type basePath: string + """ + + for pkg in client.getPackageDetail(): + # ignore the 32-bit compatibility libs - we will + # simply use the 32-bit components from the repository + if '32bit' in pkg.name: + continue + + # Don't use all arches. + if pkg.arch in self._excludeArch: + continue + + assert '-' not in pkg.version + assert '-' not in pkg.release + + pkg.location = basePath + '/' + pkg.location + + if pkg.sourcerpm == '': + self._procSrc(pkg) + else: + self._procBin(pkg) + + def _procSrc(self, package): + """ + Process source rpms. + @param package: package object + @type package: repomd.packagexml._Package + """ + + if package.name not in self.srcNameMap: + self.srcNameMap[package.name] = set() + self.srcNameMap[package.name].add(package) + + self.locationMap[package.location] = package + + self._srcPkgs.add(package) + + def _procBin(self, package): + """ + Process binary rpms. + @param package: package object + @type package: repomd.packagexml._Package + """ + + # FIXME: There should be a better way to figure out the tuple that + # represents the hash of the srcPkg. + srcParts = package.sourcerpm.split('-') + srcName = '-'.join(srcParts[:-2]) + srcVersion = srcParts[-2] + if srcParts[-1].endswith('.src.rpm'): + srcRelease = srcParts[-1][:-8] # remove '.src.rpm' + elif srcParts[-1].endswith('.nosrc.rpm'): + srcRelease = srcParts[-1][:-10] + rpmMapKey = (srcName, package.epoch, srcVersion, srcRelease, 'src') + if rpmMapKey not in self._rpmMap: + self._rpmMap[rpmMapKey] = set() + self._rpmMap[rpmMapKey].add(package) + + if package.name not in self.binNameMap: + self.binNameMap[package.name] = set() + self.binNameMap[package.name].add(package) + + self.locationMap[package.location] = package + + def finalize(self): + """ + Make some final datastructures now that we are done populating object. + """ + + # Now that we have processed all of the rpms, build some more data + # structures. + count = 0 + for pkg in self._srcPkgs: + key = (pkg.name, pkg.epoch, pkg.version, pkg.release, pkg.arch) + if pkg in self.srcPkgMap: + continue + + if key not in self._rpmMap: + #log.warn('found source without binary rpms: %s' % pkg) + #log.debug(key) + #log.debug([ x for x in self._rpmMap if x[0] == key[0] ]) + + count += 1 + if pkg in self.srcNameMap[pkg.name]: + self.srcNameMap[pkg.name].remove(pkg) + continue + + self.srcPkgMap[pkg] = self._rpmMap[key] + self.srcPkgMap[pkg].add(pkg) + + for binPkg in self.srcPkgMap[pkg]: + self.binPkgMap[binPkg] = pkg + + log.warn('found %s source rpms without matching binary rpms' % count) + + def loadFileLists(self, client, basePath): + for pkg in client.getFileLists(): + for binPkg in self.binPkgMap.iterkeys(): + if util.packageCompare(pkg, binPkg) == 0: + binPkg.files = pkg.files + break diff --git a/updatebot/rpmsource.py b/updatebot/rpmsource.py deleted file mode 100644 --- a/updatebot/rpmsource.py +++ /dev/null @@ -1,177 +0,0 @@ -# -# Copyright (c) 2006,2008 rPath, Inc. -# -# -# This program is distributed under the terms of the Common Public License, -# version 1.0. A copy of this license should have been distributed with this -# source file in a file called LICENSE. If it is not present, the license -# is always available at http://www.rpath.com/permanent/licenses/CPL-1.0. -# -# This program is distributed in the hope that it will be useful, but -# without any warranty; without even the implied warranty of merchantability -# or fitness for a particular purpose. See the Common Public License for -# full details. -# - -""" -Module for interacting with packages in multiple yum repositories. -""" - -import logging - -import repomd -from updatebot import util - -log = logging.getLogger('rpmimport.rpmsource') - -class RpmSource(object): - """ - Class that builds maps of packages from multiple yum repositories. - """ - - def __init__(self, cfg): - self._excludeArch = cfg.excludeArch - - # {srpm: {rpm: path} - self._rpmMap = dict() - - # set of all src pkg objects - self._srcPkgs = set() - - # {location: srpm} - self.locationMap = dict() - - # {srcPkg: [binPkg, ... ] } - self.srcPkgMap = dict() - - # {binPkg: srcPkg} - self.binPkgMap = dict() - - # {srcName: [srcPkg, ... ] } - self.srcNameMap = dict() - - # {binName: [binPkg, ... ] } - self.binNameMap = dict() - - def load(self, url, basePath=''): - """ - Walk the yum repository rooted at url/basePath and collect information - about rpms found. - @param url: url to common directory on host where all repositories resides. - @type url: string - @param basePath: directory where repository resides. - @type basePath: string - """ - - client = repomd.Client(url + '/' + basePath) - self.loadFromClient(client, basePath=basePath) - - def loadFromClient(self, client, basePath=''): - """ - Walk the yum repository rooted at url/basePath and collect information - about rpms found. - @param client: client object for extracting data from the repo metadata - @type client: repomd.Client - @param basePath: path to prefix location metadata with - @type basePath: string - """ - - for pkg in client.getPackageDetail(): - # ignore the 32-bit compatibility libs - we will - # simply use the 32-bit components from the repository - if '32bit' in pkg.name: - continue - - # Don't use all arches. - if pkg.arch in self._excludeArch: - continue - - assert '-' not in pkg.version - assert '-' not in pkg.release - - pkg.location = basePath + '/' + pkg.location - - if pkg.sourcerpm == '': - self._procSrc(pkg) - else: - self._procBin(pkg) - - def _procSrc(self, package): - """ - Process source rpms. - @param package: package object - @type package: repomd.packagexml._Package - """ - - if package.name not in self.srcNameMap: - self.srcNameMap[package.name] = set() - self.srcNameMap[package.name].add(package) - - self.locationMap[package.location] = package - - self._srcPkgs.add(package) - - def _procBin(self, package): - """ - Process binary rpms. - @param package: package object - @type package: repomd.packagexml._Package - """ - - # FIXME: There should be a better way to figure out the tuple that - # represents the hash of the srcPkg. - srcParts = package.sourcerpm.split('-') - srcName = '-'.join(srcParts[:-2]) - srcVersion = srcParts[-2] - if srcParts[-1].endswith('.src.rpm'): - srcRelease = srcParts[-1][:-8] # remove '.src.rpm' - elif srcParts[-1].endswith('.nosrc.rpm'): - srcRelease = srcParts[-1][:-10] - rpmMapKey = (srcName, package.epoch, srcVersion, srcRelease, 'src') - if rpmMapKey not in self._rpmMap: - self._rpmMap[rpmMapKey] = set() - self._rpmMap[rpmMapKey].add(package) - - if package.name not in self.binNameMap: - self.binNameMap[package.name] = set() - self.binNameMap[package.name].add(package) - - self.locationMap[package.location] = package - - def finalize(self): - """ - Make some final datastructures now that we are done populating object. - """ - - # Now that we have processed all of the rpms, build some more data - # structures. - count = 0 - for pkg in self._srcPkgs: - key = (pkg.name, pkg.epoch, pkg.version, pkg.release, pkg.arch) - if pkg in self.srcPkgMap: - continue - - if key not in self._rpmMap: - #log.warn('found source without binary rpms: %s' % pkg) - #log.debug(key) - #log.debug([ x for x in self._rpmMap if x[0] == key[0] ]) - - count += 1 - if pkg in self.srcNameMap[pkg.name]: - self.srcNameMap[pkg.name].remove(pkg) - continue - - self.srcPkgMap[pkg] = self._rpmMap[key] - self.srcPkgMap[pkg].add(pkg) - - for binPkg in self.srcPkgMap[pkg]: - self.binPkgMap[binPkg] = pkg - - log.warn('found %s source rpms without matching binary rpms' % count) - - def loadFileLists(self, client, basePath): - for pkg in client.getFileLists(): - for binPkg in self.binPkgMap.iterkeys(): - if util.packageCompare(pkg, binPkg) == 0: - binPkg.files = pkg.files - break From johnsonm at rpath.com Wed Aug 19 17:42:06 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:42:06 +0000 Subject: mirrorball: pylint fixups Message-ID: <200908192142.n7JLg6FR021707@scc.eng.rpath.com> changeset: 8ec55e36a0c0 user: Elliot Peele <https://issues.rpath.com/> date: Sun, 18 Jan 2009 17:47:12 +0000 pylint fixups committer: Elliot Peele <https://issues.rpath.com/> diff --git a/aptmd/container.py b/aptmd/container.py --- a/aptmd/container.py +++ b/aptmd/container.py @@ -24,6 +24,9 @@ _slots = ('_data', ) def __init__(self): + # W0212 - Access to a protected member _slots of a client class + # pylint: disable-msg=W0212 + for cls in self.__class__.__mro__: if hasattr(cls, '_slots'): for item in cls._slots: diff --git a/pmap/__init__.py b/pmap/__init__.py --- a/pmap/__init__.py +++ b/pmap/__init__.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2008 rPath, Inc. +# Copyright (c) 2008-2009 rPath, Inc. # # This program is distributed under the terms of the Common Public License, # version 1.0. A copy of this license should have been distributed with this @@ -27,7 +27,7 @@ from imputil import imp __all__ = ('InvalidBackendError', 'parse') -_supportedBackends = ('ubuntu', 'centos') +__supportedBackends = ('ubuntu', 'centos') class InvalidBackendError(Exception): """ @@ -61,9 +61,9 @@ otherwise raise an exception. """ - if backend not in _supportedBackends: + if backend not in __supportedBackends: raise InvalidBackendError('%s is not a supported backend, please ' - 'choose from %s' % (backend, ','.join(_supportedBackends))) + 'choose from %s' % (backend, ','.join(__supportedBackends))) try: path = [imp.find_module('pmap')[1], ] diff --git a/pmap/common.py b/pmap/common.py --- a/pmap/common.py +++ b/pmap/common.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2008 rPath, Inc. +# Copyright (c) 2008-2009 rPath, Inc. # # This program is distributed under the terms of the Common Public License, # version 1.0. A copy of this license should have been distributed with this @@ -98,7 +98,8 @@ # Extract info from message fromLine = msg['From'] - self._curObj.fromAddr = fromLine[:fromLine.find('(')].replace(' at ', '@') + self._curObj.fromAddr = \ + fromLine[:fromLine.find('(')].replace(' at ', '@') self._curObj.fromName = fromLine[fromLine.find('('):].strip('()') self._curObj.timestamp = ' '.join(msg.get_from().split()[4:]) self._curObj.subject = msg['Subject'].replace('\n\t', ' ') diff --git a/pmap/ubuntu.py b/pmap/ubuntu.py --- a/pmap/ubuntu.py +++ b/pmap/ubuntu.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2008 rPath, Inc. +# Copyright (c) 2008-2009 rPath, Inc. # # This program is distributed under the terms of the Common Public License, # version 1.0. A copy of this license should have been distributed with this @@ -27,6 +27,16 @@ _slots = ('pkgs', 'pkgNameVersion') def finalize(self): + """ + Finalize the instance. + """ + + # E1101 - Instance has no member + # pylint: disable-msg=E1101 + + # W0201 - Attribute defined outside of __init__ + # pylint: disable-msg=W0201 + BaseContainer.finalize(self) assert self.subject is not None @@ -67,15 +77,27 @@ self._filterLine('^The problem can be corrected.*$', 'setdescription') def _newContainer(self): + """ + Create a new container instance. + """ + if self._curObj and not self._curObj.pkgs: self._curObj = None BaseParser._newContainer(self) def _parseLine(self, cfgline): + """ + Parse a single line. + """ + cfgline = cfgline.strip() return BaseParser._parseLine(self, cfgline) def _getState(self, state): + """ + Find the correct state. + """ + state = BaseParser._getState(self, state) if state in self._states: return state @@ -86,6 +108,10 @@ return state def _headersep(self): + """ + Parse header serparator. + """ + if len(self._line) != 1: return @@ -96,16 +122,28 @@ self._inHeader = False def _the(self): + """ + Parse lines starting in "the". + """ + if self._endHeader and self._text.strip(): self._curObj.description = self._text.strip() def _ubuntu(self): + """ + Parse Ubuntu lines. + """ + if '.' in self._line[1] and len(self._line[1].split('.')) == 2: year, month = self._line[1].strip(':').split('.') if year.isdigit() and month.isdigit(): self._curDistroVer = self._line[1].strip(':') def _pkgnamever(self): + """ + Parse the package name and version line. + """ + # looks like a version line = [ x for x in self._line if x ] @@ -120,6 +158,10 @@ self._curObj.pkgNameVersion[self._curDistroVer].add((name, version)) def _repourl(self): + """ + Parse repository Url line. + """ + url = self._line[0] parts = url.split('/') @@ -130,20 +172,36 @@ self._curObj.pkgs.add('/'.join(parts[4:])) def _details(self): + """ + Find the begining of the description section. + """ + if self._line[1] == 'follow:': self._curDistroVer = None self._inDetails = True self._endHeader = False def _updated(self): + """ + Find the end of the description section. + """ + if self._line[1] == 'packages': self._processDetails() def _source(self): + """ + Find the end of the description section. + """ + if self._line[1] == 'archives:': self._processDetails() def _processDetails(self): + """ + Process the description section. + """ + if self._inDetails or self._endHeader: self._inDetails = False if self._curObj.description is None: diff --git a/updatebot/advisories/__init__.py b/updatebot/advisories/__init__.py --- a/updatebot/advisories/__init__.py +++ b/updatebot/advisories/__init__.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2008 rPath, Inc. +# Copyright (c) 2008-2009 rPath, Inc. # # This program is distributed under the terms of the Common Public License, # version 1.0. A copy of this license should have been distributed with this @@ -22,14 +22,16 @@ log = logging.getLogger('updatebot.advisories') class InvalidBackendError(Exception): - pass + """ + Raised when an unsupported backend is used. + """ -__supported_backends = ('sles', 'centos', 'ubuntu') +__supportedBackends = ('sles', 'centos', 'ubuntu') def __getBackend(backend): - if backend not in __supported_backends: + if backend not in __supportedBackends: raise InvalidBackendError('%s is not a supported backend, please ' - 'choose from %s' % (backend, ','.join(__supported_backends))) + 'choose from %s' % (backend, ','.join(__supportedBackends))) try: updatebotPath = [imp.find_module('updatebot')[1], ] @@ -42,6 +44,10 @@ % (backend, e)) def Advisor(cfg, pkgSource, backend): + """ + Get an instance of an advisor for a given backend. + """ + module = __getBackend(backend) obj = module.Advisor(cfg, pkgSource) return obj diff --git a/updatebot/advisories/centos.py b/updatebot/advisories/centos.py --- a/updatebot/advisories/centos.py +++ b/updatebot/advisories/centos.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2008 rPath, Inc. +# Copyright (c) 2008-2009 rPath, Inc. # # This program is distributed under the terms of the Common Public License, # version 1.0. A copy of this license should have been distributed with this @@ -12,6 +12,10 @@ # full details. # +""" +Advisory module for CentOS. +""" + import os import pmap import logging @@ -21,6 +25,10 @@ log = logging.getLogger('updatebot.advisories') class Advisor(BaseAdvisor): + """ + Class for processing CentOS advisory information. + """ + supportedArches = ('i386', 'i586', 'i686', 'x86_64', 'noarch', 'src') def load(self): @@ -96,11 +104,14 @@ def _hasException(self, binPkg): """ Check the config for repositories with exceptions for sending - advisories. (io. repositories that we generated metadata for.) + advisories. (ie. repositories that we generated metadata for.) @param binPkg: binary package object @type binPkg: repomd.packagexml._Package """ + # W0613 - Unused argument binPkg + # pylint: disable-msg=W0613 + return False def _isUpdatesRepo(self, binPkg): diff --git a/updatebot/advisories/common.py b/updatebot/advisories/common.py --- a/updatebot/advisories/common.py +++ b/updatebot/advisories/common.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2008 rPath, Inc. +# Copyright (c) 2008-2009 rPath, Inc. # # This program is distributed under the terms of the Common Public License, # version 1.0. A copy of this license should have been distributed with this @@ -414,8 +414,6 @@ timeTup = list(time.strptime(startDate, '%Y%m')) startYear = timeTup[0] - startMonth = timeTup[1] - endYear = time.localtime()[0] endMonth = time.localtime()[1] diff --git a/updatebot/advisories/sles.py b/updatebot/advisories/sles.py --- a/updatebot/advisories/sles.py +++ b/updatebot/advisories/sles.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2008 rPath, Inc. +# Copyright (c) 2008-2009 rPath, Inc. # # This program is distributed under the terms of the Common Public License, # version 1.0. A copy of this license should have been distributed with this @@ -12,6 +12,10 @@ # full details. # +""" +Advisory module for SLES. +""" + import logging from updatebot.advisories.common import BaseAdvisor @@ -19,6 +23,10 @@ log = logging.getLogger('updatebot.advisories') class Advisor(BaseAdvisor): + """ + Class for processing SLES advisory information. + """ + allowExtraPackages = True def load(self): @@ -90,5 +98,8 @@ return True, otherwise return False. """ + # W0613 - Unused argument patchSet + # pylint: disable-msg=W0613 + # Don't have dups on sles return False diff --git a/updatebot/advisories/ubuntu.py b/updatebot/advisories/ubuntu.py --- a/updatebot/advisories/ubuntu.py +++ b/updatebot/advisories/ubuntu.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2008 rPath, Inc. +# Copyright (c) 2008-2009 rPath, Inc. # # This program is distributed under the terms of the Common Public License, # version 1.0. A copy of this license should have been distributed with this @@ -25,6 +25,10 @@ log = logging.getLogger('updatebot.advisories') class Advisor(BaseAdvisor): + """ + Class for processing Ubuntu advisory information. + """ + def load(self): """ Parse the required data to generate a mapping of binary package @@ -93,6 +97,9 @@ @type binPkg: repomd.packagexml._Package """ + # W0613 - Unused argument + # pylint: disable-msg=W0613 + return False def _isUpdatesRepo(self, binPkg): @@ -103,6 +110,9 @@ @type binPkg: repomd.packagexml._Package """ + # W0613 - Unused argument + # pylint: disable-msg=W0613 + return True def _checkForDuplicates(self, patchSet): @@ -112,4 +122,7 @@ return True, otherwise return False. """ + # W0613 - Unused argument + # pylint: disable-msg=W0613 + return False diff --git a/updatebot/bot.py b/updatebot/bot.py --- a/updatebot/bot.py +++ b/updatebot/bot.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2008 rPath, Inc. +# Copyright (c) 2008-2009 rPath, Inc. # # This program is distributed under the terms of the Common Public License, # version 1.0. A copy of this license should have been distributed with this @@ -69,7 +69,8 @@ self._pkgSource.load() # Import sources into repository. - toBuild, fail = self._updater.create(self._cfg.package, buildAll=rebuild) + toBuild, fail = self._updater.create(self._cfg.package, + buildAll=rebuild) if not rebuild: # Build all newly imported packages. diff --git a/updatebot/lib/__init__.py b/updatebot/lib/__init__.py --- a/updatebot/lib/__init__.py +++ b/updatebot/lib/__init__.py @@ -11,3 +11,7 @@ # or fitness for a particular purpose. See the Common Public License for # full details. # + +""" +Module for general libary modules. +""" diff --git a/updatebot/lib/jira.py b/updatebot/lib/jira.py --- a/updatebot/lib/jira.py +++ b/updatebot/lib/jira.py @@ -12,6 +12,10 @@ # full details. # +""" +Module for manipulating Jira issues. +""" + import pyjira class JiraClient(pyjira.JiraClient): @@ -21,8 +25,8 @@ def __init__(self, cfg): self._cfg = cfg - self._wsdl = '%s/rpc/soap/jirasoapservice-v2?wsdl' % cfg.jiraUrl - self._login = (cfg.jiraUser, cfg.jiraPassword) + self._wsdl = '%s/rpc/soap/jirasoapservice-v2?wsdl' % self._cfg.jiraUrl + self._login = (self._cfg.jiraUser, self._cfg.jiraPassword) pyjira.JiraClient.__init__(self, self._wsdl, self._login) @@ -33,4 +37,4 @@ """ pyjira.JiraClient.addComment(self, issueKey, body, - level=cfg.jiraSecurityGroup) + level=self._cfg.jiraSecurityGroup) diff --git a/updatebot/lib/satisclient.py b/updatebot/lib/satisclient.py --- a/updatebot/lib/satisclient.py +++ b/updatebot/lib/satisclient.py @@ -17,9 +17,9 @@ """ from satis import types +from satis.types import And, Equals from satis import util as satisutil from satis.client.http import HTTPClient -from satis.types import Or, And, Equals, NewerThan class SatisClient(object): """ diff --git a/updatebot/lib/xobjects.py b/updatebot/lib/xobjects.py --- a/updatebot/lib/xobjects.py +++ b/updatebot/lib/xobjects.py @@ -27,7 +27,6 @@ """ data = str - freeze = xobj.Document.toxml @classmethod diff --git a/updatebot/pkgsource/__init__.py b/updatebot/pkgsource/__init__.py --- a/updatebot/pkgsource/__init__.py +++ b/updatebot/pkgsource/__init__.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2008 rPath, Inc. +# Copyright (c) 2008-2009 rPath, Inc. # # This program is distributed under the terms of the Common Public License, # version 1.0. A copy of this license should have been distributed with this @@ -12,11 +12,20 @@ # full details. # +""" +Module for interacting with repository metadata. +""" + from updatebot.pkgsource.rpmsource import RpmSource from updatebot.pkgsource.debsource import DebSource from updatebot.pkgsource.errors import UnsupportedRepositoryError def PackageSource(cfg): + """ + Method that returns an instance of the appropriate package source + backend based on config data. + """ + if cfg.repositoryFormat == 'apt': return DebSource(cfg) elif cfg.repositoryFormat == 'yum': diff --git a/updatebot/pkgsource/common.py b/updatebot/pkgsource/common.py --- a/updatebot/pkgsource/common.py +++ b/updatebot/pkgsource/common.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2008 rPath, Inc. +# Copyright (c) 2008-2009 rPath, Inc. # # This program is distributed under the terms of the Common Public License, # version 1.0. A copy of this license should have been distributed with this @@ -57,4 +57,8 @@ return self._clients - + def load(self): + """ + Method to parse all package data into data structures listed above. + NOTE: This method should be implmented by all backends. + """ diff --git a/updatebot/pkgsource/debsource.py b/updatebot/pkgsource/debsource.py --- a/updatebot/pkgsource/debsource.py +++ b/updatebot/pkgsource/debsource.py @@ -12,15 +12,22 @@ # full details. # +""" +Package source for APT repositories. +""" + import logging import aptmd -from updatebot.lib import util from updatebot.pkgsource.common import BasePackageSource log = logging.getLogger('updatebot.pkgsource') class DebSource(BasePackageSource): + """ + PackageSource backend for APT repositories. + """ + def __init__(self, cfg): BasePackageSource.__init__(self, cfg) @@ -41,6 +48,10 @@ self.finalize() def loadFromClient(self, client, path): + """ + Load repository metadata from a aptmd client object. + """ + for pkg in client.parse(path): if pkg.arch in self._excludeArch: continue @@ -51,6 +62,10 @@ self._procBin(pkg) def _procSrc(self, pkg): + """ + Process source packages. + """ + if pkg.name not in self.srcNameMap: self.srcNameMap[pkg.name] = set() self.srcNameMap[pkg.name].add(pkg) @@ -61,6 +76,10 @@ self._srcPkgs.add(pkg) def _procBin(self, pkg): + """ + Process binary packages. + """ + if pkg.name not in self.binNameMap: self.binNameMap[pkg.name] = set() self.binNameMap[pkg.name].add(pkg) @@ -70,6 +89,10 @@ self._binPkgs.add(pkg) def finalize(self): + """ + Finalize all data structures. + """ + for srcPkg in self._srcPkgs: if srcPkg in self.srcPkgMap: continue diff --git a/updatebot/pkgsource/errors.py b/updatebot/pkgsource/errors.py --- a/updatebot/pkgsource/errors.py +++ b/updatebot/pkgsource/errors.py @@ -12,12 +12,22 @@ # full details. # +""" +PackageSource Errors Module +""" + from updatebot.errors import UpdateBotError class PackageSourceError(UpdateBotError): - pass + """ + Base error for all package source related errors to inherit from. + """ class UnsupportedRepositoryError(PackageSourceError): + """ + Raised when an unsupported backend is used. + """ + _params = ['repo', 'supported'] _template = ('%(repo)s is not a supported repository format, please ' 'choose one of the following %(supported)s') diff --git a/updatebot/pkgsource/rpmsource.py b/updatebot/pkgsource/rpmsource.py --- a/updatebot/pkgsource/rpmsource.py +++ b/updatebot/pkgsource/rpmsource.py @@ -94,7 +94,8 @@ pkg.location = basePath + '/' + pkg.location # ignore 32bit rpms in a 64bit repo. - if pkg.arch in ('i386', 'i586', 'i686') and 'x86_64' in pkg.location: + if (pkg.arch in ('i386', 'i586', 'i686') and + 'x86_64' in pkg.location): continue if pkg.sourcerpm == '': @@ -116,7 +117,8 @@ self.locationMap[package.location] = package self._srcPkgs.add(package) - self._srcMap[(package.name, package.epoch, package.version, package.release, package.arch)] = package + self._srcMap[(package.name, package.epoch, package.version, + package.release, package.arch)] = package def _procBin(self, package): """ @@ -177,7 +179,8 @@ self.binPkgMap[binPkg] = pkg if count > 0: - log.warn('found %s source rpms without matching binary rpms' % count) + log.warn('found %s source rpms without matching binary ' + 'rpms' % count) # Defer deletes, contents of rpmMap are used more than once. for key in toDelete: @@ -204,6 +207,10 @@ log.warn('found %s binary rpms without matching srpms' % count) def loadFileLists(self, client, basePath): + """ + Parse file information. + """ + for pkg in client.getFileLists(): for binPkg in self.binPkgMap.iterkeys(): if util.packageCompare(pkg, binPkg) == 0: From johnsonm at rpath.com Wed Aug 19 17:38:53 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:38:53 +0000 Subject: mirrorball: branch merge Message-ID: <200908192139.n7JLcrmn013888@scc.eng.rpath.com> changeset: 4d94b13e4134 user: Matt Wilson <https://issues.rpath.com/> date: Tue, 27 May 2008 16:49:41 -0400 branch merge committer: Matt Wilson <https://issues.rpath.com/> diff --git a/repomd/__init__.py b/repomd/__init__.py new file mode 100644 --- /dev/null +++ b/repomd/__init__.py @@ -0,0 +1,39 @@ +# +# Copyright (c) 2008 rPath, Inc. +# +# This program is distributed under the terms of the Common Public License, +# version 1.0. A copy of this license should have been distributed with this +# source file in a file called LICENSE. If it is not present, the license +# is always available at http://www.rpath.com/permanent/licenses/CPL-1.0. +# +# This program is distributed in the hope that it will be useful, but +# without any warranty; without even the implied warranty of merchantability +# or fitness for a particular purpose. See the Common Public License for +# full details. +# + + +from repomdxml import RepoMdXml +from repository import Repository +from errors import * + +__all__ = ('Client', ) + public_errors + +class Client(object): + def __init__(self, repoUrl): + self._repoUrl = repoUrl + + self._baseMdPath = '/repodata/repomd.xml' + self._repo = Repository(self._repoUrl) + self._repomd = RepoMdXml(self._repo, self._baseMdPath).parse() + + def getRepos(self): + return self._repo + + def getPatchDetail(self): + node = self._repomd.getRepoData('patches') + return [ x.parseChildren() for x in node.parseChildren().getPatches() ] + + def getPackageDetail(self): + node = self._repomd.getRepoData('primary') + return node.parseChildren().getPackages() diff --git a/repomd/errors.py b/repomd/errors.py new file mode 100644 --- /dev/null +++ b/repomd/errors.py @@ -0,0 +1,37 @@ +# +# Copyright (c) 2008 rPath, Inc. +# +# This program is distributed under the terms of the Common Public License, +# version 1.0. A copy of this license should have been distributed with this +# source file in a file called LICENSE. If it is not present, the license +# is always available at http://www.rpath.com/permanent/licenses/CPL-1.0. +# +# This program is distributed in the hope that it will be useful, but +# without any warranty; without even the implied warranty of merchantability +# or fitness for a particular purpose. See the Common Public License for +# full details. +# + +public_errors = ('RepoMdError', 'ParseError', 'UnknownElementError') + +__all__ = public_errors + ('public_errors', ) + +class RepoMdError(Exception): + pass + +class ParseError(RepoMdError): + pass + +class UnknownElementError(ParseError): + def __init__(self, element): + self._element = element + self._error = 'Element %s is not supported by this parser.' + + def __str__(self): + return self._error % (self._element.getAbsoluteName(), ) + +class UnknownAttributeError(UnknownElementError): + def __init__(self, element, attribute): + UnknownElementError.__init__(self, element) + self._attribute = attribute + self._error = 'Attribute %s of %%s is not supported by this parser.' % (attribute, ) diff --git a/repomd/packagexml.py b/repomd/packagexml.py new file mode 100644 --- /dev/null +++ b/repomd/packagexml.py @@ -0,0 +1,179 @@ +# +# Copyright (c) 2008 rPath, Inc. +# +# This program is distributed under the terms of the Common Public License, +# version 1.0. A copy of this license should have been distributed with this +# source file in a file called LICENSE. If it is not present, the license +# is always available at http://www.rpath.com/permanent/licenses/CPL-1.0. +# +# This program is distributed in the hope that it will be useful, but +# without any warranty; without even the implied warranty of merchantability +# or fitness for a particular purpose. See the Common Public License for +# full details. +# + +__all__ = ('PackageXmlMixIn', ) + +from rpath_common.xmllib import api1 as xmllib + +from errors import UnknownElementError, UnknownAttributeError + +class _Package(xmllib.BaseNode): + name = None + arch = None + epoch = None + version = None + release = None + checksum = None + checksumType = None + summary = None + description = None + packager = None + url = None + fileTimestamp = None + buildTimestamp = None + packageSize = None + installedSize = None + archiveSize = None + location = None + license = None + vendor = None + group = None + buildhost = None + sourcerpm = None + headerStart = None + headerEnd = None + + def addChild(self, child): + if child.getName() == 'name': + self.name = child.finalize() + elif child.getName() == 'arch': + self.arch = child.finalize() + elif child.getName() == 'version': + self.epoch = child.getAttribute('epoch') + self.version = child.getAttribute('ver') + self.release = child.getAttribute('rel') + elif child.getName() == 'checksum': + self.checksum = child.finalize() + self.checksumType = child.getAttribute('type') + elif child.getName() == 'summary': + self.summary = child.finalize() + elif child.getName() == 'description': + self.description = child.finalize() + elif child.getName() == 'packager': + self.packager = child.finalize() + elif child.getName() == 'url': + self.url = child.finalize() + elif child.getName() == 'time': + self.fileTimestamp = child.getAttribute('file') + self.buildTimestamp = child.getAttribute('build') + elif child.getName() == 'size': + self.packageSize = child.getAttribute('package') + self.installedSize = child.getAttribute('installed') + self.archiveSize = child.getAttribute('archive') + elif child.getName() == 'location': + self.location = child.getAttribute('href') + elif child.getName() == 'format': + self.format = [] + for node in child.iterChildren(): + if node.getName() == 'rpm:license': + self.license = node.getText() + elif node.getName() == 'rpm:vendor': + self.vendor = node.getText() + elif node.getName() == 'rpm:group': + self.group = node.getText() + elif node.getName() == 'rpm:buildhost': + self.buildhost = node.getText() + elif node.getName() == 'rpm:sourcerpm': + self.sourcerpm = node.getText() + elif node.getName() == 'rpm:header-range': + self.headerStart = node.getAttribute('start') + self.headerEnd = node.getAttribute('end') + elif node.getName() in ('rpm:provides', 'rpm:requires', + 'rpm:obsoletes', 'rpm:recommends', + 'rpm:conflicts', 'suse:freshens'): + self.format.append(node) + elif node.getName() == 'file': + pass + else: + raise UnknownElementError(node) + elif child.getName() == 'pkgfiles': + pass + else: + raise UnknownElementError(child) + + +class _RpmRequires(xmllib.BaseNode): + def addChild(self, child): + if child.getName() in ('rpm:entry', 'suse:entry'): + for attr, value in child.iterAttributes(): + child.kind = None + child.name = None + child.epoch = None + child.version = None + child.release = None + child.flags = None + child.pre = None + + if attr == 'kind': + child.kind = value + elif attr == 'name': + child.name = value + elif attr == 'epoch': + child.epoch = value + elif attr == 'ver': + child.version = value + elif attr == 'rel': + child.release = value + elif attr == 'flags': + child.flags = value + elif attr == 'pre': + child.pre = value + else: + raise UnknownAttributeError(child, attr) + xmllib.BaseNode.addChild(self, child) + else: + raise UnknownElementError(child) + + +class _RpmRecommends(_RpmRequires): + pass + + +class _RpmProvides(_RpmRequires): + pass + + +class _RpmObsoletes(_RpmRequires): + pass + + +class _RpmConflicts(_RpmRequires): + pass + + +class _SuseFreshens(_RpmRequires): + pass + + +class PackageXmlMixIn(object): + def _registerTypes(self): + self._databinder.registerType(_Package, name='package') + self._databinder.registerType(xmllib.StringNode, name='name') + self._databinder.registerType(xmllib.StringNode, name='arch') + self._databinder.registerType(xmllib.StringNode, name='checksum') + self._databinder.registerType(xmllib.StringNode, name='summary') + self._databinder.registerType(xmllib.StringNode, name='description') + self._databinder.registerType(xmllib.StringNode, name='url') + # FIXME: really shouldn't need to comment these out + #self._databinder.registerType(xmllib.StringNode, name='license', namespace='rpm') + #self._databinder.registerType(xmllib.StringNode, name='vendor', namespace='rpm') + #self._databinder.registerType(xmllib.StringNode, name='group', namespace='rpm') + #self._databinder.registerType(xmllib.StringNode, name='buildhost', namespace='rpm') + #self._databinder.registerType(xmllib.StringNode, name='sourcerpm', namespace='rpm') + self._databinder.registerType(_RpmRequires, name='requires', namespace='rpm') + self._databinder.registerType(_RpmRecommends, name='recommends', namespace='rpm') + self._databinder.registerType(_RpmProvides, name='provides', namespace='rpm') + self._databinder.registerType(_RpmObsoletes, name='obsoletes', namespace='rpm') + self._databinder.registerType(_RpmConflicts, name='conflicts', namespace='rpm') + self._databinder.registerType(_SuseFreshens, name='freshens', namespace='suse') diff --git a/repomd/patchesxml.py b/repomd/patchesxml.py new file mode 100644 --- /dev/null +++ b/repomd/patchesxml.py @@ -0,0 +1,58 @@ +# +# Copyright (c) 2008 rPath, Inc. +# +# This program is distributed under the terms of the Common Public License, +# version 1.0. A copy of this license should have been distributed with this +# source file in a file called LICENSE. If it is not present, the license +# is always available at http://www.rpath.com/permanent/licenses/CPL-1.0. +# +# This program is distributed in the hope that it will be useful, but +# without any warranty; without even the implied warranty of merchantability +# or fitness for a particular purpose. See the Common Public License for +# full details. +# + +__all__ = ('PatchesXml', ) + +# import stable api +from rpath_common.xmllib import api1 as xmllib + +from patchxml import PatchXml +from xmlcommon import XmlFileParser +from errors import UnknownElementError + +class _Patches(xmllib.BaseNode): + def addChild(self, child): + if child.getName() == 'patch': + child.id = child.getAttribute('id') + child._parser = PatchXml(None, child.location) + child.parseChildren = child._parser.parse + xmllib.BaseNode.addChild(self, child) + else: + raise UnknownElementError(child) + + def getPatches(self): + return self.getChildren('patch') + + +class _PatchElement(xmllib.BaseNode): + id = None + checksum = '' + checksumType = 'sha' + location = '' + + def addChild(self, child): + if child.getName() == 'checksum': + self.checksum = child.finalize() + self.checksumType = child.getAttribute('type') + elif child.getName() == 'location': + self.location = child.getAttribute('href') + else: + raise UnkownElementError(child) + + +class PatchesXml(XmlFileParser): + def _registerTypes(self): + self._databinder.registerType(_Patches, name='patches') + self._databinder.registerType(_PatchElement, name='patch') + self._databinder.registerType(xmllib.StringNode, name='checksum') diff --git a/repomd/patchxml.py b/repomd/patchxml.py new file mode 100644 --- /dev/null +++ b/repomd/patchxml.py @@ -0,0 +1,88 @@ +# +# Copryright (c) 2008 rPath, Inc. +# + +__all__ = ('PatchXml', ) + +from rpath_common.xmllib import api1 as xmllib + +from packagexml import * +from xmlcommon import XmlFileParser +from errors import UnknownElementError + +class _Patch(xmllib.BaseNode): + name = None + summary = None + description = None + version = None + release = None + requires = None + recommends = None + rebootNeeded = False + licenseToConfirm = None + packageManager = False + category = None + + def addChild(self, child): + if child.getName() == 'yum:name': + self.name = child.finalize() + elif child.getName() == 'summary': + if child.getAttribute('lang') == 'en': + self.summary = child.finalize() + elif child.getName() == 'description': + if child.getAttribute('lang') == 'en': + self.description = child.finalize() + elif child.getName() == 'yum:version': + self.version = child.getAttribute('ver') + self.release = child.getAttribute('rel') + elif child.getName() == 'rpm:requires': + self.requires = child.getChildren('entry', namespace='rpm') + elif child.getName() == 'rpm:recommends': + self.recommneds = child.getChildren('entry', namespace='rpm') + elif child.getName() == 'reboot-needed': + self.rebootNeeded = True + elif child.getName() == 'license-to-confirm': + self.licenseToConfirm = child.finalize() + elif child.getName() == 'package-manager': + self.packageManager = True + elif child.getName() == 'category': + self.category = child.finalize() + elif child.getName() == 'atoms': + self.packages = child.getChildren('package') + else: + raise UnknownElementError(child) + + def __cmp__(self, other): + if self.version > other.version: + return 1 + elif self.version < other.version: + return -1 + elif self.release > other.release: + return 1 + elif self.release < other.release: + return -1 + else: + return 0 + + +class _Atoms(xmllib.BaseNode): + def addChild(self, child): + if child.getName() == 'package': + child.type = child.getAttribute('type') + xmllib.BaseNode.addChild(self, child) + elif child.getName() == 'message': + pass + elif child.getName() == 'script': + pass + else: + raise UnknownElementError(child) + + +class PatchXml(XmlFileParser, PackageXmlMixIn): + def _registerTypes(self): + PackageXmlMixIn._registerTypes(self) + self._databinder.registerType(_Patch, name='patch') + self._databinder.registerType(xmllib.StringNode, name='name', namespace='yum') + self._databinder.registerType(xmllib.StringNode, name='category') + self._databinder.registerType(_Atoms, name='atoms') + self._databinder.registerType(xmllib.StringNode, name='license-to-confirm') diff --git a/repomd/primaryxml.py b/repomd/primaryxml.py new file mode 100644 --- /dev/null +++ b/repomd/primaryxml.py @@ -0,0 +1,28 @@ +# +# Copryright (c) 2008 rPath, Inc. +# + +__all__ = ('PrimaryXml', ) + +from rpath_common.xmllib import api1 as xmllib + +from packagexml import * +from xmlcommon import XmlFileParser +from errors import UnknownElementError + +class _Metadata(xmllib.BaseNode): + def addChild(self, child): + if child.getName() == 'package': + child.type = child.getAttribute('type') + xmllib.BaseNode.addChild(self, child) + else: + raise UnknownElementError(child) + + def getPackages(self): + return self.getChildren('package') + + +class PrimaryXml(XmlFileParser, PackageXmlMixIn): + def _registerTypes(self): + PackageXmlMixIn._registerTypes(self) + self._databinder.registerType(_Metadata, name='metadata') diff --git a/repomd/repomdxml.py b/repomd/repomdxml.py new file mode 100644 --- /dev/null +++ b/repomd/repomdxml.py @@ -0,0 +1,79 @@ +# +# Copyright (c) 2008 rPath, Inc. +# +# This program is distributed under the terms of the Common Public License, +# version 1.0. A copy of this license should have been distributed with this +# source file in a file called LICENSE. If it is not present, the license +# is always available at http://www.rpath.com/permanent/licenses/CPL-1.0. +# +# This program is distributed in the hope that it will be useful, but +# without any warranty; without even the implied warranty of merchantability +# or fitness for a particular purpose. See the Common Public License for +# full details. +# + +__all__ = ('RepoMdXml', ) + +# use stable api +from rpath_common.xmllib import api1 as xmllib + +from primaryxml import PrimaryXml +from patchesxml import PatchesXml +from xmlcommon import XmlFileParser +from errors import UnknownElementError + +class _RepoMd(xmllib.BaseNode): + def addChild(self, child): + if child.getName() == 'data': + child.type = child.getAttribute('type') + if child.type == 'patches': + child._parser = PatchesXml(None, child.location) + child.parseChildren = child._parser.parse + elif child.type == 'primary': + child._parser = PrimaryXml(None, child.location) + child.parseChildren = child._parser.parse + xmllib.BaseNode.addChild(self, child) + else: + raise UnknownElementError(child) + + def getRepoData(self, name=None): + if not name: + return self.getChildren('data') + + for node in self.getChildren('data'): + if node.type == name: + return node + + return None + + +class _RepoMdDataElement(xmllib.BaseNode): + location = '' + checksum = '' + checksumType = 'sha' + timestamp = '' + openChecksum = '' + openChecksumType = 'sha' + + def addChild(self, child): + if child.getName() == 'location': + self.location = child.getAttribute('href') + elif child.getName() == 'checksum': + self.checksum = child.finalize() + self.checksumType = child.getAttribute('type') + elif child.getName() == 'timestamp': + self.timestamp = child.finalize() + elif child.getName() == 'open-checksum': + self.openChecksum = child.finalize() + self.openChecksumType = child.getAttribute('type') + else: + raise UnknownElementError(child) + + +class RepoMdXml(XmlFileParser): + def _registerTypes(self): + self._databinder.registerType(_RepoMd, name='repomd') + self._databinder.registerType(_RepoMdDataElement, name='data') + self._databinder.registerType(xmllib.StringNode, name='checksum') + self._databinder.registerType(xmllib.IntegerNode, name='timestamp') + self._databinder.registerType(xmllib.StringNode, name='open-checksum') diff --git a/repomd/repository.py b/repomd/repository.py new file mode 100644 --- /dev/null +++ b/repomd/repository.py @@ -0,0 +1,40 @@ +# +# Copyright (c) 2008 rPath, Inc. +# +# This program is distributed under the terms of the Common Public License, +# version 1.0. A copy of this license should have been distributed with this +# source file in a file called LICENSE. If it is not present, the license +# is always available at http://www.rpath.com/permanent/licenses/CPL-1.0. +# +# This program is distributed in the hope that it will be useful, but +# without any warranty; without even the implied warranty of merchantability +# or fitness for a particular purpose. See the Common Public License for +# full details. +# + +__all__ = ('Repository', ) + +import os +import gzip +import tempfile +import urlgrabber + +class Repository(object): + def __init__(self, repoUrl): + self._repoUrl = repoUrl + + def get(self, fileName): + fn = self._getTempFile() + realUrl = self._getRealUrl(fileName) + dlFile = urlgrabber.urlgrab(realUrl, filename=fn) + + if os.path.basename(fileName).endswith('.gz'): + return gzip.open(dlFile) + else: + return open(dlFile) + + def _getTempFile(self): + return tempfile.mktemp(prefix='mdparse') + + def _getRealUrl(self, path): + return self._repoUrl + '/' + path diff --git a/repomd/xmlcommon.py b/repomd/xmlcommon.py new file mode 100644 --- /dev/null +++ b/repomd/xmlcommon.py @@ -0,0 +1,41 @@ +# +# Copyright (c) 2008 rPath, Inc. +# +# This program is distributed under the terms of the Common Public License, +# version 1.0. A copy of this license should have been distributed with this +# source file in a file called LICENSE. If it is not present, the license +# is always available at http://www.rpath.com/permanent/licenses/CPL-1.0. +# +# This program is distributed in the hope that it will be useful, but +# without any warranty; without even the implied warranty of merchantability +# or fitness for a particular purpose. See the Common Public License for +# full details. +# + +__all__ = ('XmlFileParser', ) + +from rpath_common.xmllib import api1 as xmllib + +class XmlFileParser(object): + def __init__(self, repository, path): + self._repository = repository + self._path = path + + self._databinder = xmllib.DataBinder() + self._registerTypes() + + self._data = None + + def _registerTypes(self): + pass + + def parse(self, refresh=False): + if not self._data or refresh: + fn = self._repository.get(self._path) + self._data = self._databinder.parseFile(fn) + + for child in self._data.iterChildren(): + if hasattr(child, '_parser'): + child._parser._repository = self._repository + + return self._data From johnsonm at rpath.com Wed Aug 19 17:39:05 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:39:05 +0000 Subject: mirrorball: switch to double triple quotes, single quotes breaks some editors Message-ID: <200908192139.n7JLd5aw014451@scc.eng.rpath.com> changeset: 76cb68d0e43f user: Elliot Peele <http://issues.rpath.com/> date: Mon, 02 Jun 2008 21:48:35 -0400 switch to double triple quotes, single quotes breaks some editors committer: Elliot Peele <http://issues.rpath.com/> diff --git a/repomd/__init__.py b/repomd/__init__.py --- a/repomd/__init__.py +++ b/repomd/__init__.py @@ -12,7 +12,7 @@ # full details. # -''' +""" Common repository metadata parsing library. Handles parsing most yum metadata including SuSE proprietary patch/delta rpm @@ -30,7 +30,7 @@ > for patch in patches: > # print all of the advisories in the repository > print patch.description -''' +""" from repomd.repomdxml import RepoMdXml from repomd.repository import Repository @@ -39,9 +39,9 @@ __all__ = ('Client', 'RepoMdError', 'ParseError', 'UnknownElementError') class Client(object): - ''' + """ Client object for extracting information from repository metadata. - ''' + """ def __init__(self, repoUrl): self._repoUrl = repoUrl @@ -51,27 +51,27 @@ self._repomd = RepoMdXml(self._repo, self._baseMdPath).parse() def getRepos(self): - ''' + """ Get a repository instance. @return instance of repomd.repository.Repository - ''' + """ return self._repo def getPatchDetail(self): - ''' + """ Get a list instances representing all patch data in the repository. @return [repomd.patchxml._Patch, ...] - ''' + """ node = self._repomd.getRepoData('patches') return [ x.parseChildren() for x in node.parseChildren().getPatches() ] def getPackageDetail(self): - ''' + """ Get a list instances representing all packages in the repository. @ return [repomd.packagexml._Package, ...] - ''' + """ node = self._repomd.getRepoData('primary') return node.parseChildren().getPackages() diff --git a/repomd/errors.py b/repomd/errors.py --- a/repomd/errors.py +++ b/repomd/errors.py @@ -12,27 +12,27 @@ # full details. # -''' +""" Errors specific to repomd module. -''' +""" __all__ = ('RepoMdError', 'ParseError', 'UnknownElementError') class RepoMdError(Exception): - ''' + """ Base exception for all repomd exceptions. This should never be expllicitly raised. - ''' + """ class ParseError(RepoMdError): - ''' + """ Base parsing error. - ''' + """ class UnknownElementError(ParseError): - ''' + """ Raised when unhandled elements are found in the parser. - ''' + """ def __init__(self, element): ParseError.__init__(self) @@ -43,9 +43,9 @@ return self._error % (self._element.getAbsoluteName(), ) class UnknownAttributeError(UnknownElementError): - ''' + """ Raised when unhandled attributes are found in the parser. - ''' + """ def __init__(self, element, attribute): UnknownElementError.__init__(self, element) diff --git a/repomd/packagexml.py b/repomd/packagexml.py --- a/repomd/packagexml.py +++ b/repomd/packagexml.py @@ -12,9 +12,9 @@ # full details. # -''' +""" Module for parsing package sections of xml files from the repository metadata. -''' +""" __all__ = ('PackageXmlMixIn', ) @@ -24,10 +24,10 @@ from repomd.xmlcommon import SlotNode class _Package(SlotNode): - ''' + """ Python representation of package section of xml files from the repository metadata. - ''' + """ __slots__ = ('name', 'arch', 'epoch', 'version', 'release', 'checksum', 'checksumType', 'summary', 'description', 'fileTimestamp', 'buildTimestamp', 'packageSize', @@ -37,9 +37,9 @@ 'licenseToConfirm') def addChild(self, child): - ''' + """ Parse children of package element. - ''' + """ # FIXME: There should be a better way to setup a parser. # R0912 - Too many branches @@ -114,14 +114,14 @@ class _RpmRequires(xmllib.BaseNode): - ''' + """ Parse any element that contains rpm:entry or suse:entry elements. - ''' + """ def addChild(self, child): - ''' + """ Parse rpm:entry and suse:entry nodes. - ''' + """ if child.getName() in ('rpm:entry', 'suse:entry'): for attr, value in child.iterAttributes(): @@ -155,65 +155,65 @@ class _RpmRecommends(_RpmRequires): - ''' + """ Parse rpm:recommends children. - ''' + """ class _RpmProvides(_RpmRequires): - ''' + """ Parse rpm:provides children. - ''' + """ class _RpmObsoletes(_RpmRequires): - ''' + """ Parse rpm:obsoletes children. - ''' + """ class _RpmConflicts(_RpmRequires): - ''' + """ Parse rpm:conflicts children. - ''' + """ class _RpmEnhances(_RpmRequires): - ''' + """ Parse rpm:enhances children. - ''' + """ class _RpmSupplements(_RpmRequires): - ''' + """ Parse rpm:supplements children. - ''' + """ class _RpmSuggests(_RpmRequires): - ''' + """ Parse rpm:suggests children. - ''' + """ class _SuseFreshens(_RpmRequires): - ''' + """ Parse suse:freshens children. - ''' + """ class PackageXmlMixIn(object): - ''' + """ Handle registering all types for parsing package elements. - ''' + """ # R0903 - Too few public methods # pylint: disable-msg=R0903 def _registerTypes(self): - ''' + """ Setup databinder to parse xml. - ''' + """ self._databinder.registerType(_Package, name='package') self._databinder.registerType(xmllib.StringNode, name='name') diff --git a/repomd/patchesxml.py b/repomd/patchesxml.py --- a/repomd/patchesxml.py +++ b/repomd/patchesxml.py @@ -12,9 +12,9 @@ # full details. # -''' +""" Module for parsing patches.xml from the repository metadata. -''' +""" __all__ = ('PatchesXml', ) @@ -26,14 +26,14 @@ from repomd.errors import UnknownElementError class _Patches(xmllib.BaseNode): - ''' + """ Python representation of patches.xml from the repository metadata. - ''' + """ def addChild(self, child): - ''' + """ Parse children of patches element. - ''' + """ # W0212 - Access to a protected member _parser of a client class # pylint: disable-msg=W0212 @@ -47,24 +47,24 @@ raise UnknownElementError(child) def getPatches(self): - ''' + """ Get a list of all patches in the repository. @return list of _PatchElement instances - ''' + """ return self.getChildren('patch') class _PatchElement(SlotNode): - ''' + """ Parser for patch element of patches.xml. - ''' + """ __slots__ = ('id', 'checksum', 'checksumType', 'location') def addChild(self, child): - ''' + """ Parse children of patch element. - ''' + """ if child.getName() == 'checksum': self.checksum = child.finalize() @@ -76,17 +76,17 @@ class PatchesXml(XmlFileParser): - ''' + """ Handle registering all types for parsing patches.xml. - ''' + """ # R0903 - Too few public methods # pylint: disable-msg=R0903 def _registerTypes(self): - ''' + """ Setup databinder to parse xml. - ''' + """ self._databinder.registerType(_Patches, name='patches') self._databinder.registerType(_PatchElement, name='patch') diff --git a/repomd/patchxml.py b/repomd/patchxml.py --- a/repomd/patchxml.py +++ b/repomd/patchxml.py @@ -2,9 +2,9 @@ # Copryright (c) 2008 rPath, Inc. # -''' +""" Module for parsing patch-*.xml files from the repository metadata. -''' +""" __all__ = ('PatchXml', ) @@ -15,18 +15,18 @@ from repomd.errors import UnknownElementError class _Patch(SlotNode): - ''' + """ Python representation of patch-*.xml from the repository metadata. - ''' + """ __slots__ = ('name', 'summary', 'description', 'version', 'release', 'requires', 'recommends', 'rebootNeeded', 'licenseToConfirm', 'packageManager', 'category', 'packages') def addChild(self, child): - ''' + """ Parse children of patch element. - ''' + """ # FIXME: There should be a better way to setup a parser. # R0912 - Too many branches @@ -74,14 +74,14 @@ class _Atoms(xmllib.BaseNode): - ''' + """ Parser for the atoms element of a path-*.xml file. - ''' + """ def addChild(self, child): - ''' + """ Parse children of atoms element. - ''' + """ if child.getName() == 'package': child.type = child.getAttribute('type') @@ -95,17 +95,17 @@ class PatchXml(XmlFileParser, PackageXmlMixIn): - ''' + """ Handle registering all types for parsing patch-*.xml files. - ''' + """ # R0903 - Too few public methods # pylint: disable-msg=R0903 def _registerTypes(self): - ''' + """ Setup databinder to parse xml. - ''' + """ PackageXmlMixIn._registerTypes(self) self._databinder.registerType(_Patch, name='patch') diff --git a/repomd/primaryxml.py b/repomd/primaryxml.py --- a/repomd/primaryxml.py +++ b/repomd/primaryxml.py @@ -2,9 +2,9 @@ # Copryright (c) 2008 rPath, Inc. # -''' +""" Module for parsing primary.xml.gz from the repository metadata. -''' +""" __all__ = ('PrimaryXml', ) @@ -15,14 +15,14 @@ from repomd.errors import UnknownElementError class _Metadata(xmllib.BaseNode): - ''' + """ Python representation of primary.xml.gz from the repository metadata. - ''' + """ def addChild(self, child): - ''' + """ Parse children of metadata element. - ''' + """ if child.getName() == 'package': child.type = child.getAttribute('type') @@ -31,26 +31,26 @@ raise UnknownElementError(child) def getPackages(self): - ''' + """ Get a list of all packages contained in primary.xml.gz. @return list of repomd.packagexml._Package objects - ''' + """ return self.getChildren('package') class PrimaryXml(XmlFileParser, PackageXmlMixIn): - ''' + """ Handle registering all types for parsing primary.xml.gz. - ''' + """ # R0903 - Too few public methods # pylint: disable-msg=R0903 def _registerTypes(self): - ''' + """ Setup databinder to parse xml. - ''' + """ PackageXmlMixIn._registerTypes(self) self._databinder.registerType(_Metadata, name='metadata') diff --git a/repomd/repomdxml.py b/repomd/repomdxml.py --- a/repomd/repomdxml.py +++ b/repomd/repomdxml.py @@ -12,9 +12,9 @@ # full details. # -''' +""" Module for parsing repomd.xml files from the repository metadata. -''' +""" __all__ = ('RepoMdXml', ) @@ -27,14 +27,14 @@ from repomd.errors import UnknownElementError class _RepoMd(xmllib.BaseNode): - ''' + """ Python representation of repomd.xml from the repository metadata. - ''' + """ def addChild(self, child): - ''' + """ Parse children of repomd element. - ''' + """ # W0212 - Access to a protected member _parser of a client class # pylint: disable-msg=W0212 @@ -52,14 +52,14 @@ raise UnknownElementError(child) def getRepoData(self, name=None): - ''' + """ Get data elements of repomd xml file. @param name: filter by type of node @type name: string @return list of nodes @return single node @return None - ''' + """ if not name: return self.getChildren('data') @@ -72,16 +72,16 @@ class _RepoMdDataElement(SlotNode): - ''' + """ Parser for repomd.xml data elements. - ''' + """ __slots__ = ('location', 'checksum', 'checksumType', 'timestamp', 'openChecksum', 'openChecksumType') def addChild(self, child): - ''' + """ Parse children of data element. - ''' + """ if child.getName() == 'location': self.location = child.getAttribute('href') @@ -98,17 +98,17 @@ class RepoMdXml(XmlFileParser): - ''' + """ Handle registering all types for parsing repomd.xml file. - ''' + """ # R0903 - Too few public methods # pylint: disable-msg=R0903 def _registerTypes(self): - ''' + """ Setup databinder to parse xml. - ''' + """ self._databinder.registerType(_RepoMd, name='repomd') self._databinder.registerType(_RepoMdDataElement, name='data') diff --git a/repomd/repository.py b/repomd/repository.py --- a/repomd/repository.py +++ b/repomd/repository.py @@ -12,9 +12,9 @@ # full details. # -''' +""" Repository access module. -''' +""" __all__ = ('Repository', ) @@ -25,9 +25,9 @@ import urllib2 class Repository(object): - ''' + """ Access files from the repository. - ''' + """ # R0903 - Too few public methods # pylint: disable-msg=R0903 @@ -36,12 +36,12 @@ self._repoUrl = repoUrl def get(self, fileName): - ''' + """ Download a file from the repository. @param fileName: relative path to file @type fileName: string @return open file instance - ''' + """ fn = self._getTempFile() realUrl = self._getRealUrl(fileName) @@ -58,18 +58,18 @@ @classmethod def _getTempFile(cls): - ''' + """ Generate a tempory filename. @return name of tempory file - ''' + """ return tempfile.mktemp(prefix='mdparse') def _getRealUrl(self, path): - ''' + """ @param path: relative path to repository file @type path: string @return full repository url - ''' + """ return self._repoUrl + '/' + path diff --git a/repomd/xmlcommon.py b/repomd/xmlcommon.py --- a/repomd/xmlcommon.py +++ b/repomd/xmlcommon.py @@ -12,18 +12,18 @@ # full details. # -''' +""" Base module for common super classes for repomd. -''' +""" __all__ = ('XmlFileParser', ) from rpath_common.xmllib import api1 as xmllib class XmlFileParser(object): - ''' + """ Base class for handling databinder setup. - ''' + """ # R0903 - Too few public methods # pylint: disable-msg=R0903 @@ -38,17 +38,17 @@ self._data = None def _registerTypes(self): - ''' + """ Method stub for sub classes to implement. - ''' + """ def parse(self, refresh=False): - ''' + """ Parse an xml file. @param refresh: refresh the cached parser results @type refresh: bool @return sub class xmllib.BaseNode - ''' + """ # W0212 - Access to a protected member _parser of a client class # pylint: disable-msg=W0212 diff --git a/rpmimport/__init__.py b/rpmimport/__init__.py --- a/rpmimport/__init__.py +++ b/rpmimport/__init__.py @@ -12,7 +12,7 @@ # full details. # -''' +""" Module for managing (importing, creating, updating) RPMs in a conary repository. -''' +""" diff --git a/rpmimport/rpmhelper.py b/rpmimport/rpmhelper.py --- a/rpmimport/rpmhelper.py +++ b/rpmimport/rpmhelper.py @@ -12,18 +12,18 @@ # full details. # -''' +""" Wrappers around conary.rpmhelper. -''' +""" import urllib2 from conary import rpmhelper class _SeekableStream(object): - ''' + """ File like object that can be seeked forward. - ''' + """ def __init__(self, url): self._url = url @@ -32,43 +32,43 @@ self._pos = 0 def read(self, *args): - ''' + """ Wrapper around urllib's file object's read method that keeps track of position. - ''' + """ buf = self._fh.read(*args) self._pos += len(buf) return buf def seek(self, amount, sense): - ''' + """ Simple seek implementation that only goes forward. @param amount: amount to seek into file. @type amount: integer @param sense: direction to seek into file (only valid value is 1) @type sense: integer - ''' + """ assert(sense == 1) self.read(amount) def tell(self): - ''' + """ Report the current position in the file. @return current position in the file - ''' + """ return self._pos def readHeader(url): - ''' + """ Read an RPM header (and only the RPM header) from a remotely hosted RPM. @param url: url to RPM file. @type url: string @return conary.rpmhelper.RpmHeader object - ''' + """ fh = _SeekableStream(url) diff --git a/rpmimport/rpmsource.py b/rpmimport/rpmsource.py --- a/rpmimport/rpmsource.py +++ b/rpmimport/rpmsource.py @@ -110,14 +110,14 @@ self.loadFromClient(client) def loadFromClient(self, client, basePath=''): - ''' + """ Walk the yum repository rooted at url/basePath and collect information about rpms found. @param client: client object for extracting data from the repo metadata @type client: repomd.Client @param basePath: path to prefix location metadata with @type basePath: string - ''' + """ for pkg in client.getPackageDetail(): # ignore the 32-bit compatibility libs - we will diff --git a/updatebot/__init__.py b/updatebot/__init__.py --- a/updatebot/__init__.py +++ b/updatebot/__init__.py @@ -12,7 +12,7 @@ # full details. # -''' +""" UpdateBot is a module for automated updating of a conary repository from a SLES yum/rpm repository. -''' +""" diff --git a/updatebot/build.py b/updatebot/build.py --- a/updatebot/build.py +++ b/updatebot/build.py @@ -12,9 +12,9 @@ # full details. # -''' +""" Builder object implementation. -''' +""" import time import logging @@ -26,12 +26,12 @@ log = logging.getLogger('updateBot.build') class Builder(object): - ''' + """ Class for wrapping the rMake api until we can switch to using rBuild. @param cfg: updateBot configuration object @type cfg: config.UpdateBotConfig - ''' + """ # R0903 - Too few public methods # pylint: disable-msg=R0903 @@ -42,12 +42,12 @@ self._helper = helper.rMakeHelper(root=self._cfg.configPath) def build(self, troveSpecs): - ''' + """ Build a list of troves. @param troveSpecs: list of trove specs @type troveSpecs: [(name, versionObj, flavorObj), ...] @return troveMap: dictionary of troveSpecs to built troves - ''' + """ jobId = self._startJob(troveSpecs) self._monitorJob(jobId) @@ -57,12 +57,12 @@ return trvMap def _startJob(self, troveSpecs): - ''' + """ Create and start a rMake build. @param troveSpecs: list of trove specs @type troveSpecs: [(name, versionObj, flavorObj), ...] @return integer jobId - ''' + """ # Create rMake job log.info('Creating build job: %s' % (troveSpecs, )) @@ -73,22 +73,22 @@ return jobId def _monitorJob(self, jobId): - ''' + """ Monitor job status, block until complete. @param jobId: rMake job ID @type jobId: integer - ''' + """ # Watch build, wait for completion monitor.monitorJob(self._helper.client, jobId, exitOnFinish=True, displayClass=_StatusOnlyDisplay) def _sanityCheckJob(self, jobId): - ''' + """ Verify the status of a job. @param jobId: rMake job ID @type jobId: integer - ''' + """ # Check for errors job = self._helper.getJob(jobId) @@ -103,12 +103,12 @@ raise JobFailedError(jobId=jobId, why='Job built no troves') def _commitJob(self, jobId): - ''' + """ Commit completed job. @param jobId: rMake job ID @type jobId: integer @return troveMap: dictionary of troveSpecs to built troves - ''' + """ # Do the commit startTime = time.time() @@ -137,17 +137,17 @@ class _StatusOnlyDisplay(monitor.JobLogDisplay): - ''' + """ Display only job and trove status. No log output. Copied from bob3 - ''' + """ # R0901 - Too many ancestors # pylint: disable-msg=R0901 def _troveLogUpdated(self, (jobId, troveTuple), state, status): - '''Don't care about trove logs''' + """Don't care about trove logs''' def _trovePreparingChroot(self, (jobId, troveTuple), host, path): - '''Don't care about resolving/installing chroot''' + """Don't care about resolving/installing chroot''' diff --git a/updatebot/config.py b/updatebot/config.py --- a/updatebot/config.py +++ b/updatebot/config.py @@ -12,18 +12,18 @@ # full details. # -''' +""" Configuration module for updatebot. -''' +""" from conary.lib import cfg from conary.lib.cfgtypes import CfgString, CfgList from rmake.build.buildcfg import CfgTroveSpec class UpdateBotConfig(cfg.SectionedConfigFile): - ''' + """ Config class for updatebot. - ''' + """ # R0904 - to many public methods # pylint: disable-msg=R0904 diff --git a/updatebot/errors.py b/updatebot/errors.py --- a/updatebot/errors.py +++ b/updatebot/errors.py @@ -12,14 +12,14 @@ # full details. # -''' +""" UpdateBot specific errors. -''' +""" class UpdateBotError(Exception): - ''' + """ Base UpdateBot Error for all other errors to inherit from. - ''' + """ _params = [] _template = 'An unknown error has occured.' @@ -42,18 +42,18 @@ class CommitFailedError(UpdateBotError): - ''' + """ CommitFailedError, raised when failing to commit to a repository. - ''' + """ _params = ['jobId', 'why'] _template = 'rMake job %(jobId)d failed to commit: %(why)s' class JobFailedError(UpdateBotError): - ''' + """ JobFailedError, raised when an rMake job fails. - ''' + """ _params = ['jobId', 'why'] _templates = 'rMake job %(jobId)s failed: %(why)s' diff --git a/updatebot/log.py b/updatebot/log.py --- a/updatebot/log.py +++ b/updatebot/log.py @@ -12,17 +12,17 @@ # full details. # -''' +""" Module of logging related functions. -''' +""" import sys import logging def addRootLogger(): - ''' + """ Setup the root logger that should be inherited by all other loggers. - ''' + """ rootLog = logging.getLogger('') handler = logging.StreamHandler(sys.stdout) From johnsonm at rpath.com Wed Aug 19 17:40:48 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:40:48 +0000 Subject: mirrorball: Using the new test infrastructure tools Message-ID: <200908192140.n7JLem23018600@scc.eng.rpath.com> changeset: c328ee0026a6 user: Mihai Ibanescu <https://issues.rpath.com/> date: Mon, 15 Sep 2008 15:30:45 -0400 Using the new test infrastructure tools committer: Mihai Ibanescu <https://issues.rpath.com/> diff --git a/.hgignore b/.hgignore --- a/.hgignore +++ b/.hgignore @@ -4,7 +4,6 @@ .*\,cover$ .*\.swp$ .*\.orig$ -.*\/testsetup\.py$ .*\/\.times$ .*\/\.coverage\/.* ^pylint\/reports diff --git a/aptmd/packages.py b/aptmd/packages.py --- a/aptmd/packages.py +++ b/aptmd/packages.py @@ -43,6 +43,13 @@ 'task' : self._keyval, }) + def parse(self, fn): + BaseParser.parse(self, fn) + # If there is any text left, collect it in the description + if self._text: + self._curObj.description = self._text + self._text = '' + def _source(self): source = self._getLine() assert source != '' @@ -67,4 +74,5 @@ def _bugs(self): self._curObj.description = self._text + self._text = '' self._keyval() diff --git a/test/bootstrap.py b/test/bootstrap.py new file mode 100644 --- /dev/null +++ b/test/bootstrap.py @@ -0,0 +1,11 @@ +import os +import sys + +testUtilDir = os.environ.get('TESTUTILS_PATH', '../testutils') +if os.path.exists(testUtilDir): + sys.path.insert(0, testUtilDir) + +try: + import testrunner +except ImportError: + raise RuntimeError('Could not find testrunner - set TESTUTILS_PATH') diff --git a/test/functionaltest/testsetup.py b/test/functionaltest/testsetup.py new file mode 120000 --- /dev/null +++ b/test/functionaltest/testsetup.py @@ -0,0 +1,1 @@ +../testsetup.py \ No newline at end of file diff --git a/test/mock.py b/test/mock.py deleted file mode 100644 --- a/test/mock.py +++ /dev/null @@ -1,361 +0,0 @@ -#!/usr/bin/python2.4 -# -*- mode: python -*- -# -# Copyright (c) 2006-2007 rPath, Inc. All Rights Reserved. -# This program is distributed under the terms of the Common Public License, -# version 1.0. A copy of this license should have been distributed with this -# source file in a file called LICENSE. If it is not present, the license -# is always available at http://www.rpath.com/permanent/licenses/CPL-1.0. -# -# This program is distributed in the hope that it will be useful, but -# without any warranty; without even the implied warranty of merchantability -# or fitness for a particular purpose. See the Common Public License for -# full details. -# -""" -Mock object implementation. - -This mock object implementation is meant to be very forgiving - it returns a -new child Mock Object for every attribute accessed, and a mock object is -returned from every method call. - -It is the tester's job to enabled the calls that they are interested in -testing, all calls where the return value of the call and side effects are not -recorded (logging, for example) are likely to succeed w/o effort. - -If you wish to call the actual implementation of a function on a MockObject, -you have to enable it using enableMethod. If you wish to use an actual variable setting, you need to set it. - -All enabling/checking methods for a MockObject are done through the _mock attribute. Example: - -class Foo(object): - def __init__(self): - # NOTE: this initialization is not called by default with the mock - # object. - self.one = 'a' - self.two = 'b' - - def method(self, param): - # this method is enabled by calling _mock.enableMethod - param.bar('print some data') - self.printMe('some other data', self.one) - return self.two - - def printMe(self, otherParam): - # this method is not enabled and so is stubbed out in the MockInstance. - print otherParam - -def test(): - m = MockInstance(Foo) - m._mock.set(two=123) - m._mock.enableMethod('method') - param = MockObject() - rv = m.method(param) - assert(rv == 123) #m.two is returned - # note that param.bar is created on the fly as it is accessed, and - # stores how it was called. - assert(param.bar._mock.assertCalled('print some data') - # m.one and m.printMe were created on the fly as well - # m.printMe remembers how it was called. - m.printMe._mock.assertCalled('some other data', m.one) - # attribute values are generated on the fly but are retained between - # accesses. - assert(m.foo is m.foo) - -TODO: set the return values for particular function calls w/ particular -parameters. -""" -import new - -_mocked = [] - -class MockObject(object): - """ - Base mock object. - - Creates attributes on the fly, affect attribute values by using - the _mock attribute, which is a MockManager. - - Initial attributes can be assigned by key/value pairs passed in. - """ - - def __init__(self, **kw): - stableReturnValues = kw.pop('stableReturnValues', False) - self._mock = MockManager(self, stableReturnValues=stableReturnValues) - self.__dict__.update(kw) - self._mock._dict = {} - - def __getattribute__(self, key): - if key == '_mock' or self._mock.enabled(key): - return object.__getattribute__(self, key) - if key in self.__dict__: - return self.__dict__[key] - m = self._mock.getCalled(key) - self.__dict__[key] = m - return m - - def __setattr__(self, key, value): - if key == '_mock' or self._mock.enabled(key): - object.__setattr__(self, key, value) - else: - m = self._mock.setCalled(key, value) - if not hasattr(self, key): - object.__setattr__(self, key, m) - - def __setitem__(self, key, value): - m = self._mock.setItemCalled(key, value) - self._mock._dict[key] = m - - def __len__(self): - return self._mock.length - - def __deepcopy__(self, memo): - return self - - def __iter__(self): - for i in range(len(self)): - yield self[i] - - def __getitem__(self, key): - if key in self._mock._dict: - return self._mock._dict[key] - else: - m = self._mock.getItemCalled(key) - self._mock._dict[key] = m - return m - - def __hasattr__(self, key): - if key == '_mock' or self._mock.enabled(key): - return object.__hasattr__(self, key) - return True - - def __call__(self, *args, **kw): - return self._mock.called(args, kw) - -class MockManager(object): - noReturnValue = object() - - def __init__(self, obj, stableReturnValues=False): - self._enabledByDefault = False - self._enabled = set(['__dict__', '__methods__', '__class__', - '__members__', '__deepcopy__']) - self._disabled = set([]) - self._errorToRaise = None - self.calls = [] - self.callReturns = [] - self.getCalls = [] - self.setCalls = [] - self.getItemCalls = [] - self.setItemCalls = [] - self.hasCalls = [] - self.eqCalls = [] - self.obj = obj - self.superClass = object - self.length = 1 - self.stableReturnValues = stableReturnValues - self.returnValue = self.noReturnValue - - def enableByDefault(self): - self._enabledByDefault = True - - def disableByDefault(self): - self._enabledByDefault = False - - def setDefaultReturn(self, returnValue): - self.returnValue = returnValue - - def setReturn(self, returnValue, *args, **kw): - self.callReturns.append((args, tuple(sorted(kw.items())), returnValue)) - - def enableMethod(self, name): - """ - Enables a method to be called from the given superclass. - - The function underlying the method is slurped up and assigned to - this class. - """ - self.enable(name) - func = getattr(self.superClass, name).im_func - method = new.instancemethod(func, self.obj, self.obj.__class__) - object.__setattr__(self.obj, name, method) - - def enable(self, *names): - self._enabled.update(names) - self._disabled.difference_update(names) - - def disable(self, *names): - self._enabled.difference_update(names) - self._disabled.update(names) - for name in names: - object.__setattr__(self.obj, name, MockObject()) - - def enabled(self, name): - if self._enabledByDefault: - return name not in self._disabled - else: - return name in self._enabled - - def set(self, **kw): - for key, value in kw.iteritems(): - self._enabled.add(key) - setattr(self.obj, key, value) - - def raiseErrorOnAccess(self, error): - self._errorToRaise = error - - def assertCalled(self, *args, **kw): - kw = tuple(sorted(kw.items())) - assert((args, kw) in self.calls) - self.calls.remove((args, kw)) - - def assertNotCalled(self, *args, **kw): - assert(not self.calls) - - def setCalled(self, key, value): - if self._errorToRaise: - self._raiseError() - m = MockObject() - self.setCalls.append((key, value, m)) - return m - - def setItemCalled(self, key, value): - if self._errorToRaise: - self._raiseError() - m = MockObject() - self.setItemCalls.append((key, value, m)) - return m - - - def _raiseError(self): - err = self._errorToRaise - self._errorToRaise = None - raise err - - def getCalled(self, key): - if self._errorToRaise: - self._raiseError() - m = MockObject(stableReturnValues=self.stableReturnValues) - self.getCalls.append((key, m)) - return m - - def getItemCalled(self, key): - if self._errorToRaise: - self._raiseError() - m = MockObject(stableReturnValues=self.stableReturnValues) - self.getItemCalls.append((key, m)) - return m - - - def called(self, args, kw): - kw = tuple(sorted(kw.items())) - self.calls.append((args, kw)) - if self._errorToRaise: - self._raiseError() - else: - rv = [x[2] for x in self.callReturns if (x[0], x[1]) == (args, kw)] - if rv: - return rv[-1] - rv = [x[2] for x in self.callReturns - if not x[0] and x[1] == (('_mockAll', True),)] - if rv: - return rv[-1] - else: - if self.returnValue is not self.noReturnValue: - return self.returnValue - if self.stableReturnValues: - self.returnValue = MockObject(stableReturnValues=True) - return self.returnValue - return MockObject() - - def getCalls(self): - return self.calls - - def popCall(self): - call = self.calls[0] - self.calls = self.calls[1:] - return call - -class MockInstance(MockObject): - - def __init__(self, superClass, **kw): - MockObject.__init__(self, **kw) - self._mock.superClass = superClass - -def attach(obj): - if hasattr(obj, '__setattr__'): - oldsetattr = obj.__setattr__ - if hasattr(obj, '__getattribute__'): - oldgetattr = obj.__getattribute__ - - def __setattr__(self, key, value): - if not isinstance(getattr(self, key), mock.MockObject()): - oldsetattr(key, value) - - def __getattribute__(self, key): - if not hasattr(self, key): - oldsetattr(key, mock.MockObject()) - return oldgetattr(key) - oldsetattr('__setattr__', new.instancemethod(__setattr__, obj, - obj.__class__)) - oldsetattr('__getattribute__', new.instancemethod(__getattribute__, obj, obj.__class__)) - -def mockMethod(method): - self = method.im_self - name = method.__name__ - origMethod = getattr(self, name) - setattr(self, name, MockObject()) - getattr(self, name)._mock.method = origMethod - getattr(self, name)._mock.origValue = origMethod - _mocked.append((self, name)) - return getattr(self, name) - -def mock(obj, attr): - m = MockObject() - if hasattr(obj, attr): - m._mock.origValue = getattr(obj, attr) - setattr(obj, attr, m) - _mocked.append((obj, attr)) - -def unmockAll(): - for obj, attr in _mocked: - if not hasattr(getattr(obj, attr), '_mock'): - continue - setattr(obj, attr, getattr(obj, attr)._mock.origValue) - _mocked[:] = [] - -def mockClass(class_, *args, **kw): - commands = [] - runInit = kw.pop('mock_runInit', False) - for k, v in kw.items(): - if k.startswith('mock_'): - if not isinstance(v, (list, tuple)): - v = [v] - commands.append((k[5:], v)) - kw.pop(k) - class _MockClass(MockInstance, class_): - def __init__(self, *a, **k): - MockInstance.__init__(self, class_, *args, **kw) - if runInit: - self._mock.enableByDefault() - class_.__init__(self, *a, **k) - self._mock.called(a, k) - for command, params in commands: - getattr(self._mock, command)(*params) - - return _MockClass - -def mockFunctionOnce(obj, attr, returnValue): - newFn = lambda *args, **kw: returnValue - return replaceFunctionOnce(obj, attr, newFn) - -def replaceFunctionOnce(obj, attr, newFn): - curValue = getattr(obj, attr) - def restore(): - setattr(obj, attr, curValue) - - def fun(*args, **kw): - restore() - return newFn(*args, **kw) - setattr(obj, attr, fun) - fun.func_name = attr - fun.restore = restore diff --git a/test/slehelp.py b/test/slehelp.py --- a/test/slehelp.py +++ b/test/slehelp.py @@ -16,7 +16,7 @@ testsuite.setup() import os -import rmakehelp +from rmake_test import rmakehelp from updatebot import config diff --git a/test/smoketest/testsetup.py b/test/smoketest/testsetup.py new file mode 120000 --- /dev/null +++ b/test/smoketest/testsetup.py @@ -0,0 +1,1 @@ +../testsetup.py \ No newline at end of file diff --git a/test/testsetup.py b/test/testsetup.py --- a/test/testsetup.py +++ b/test/testsetup.py @@ -1,20 +1,22 @@ import os import sys -dirlevel = 0; + + curDir = os.path.dirname(__file__) -testsuitePath = os.path.realpath(curDir + '/..' * dirlevel) -while (not os.path.exists(testsuitePath + '/testsuite.py') and dirlevel < 10): - dirlevel+=1 +for dirlevel in range(10): testsuitePath = os.path.realpath(curDir + '/..' * dirlevel) - -if dirlevel == 10: + if os.path.exists(testsuitePath + '/testsuite.py'): + break +else: raise RuntimeError('Could not find testsuite.py!') if not testsuitePath in sys.path: sys.path.insert(0, testsuitePath) + import testsuite testsuite.setup() + def main(): if sys._getframe(1).f_globals['__name__'] == '__main__': testsuite.main() diff --git a/test/testsuite.py b/test/testsuite.py --- a/test/testsuite.py +++ b/test/testsuite.py @@ -3,6 +3,8 @@ # # Copyright (c) 2006-2007 rPath, Inc. All Rights Reserved. # +# W0603: using the global statement +#pylint: disable-msg=W0603 # This program is distributed under the terms of the Common Public License, # version 1.0. A copy of this license should have been distributed with this # source file in a file called LICENSE. If it is not present, the license @@ -17,18 +19,17 @@ import sys import os import pwd +import bootstrap archivePath = None testPath = None pluginPath = None -nodePath = None conaryDir = None _setupPath = None _individual = False def isIndividual(): - global _individual return _individual def setup(): @@ -37,48 +38,35 @@ return _setupPath global testPath global archivePath + global conaryDir global pluginPath - global nodePath - global rmakePath - - # set default SLEESTACK_PATH, if it was not set. - parDir = '/'.join(os.path.realpath(__file__).split('/')[:-1]) - parDir = os.path.dirname(parDir) - mirrorballPath = os.getenv('SLEESTACK_PATH', parDir) - os.environ['SLEESTACK_PATH'] = mirrorballPath def setPathFromEnv(variable, directory): - parDir = '/'.join(os.path.realpath(__file__).split('/')[:-2]) - parDir = os.path.dirname(parDir) + '/' + directory + parDir = os.path.dirname(os.path.realpath(__file__)) + if not directory.startswith('/'): + parDir = os.path.dirname(parDir) + '/' + directory + else: + parDir = directory thisPath = os.getenv(variable, parDir) os.environ[variable] = thisPath - if thisPath not in sys.path: - sys.path.insert(0, thisPath) + if thisPath in sys.path: + sys.path.remove(thisPath) + sys.path.insert(0, thisPath) return thisPath - # set default COVERAGE_PATH, if it was not set. - coveragePath = setPathFromEnv('COVERAGE_PATH', 'utils') + conaryDir = setPathFromEnv('CONARY_PATH', 'conary') + conaryTestPath = setPathFromEnv('CONARY_TEST_PATH', 'conary-test') + setPathFromEnv('CONARY_POLICY_PATH', '/usr/lib/conary/policy') + mirrorballPath = setPathFromEnv('SLEESTACK_PATH', '') - # set default CONARY_PATH, if it was not set. - conaryPath = setPathFromEnv('CONARY_PATH', 'conary') - - # set default CONARY_TEST_PATH, if it was not set. - conaryTestPath = setPathFromEnv('CONARY_TEST_PATH', 'conary-test') - - # set default RMAKE_PATH, if it was not set. rmakePath = setPathFromEnv('RMAKE_PATH', 'rmake') - - # set default RMAKE_TEST_PATH, if it was not set. rmakeTestPath = setPathFromEnv('RMAKE_TEST_PATH', 'rmake-private/test') - - # set default XMLLIB_PATH, if it was not set. xmllibPath = setPathFromEnv('XMLLIB_PATH', 'rpath-xmllib') - - testDir = os.path.dirname(os.path.realpath(__file__)) + pluginPath = os.path.realpath(rmakeTestPath + '/rmake_plugins') # Insert the following paths into the python path and sys path in # listed order. - paths = (mirrorballPath, rmakePath, testDir, conaryPath, conaryTestPath, + paths = (mirrorballPath, rmakePath, conaryDir, conaryTestPath, rmakeTestPath, xmllibPath) pythonPath = os.environ.get('PYTHONPATH', "") for p in reversed(paths): @@ -92,77 +80,58 @@ if isIndividual(): serverDir = '/tmp/conary-server' + #pylint: disable-msg=E1103 if os.path.exists(serverDir) and not os.path.access(serverDir, os.W_OK): serverDir = serverDir + '-' + pwd.getpwuid(os.getuid())[0] os.environ['SERVER_FILE_PATH'] = serverDir - invokedAs = sys.argv[0] - if invokedAs.find("/") != -1: - if invokedAs[0] != "/": - invokedAs = os.getcwd() + "/" + invokedAs - path = os.path.dirname(invokedAs) - else: - path = os.getcwd() - - import testhelp - from conary_test import resources + from testrunner import resources, testhelp testPath = testhelp.getTestPath() - archivePath = testPath + '/' + "archive" + archivePath = testPath + '/archive' resources.archivePath = archivePath - pluginPath = os.path.realpath(testPath + '/../../rmake-private/rmake_plugins') - nodePath = os.path.realpath(pluginPath + '/..') - if nodePath not in sys.path: - sys.path.insert(0, nodePath) + resources.pluginPath = pluginPath + resources.rmakePath = rmakePath - global conaryDir - conaryDir = os.environ['CONARY_PATH'] - + # W0621 - redefining name util, W0404 - reimporting util + #pylint: disable-msg=W0621,W0404 from conary.lib import util sys.excepthook = util.genExcepthook(True) # import tools normally expected in testsuite. - from testhelp import context, TestCase, findPorts, SkipTestException - sys.modules[__name__].context = context - sys.modules[__name__].TestCase = TestCase - sys.modules[__name__].findPorts = findPorts - sys.modules[__name__].SkipTestException = SkipTestException + sys.modules[__name__].context = testhelp.context + sys.modules[__name__].TestCase = testhelp.TestCase + sys.modules[__name__].findPorts = testhelp.findPorts + sys.modules[__name__].SkipTestException = testhelp.SkipTestException _setupPath = testPath return testPath + _individual = False -def isIndividual(): - global _individual - return _individual -def getCoverageDirs(handler, environ): - basePath = os.environ['SLEESTACK_PATH'] - coverageDirs = [ 'updatebot', 'rpmutils', 'repomd', ] +def getCoverageDirs(*_): + codePath = os.environ['SLEESTACK_PATH'].rstrip('/') + coverageDirs = [ 'aptmd', 'repomd', 'rpmutils', 'updatebot' ] + return [ os.path.join(codePath, x) for x in coverageDirs ] - coveragePath = [] - for path in coverageDirs: - coveragePath.append(os.path.normpath(os.path.join(basePath, path))) - - return coveragePath - -def getCoverageExclusions(self, environ): +def getCoverageExclusions(*_): return ['test/.*'] def sortTests(tests): - order = {'smoketest': 0, - 'unit_test' :1, - 'functionaltest':2} + order = {'rbuild_test.smoketest': 0, + 'rbuild_test.unit_test': 1, + 'rbuild_test.functionaltest': 2} maxNum = len(order) - tests = [ (test,test.index('test')) for test in tests] + tests = [ (test, test.index('test')) for test in tests] tests = sorted((order.get(test[:index+4], maxNum), test) for (test, index) in tests) tests = [ x[1] for x in tests ] return tests def main(argv=None, individual=True): - import testhelp + from testrunner import testhelp handlerClass = testhelp.getHandlerClass(testhelp.ConaryTestSuite, getCoverageDirs, getCoverageExclusions, @@ -185,6 +154,9 @@ if __name__ == '__main__': setup() + # unused import coveragehook + # redefining util from outer scope + #pylint: disable-msg=W0611,W0621 from conary.lib import util from conary.lib import coveragehook sys.excepthook = util.genExcepthook(True) diff --git a/test/unit_test/aptmdtest/testsetup.py b/test/unit_test/aptmdtest/testsetup.py new file mode 120000 --- /dev/null +++ b/test/unit_test/aptmdtest/testsetup.py @@ -0,0 +1,1 @@ +../../testsetup.py \ No newline at end of file diff --git a/test/unit_test/rpmutiltest/testsetup.py b/test/unit_test/rpmutiltest/testsetup.py new file mode 120000 --- /dev/null +++ b/test/unit_test/rpmutiltest/testsetup.py @@ -0,0 +1,1 @@ +../../testsetup.py \ No newline at end of file diff --git a/test/unit_test/rpmutiltest/vercmptest.py b/test/unit_test/rpmutiltest/vercmptest.py --- a/test/unit_test/rpmutiltest/vercmptest.py +++ b/test/unit_test/rpmutiltest/vercmptest.py @@ -13,8 +13,8 @@ # import testsetup +from testutils import mock -import mock import slehelp from rpmutils import rpmvercmp diff --git a/test/unit_test/testsetup.py b/test/unit_test/testsetup.py new file mode 120000 --- /dev/null +++ b/test/unit_test/testsetup.py @@ -0,0 +1,1 @@ +../testsetup.py \ No newline at end of file diff --git a/test/unit_test/updatebottest/advisetest.py b/test/unit_test/updatebottest/advisetest.py --- a/test/unit_test/updatebottest/advisetest.py +++ b/test/unit_test/updatebottest/advisetest.py @@ -13,8 +13,8 @@ # import testsetup +from testutils import mock -import mock import slehelp from updatebot import advise diff --git a/test/unit_test/updatebottest/buildtest.py b/test/unit_test/updatebottest/buildtest.py --- a/test/unit_test/updatebottest/buildtest.py +++ b/test/unit_test/updatebottest/buildtest.py @@ -14,8 +14,8 @@ # import testsetup +from testutils import mock -import mock import slehelp from rmake.cmdline import monitor, commit diff --git a/test/unit_test/updatebottest/conaryhelpertest.py b/test/unit_test/updatebottest/conaryhelpertest.py --- a/test/unit_test/updatebottest/conaryhelpertest.py +++ b/test/unit_test/updatebottest/conaryhelpertest.py @@ -14,9 +14,9 @@ # import testsetup +from testutils import mock import os -import mock import slehelp from conary import trove diff --git a/test/unit_test/updatebottest/errortest.py b/test/unit_test/updatebottest/errortest.py --- a/test/unit_test/updatebottest/errortest.py +++ b/test/unit_test/updatebottest/errortest.py @@ -14,7 +14,6 @@ import testsetup -import mock import slehelp from updatebot import errors diff --git a/test/unit_test/updatebottest/patchsourcetest.py b/test/unit_test/updatebottest/patchsourcetest.py --- a/test/unit_test/updatebottest/patchsourcetest.py +++ b/test/unit_test/updatebottest/patchsourcetest.py @@ -13,8 +13,8 @@ # import testsetup +from testutils import mock -import mock import slehelp from updatebot import patchsource diff --git a/test/unit_test/updatebottest/testsetup.py b/test/unit_test/updatebottest/testsetup.py new file mode 120000 --- /dev/null +++ b/test/unit_test/updatebottest/testsetup.py @@ -0,0 +1,1 @@ +../../testsetup.py \ No newline at end of file diff --git a/test/unit_test/updatebottest/updatetest.py b/test/unit_test/updatebottest/updatetest.py --- a/test/unit_test/updatebottest/updatetest.py +++ b/test/unit_test/updatebottest/updatetest.py @@ -13,8 +13,8 @@ # import testsetup +from testutils import mock -import mock import slehelp from updatebot import util diff --git a/test/unit_test/updatebottest/utiltest.py b/test/unit_test/updatebottest/utiltest.py --- a/test/unit_test/updatebottest/utiltest.py +++ b/test/unit_test/updatebottest/utiltest.py @@ -13,8 +13,8 @@ # import testsetup +from testutils import mock -import mock import slehelp from updatebot import util From johnsonm at rpath.com Wed Aug 19 17:38:50 2009 From: johnsonm at rpath.com (Michael K. Johnson) Date: Wed, 19 Aug 2009 21:38:50 +0000 Subject: mirrorball: fix up pylint errors Message-ID: <200908192138.n7JLcoRU013785@scc.eng.rpath.com> changeset: 7948a4ac4f30 user: Elliot Peele <http://issues.rpath.com/> date: Wed, 28 May 2008 00:35:40 -0400 fix up pylint errors committer: Elliot Peele <http://issues.rpath.com/> diff --git a/repomd/__init__.py b/repomd/__init__.py --- a/repomd/__init__.py +++ b/repomd/__init__.py @@ -12,14 +12,37 @@ # full details. # +''' +Common repository metadata parsing library. -from repomdxml import RepoMdXml -from repository import Repository -from errors import * +Handles parsing most yum metadata including SuSE proprietary patch/delta rpm +data. -__all__ = ('Client', ) + public_errors +NOTE: parsing of the following files is not implemented: + * filelists.xml.gz + * other.xml.gz + * product.xml + +Example: +> import repomd +> client = repomd.Client(url) +> patches = client.getPatchDetail() +> for patch in patches: +> # print all of the advisories in the repository +> print patch.description +''' + +from repomd.repomdxml import RepoMdXml +from repomd.repository import Repository +from repomd.errors import RepoMdError, ParseError, UnknownElementError + +__all__ = ('Client', 'RepoMdError', 'ParseError', 'UnknownElementError') class Client(object): + ''' + Client object for extracting information from repository metadata. + ''' + def __init__(self, repoUrl): self._repoUrl = repoUrl @@ -28,12 +51,27 @@ self._repomd = RepoMdXml(self._repo, self._baseMdPath).parse() def getRepos(self): + ''' + Get a repository instance. + @return instance of repomd.repository.Repository + ''' + return self._repo def getPatchDetail(self): + ''' + Get a list instances representing all patch data in the repository. + @return [repomd.patchxml._Patch, ...] + ''' + node = self._repomd.getRepoData('patches') return [ x.parseChildren() for x in node.parseChildren().getPatches() ] def getPackageDetail(self): + ''' + Get a list instances representing all packages in the repository. + @ return [repomd.packagexml._Package, ...] + ''' + node = self._repomd.getRepoData('primary') return node.parseChildren().getPackages() diff --git a/repomd/errors.py b/repomd/errors.py --- a/repomd/errors.py +++ b/repomd/errors.py @@ -12,18 +12,30 @@ # full details. # -public_errors = ('RepoMdError', 'ParseError', 'UnknownElementError') +''' +Errors specific to repomd module. +''' -__all__ = public_errors + ('public_errors', ) +__all__ = ('RepoMdError', 'ParseError', 'UnknownElementError') class RepoMdError(Exception): - pass + ''' + Base exception for all repomd exceptions. This should never be + expllicitly raised. + ''' class ParseError(RepoMdError): - pass + ''' + Base parsing error. + ''' class UnknownElementError(ParseError): + ''' + Raised when unhandled elements are found in the parser. + ''' + def __init__(self, element): + ParseError.__init__(self) self._element = element self._error = 'Element %s is not supported by this parser.' @@ -31,7 +43,12 @@ return self._error % (self._element.getAbsoluteName(), ) class UnknownAttributeError(UnknownElementError): + ''' + Raised when unhandled attributes are found in the parser. + ''' + def __init__(self, element, attribute): UnknownElementError.__init__(self, element) self._attribute = attribute - self._error = 'Attribute %s of %%s is not supported by this parser.' % (attribute, ) + self._error = ('Attribute %s of %%s is not supported by this ' + 'parser.' % (attribute, )) diff --git a/repomd/packagexml.py b/repomd/packagexml.py --- a/repomd/packagexml.py +++ b/repomd/packagexml.py @@ -12,13 +12,25 @@ # full details. # +''' +Module for parsing package sections of xml files from the repository metadata. +''' + __all__ = ('PackageXmlMixIn', ) from rpath_common.xmllib import api1 as xmllib -from errors import UnknownElementError, UnknownAttributeError +from repomd.errors import UnknownElementError, UnknownAttributeError class _Package(xmllib.BaseNode): + ''' + Python representation of package section of xml files from the repository + metadata. + ''' + + # R0902 - Too many instance attributes + # pylint: disable-msg=R0902 + name = None arch = None epoch = None @@ -36,6 +48,7 @@ installedSize = None archiveSize = None location = None + format = None license = None vendor = None group = None @@ -45,6 +58,17 @@ headerEnd = None def addChild(self, child): + ''' + Parse children of package element. + ''' + + # FIXME: There should be a better way to setup a parser. + # R0912 - Too many branches + # pylint: disable-msg=R0912 + + # R0915 - Too many statements + # pylint: disable-msg=R0915 + if child.getName() == 'name': self.name = child.finalize() elif child.getName() == 'arch': @@ -104,7 +128,15 @@ class _RpmRequires(xmllib.BaseNode): + ''' + Parse any element that contains rpm:entry or suse:entry elements. + ''' + def addChild(self, child): + ''' + Parse rpm:entry and suse:entry nodes. + ''' + if child.getName() in ('rpm:entry', 'suse:entry'): for attr, value in child.iterAttributes(): child.kind = None @@ -137,27 +169,48 @@ class _RpmRecommends(_RpmRequires): - pass + ''' + Parse rpm:recommends children. + ''' class _RpmProvides(_RpmRequires): - pass + ''' + Parse rpm:provides children. + ''' class _RpmObsoletes(_RpmRequires): - pass + ''' + Parse rpm:obsoletes children. + ''' class _RpmConflicts(_RpmRequires): - pass + ''' + Parse rpm:conflicts children. + ''' class _SuseFreshens(_RpmRequires): - pass + ''' + Parse suse:freshens children. + ''' class PackageXmlMixIn(object): + ''' + Handle registering all types for parsing package elements. + ''' + + # R0903 - Too few public methods + # pylint: disable-msg=R0903 + def _registerTypes(self): + ''' + Setup databinder to parse xml. + ''' + self._databinder.registerType(_Package, name='package') self._databinder.registerType(xmllib.StringNode, name='name') self._databinder.registerType(xmllib.StringNode, name='arch') @@ -166,14 +219,25 @@ self._databinder.registerType(xmllib.StringNode, name='description') self._databinder.registerType(xmllib.StringNode, name='url') # FIXME: really shouldn't need to comment these out - #self._databinder.registerType(xmllib.StringNode, name='license', namespace='rpm') - #self._databinder.registerType(xmllib.StringNode, name='vendor', namespace='rpm') - #self._databinder.registerType(xmllib.StringNode, name='group', namespace='rpm') - #self._databinder.registerType(xmllib.StringNode, name='buildhost', namespace='rpm') - #self._databinder.registerType(xmllib.StringNode, name='sourcerpm', namespace='rpm') - self._databinder.registerType(_RpmRequires, name='requires', namespace='rpm') - self._databinder.registerType(_RpmRecommends, name='recommends', namespace='rpm') - self._databinder.registerType(_RpmProvides, name='provides', namespace='rpm') - self._databinder.registerType(_RpmObsoletes, name='obsoletes', namespace='rpm') - self._databinder.registerType(_RpmConflicts, name='conflicts', namespace='rpm') - self._databinder.registerType(_SuseFreshens, name='freshens', namespace='suse') + # self._databinder.registerType(xmllib.StringNode, name='license', + # namespace='rpm') + # self._databinder.registerType(xmllib.StringNode, name='vendor', + # namespace='rpm') + # self._databinder.registerType(xmllib.StringNode, name='group', + # namespace='rpm') + # self._databinder.registerType(xmllib.StringNode, name='buildhost', + # namespace='rpm') + # self._databinder.registerType(xmllib.StringNode, name='sourcerpm', + # namespace='rpm') + self._databinder.registerType(_RpmRequires, name='requires', + namespace='rpm') + self._databinder.registerType(_RpmRecommends, name='recommends', + namespace='rpm') + self._databinder.registerType(_RpmProvides, name='provides', + namespace='rpm') + self._databinder.registerType(_RpmObsoletes, name='obsoletes', + namespace='rpm') + self._databinder.registerType(_RpmConflicts, name='conflicts', + namespace='rpm') + self._databinder.registerType(_SuseFreshens, name='freshens', + namespace='suse') diff --git a/repomd/patchesxml.py b/repomd/patchesxml.py --- a/repomd/patchesxml.py +++ b/repomd/patchesxml.py @@ -12,17 +12,32 @@ # full details. # +''' +Module for parsing patches.xml from the repository metadata. +''' + __all__ = ('PatchesXml', ) # import stable api from rpath_common.xmllib import api1 as xmllib -from patchxml import PatchXml -from xmlcommon import XmlFileParser -from errors import UnknownElementError +from repomd.patchxml import PatchXml +from repomd.xmlcommon import XmlFileParser +from repomd.errors import UnknownElementError class _Patches(xmllib.BaseNode): + ''' + Python representation of patches.xml from the repository metadata. + ''' + def addChild(self, child): + ''' + Parse children of patches element. + ''' + + # W0212 - Access to a protected member _parser of a client class + # pylint: disable-msg=W0212 + if child.getName() == 'patch': child.id = child.getAttribute('id') child._parser = PatchXml(None, child.location) @@ -32,27 +47,51 @@ raise UnknownElementError(child) def getPatches(self): + ''' + Get a list of all patches in the repository. + @return list of _PatchElement instances + ''' + return self.getChildren('patch') class _PatchElement(xmllib.BaseNode): + ''' + Parser for patch element of patches.xml. + ''' + id = None checksum = '' checksumType = 'sha' location = '' def addChild(self, child): + ''' + Parse children of patch element. + ''' + if child.getName() == 'checksum': self.checksum = child.finalize() self.checksumType = child.getAttribute('type') elif child.getName() == 'location': self.location = child.getAttribute('href') else: - raise UnkownElementError(child) + raise UnknownElementError(child) class PatchesXml(XmlFileParser): + ''' + Handle registering all types for parsing patches.xml. + ''' + + # R0903 - Too few public methods + # pylint: disable-msg=R0903 + def _registerTypes(self): + ''' + Setup databinder to parse xml. + ''' + self._databinder.registerType(_Patches, name='patches') self._databinder.registerType(_PatchElement, name='patch') self._databinder.registerType(xmllib.StringNode, name='checksum') diff --git a/repomd/patchxml.py b/repomd/patchxml.py --- a/repomd/patchxml.py +++ b/repomd/patchxml.py @@ -2,15 +2,32 @@ # Copryright (c) 2008 rPath, Inc. # +''' +Module for parsing patch-*.xml files from the repository metadata. +''' + __all__ = ('PatchXml', ) from rpath_common.xmllib import api1 as xmllib -from packagexml import * -from xmlcommon import XmlFileParser -from errors import UnknownElementError +from repomd.xmlcommon import XmlFileParser +from repomd.packagexml import PackageXmlMixIn +from repomd.errors import UnknownElementError class _Patch(xmllib.BaseNode): + ''' + Python representation of patch-*.xml from the repository metadata. + ''' + + # R0902 - Too many instance attributes + # pylint: disable-msg=R0902 + + # W0232 - Class has no __init__ method (Yes, really it does) + # pylint: disable-msg=W0232 + + # R0903 - Too few public methods + # pylint: disable-msg=R0903 + name = None summary = None description = None @@ -22,8 +39,17 @@ licenseToConfirm = None packageManager = False category = None + packages = None def addChild(self, child): + ''' + Parse children of patch element. + ''' + + # FIXME: There should be a better way to setup a parser. + # R0912 - Too many branches + # pylint: disable-msg=R0912 + if child.getName() == 'yum:name': self.name = child.finalize() elif child.getName() == 'summary': @@ -38,7 +64,7 @@ elif child.getName() == 'rpm:requires': self.requires = child.getChildren('entry', namespace='rpm') elif child.getName() == 'rpm:recommends': - self.recommneds = child.getChildren('entry', namespace='rpm') + self.recommends = child.getChildren('entry', namespace='rpm') elif child.getName() == 'reboot-needed': self.rebootNeeded = True elif child.getName() == 'license-to-confirm': @@ -66,7 +92,21 @@ class _Atoms(xmllib.BaseNode): + ''' + Parser for the atoms element of a path-*.xml file. + ''' + + # W0232 - Class has no __init__ method (Yes, really it does) + # pylint: disable-msg=W0232 + + # R0903 - Too few public methods + # pylint: disable-msg=R0903 + def addChild(self, child): + ''' + Parse children of atoms element. + ''' + if child.getName() == 'package': child.type = child.getAttribute('type') xmllib.BaseNode.addChild(self, child) @@ -79,10 +119,23 @@ class PatchXml(XmlFileParser, PackageXmlMixIn): + ''' + Handle registering all types for parsing patch-*.xml files. + ''' + + # R0903 - Too few public methods + # pylint: disable-msg=R0903 + def _registerTypes(self): + ''' + Setup databinder to parse xml. + ''' + PackageXmlMixIn._registerTypes(self) self._databinder.registerType(_Patch, name='patch') - self._databinder.registerType(xmllib.StringNode, name='name', namespace='yum') + self._databinder.registerType(xmllib.StringNode, name='name', + namespace='yum') self._databinder.registerType(xmllib.StringNode, name='category') self._databinder.registerType(_Atoms, name='atoms') - self._databinder.registerType(xmllib.StringNode, name='license-to-confirm') + self._databinder.registerType(xmllib.StringNode, + name='license-to-confirm') diff --git a/repomd/primaryxml.py b/repomd/primaryxml.py --- a/repomd/primaryxml.py +++ b/repomd/primaryxml.py @@ -2,16 +2,28 @@ # Copryright (c) 2008 rPath, Inc. # +''' +Module for parsing primary.xml.gz from the repository metadata. +''' + __all__ = ('PrimaryXml', ) from rpath_common.xmllib import api1 as xmllib -from packagexml import * -from xmlcommon import XmlFileParser -from errors import UnknownElementError +from repomd.xmlcommon import XmlFileParser +from repomd.packagexml import PackageXmlMixIn +from repomd.errors import UnknownElementError class _Metadata(xmllib.BaseNode): + ''' + Python representation of primary.xml.gz from the repository metadata. + ''' + def addChild(self, child): + ''' + Parse children of metadata element. + ''' + if child.getName() == 'package': child.type = child.getAttribute('type') xmllib.BaseNode.addChild(self, child) @@ -19,10 +31,26 @@ raise UnknownElementError(child) def getPackages(self): + ''' + Get a list of all packages contained in primary.xml.gz. + @return list of repomd.packagexml._Package objects + ''' + return self.getChildren('package') class PrimaryXml(XmlFileParser, PackageXmlMixIn): + ''' + Handle registering all types for parsing primary.xml.gz. + ''' + + # R0903 - Too few public methods + # pylint: disable-msg=R0903 + def _registerTypes(self): + ''' + Setup databinder to parse xml. + ''' + PackageXmlMixIn._registerTypes(self) self._databinder.registerType(_Metadata, name='metadata') diff --git a/repomd/repomdxml.py b/repomd/repomdxml.py --- a/repomd/repomdxml.py +++ b/repomd/repomdxml.py @@ -12,18 +12,33 @@ # full details. # +''' +Module for parsing repomd.xml files from the repository metadata. +''' + __all__ = ('RepoMdXml', ) # use stable api from rpath_common.xmllib import api1 as xmllib -from primaryxml import PrimaryXml -from patchesxml import PatchesXml -from xmlcommon import XmlFileParser -from errors import UnknownElementError +from repomd.primaryxml import PrimaryXml +from repomd.patchesxml import PatchesXml +from repomd.xmlcommon import XmlFileParser +from repomd.errors import UnknownElementError class _RepoMd(xmllib.BaseNode): + ''' + Python representation of repomd.xml from the repository metadata. + ''' + def addChild(self, child): + ''' + Parse children of repomd element. + ''' + + # W0212 - Access to a protected member _parser of a client class + # pylint: disable-msg=W0212 + if child.getName() == 'data': child.type = child.getAttribute('type') if child.type == 'patches': @@ -37,6 +52,15 @@ raise UnknownElementError(child) def getRepoData(self, name=None): + ''' + Get data elements of repomd xml file. + @param name: filter by type of node + @type name: string + @return list of nodes + @return single node + @return None + ''' + if not name: return self.getChildren('data') @@ -48,6 +72,10 @@ class _RepoMdDataElement(xmllib.BaseNode): + ''' + Parser for repomd.xml data elements. + ''' + location = '' checksum = '' checksumType = 'sha' @@ -56,6 +84,10 @@ openChecksumType = 'sha' def addChild(self, child): + ''' + Parse children of data element. + ''' + if child.getName() == 'location': self.location = child.getAttribute('href') elif child.getName() == 'checksum': @@ -71,7 +103,18 @@ class RepoMdXml(XmlFileParser): + ''' + Handle registering all types for parsing repomd.xml file. + ''' + + # R0903 - Too few public methods + # pylint: disable-msg=R0903 + def _registerTypes(self): + ''' + Setup databinder to parse xml. + ''' + self._databinder.registerType(_RepoMd, name='repomd') self._databinder.registerType(_RepoMdDataElement, name='data') self._databinder.registerType(xmllib.StringNode, name='checksum') diff --git a/repomd/repository.py b/repomd/repository.py --- a/repomd/repository.py +++ b/repomd/repository.py @@ -12,6 +12,10 @@ # full details. # +''' +Repository access module. +''' + __all__ = ('Repository', ) import os @@ -20,10 +24,24 @@ import urlgrabber class Repository(object): + ''' + Access files from the repository. + ''' + + # R0903 - Too few public methods + # pylint: disable-msg=R0903 + def __init__(self, repoUrl): self._repoUrl = repoUrl def get(self, fileName): + ''' + Download a file from the repository. + @param fileName: relative path to file + @type fileName: string + @return open file instance + ''' + fn = self._getTempFile() realUrl = self._getRealUrl(fi