# # Licensed under the GNU General Public License Version 3 # # 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 3 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. # # Copyright 2011,2012 Joerg Steffens # # NOTE: the 'self' variable is an instance of SpacewalkShell import shlex from optparse import Option from pprint import pprint import sys from spacecmd.utils import * _STAGE1='dev' _STAGE2='stg' _STAGE3='prd' _STAGES=[ _STAGE1, _STAGE2, _STAGE3 ] _STAGE_NAMES={ 'dev': 'Development', # alternative 'qas': 'QualityAssurance', 'stg': 'Staging', 'prd': 'Production' } _STAGE_TRANSITIONS={ _STAGE1: _STAGE2, _STAGE2: _STAGE3 } _STAGE_STATUS={ "uptodate": " ", "modified": "M", "dontexist": "!", "unknown": "?" } _DUMP_BASE_DIR="/tmp/spacecmd-stage-dump/" #################### def do_stage_help( self, args, doreturn = False ): print """ Staging: The basic principle is to have every component in multiple stages. The stages in this environment are:""" for stage in _STAGES: successor=self.get_next_stage(stage) print " " + stage + ":" , _STAGE_NAMES.get(stage) print """ A stage can have a successor, in our enviroment these are:""" for stage in _STAGES: successor=self.get_next_stage(stage) if successor: print " " + stage, "->" , successor print """ Workflow example: * creating a new package/package version the new package is added to a {stage1} softwarechannel. If the package seams to work correctly, a new integration phase can be started. For this, the packages are copied from {stage1} to {stage2}. The {stage2} stage is then tested. After a successful test, all content of {stage2} is transfered to {stage3}. When the content has arrived in {stage3}, all productively used systems are able to update to the new content. Summary: {stage1}: all changes are done to {stage1} {stage2}: integration tests are done in {stage2} only {stage3}: productively used systems using {stage3} only Changes are not only adding new packages, but also changing configuration files in the configuration channels, changing kickstart settings or changing activation keys. For all these changes, spacecmd stage_* commands offers functionality to simplify staging. Usage: * create your channels, actionvationkey and so on. Because Spacewalk does not know about staging directly, staging information must be coded into the name of the components. The name must include the stage, separeted by '-', eg. centos6-x86_64-{stage1}, centos6-x86_64-{stage1}-subchannel, ks-centos6-x86_64-{stage1}-common, ... To create a initial structure, the comamnd 'stage_create_skel' can be used. * check the staging status by 'stage_status STAGE' This will select all components from stage 'STAGE' and compare each component with the correcponding component from the successor stage, eg.: 'stage_status {stage1}' INFO: softwarechannel centos6-x86_64-{stage1} -> centos6-x86_64-{stage2} M centos6-x86_64-{stage1}-app1 -> centos6-x86_64-{stage2}-app1 ! centos6-x86_64-{stage1}-app2 INFO: configchannel cfg-centos6-x86_64-{stage1}-app1 -> cfg-centos6-x86_64-{stage2}-app1 INFO: kickstart M ks-centos6-x86_64-{stage1}-app1 -> ks-centos6-x86_64-{stage2}-app1 INFO: activationkey 1-centos6-x86_64-{stage1}-app1 -> 1-centos6-x86_64-{stage2}-app1 This first column indicates the state: : empty: no differences. The components from both stages are indentical ! : no correcponding component in successor stage found M : modification. The component differs between the current and the successor stage * The most interessting entries are the modified entires. To check this more specifically, use the corresponding 'stage_*_diff' function, eg. 'stage_softwarechannel_diff centos7-x86_64-{stage1}-app1' --- centos6-x86_64-{stage1}-app1 +++ centos6-x86_64-{stage2}-app1 @@ -1,1 +1,0 @@ -newpackage-1.0.1-1.1.noarch (it is also possible to compare two specific subchannel, eg. 'stage_softwarechannel_diff centos6-x86_64-{stage1}-subchannel1 centos6-x86_64-{stage2}-subchannel1' but the corresponding successor stage component is found automatically by its name) * Softwarechannel and configchannel also offers the stage_*_sync function. Use them, to copy the content of a component to the next stage, e.g. 'stage_softwarechannel_sync centos6-x86_64-{stage1}-app1' INFO: syncing packages from softwarechannel centos6-x86_64-{stage1}-app1 to centos6-x86_64-{stage2}-app1 packages to add to channel "centos6-x86_64-{stage2}-app1": newpackage-1.0.1-1.1.noarch Perform these changes to channel centos6-x86_64-{stage2}-app1 [y/N]: * Repeat these steps, until 'stage_status STAGE' shows no differences between the two stages """.format(stage1=_STAGE1, stage2=_STAGE2, stage3=_STAGE3) def help_stage_create_skel(self): print 'stage_create_skel: create initial staging structure' print '''usage: stage_create_skel [options] options: -l LABEL -a ARCHITECTURE ['ia32', 'x86_64'] -s SUB (e.g. application1)''' def do_stage_create_skel(self, args): options = [ Option('-l', '--label', action='store'), Option('-a', '--arch', action='store'), Option('-s', '--sub', action='store'), ] (args, options) = parse_arguments(args, options) if is_interactive(options): options.label = prompt_user('Channel Label:', noblank = True) print print 'Architecture' print '------------' print '\n'.join(sorted(self.ARCH_LABELS)) print options.arch = prompt_user('Select:') options.arch = prompt_user('Sub:') else: if not options.label: logging.error('A channel label is required') return if not options.arch: logging.error('An architecture is required') return if self.stage_create_skel( options.label, options.arch, options.sub, create=False ): self.stage_create_skel( options.label, options.arch, options.sub, create=True ) def stage_create_skel(self, dist, arch, sub, create = False): org = "1" disttype = "rhel_6" application = sub print for stage in _STAGES: base = dist + "-" + arch + "-" + stage softwarechannel_base = base softwarechannel_sub = base + "-" + application distribution = "dist-" + base distributionpath = "/srv/dist/" + base configchannel = "cfg-" + base + "-" + application activationkey_create = base + "-" + application activationkey = org + "-" + activationkey_create kickstart = "ks-" + base + "-" + application print "stage: " + stage print "softwarechannel base: " + softwarechannel_base, if self.is_softwarechannel( softwarechannel_base ): print " [exists]", elif create: self.do_softwarechannel_create( "-n " + softwarechannel_base + " -l " + softwarechannel_base + " -a " + arch ) print print "softwarechannel subchannel: " + softwarechannel_sub, if self.is_softwarechannel( softwarechannel_sub ): print " [exists]", elif create: self.do_softwarechannel_create( "-n " + softwarechannel_sub + " -l " + softwarechannel_sub + " -a " + arch + " -p " + base ) print print "distribution: " + distribution + " (distribution path: " + distributionpath + ")", if distribution in self.do_distribution_list(distribution, True): print " [exists]", elif create: self.do_distribution_create( "--name " + distribution + " --path " + distributionpath + " --base-channel " + base + " --install-type " + disttype ) print print "configchannel: " + configchannel, if self.is_configchannel( configchannel ): print " [exists]", elif create: self.do_configchannel_create( "-n " + configchannel ) print print "activationkey: " + activationkey, if self.is_activationkey( activationkey ): print " [exists]", elif create: self.do_activationkey_create( "-n " + activationkey_create + " -d " + activationkey + " -b " + base + " -e provisioning_entitled" ) self.do_activationkey_addchildchannels( activationkey + " " + softwarechannel_sub ) self.do_activationkey_enableconfigdeployment( activationkey ) self.do_activationkey_addconfigchannels( activationkey + " " + configchannel + " -t" ) print print "kickstart: " + kickstart, if self.is_kickstart( kickstart ): print " [exists]", elif create: self.do_kickstart_create( "--name=" + kickstart + " --distribution=" + distribution + " --root-password=CHANGEME --virt-type=none" ) self.do_kickstart_addactivationkeys( kickstart + " " + activationkey ) self.do_kickstart_enableconfigmanagement( kickstart ) self.do_kickstart_enablelogging( kickstart ) print print if not create: print "Make sure, distribution trees are available at the specified distribution paths." return self.user_confirm('Create this components [y/N]:') #################### # # helper functions # def is_stage( self, name ): return name in _STAGES def check_stage( self, name ): """Checks if name describes a vaild stage""" if not name: logging.error( "no stage given" ) return False if not self.is_stage( name ): logging.error( "invalid stage " + name ) return False return True def is_current_stage(self, name): return "-"+self.stage in name def get_common_name( self, name ): """Returns the name with the stage replaced by 'STAGE' To check the differences from 2 components that are in different stages, the specific stage is replaced by the word 'STAGE' """ return self.replace_stage_in_name( name, self.stage, "STAGE" ) def get_stage_from_name( self, name ): for i in _STAGES: if "-"+i in name: return i def get_next_stage( self, current_stage ): return _STAGE_TRANSITIONS.get(current_stage) def replace_stage_in_name( self, name, current_stage, new_stage ): """Return the name with current stage replaced by new stage""" return name.replace( "-"+current_stage, "-"+new_stage ) def get_next_stage_name( self, name ): current_stage = self.get_stage_from_name( name ) if not current_stage: return next_stage = self.get_next_stage( current_stage ) if not next_stage: return next_stage_name = self.replace_stage_in_name( name, current_stage, next_stage ) return next_stage_name def print_stage_status( self, name, name_next=None, status="unknown", indent="" ): width=48-len(indent) string = '{status_code} {indent}{name:{width}}'.format(status_code=_STAGE_STATUS.get(status), indent=indent, name=name, width=width ) if name_next: string = string + " -> " + indent + name_next print string def mkdir(self, name ): try: if not os.path.isdir( name ): os.makedirs( name ) logging.debug( "creating directory " + name ) return True except: logging.error('Failed to create directory ' + name ) return False def dump(self, filename, data, raw=False): """Writes data to filename""" if not self.mkdir( os.path.dirname( filename )): return False try: fh = open( filename, 'w' ) if( raw ): fh.write(data) else: fh.write("\n".join(data)) fh.close() except: logging.error('failed to create file ' + filename ) return False #################### # # softwarechannel # def get_softwarechannel_childchannel( self, base_channel ): result=[] for child_channel in self.list_child_channels(): details = self.client.channel.software.getDetails(\ self.session, child_channel) if details.get('parent_channel_label') == base_channel: result.append( child_channel ) return result # softwarechannel next def help_stage_softwarechannel_next(self): print 'stage_softwarechannel_next: get softwarechannel name for the next stage' print ' ' print 'usage: stage_softwarechannel_next CHANNEL' def complete_stage_softwarechannel_next(self, text, line, beg, end): parts = shlex.split(line) if line[-1] == ' ': parts.append('') args = len(parts) if args == 2: return tab_completer(self.do_softwarechannel_list('', True), text) return [] def do_stage_softwarechannel_next(self, args): (args, options) = parse_arguments(args) if len(args) != 1: self.help_stage_softwarechannel_next() return source_name = args[0] if not self.is_softwarechannel(source_name): logging.warning( "invalid softwarechannel "+source_name ) return logging.debug( "source: " + str(source_name) ) target_name = self.get_next_stage_name( source_name ) logging.debug( "target: " + str(target_name) ) if not target_name: return # check target name if not self.is_softwarechannel(target_name): logging.debug( "a next stage softwarechannel for "+source_name+" ("+target_name+") does not exist" ) return return target_name #################### # # configchannel # # configchannel next def help_stage_configchannel_next(self): print 'stage_configchannel_next: get configchannel name for the next stage' print ' ' print 'usage: stage_configchannel_next CHANNEL' def complete_stage_configchannel_next(self, text, line, beg, end): parts = shlex.split(line) if line[-1] == ' ': parts.append('') args = len(parts) if args == 2: return tab_completer(self.do_configchannel_list('', True), text) return [] def do_stage_configchannel_next(self, args): (args, options) = parse_arguments(args) if len(args) != 1: self.help_stage_configchannel_next() return source_name = args[0] if not self.is_configchannel(source_name): logging.warning( "invalid configchannel "+source_name ) return logging.debug( "source: " + str(source_name) ) target_name = self.get_next_stage_name( source_name ) logging.debug( "target: " + str(target_name) ) if not target_name: return # check target name if not self.is_configchannel(target_name): logging.debug( "a next stage configchannel for "+source_name+" ("+target_name+") does not exist" ) return return target_name #################### # # kickstart # # kickstart next def help_stage_kickstart_next(self): print 'stage_kickstart_next: get kickstart name for the next stage' print ' ' print 'usage: stage_kickstart_next CHANNEL' def complete_stage_kickstart_next(self, text, line, beg, end): parts = shlex.split(line) if line[-1] == ' ': parts.append('') args = len(parts) if args == 2: return tab_completer(self.do_kickstart_list('', True), text) return [] def do_stage_kickstart_next(self, args): (args, options) = parse_arguments(args) if len(args) != 1: self.help_stage_kickstart_next() return source_name = args[0] if not self.is_kickstart(source_name): logging.warning( "invalid kickstart "+source_name ) return logging.debug( "source: " + str(source_name) ) target_name = self.get_next_stage_name( source_name ) logging.debug( "target: " + str(target_name) ) if not target_name: return # check target name if not self.is_kickstart(target_name): logging.debug( "a next stage kickstart for "+source_name+" ("+target_name+") does not exist" ) return return target_name #################### # # activationkey # # activationkey next def help_stage_activationkey_next(self): print 'stage_activationkey_next: get activationkey name for the next stage' print ' ' print 'usage: stage_activationkey_next CHANNEL' def complete_stage_activationkey_next(self, text, line, beg, end): parts = shlex.split(line) if line[-1] == ' ': parts.append('') args = len(parts) if args == 2: return tab_completer(self.do_activationkey_list('', True), text) return [] def do_stage_activationkey_next(self, args): (args, options) = parse_arguments(args) if len(args) != 1: self.help_stage_activationkey_next() return source_name = args[0] if not self.is_activationkey(source_name): logging.warning( "invalid activationkey "+source_name ) return logging.debug( "source: " + str(source_name) ) target_name = self.get_next_stage_name( source_name ) logging.debug( "target: " + str(target_name) ) if not target_name: return # check target name if not self.is_activationkey(target_name): logging.debug( "a next stage activationkey for "+source_name+" ("+target_name+") does not exist" ) return return target_name #################### # # stage_status # stage_*_status # def help_stage_status(self): print 'stage_status: status of a stage' print '' print 'usage: stage_status STAGE\n' print 'STAGE: ' + " | ".join( _STAGES ) def complete_stage_status(self, text, line, beg, end): parts = shlex.split(line) if line[-1] == ' ': parts.append('') args = len(parts) if args == 2: return tab_completer( _STAGES, text) return [] def do_stage_status(self, args): (args, options) = parse_arguments(args) if not len(args): self.help_stage_status() return stage = args[0] if not self.check_stage( stage ): return self.stage = stage self.stage_softwarechannels_status() self.stage_configchannels_status() self.stage_kickstarts_status() self.stage_activationkeys_status() def stage_softwarechannels_status( self ): logging.info( "softwarechannel" ) base_channels = self.list_base_channels() for base_channel in base_channels: if self.is_current_stage( base_channel ): self.check_stage_softwarechannel_status( base_channel, indent="" ) for child_channel in self.get_softwarechannel_childchannel( base_channel ): self.check_stage_softwarechannel_status( child_channel, indent=" " ) def check_stage_softwarechannel_status( self, name, indent="" ): status="unknown" name_next = self.do_stage_softwarechannel_next( name ) if name_next: if self.do_softwarechannel_diff( name + " " + name_next ): status="modified" else: status="uptodate" else: status="dontexist" print_stage_status( self, name, name_next=name_next, status=status, indent=indent ) return status def stage_configchannels_status( self ): logging.info( "configchannel" ) configchannels = self.do_configchannel_list('', True) for name in configchannels: if self.is_current_stage( name ): self.check_stage_configchannels_status( name ) def check_stage_configchannels_status( self, name, indent="" ): status="unknown" name_next = self.do_stage_configchannel_next( name ) if name_next: if self.do_configchannel_diff( name + " " + name_next ): status="modified" else: status="uptodate" else: status="dontexist" print_stage_status( self, name, name_next=name_next, status=status, indent=indent ) return status def stage_kickstarts_status( self ): logging.info( "kickstart" ) kickstarts = self.do_kickstart_list('', True) for name in kickstarts: if self.is_current_stage( name ): self.check_stage_kickstarts_status( name ) def check_stage_kickstarts_status( self, name, indent="" ): status="unknown" name_next = self.do_stage_kickstart_next( name ) if name_next: if self.do_kickstart_diff( name + " " + name_next ): status="modified" else: status="uptodate" else: status="dontexist" print_stage_status( self, name, name_next=name_next, status=status, indent=indent ) return status def stage_activationkeys_status( self ): logging.info( "activationkey" ) activationkeys = self.do_activationkey_list('', True) for name in activationkeys: if self.is_current_stage( name ): self.check_stage_activationkey_status( name ) def check_stage_activationkey_status( self, name, indent="" ): status="unknown" name_next = self.do_stage_activationkey_next( name ) if name_next: if self.do_activationkey_diff( name + " " + name_next ): status="modified" else: status="uptodate" else: status="dontexist" print_stage_status( self, name, name_next=name_next, status=status, indent=indent ) return status #################### # # stage_dump # dump_* # def help_stage_dump(self): print 'stage_dump: dump infos about a stage to files' print '' print 'usage: stage_dump STAGE [OUTDIR]\n' print 'STAGE: ' + " | ".join( _STAGES ) print 'OUTDIR defaults to ' + _DUMP_BASE_DIR def complete_stage_dump(self, text, line, beg, end): parts = shlex.split(line) if line[-1] == ' ': parts.append('') args = len(parts) if args == 2: return tab_completer( _STAGES, text) return [] def do_stage_dump(self, args): (args, options) = parse_arguments(args) if not len(args): self.help_stage_dump() return stage = args[0] if not self.check_stage( stage ): return self.stage = stage if len(args) == 2: outputpath_base = datetime.now().strftime(os.path.expanduser(args[1])) else: # make the final output path be /date/channel outputpath_base = os.path.join( _DUMP_BASE_DIR, datetime.now().strftime("%Y-%m-%d"), stage ) if not self.mkdir( outputpath_base ): return self.dump_softwarechannels( outputpath_base + "/softwarechannel/" ) self.dump_configchannels( outputpath_base + "/configchannel/" ) self.dump_kickstarts( outputpath_base + "/kickstart/" ) self.dump_activationkeys( outputpath_base + "/activationkey/" ) def dump_softwarechannels(self, basedir): logging.info( "softwarechannel" ) base_channels = self.list_base_channels() for base_channel in base_channels: if self.is_current_stage( base_channel ): logging.info( " " + base_channel ) base_channel_dir = basedir + self.get_common_name(base_channel) if not self.mkdir( base_channel_dir ): return packages = self.do_softwarechannel_listallpackages( base_channel, doreturn=True ) self.dump( base_channel_dir + '/' + self.get_common_name(base_channel), packages ) # get all child channels and pick the channels that belongs to the base channel for child_channel in self.get_softwarechannel_childchannel( base_channel ): logging.info( " " + child_channel ) packages = self.dump_softwarechannel( child_channel ) self.dump( base_channel_dir + '/' + self.get_common_name(child_channel), packages ) def dump_configchannels(self, basedir): logging.info( "configchannel" ) configchannels = self.do_configchannel_list( '', doreturn = True) for name in configchannels: if self.is_current_stage( name ): logging.info( " " + name ) directory = basedir + self.get_common_name(name) self.do_configchannel_backup( name+" "+directory ) def dump_kickstarts(self, basedir): logging.info( "kickstart" ) kickstarts = self.client.kickstart.listKickstarts(self.session) for kickstart in kickstarts: name = kickstart.get('name') if self.is_current_stage( name ): logging.info( " " + name ) dir = basedir + self.get_common_name(name) content = self.dump_kickstart( name ) # dump kickstart details and ks file content. # use separate files self.dump( dir + '/' + self.get_common_name(name), content ) #self.dump( dir + '/' + self.get_common_name(name) + ".content", dump_kickstart_content(self, name) ) def dump_activationkeys(self, basedir): logging.info( "activationkey" ) activationkeys = self.do_activationkey_list('', True) for name in activationkeys: if self.is_current_stage( name ): logging.info( " " + name ) content = self.dump_activationkey( name ) dir = basedir + self.get_common_name(name) self.dump( dir + '/' + self.get_common_name(name), content ) # vim:ts=4:expandtab: