├── autopromoter.py └── README.md /autopromoter.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | import os 4 | import argparse 5 | import sys 6 | import plistlib 7 | import datetime 8 | 9 | DEFAULT_DAYS = 5 10 | DEFAULT_CATS = ['development', 'testing', 'production'] 11 | 12 | def update_catalogs_per_prodems(catalogs, prodems, now_base=datetime.datetime.now()): 13 | prodem_catalogs = set([i.get('catalog') for i in prodems]) 14 | catalogs = set(catalogs) 15 | 16 | # check for any non-policy catalogs and warn user 17 | addl_catalogs = catalogs.difference(prodem_catalogs) 18 | if addl_catalogs: 19 | print 'WARNING: additional catalogs found in pkginfo that are not in the' 20 | print ' promotion/demotion set:' 21 | print ' ' + ', '.join(addl_catalogs) 22 | print ' please remove catalog(s) from pkginfo file if not desired' 23 | 24 | # start building the current set of managed pro/dem catalogs by including 25 | # non-managed catalogs 26 | cur_catalogs = list(addl_catalogs) 27 | 28 | cats_changed = False 29 | 30 | # the meat and potatoes: 31 | # analyze the promotion & demotion timestamps, compare them with our 32 | # current now() timestamp and decide whether catalogs should be active 33 | # in the pkginfo or not. contrast against existing pkginfo to see if 34 | # there are changes 35 | for pd in prodems: 36 | if not isinstance(pd.get('demotion_date'), datetime.datetime): 37 | # faux demotion date that never expires (is a day in the future) 38 | demotion_date = now_base + datetime.timedelta(days=1) 39 | else: 40 | demotion_date = pd['demotion_date'] 41 | if not isinstance(pd.get('promotion_date'), datetime.datetime): 42 | # faux promotion that's always promoted (is a day in the past) 43 | promotion_date = now_base - datetime.timedelta(days=1) 44 | else: 45 | promotion_date = pd['promotion_date'] 46 | 47 | if now_base >= promotion_date and now_base < demotion_date: 48 | cur_catalogs.append(pd['catalog']) 49 | 50 | if not pd['catalog'] in catalogs: 51 | print 'catalog %s ought to be active now: promoting' % pd['catalog'] 52 | cats_changed = True 53 | else: 54 | if pd['catalog'] in catalogs: 55 | print 'catalog %s should not be active now: demoting' % pd['catalog'] 56 | cats_changed = True 57 | 58 | if cats_changed: 59 | return cur_catalogs 60 | else: 61 | print 'no catalog promotions or demotions' 62 | return None 63 | 64 | 65 | def pkginfo_catalog_prodem(pkginfo_fn, catdurs, keep_catalogs=False): 66 | print 'processing pkginfo "%s"%s...' % (pkginfo_fn, ' keeping catalogs' if keep_catalogs else '') 67 | 68 | plist_changed = False 69 | pkginfo_d = plistlib.readPlist(pkginfo_fn) 70 | 71 | if '_metadata' not in pkginfo_d.keys(): 72 | # create _metadata key if it doesn't exist. this is to catch older 73 | # pkginfos that didn't automatically generate this field 74 | pkginfo_d['_metadata'] = {} 75 | plist_changed = True 76 | 77 | if 'catalog_promotion' not in pkginfo_d['_metadata'].keys(): 78 | pkginfo_d['_metadata']['catalog_promotion'] = [] 79 | plist_changed = True 80 | 81 | prodems = pkginfo_d['_metadata']['catalog_promotion'] 82 | 83 | # cache a copy of now() so it doesn't update on us while we process 84 | now_base = datetime.datetime.now() 85 | 86 | for i, (catdur_name, catdur_days) in enumerate(catdurs): 87 | # we're searching on a catalog-by-catalog basis for the set of pro/dem 88 | # catalogs in case future policy changes (i.e. diff. catalogs or dates) 89 | # are adjusted on the CLI. this could be much easier if we only 90 | # supported setting the pro/dem set once and then had the munki admin 91 | # make any future adjustments. up for discussion. 92 | 93 | # find index of pkginfo promotion/demotion set that matches the 94 | # current "policy" promotion/demotion set. this is so we know we 95 | # have the initial data for each catalog in the pkginfo or not 96 | found_prodem = False 97 | for j, prodem in enumerate(prodems): 98 | if catdur_name == prodem['catalog']: 99 | found_prodem = True 100 | break 101 | 102 | if not found_prodem: 103 | print 'creating initial promotion/demotion record for catalog %s' % catdur_name 104 | 105 | # if this catalog is the first in our list promote/demote list.. 106 | new_prodem = {'catalog': catdur_name, 'creation_date': now_base} 107 | if i == 0: 108 | # first item in existing promotion/demotion set 109 | # only needs a demotion date (first step) 110 | new_prodem['demotion_date'] = now_base + datetime.timedelta(days=catdur_days) 111 | 112 | print ' promoting on: (now until demotion)' 113 | print ' demoting on: ', new_prodem['demotion_date'], '(+%d days from now)' % catdur_days 114 | elif i > 0 and i < (len(catdurs) - 1): 115 | # a middle item in existing promotion/demotion set 116 | # needs promotion and demotion date (middle steps) 117 | new_prodem['promotion_date'] = prodems[-1]['demotion_date'] 118 | new_prodem['demotion_date'] = prodems[-1]['demotion_date'] + datetime.timedelta(days=catdur_days) 119 | 120 | print ' promoting on:', new_prodem['promotion_date'], '(same as prev. cat demotion)' 121 | print ' demoting on: ', new_prodem['demotion_date'], '(+%d days from prev. cat demotion)' % catdur_days 122 | elif i == len(catdurs) - 1: 123 | # last item in existing promotion/demotion set 124 | # only needs a promotion date (last step) 125 | new_prodem['promotion_date'] = prodems[-1]['demotion_date'] 126 | 127 | print ' promoting on:', new_prodem['promotion_date'], '(same as prev. cat demotion)' 128 | print ' demoting on: (never after promotion)' 129 | 130 | prodems.append(new_prodem) 131 | plist_changed = True 132 | 133 | changed_cats = update_catalogs_per_prodems(pkginfo_d['catalogs'], prodems, now_base) 134 | 135 | if changed_cats: 136 | pkginfo_d['catalogs'] = changed_cats 137 | plist_changed = True 138 | 139 | if plist_changed: 140 | print 'writing changed pkginfo' 141 | plistlib.writePlist(pkginfo_d, pkginfo_fn) 142 | 143 | def main(): 144 | parser = argparse.ArgumentParser(description='Munki pkginfo catalog promotion & demotion manager') 145 | parser.add_argument('pkginfo', nargs='+', help='filename(s) of pkginfo file(s)') 146 | # parser.add_argument('--keep', action='store_true', 147 | # help='ignore demotion dates for catalogs. this keeps catalogs listed ' 148 | # 'in the pkginfo (never demoting them)') 149 | parser.add_argument('--catalog', metavar='cat[:days]', action='append', 150 | help='name of catalogs and optional demotion/promotion duration in ' 151 | 'number of days ("--catalog testing:14"). note only sets duration' 152 | 'dates once for a pkginfo') 153 | 154 | args = parser.parse_args() 155 | 156 | for pkginfo in args.pkginfo: 157 | if not os.path.isfile(pkginfo): 158 | print 'pkginfo "%s" is not a file' % pkginfo 159 | return 1 160 | 161 | # get list of catalogs in tuple of (catname, days) 162 | catdurlist = [] 163 | if args.catalog: 164 | for catdur in [x.split(':') for x in args.catalog]: 165 | if len(catdur) == 2: 166 | catdurlist.append((catdur[0], int(catdur[1]), )) 167 | elif len(catdur) == 1: 168 | # no days specified, let's assume default 169 | catdurlist.append((catdur[0], DEFAULT_DAYS, )) 170 | else: 171 | print 'invalid catalog days specified, must be "catalog[:days]" like "testing:25"' 172 | return 1 173 | else: 174 | for cat in DEFAULT_CATS: 175 | catdurlist.append((cat, DEFAULT_DAYS, )) 176 | 177 | for pkginfo in args.pkginfo: 178 | pkginfo_catalog_prodem(pkginfo, catdurlist) # , args.keep) 179 | 180 | return 0 181 | 182 | if __name__ == '__main__': 183 | sys.exit(int(main())) 184 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # autopromoter 2 | 3 | Autopromoter is a tool for [Munki](https://github.com/munki/munki) [pkginfo files](https://github.com/munki/munki/wiki/Pkginfo-Files) that automatically promotes (and demotes) Munki catalogs in a given pkginfo file based on a configured "policy" of dates and times. 4 | 5 | This policy is referred to as the promotion/demotion set and simply consists of the timestamp range a given catalog name should be present in a pkginfo's catalogs key. Once the initial pro/dem set is configured in the pkginfo autopromoter will then manage the lifecycle of which catalogs ought to be in the pkginfo at any given time. The most common use case is to remove some of the monotomy in the workflow of moving a pkginfo from, say, the testing catalog to the production catalog. 6 | 7 | ## Getting started 8 | 9 | To initialize the pro/dem set in a given pkginfo with the default policy simply run autopromoter on a pkginfo file with no other arguments: 10 | 11 | ```bash 12 | autopromoter.py Firefox-39.0.plist 13 | ``` 14 | 15 | For a pkginfo that has no pro/dem set yet you'll probably see output similar to this: 16 | 17 | ``` 18 | processing pkginfo "Firefox-39.0.plist"... 19 | creating initial promotion/demotion record for catalog development 20 | promoting on: (now until demotion) 21 | demoting on: 2015-07-16 20:16:29.231466 (+5 days from now) 22 | creating initial promotion/demotion record for catalog testing 23 | promoting on: 2015-07-16 20:16:29.231466 (same as prev. cat demotion) 24 | demoting on: 2015-07-21 20:16:29.231466 (+5 days from prev. cat demotion) 25 | creating initial promotion/demotion record for catalog production 26 | promoting on: 2015-07-21 20:16:29.231466 (same as prev. cat demotion) 27 | demoting on: (never after promotion) 28 | catalog development ought to be active now: promoting 29 | catalog testing should not be active now: demoting 30 | writing changed pkginfo 31 | ``` 32 | 33 | As you can see the default policy is to have three catalogs managed: development, testing, and production with a time of five days in each catalog relative to each other between promotions/demotions. 34 | 35 | *Keep in mind the timestamps are UTC and not your local time.* 36 | 37 | Now with the initial pro/dem having been set you can keep running autopromoter on this same pkginfo and, when the time is right according to the policy, it will update the pkginfo to demote or promote the appropriate catalogs similar to this: 38 | 39 | ``` 40 | processing pkginfo "Firefox-39.0.plist"... 41 | catalog development should not be active now: demoting 42 | catalog testing ought to be active now: promoting 43 | writing changed pkginfo 44 | ``` 45 | 46 | If there's no changes to be made yet becuase, say, not enough time has yet passed to trigger a promotion or demotion: 47 | 48 | ``` 49 | processing pkginfo "Firefox-39.0.plist"... 50 | no catalog promotions or demotions 51 | ``` 52 | 53 | Because autopromoter does not change pkginfo files unless necessary it is expected that it would be run in an automated fashion (say, nightly) so that promotions and demotions can happen automatically. Though this isn't strictly necessary if you'd like it to run in a semi-automated fashion. 54 | 55 | ## Understanding pro/dem sets (autopromoter policy), promotions, and demotions 56 | 57 | Munki administrators are highly encouraged to manage software packages in different catalogs often representing the stages of testing and deployment they've been through. 58 | 59 | As an example workflow: a package that's currently being developed & tested by an IT team would be placed in a "development" catalog but no other catalog yet as a full testing and vetting hasn't been completed. As time passes the vetting and testing (by that IT team) has shown no serious flaws and so it's time to push the package out to a limited set of end-users. Thus it is placed in the "testing" catalog. Finally if no problems are found with the testing user group the package is released to all users by being in a "production" catalog. 60 | 61 | This all works well but the the problem arises when keeping track of when these different catalogs ought to be enabled and disabled across an entire repository of packages. Autopromoter aims to fix this by automating these promotions/demotions and keeping track, on a per-package basis, what the current status and expected future status of the packages catalog membership is. 62 | 63 | As well there is no method of tracking institutional knowledge about outlier packages. For example say some package needs additional testing time from end-users. By simply changing the pro/dem policy in the pkginfo for that one package it is trivial to manage and keep track of such outliers. 64 | 65 | Per above the autopromoter pro/dem sets is just a collection of "valid" timestamps that a catalog should be active for. This collection is stored in the pkginfo file under the 'catalog_promotion' key within the '_metadata' top-level key. It's structure is fairly straight forward nothing when each item was created, and the timestamp of it's promotion and/or demotion. If a promotion or demotion is missing then it means that package will not be demoted or promoted; only it's opposite action will be tested against. For example in the default policy the "production" catalog has no demotion date. This is because once it is finally promoted it's not expected to ever be demoted. 66 | 67 | When a catalog in the pro/dem set is valid (that is to say that the current date & time is past that catalog's demotion date or before it's promotion date), but not in the catalog then autopromoter enables that catalog in the pkginfo. Likewise if the catalog is not valid then autopromoter disables that catalog in the pkginfo. 68 | 69 | Autopromoter will issue a warning for catalogs that are in the pkginfo but not managed in the pro/dem policy set and will continue to include that catalog in the pkginfo even amongst other promotions/demotions. 70 | 71 | ## Specifing your own initial pro/dem sets 72 | 73 | Autopromoter allows the specification of your own pro/dem policy set. It is specified on the command line with the `--catalog` switch: 74 | 75 | ```bash 76 | autopromoter.py --catalog pluto --catalog mars:10 --catalog saturn:10 Firefox-39.0.plist 77 | ``` 78 | 79 | This will override the default policy and create pro/dem catalog sets for pluto (whith the default demotion delta of five days) and for mars with a specified 10 day demotion delta and the same with the saturn catalog: 80 | 81 | ``` 82 | processing pkginfo "Firefox-39.0.plist"... 83 | creating initial promotion/demotion record for catalog pluto 84 | promoting on: (now until demotion) 85 | demoting on: 2015-07-16 21:01:47.524423 (+5 days from now) 86 | creating initial promotion/demotion record for catalog mars 87 | promoting on: 2015-07-16 21:01:47.524423 (same as prev. cat demotion) 88 | demoting on: 2015-07-26 21:01:47.524423 (+10 days from prev. cat demotion) 89 | creating initial promotion/demotion record for catalog saturn 90 | promoting on: 2015-07-26 21:01:47.524423 (same as prev. cat demotion) 91 | demoting on: (never after promotion) 92 | WARNING: additional catalogs found in pkginfo that are not in the 93 | promotion/demotion set: 94 | testing 95 | please remove catalog(s) from pkginfo file if not desired 96 | catalog pluto ought to be active now: promoting 97 | writing changed pkginfo 98 | ``` 99 | 100 | Note that the day time deltas are based on relative timestamps for the given catalog sets. For example in the above command the pluto catalog has no set promotion date because it should start as promoted. It has a demotion time of 5 days (the default) from the current date and time. The mars catalog has a demotion day the same as the previous catalog's promotion date (so that the two catalogs can be demoted and promoted at the same time). 101 | 102 | While saturn did specify a 10 day delta it gets no demotion time. The implicit behaviour of managing a list of catalogs is that the beginning catalog is immediately available and the last catalog will never be demoted (because it's the last catalog in the chain). This order is established by the order of catalog arguments specified to autopromoter or the default set of catalogs (development, testing, production). 103 | 104 | ## Future things 105 | 106 | Autopromoter is a very simple (perhaps too simple) tool to manage the autopromotion and demotion of catalogs in an escalating testing-type workflow. 107 | 108 | Ideally it would have the ability to manage one's entire Munki repository being somewhat smart about initializations. Right now it can perform it's normal promotion/workflow logic on multiple pkginfos but it's not really suitable nor user freindly to specifiy all pkginfos at this time (it will likely un-production your existing production pkginfos). 109 | 110 | As well it would be good to make it far more extensible such that it can have Plist output modes and other automation-freindly features. Perhaps it can be plugged into other catalog management or web UI management tools, etc. 111 | 112 | Cheers! 113 | --------------------------------------------------------------------------------