source: people/joerg.steffens/technical/spacecmd/stage.py @ 1019

Last change on this file since 1019 was 1019, checked in by joergs, 11 years ago

current staging script, before moving to official project

File size: 45.8 KB
Line 
1#
2# Licensed under the GNU General Public License Version 3
3#
4# This program is free software; you can redistribute it and/or modify
5# it under the terms of the GNU General Public License as published by
6# the Free Software Foundation; either version 3 of the License, or
7# (at your option) any later version.
8#
9# This program is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12# GNU General Public License for more details.
13#
14# You should have received a copy of the GNU General Public License
15# along with this program; if not, write to the Free Software
16# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
17#
18# Copyright 2011,2012 Joerg Steffens <joerg.steffens@dass-it.de>
19#
20
21# NOTE: the 'self' variable is an instance of SpacewalkShell
22
23import shlex
24from optparse import Option
25from pprint import pprint
26import sys
27from StringIO import StringIO
28from tempfile import mkdtemp
29import difflib
30from spacecmd.utils import *
31
32_STAGE1='dev'
33_STAGE2='stg'
34_STAGE3='prd'
35
36_STAGES=[ _STAGE1, _STAGE2, _STAGE3 ]
37
38_STAGE_NAMES={
39    'dev': 'Development',
40    # alternative
41    'qas': 'QualityAssurance',
42    'stg': 'Staging',
43    'prd': 'Production'
44}
45_STAGE_TRANSITIONS={
46    _STAGE1: _STAGE2,
47    _STAGE2: _STAGE3
48}
49_STAGE_STATUS={
50    "uptodate":  " ",
51    "modified":  "M",
52    "dontexist": "!",
53    "unknown":   "?"
54}
55# when normalizing texts, exclude lines that start with following keywords,
56# because they result in differences
57# that are not relevant for our staging diffs
58_STAGE_TEXT_EXCLUDES=[
59    # configchannel -> file details
60    "Revision: ", "Created: ", "Modified: ",
61    # kickstart
62    "Org Default: ",
63    # activation key
64    "Universal Default: ",
65]
66
67_DUMP_BASE_DIR="/tmp/spacecmd-stage-dump/"
68
69####################
70
71def do_stage_help( self, args, doreturn = False ):
72    print """
73Staging:
74
75The basic principle is to have every component in multiple stages.
76The stages in this environment are:"""
77    for stage in _STAGES:
78        successor=self.get_next_stage(stage)
79        print "    " + stage + ":" , _STAGE_NAMES.get(stage)
80
81    print """
82A stage can have a successor, in our enviroment these are:"""
83    for stage in _STAGES:
84        successor=self.get_next_stage(stage)
85        if successor:
86            print "    " + stage, "->" , successor
87
88    print """
89Workflow example:
90  * creating a new package/package version
91    the new package is added to a {stage1} softwarechannel.
92    If the package seams to work correctly,
93    a new integration phase can be started.
94    For this, the packages are copied from {stage1} to {stage2}.
95    The {stage2} stage is then tested.
96    After a successful test, all content of {stage2} is transfered to {stage3}.
97    When the content has arrived in {stage3},
98    all productively used systems are able to update to the new content.
99
100Summary:
101  {stage1}: all changes are done to {stage1}
102  {stage2}: integration tests are done in {stage2} only
103  {stage3}: productively used systems using {stage3} only
104
105Changes are not only adding new packages,
106but also changing configuration files in the configuration channels,
107changing kickstart settings or changing activation keys.
108
109For all these changes, spacecmd stage_* commands offers functionality
110to simplify staging.
111
112Usage:
113  * create your channels, actionvationkey and so on.
114    Because Spacewalk does not know about staging directly,
115    staging information must be coded into the name of the components.
116    The name must include the stage, separeted by '-',
117    eg. centos6-x86_64-{stage1}, centos6-x86_64-{stage1}-subchannel, ks-centos6-x86_64-{stage1}-common, ...
118    To create a initial structure, the comamnd 'stage_create_skel' can be used.
119
120  * check the staging status by 'stage_status STAGE'
121    This will select all components from stage 'STAGE' and compare each component with the correcponding component from the successor stage, eg.:
122    'stage_status {stage1}'
123    INFO: softwarechannel
124      centos6-x86_64-{stage1}                               -> centos6-x86_64-{stage2}
125    M   centos6-x86_64-{stage1}-app1                        ->   centos6-x86_64-{stage2}-app1
126    !   centos6-x86_64-{stage1}-app2
127    INFO: configchannel
128      cfg-centos6-x86_64-{stage1}-app1                      -> cfg-centos6-x86_64-{stage2}-app1
129    INFO: kickstart
130    M ks-centos6-x86_64-{stage1}-app1                       -> ks-centos6-x86_64-{stage2}-app1
131    INFO: activationkey
132      1-centos6-x86_64-{stage1}-app1                        -> 1-centos6-x86_64-{stage2}-app1
133
134    This first column indicates the state:
135      : empty: no differences. The components from both stages are indentical
136    ! : no correcponding component in successor stage found
137    M : modification. The component differs between the current and the successor stage
138
139  * The most interessting entries are the modified entires.
140    To check this more specifically, use the corresponding 'stage_*_diff' function, eg.
141    'stage_softwarechannel_diff centos7-x86_64-{stage1}-app1'
142    --- centos6-x86_64-{stage1}-app1
143
144    +++ centos6-x86_64-{stage2}-app1
145
146    @@ -1,1 +1,0 @@
147
148    -newpackage-1.0.1-1.1.noarch
149
150    (it is also possible to compare two specific subchannel, eg.
151    'stage_softwarechannel_diff centos6-x86_64-{stage1}-subchannel1 centos6-x86_64-{stage2}-subchannel1'
152    but the corresponding successor stage component is found automatically by its name)
153
154  * Softwarechannel and configchannel also offers the stage_*_sync function.
155    Use them, to copy the content of a component to the next stage, e.g.
156    'stage_softwarechannel_sync centos6-x86_64-{stage1}-app1'
157    INFO: syncing packages from softwarechannel centos6-x86_64-{stage1}-app1 to centos6-x86_64-{stage2}-app1
158    packages to add to channel "centos6-x86_64-{stage2}-app1":
159    newpackage-1.0.1-1.1.noarch
160    Perform these changes to channel centos6-x86_64-{stage2}-app1 [y/N]:
161
162  * Repeat these steps, until 'stage_status STAGE' shows no differences between the two stages
163    """.format(stage1=_STAGE1, stage2=_STAGE2, stage3=_STAGE3)
164
165def help_stage_create_skel(self):
166    print 'stage_create_skel: create initial staging structure'
167    print '''usage: stage_create_skel [options]
168
169options:
170  -l LABEL
171  -a ARCHITECTURE ['ia32', 'x86_64']
172  -s SUB (e.g. application1)'''
173
174def do_stage_create_skel(self, args, doreturn = False):
175    options = [
176                Option('-l', '--label', action='store'),
177                Option('-a', '--arch',  action='store'),
178                Option('-s', '--sub',  action='store'),
179               ]
180
181    (args, options) = parse_arguments(args, options)
182
183    if is_interactive(options):
184        options.label = prompt_user('Channel Label:', noblank = True)
185
186        print
187        print 'Architecture'
188        print '------------'
189        print '\n'.join(sorted(self.ARCH_LABELS))
190        print
191        options.arch = prompt_user('Select:')
192        options.arch = prompt_user('Sub:')
193    else:
194        if not options.label:
195            logging.error('A channel label is required')
196            return
197
198        if not options.arch:
199            logging.error('An architecture is required')
200            return
201
202    dist        = options.label
203    arch        = options.arch
204    if self.stage_create_skel( options.label, options.arch, options.sub, create=False ):
205        self.stage_create_skel( options.label, options.arch, options.sub, create=True )
206
207
208def stage_create_skel(self, dist, arch, sub, create = False):
209
210    org         = "1"
211    disttype    = "rhel_6"
212    application = sub
213
214    print
215    for stage in _STAGES:
216        base                 = dist + "-" + arch + "-" + stage
217        softwarechannel_base = base
218        softwarechannel_sub  = base + "-" + application
219        distribution         = "dist-"   + base
220        distributionpath     = "/srv/dist/" + base
221        configchannel        = "cfg-"    + base + "-" + application
222        activationkey_create = base + "-" + application
223        activationkey        = org + "-" + activationkey_create
224        kickstart            = "ks-"     + base + "-" + application
225
226        print "stage: " + stage
227
228        print "softwarechannel base:       " + softwarechannel_base,
229        if self.is_softwarechannel( softwarechannel_base ):
230            print " [exists]",
231        elif create:
232            self.do_softwarechannel_create( "-n " + softwarechannel_base + " -l " + softwarechannel_base + " -a " + arch )
233        print
234
235        print "softwarechannel subchannel: " + softwarechannel_sub,
236        if self.is_softwarechannel( softwarechannel_sub ):
237            print " [exists]",
238        elif create:
239            self.do_softwarechannel_create( "-n " + softwarechannel_sub  + " -l " + softwarechannel_sub  + " -a " + arch + " -p " + base )
240        print
241
242
243        print "distribution:               " + distribution + " (distribution path: " + distributionpath + ")",
244        if distribution in self.do_distribution_list(distribution, True):
245            print " [exists]",
246        elif create:
247            self.do_distribution_create( "--name " + distribution + " --path " + distributionpath + " --base-channel " + base + " --install-type rhel_6" )
248        print
249
250        print "configchannel:              " + configchannel,
251        if self.is_configchannel( configchannel ):
252            print " [exists]",
253        elif create:
254            self.do_configchannel_create( "-n " + configchannel )
255        print
256
257        print "activationkey:              " + activationkey,
258        if self.is_activationkey( activationkey ):
259            print " [exists]",
260        elif create:
261            self.do_activationkey_create( "-n " + activationkey_create + " -d " + activationkey + " -b " + base + " -e provisioning_entitled" )
262            self.do_activationkey_addchildchannels( activationkey + " " + softwarechannel_sub )
263            self.do_activationkey_enableconfigdeployment( activationkey )
264            self.do_activationkey_addconfigchannels( activationkey + " " + configchannel + " -t" )
265        print
266
267        print "kickstart:                  " + kickstart,
268        if self.is_kickstart( kickstart ):
269            print " [exists]",
270        elif create:
271            self.do_kickstart_create( "--name=" + kickstart + " --distribution=" + distribution + " --root-password=CHANGEME --virt-type=none" )
272            self.do_kickstart_addactivationkeys( kickstart + " " + activationkey )
273            self.do_kickstart_enableconfigmanagement( kickstart )
274            self.do_kickstart_enablelogging( kickstart )
275        print
276
277        print
278
279    if not create:
280        print "Make sure, distribution trees are available at the specified distribution paths."
281        return self.user_confirm('Create this components [y/N]:')
282
283
284
285####################
286
287#
288# helper functions
289#
290
291def is_stage( self, name ):
292    return name in _STAGES
293
294def check_stage( self, name ):
295    """Checks if name describes a vaild stage"""
296    if not name:
297        logging.error( "no stage given" )
298        return False
299    if not self.is_stage( name ):
300        logging.error( "invalid stage " + name )
301        return False
302    return True
303
304def is_current_stage(self, name):
305    return "-"+self.stage in name
306
307def get_common_name( self, name ):
308    """Returns the name with the stage replaced by 'STAGE'
309
310    To check the differences from 2 components that are in different stages,
311    the specific stage is replaced by the word 'STAGE'
312    """
313    return self.replace_stage_in_name( name, self.stage, "STAGE" )
314
315
316def get_stage_from_name( self, name ):
317    for i in _STAGES:
318        if "-"+i in name:
319            return i
320
321def get_next_stage( self, current_stage ):
322    return _STAGE_TRANSITIONS.get(current_stage)
323
324def replace_stage_in_name( self, name, current_stage, new_stage ):
325    """Return the name with current stage replaced by new stage"""
326    return name.replace( "-"+current_stage, "-"+new_stage )
327
328def get_next_stage_name( self, name ):
329    current_stage = self.get_stage_from_name( name )
330    if not current_stage: return
331    next_stage = self.get_next_stage( current_stage )
332    if not next_stage: return
333    next_stage_name = self.replace_stage_in_name( name, current_stage, next_stage )
334    return next_stage_name
335
336def get_normalized_text( self, text, stage, excludes=_STAGE_TEXT_EXCLUDES ):
337    """Replace all occurances of stage by the word 'STAGE'"""
338    normalized_text = []
339    for line in text:
340        if not line.startswith( tuple(excludes) ):
341            normalized_text.append( self.replace_stage_in_name( line, stage, "STAGE" ) )
342    return normalized_text
343
344def print_stage_status( self, name, name_next=None, status="unknown", indent="" ):
345    width=48-len(indent)
346    string = '{status_code} {indent}{name:{width}}'.format(status_code=_STAGE_STATUS.get(status), indent=indent, name=name, width=width )
347    if name_next:
348        string = string + " -> " + indent + name_next
349    print string
350
351def mkdir(self, name ):
352    try:
353        if not os.path.isdir( name ):
354            os.makedirs( name )
355            logging.debug( "creating directory " + name )
356        return True
357    except:
358        logging.error('Failed to create directory ' + name )
359        return False
360
361def dump(self, filename, data, raw=False):
362    """Writes data to filename"""
363    if not self.mkdir( os.path.dirname( filename )): return False
364    try:
365        fh = open( filename, 'w' )
366        if( raw ):
367            fh.write(data)
368        else:
369            fh.write("\n".join(data))
370        fh.close()
371    except:
372        logging.error('failed to create file ' + filename )
373        return False
374
375
376####################
377
378#
379# softwarechannel
380#
381
382# softwarechannel helper
383
384def is_softwarechannel( self, name ):
385    if not name: return
386    return name in self.do_softwarechannel_list( name, True )
387
388def check_softwarechannel( self, name ):
389    if not name:
390        logging.error( "no softwarechannel label given" )
391        return False
392    if not self.is_softwarechannel( name ):
393        logging.error( "invalid softwarechannel label " + name )
394        return False
395    return True
396
397def get_softwarechannel_childchannel( self, base_channel ):
398    result=[]
399    for child_channel in self.list_child_channels():
400        details = self.client.channel.software.getDetails(\
401                            self.session, child_channel)
402        if details.get('parent_channel_label') == base_channel:
403            result.append( child_channel )
404    return result
405
406
407# softwarechannel next
408
409def help_stage_softwarechannel_next(self):
410    print 'stage_softwarechannel_next: get softwarechannel name for the next stage'
411    print '                       '
412    print 'usage: stage_softwarechannel_next CHANNEL'
413
414def complete_stage_softwarechannel_next(self, text, line, beg, end):
415    parts = shlex.split(line)
416    if line[-1] == ' ': parts.append('')
417    args = len(parts)
418
419    if args == 2:
420        return tab_completer(self.do_softwarechannel_list('', True), text)
421    return []
422
423def do_stage_softwarechannel_next(self, args, doreturn = False):
424    (args, options) = parse_arguments(args)
425
426    if len(args) != 1:
427        self.help_stage_softwarechannel_next()
428        return
429
430    source_name = args[0]
431    if not self.is_softwarechannel(source_name):
432        logging.warning( "invalid softwarechannel "+source_name )
433        return
434    logging.debug( "source: " + str(source_name) )
435    target_name = self.get_next_stage_name( source_name )
436    logging.debug( "target: " + str(target_name) )
437    if not target_name: return
438    # check target name
439    if not self.is_softwarechannel(target_name):
440        if not doreturn:
441            logging.warning( "a next stage softwarechannel for "+source_name+" ("+target_name+") does not exist" )
442        return
443
444    if doreturn:
445        return target_name
446    else:
447        print target_name
448
449
450# softwarechannel diff
451
452def help_stage_softwarechannel_diff(self):
453    print 'stage_softwarechannel_diff: diff softwarechannel files'
454    print ''
455    print 'usage: stage_softwarechannel_diff SOURCE_CHANNEL [TARGET_CHANNEL]'
456
457def complete_stage_softwarechannel_diff(self, text, line, beg, end):
458    parts = shlex.split(line)
459    if line[-1] == ' ': parts.append('')
460    args = len(parts)
461
462    if args == 2:
463        return tab_completer(self.do_softwarechannel_list('', True), text)
464    if args == 3:
465        return tab_completer(self.do_softwarechannel_list('', True), text)
466    return []
467
468def do_stage_softwarechannel_diff(self, args, doreturn = False):
469    options = []
470
471    (args, options) = parse_arguments(args, options)
472
473    if len(args) != 1 and len(args) != 2:
474        self.help_stage_softwarechannel_diff()
475        return
476
477    source_channel = args[0]
478    if not self.check_softwarechannel( source_channel ): return
479
480    if len(args) == 2:
481        target_channel = args[1]
482    else:
483        target_channel=self.do_stage_softwarechannel_next( source_channel, doreturn = True)
484    if not self.check_softwarechannel( target_channel ): return
485
486
487    source_data = self.dump_softwarechannel( source_channel, normalize=True )
488    target_data = self.dump_softwarechannel( target_channel, normalize=True )
489
490    result=[]
491
492    for line in difflib.unified_diff( source_data, target_data, source_channel, target_channel ):
493        if doreturn:
494            result.append(line)
495        else:
496            print line
497    return result
498
499
500
501# softwarechannel sync
502
503def help_stage_softwarechannel_sync(self):
504    print 'stage_softwarechannel_sync: sync the (most recent) packages'
505    print '                            from a software channel to another'
506    print 'usage: stage_softwarechannel_sync SOURCE_CHANNEL [TARGET_CHANNEL]'
507
508def complete_stage_softwarechannel_sync(self, text, line, beg, end):
509    parts = shlex.split(line)
510    if line[-1] == ' ': parts.append('')
511    args = len(parts)
512
513
514    if args == 2:
515        return tab_completer(self.do_softwarechannel_list('', True), text)
516    if args == 3:
517        return tab_completer(self.do_softwarechannel_list('', True), text)
518    return []
519
520def do_stage_softwarechannel_sync(self, args):
521    options = []
522
523    (args, options) = parse_arguments(args, options)
524
525    if len(args) != 1 and len(args) != 2:
526        self.help_stage_softwarechannel_sync()
527        return
528
529    source_channel = args[0]
530    if len(args) == 2:
531        target_channel = args[1]
532    else:
533        target_channel=self.do_stage_softwarechannel_next( source_channel, doreturn = True)
534
535    logging.info( "syncing packages from softwarechannel "+source_channel+" to "+target_channel )
536
537    # use API call instead of spacecmd function
538    # to get detailed infos about the packages
539    # and not just there names
540    source_packages = self.client.channel.software.listAllPackages(self.session,
541                                                               source_channel)
542    target_packages = self.client.channel.software.listAllPackages(self.session,
543        target_channel)
544
545
546    # get the package IDs
547    source_package_ids = set()
548    for package in source_packages:
549        try:
550            source_package_ids.add(package['id'])
551        except KeyError:
552            logging.error( "failed to read key id" )
553            continue
554
555    target_package_ids = set()
556    for package in target_packages:
557        try:
558            target_package_ids.add(package['id'])
559        except KeyError:
560            logging.error( "failed to read key id" )
561            continue
562
563    print "packages common in both channels:"
564    for i in ( source_package_ids & target_package_ids ):
565        print self.get_package_name( i )
566    print
567
568    source_only = source_package_ids.difference(target_package_ids)
569    if source_only:
570        print 'packages to add to channel "' + target_channel + '":'
571        for i in source_only:
572            print self.get_package_name( i )
573        print
574
575
576    # check for packages only in target
577    target_only=target_package_ids.difference( source_package_ids )
578    if target_only:
579        print 'packages to remove from channel "' + target_channel + '":'
580        for i in target_only:
581            print self.get_package_name( i )
582        print
583
584    if source_only or target_only:
585        if not self.user_confirm('Perform these changes to channel ' + target_channel + ' [y/N]:'): return
586
587        self.client.channel.software.addPackages(self.session,
588                                                target_channel,
589                                                list(source_only) )
590        self.client.channel.software.removePackages(self.session,
591                                                target_channel,
592                                                list(target_only) )
593
594####################
595
596#
597# configchannel
598#
599
600# configchannel helper
601
602def is_configchannel( self, name ):
603    if not name: return
604    return name in self.do_configchannel_list( name, True )
605
606def check_configchannel( self, name ):
607    if not name:
608        logging.error( "no configchannel given" )
609        return False
610    if not self.is_configchannel( name ):
611        logging.error( "invalid configchannel label " + name )
612        return False
613    return True
614
615
616# configchannel next
617
618def help_stage_configchannel_next(self):
619    print 'stage_configchannel_next: get configchannel name for the next stage'
620    print '                       '
621    print 'usage: stage_configchannel_next CHANNEL'
622
623def complete_stage_configchannel_next(self, text, line, beg, end):
624    parts = shlex.split(line)
625    if line[-1] == ' ': parts.append('')
626    args = len(parts)
627
628    if args == 2:
629        return tab_completer(self.do_configchannel_list('', True), text)
630    return []
631
632def do_stage_configchannel_next(self, args, doreturn = False):
633    (args, options) = parse_arguments(args)
634
635    if len(args) != 1:
636        self.help_stage_configchannel_next()
637        return
638
639    source_name = args[0]
640    if not self.is_configchannel(source_name):
641        logging.warning( "invalid configchannel "+source_name )
642        return
643    logging.debug( "source: " + str(source_name) )
644    target_name = self.get_next_stage_name( source_name )
645    logging.debug( "target: " + str(target_name) )
646    if not target_name: return
647    # check target name
648    if not self.is_configchannel(target_name):
649        if not doreturn:
650            logging.warning( "a next stage configchannel for "+source_name+" ("+target_name+") does not exist" )
651        return
652
653    if doreturn:
654        return target_name
655    else:
656        print target_name
657
658
659# configchannel diff
660
661def help_stage_configchannel_diff(self):
662    print 'stage_configchannel_diff: diff between config channels'
663    print '                          '
664    print 'usage: stage_configchannel_diff SOURCE_CHANNEL [TARGET_CHANNEL]'
665
666def complete_stage_configchannel_diff(self, text, line, beg, end):
667    parts = shlex.split(line)
668    if line[-1] == ' ': parts.append('')
669    args = len(parts)
670
671    if args == 2:
672        return tab_completer(self.do_configchannel_list('', True), text)
673    if args == 3:
674        return tab_completer(self.do_configchannel_list('', True), text)
675    return []
676
677def do_stage_configchannel_diff(self, args, doreturn = False):
678    options = []
679
680    (args, options) = parse_arguments(args, options)
681
682    if len(args) != 1 and len(args) != 2:
683        self.help_stage_configchannel_diff()
684        return
685
686    source_channel = args[0]
687    if not self.check_configchannel( source_channel ): return
688
689    if len(args) == 2:
690        target_channel = args[1]
691    else:
692        target_channel=self.do_stage_configchannel_next( source_channel, doreturn = True)
693    if not self.check_configchannel( target_channel ): return
694
695    source_data = self.dump_configchannel( source_channel, normalize=True )
696    target_data = self.dump_configchannel( target_channel, normalize=True )
697
698    result=[]
699    for line in difflib.unified_diff( source_data, target_data, source_channel, target_channel ):
700        if doreturn:
701            result.append(line)
702        else:
703            print line
704    return result
705
706
707# configchannel sync
708
709def help_stage_configchannel_sync(self):
710    print 'stage_configchannel_sync: sync config files'
711    print '                          from a config channel to another'
712    print 'usage: stage_configchannel_sync SOURCE_CHANNEL [TARGET_CHANNEL]'
713
714def complete_stage_configchannel_sync(self, text, line, beg, end):
715    parts = shlex.split(line)
716    if line[-1] == ' ': parts.append('')
717    args = len(parts)
718
719
720    if args == 2:
721        return tab_completer(self.do_configchannel_list('', True), text)
722    if args == 3:
723        return tab_completer(self.do_configchannel_list('', True), text)
724    return []
725
726def do_stage_configchannel_sync(self, args, doreturn = False):
727    options = []
728
729    (args, options) = parse_arguments(args, options)
730
731    if len(args) != 1 and len(args) != 2:
732        self.help_stage_configchannel_sync()
733        return
734
735    source_channel = args[0]
736    if not self.check_configchannel( source_channel ): return
737
738    if len(args) == 2:
739        target_channel = args[1]
740    else:
741        target_channel=self.do_stage_configchannel_next( source_channel, doreturn = True)
742    if not self.check_configchannel( target_channel ): return
743
744    logging.info( "syncing files from configchannel "+source_channel+" to "+target_channel )
745
746    source_files = set( self.do_configchannel_listfiles( source_channel, doreturn = True ) )
747    target_files = set( self.do_configchannel_listfiles( target_channel, doreturn = True ) )
748
749    both=source_files & target_files
750    if both:
751        print "files common in both channels:"
752        print "\n".join( both )
753        print
754
755    source_only=source_files.difference( target_files )
756    if source_only:
757        print "files only in source "+source_channel
758        print "\n".join( source_only )
759        print
760
761    target_only=target_files.difference( source_files )
762    if target_only:
763        print "files only in target "+target_channel
764        print "\n".join( target_only )
765        print
766
767    if both:
768        print "files that are in both channels will be overwritten in the target channel"
769    if source_only:
770        print "files only in the source channel will be added to the target channel"
771    if target_only:
772        print "files only in the target channel will be deleted"
773
774    if not (both or source_only or target_only):
775        logging.info( "nothing to do" )
776        return
777
778    if not self.user_confirm('perform synchronisation [y/N]:'): return
779
780    source_data_list = self.client.configchannel.lookupFileInfo(\
781                                      self.session, source_channel,
782                                      list( both  ) + list(source_only) )
783
784
785    # TODO: check if this newly available function can be used instead:
786    # self.configchannel_sync_by_backup_import()
787    for source_data in source_data_list:
788        if source_data.get('type') == 'file' or source_data.get('type') == 'directory':
789            if source_data.get('contents') and not source_data.get('binary'):
790                contents = source_data.get('contents').encode('base64')
791            else:
792                contents = source_data.get('contents')
793            target_data = {
794                'contents':                 contents,
795                'contents_enc64':           True,
796                'owner':                    source_data.get('owner'),
797                'group':                    source_data.get('group'),
798                #'permissions':              str(source_data.get('permissions')),
799                'permissions':              source_data.get('permissions_mode'),
800                'selinux_ctx':              source_data.get('selinux_ctx'),
801                'macro-start-delimiter':    source_data.get('macro-start-delimiter'),
802                'macro-end-delimiter':      source_data.get('macro-end-delimiter'),
803            }
804            for k,v in target_data.items():
805                if not v:
806                    del target_data[k]
807            logging.debug( source_data.get('path') + ": " + str(target_data) )
808            self.client.configchannel.createOrUpdatePath(self.session,
809                                                         target_channel,
810                                                         source_data.get('path'),
811                                                         source_data.get('type') == 'directory',
812                                                         target_data)
813
814        elif source_data.get('type') == 'symlink':
815            target_data = {
816                'target_path':  source_data.get('target_path'),
817                'selinux_ctx':  source_data.get('selinux_ctx'),
818            }
819            logging.debug( source_data.get('path') + ": " + str(target_data) )
820            self.client.configchannel.createOrUpdateSymlink(self.session,
821                                                            target_channel,
822                                                            source_data.get('path'),
823                                                            target_data )
824
825        else:
826            logging.warning( "unknown file type " + source_data.type )
827
828
829    # removing all files from target channel that did not exist on source channel
830    if target_only:
831        self.do_configchannel_removefiles( target_channel + " " + " ".join(target_only) )
832
833
834####################
835
836#
837# kickstart
838#
839
840# kickstart helper
841
842def is_kickstart( self, name ):
843    if not name: return
844    return name in self.do_kickstart_list( name, True )
845
846def check_kickstart( self, name ):
847    if not name:
848        logging.error( "no kickstart label given" )
849        return False
850    if not self.is_kickstart( name ):
851        logging.error( "invalid kickstart label " + name )
852        return False
853    return True
854
855
856# kickstart next
857
858def help_stage_kickstart_next(self):
859    print 'stage_kickstart_next: get kickstart name for the next stage'
860    print '                       '
861    print 'usage: stage_kickstart_next CHANNEL'
862
863def complete_stage_kickstart_next(self, text, line, beg, end):
864    parts = shlex.split(line)
865    if line[-1] == ' ': parts.append('')
866    args = len(parts)
867
868    if args == 2:
869        return tab_completer(self.do_kickstart_list('', True), text)
870    return []
871
872def do_stage_kickstart_next(self, args, doreturn = False):
873    (args, options) = parse_arguments(args)
874
875    if len(args) != 1:
876        self.help_stage_kickstart_next()
877        return
878
879    source_name = args[0]
880    if not self.is_kickstart(source_name):
881        logging.warning( "invalid kickstart "+source_name )
882        return
883    logging.debug( "source: " + str(source_name) )
884    target_name = self.get_next_stage_name( source_name )
885    logging.debug( "target: " + str(target_name) )
886    if not target_name: return
887    # check target name
888    if not self.is_kickstart(target_name):
889        if not doreturn:
890            logging.warning( "a next stage kickstart for "+source_name+" ("+target_name+") does not exist" )
891        return
892
893    if doreturn:
894        return target_name
895    else:
896        print target_name
897
898
899# kickstart diff
900
901def help_stage_kickstart_diff(self):
902    print 'stage_kickstart_diff: diff kickstart files'
903    print ''
904    print 'usage: stage_kickstart_diff SOURCE_CHANNEL [TARGET_CHANNEL]'
905
906def complete_stage_kickstart_diff(self, text, line, beg, end):
907    parts = shlex.split(line)
908    if line[-1] == ' ': parts.append('')
909    args = len(parts)
910
911    if args == 2:
912        return tab_completer(self.do_kickstart_list('', True), text)
913    if args == 3:
914        return tab_completer(self.do_kickstart_list('', True), text)
915    return []
916
917def do_stage_kickstart_diff(self, args, doreturn = False):
918    options = []
919
920    (args, options) = parse_arguments(args, options)
921
922    if len(args) != 1 and len(args) != 2:
923        self.help_stage_kickstart_diff()
924        return
925
926    source_channel = args[0]
927    if not self.check_kickstart( source_channel ): return
928
929    if len(args) == 2:
930        target_channel = args[1]
931    else:
932        target_channel=self.do_stage_kickstart_next( source_channel, doreturn = True)
933    if not self.check_kickstart( target_channel ): return
934
935
936    source_data = self.dump_kickstart( source_channel, normalize=True )
937    target_data = self.dump_kickstart( target_channel, normalize=True )
938
939    result=[]
940    for line in difflib.unified_diff( source_data, target_data, source_channel, target_channel ):
941        if doreturn:
942            result.append(line)
943        else:
944            print line
945    return result
946
947
948
949####################
950
951#
952# activationkey
953#
954
955# activationkey helper
956
957def is_activationkey( self, name ):
958    if not name: return
959    return name in self.do_activationkey_list( name, True )
960
961def check_activationkey( self, name ):
962    if not name:
963        logging.error( "no activationkey label given" )
964        return False
965    if not self.is_activationkey( name ):
966        logging.error( "invalid activationkey label " + name )
967        return False
968    return True
969
970
971# activationkey next
972
973def help_stage_activationkey_next(self):
974    print 'stage_activationkey_next: get activationkey name for the next stage'
975    print '                       '
976    print 'usage: stage_activationkey_next CHANNEL'
977
978def complete_stage_activationkey_next(self, text, line, beg, end):
979    parts = shlex.split(line)
980    if line[-1] == ' ': parts.append('')
981    args = len(parts)
982
983    if args == 2:
984        return tab_completer(self.do_activationkey_list('', True), text)
985    return []
986
987def do_stage_activationkey_next(self, args, doreturn = False):
988    (args, options) = parse_arguments(args)
989
990    if len(args) != 1:
991        self.help_stage_activationkey_next()
992        return
993
994    source_name = args[0]
995    if not self.is_activationkey(source_name):
996        logging.warning( "invalid activationkey "+source_name )
997        return
998    logging.debug( "source: " + str(source_name) )
999    target_name = self.get_next_stage_name( source_name )
1000    logging.debug( "target: " + str(target_name) )
1001    if not target_name: return
1002    # check target name
1003    if not self.is_activationkey(target_name):
1004        if not doreturn:
1005            logging.warning( "a next stage activationkey for "+source_name+" ("+target_name+") does not exist" )
1006        return
1007
1008    if doreturn:
1009        return target_name
1010    else:
1011        print target_name
1012
1013
1014# activationkey diff
1015
1016def help_stage_activationkey_diff(self):
1017    print 'stage_activationkeyt_diff: diff activationkeys'
1018    print ''
1019    print 'usage: stage_activationkey_diff SOURCE_ACTIVATIONKEY [TARGET_ACTIVATIONKEY]'
1020
1021def complete_stage_activationkey_diff(self, text, line, beg, end):
1022    parts = shlex.split(line)
1023    if line[-1] == ' ': parts.append('')
1024    args = len(parts)
1025
1026
1027    if args == 2:
1028        return tab_completer(self.do_activationkey_list('', True), text)
1029    if args == 3:
1030        return tab_completer(self.do_activationkey_list('', True), text)
1031    return []
1032
1033def do_stage_activationkey_diff(self, args, doreturn = False):
1034    options = []
1035
1036    (args, options) = parse_arguments(args, options)
1037
1038    if len(args) != 1 and len(args) != 2:
1039        self.help_stage_activationkey_diff()
1040        return
1041
1042    source_channel = args[0]
1043    if not self.check_activationkey( source_channel ): return
1044
1045    if len(args) == 2:
1046        target_channel = args[1]
1047    else:
1048        target_channel=self.do_stage_activationkey_next( source_channel, doreturn = True)
1049    if not self.check_activationkey( target_channel ): return
1050
1051
1052    source_data = self.dump_activationkey( source_channel, normalize=True )
1053    target_data = self.dump_activationkey( target_channel, normalize=True )
1054
1055    result=[]
1056    for line in difflib.unified_diff( source_data, target_data, source_channel, target_channel ):
1057        if doreturn:
1058            result.append(line)
1059        else:
1060            print line
1061    return result
1062
1063
1064
1065
1066####################
1067
1068#
1069# stage_status
1070# stage_*_status
1071#
1072
1073def help_stage_status(self):
1074    print 'stage_status: status of a stage'
1075    print ''
1076    print 'usage: stage_status STAGE\n'
1077    print 'STAGE: ' + " | ".join( _STAGES )
1078
1079def complete_stage_status(self, text, line, beg, end):
1080    parts = shlex.split(line)
1081    if line[-1] == ' ': parts.append('')
1082    args = len(parts)
1083
1084
1085    if args == 2:
1086        return tab_completer( _STAGES, text)
1087
1088    return []
1089
1090def do_stage_status(self, args):
1091    (args, options) = parse_arguments(args)
1092
1093    if not len(args):
1094        self.help_stage_status()
1095        return
1096
1097    stage = args[0]
1098    if not self.check_stage( stage ): return
1099    self.stage = stage
1100
1101    self.stage_softwarechannels_status()
1102    self.stage_configchannels_status()
1103    self.stage_kickstarts_status()
1104    self.stage_activationkeys_status()
1105
1106def stage_softwarechannels_status( self ):
1107    logging.info( "softwarechannel" )
1108    base_channels = self.list_base_channels()
1109    for base_channel in base_channels:
1110        if self.is_current_stage( base_channel ):
1111            self.check_stage_softwarechannel_status( base_channel, indent="" )
1112            for child_channel in self.get_softwarechannel_childchannel( base_channel ):
1113                self.check_stage_softwarechannel_status( child_channel, indent="  " )
1114
1115def check_stage_softwarechannel_status( self, name, indent="" ):
1116    status="unknown"
1117    name_next = self.do_stage_softwarechannel_next( name, doreturn=True )
1118    if name_next:
1119        if self.do_stage_softwarechannel_diff( name + " " + name_next, doreturn=True ):
1120            status="modified"
1121        else:
1122            status="uptodate"
1123    else:
1124        status="dontexist"
1125    print_stage_status( self, name, name_next=name_next, status=status, indent=indent )
1126    return status
1127
1128
1129def stage_configchannels_status( self ):
1130    logging.info( "configchannel" )
1131    configchannels = self.do_configchannel_list('', True)
1132
1133    for name in configchannels:
1134        if self.is_current_stage( name ):
1135            self.check_stage_configchannels_status( name )
1136
1137def check_stage_configchannels_status( self, name, indent="" ):
1138    status="unknown"
1139    name_next = self.do_stage_configchannel_next( name, doreturn=True )
1140    if name_next:
1141        if self.do_stage_configchannel_diff( name + " " + name_next, doreturn=True ):
1142            status="modified"
1143        else:
1144            status="uptodate"
1145    else:
1146        status="dontexist"
1147    print_stage_status( self, name, name_next=name_next, status=status, indent=indent )
1148    return status
1149
1150
1151
1152def stage_kickstarts_status( self ):
1153    logging.info( "kickstart" )
1154    kickstarts = self.do_kickstart_list('', True)
1155
1156    for name in kickstarts:
1157        if self.is_current_stage( name ):
1158            self.check_stage_kickstarts_status( name )
1159
1160def check_stage_kickstarts_status( self, name, indent="" ):
1161    status="unknown"
1162    name_next = self.do_stage_kickstart_next( name, doreturn=True )
1163    if name_next:
1164        if self.do_stage_kickstart_diff( name + " " + name_next, doreturn=True ):
1165            status="modified"
1166        else:
1167            status="uptodate"
1168    else:
1169        status="dontexist"
1170    print_stage_status( self, name, name_next=name_next, status=status, indent=indent )
1171    return status
1172
1173
1174
1175def stage_activationkeys_status( self ):
1176    logging.info( "activationkey" )
1177    activationkeys = self.do_activationkey_list('', True)
1178
1179    for name in activationkeys:
1180        if self.is_current_stage( name ):
1181            self.check_stage_activationkey_status( name )
1182
1183def check_stage_activationkey_status( self, name, indent="" ):
1184    status="unknown"
1185    name_next = self.do_stage_activationkey_next( name, doreturn=True )
1186    if name_next:
1187        if self.do_stage_activationkey_diff( name + " " + name_next, doreturn=True ):
1188            status="modified"
1189        else:
1190            status="uptodate"
1191    else:
1192        status="dontexist"
1193    print_stage_status( self, name, name_next=name_next, status=status, indent=indent )
1194    return status
1195
1196
1197
1198####################
1199
1200#
1201# stage_dump
1202# dump_*
1203#
1204
1205def help_stage_dump(self):
1206    print 'stage_dump: dump infos about a stage to files'
1207    print ''
1208    print 'usage: stage_dump STAGE [OUTDIR]\n'
1209    print 'STAGE: ' + " | ".join( _STAGES )
1210    print 'OUTDIR defaults to ' + _DUMP_BASE_DIR
1211
1212def complete_stage_dump(self, text, line, beg, end):
1213    parts = shlex.split(line)
1214    if line[-1] == ' ': parts.append('')
1215    args = len(parts)
1216
1217
1218    if args == 2:
1219        return tab_completer( _STAGES, text)
1220
1221    return []
1222
1223def do_stage_dump(self, args):
1224    (args, options) = parse_arguments(args)
1225
1226    if not len(args):
1227        self.help_stage_dump()
1228        return
1229
1230    stage = args[0]
1231    if not self.check_stage( stage ): return
1232    self.stage = stage
1233
1234
1235    if len(args) == 2:
1236        outputpath_base = datetime.now().strftime(os.path.expanduser(args[1]))
1237    else:
1238        # make the final output path be <base>/date/channel
1239        outputpath_base = os.path.join( _DUMP_BASE_DIR,
1240                                        datetime.now().strftime("%Y-%m-%d"),
1241                                        stage )
1242
1243    if not self.mkdir( outputpath_base ): return
1244
1245    self.dump_softwarechannels( outputpath_base + "/softwarechannel/" )
1246    self.dump_configchannels( outputpath_base + "/configchannel/" )
1247    self.dump_kickstarts( outputpath_base + "/kickstart/" )
1248    self.dump_activationkeys( outputpath_base + "/activationkey/" )
1249
1250
1251
1252
1253def dump_softwarechannels(self, basedir):
1254    logging.info( "softwarechannel" )
1255    base_channels = self.list_base_channels()
1256    for base_channel in base_channels:
1257        if self.is_current_stage( base_channel ):
1258            logging.info( "  " + base_channel )
1259            base_channel_dir = basedir + self.get_common_name(base_channel)
1260            if not self.mkdir( base_channel_dir ): return
1261
1262            packages = self.do_softwarechannel_listallpackages( base_channel, doreturn=True )
1263            self.dump( base_channel_dir + '/' + self.get_common_name(base_channel), packages )
1264            # get all child channels and pick the channels that belongs to the base channel
1265            for child_channel in self.get_softwarechannel_childchannel( base_channel ):
1266                logging.info( "    " + child_channel )
1267                packages = self.dump_softwarechannel( child_channel, onlyLastestPackages=False, normalize=True )
1268                self.dump( base_channel_dir + '/' + self.get_common_name(child_channel), packages )
1269
1270def dump_softwarechannel(self, name, onlyLastestPackages=True, normalize=False):
1271    if onlyLastestPackages:
1272        content = self.do_softwarechannel_listallpackages( name, doreturn=True )
1273    else:
1274        content = self.do_softwarechannel_listallpackages( name, doreturn=True )
1275    return content
1276
1277def dump_configchannel_filedetails(self, name, filename, normalize=False):
1278    # redirect stdout to string.
1279    # to be able to reuse the existing do_activationkey_details function
1280    old_stdout=sys.stdout
1281    output=StringIO()
1282    sys.stdout=output
1283    self.do_configchannel_filedetails( name +" "+ filename )
1284    sys.stdout=old_stdout
1285    content=output.getvalue().split("\n")
1286    output.close()
1287
1288    if normalize:
1289        stage = self.get_stage_from_name( name )
1290        content=self.get_normalized_text( content, stage )
1291    return content
1292
1293def dump_configchannel(self, name, normalize=False):
1294    # redirect stdout to string.
1295    # to be able to reuse the existing do_activationkey_details function
1296    old_stdout=sys.stdout
1297    output=StringIO()
1298    sys.stdout=output
1299    self.do_configchannel_details( name )
1300    sys.stdout=old_stdout
1301    content=output.getvalue().split("\n")
1302    output.close()
1303
1304    for filename in self.do_configchannel_listfiles(name, True):
1305        content = content + self.dump_configchannel_filedetails(name, filename, normalize=True)
1306
1307    if normalize:
1308        stage = self.get_stage_from_name( name )
1309        content=self.get_normalized_text( content, stage )
1310
1311    return content
1312
1313
1314def dump_configchannels(self, basedir):
1315    logging.info( "configchannel" )
1316    configchannels = self.do_configchannel_list( '', doreturn = True)
1317
1318    for name in configchannels:
1319        if self.is_current_stage( name ):
1320            logging.info( "  " + name )
1321            dir = basedir + self.get_common_name(name)
1322            self.do_configchannel_backup( name+" "+dir )
1323
1324
1325def dump_kickstart_content(self, name, normalize=False):
1326    content = self.client.kickstart.profile.downloadRenderedKickstart(\
1327                                                self.session, name ).split("\n")
1328    stage = self.get_stage_from_name( name )
1329    if normalize:
1330        content=self.get_normalized_text( content, stage )
1331    return content
1332
1333def dump_kickstart_details(self, name, normalize=False):
1334    # redirect stdout to string.
1335    # to be able to reuse the existing do_activationkey_details function
1336    old_stdout=sys.stdout
1337    output=StringIO()
1338    sys.stdout=output
1339    self.do_kickstart_details( name )
1340    sys.stdout=old_stdout
1341    content=output.getvalue().split("\n")
1342    output.close()
1343
1344    if normalize:
1345        stage = self.get_stage_from_name( name )
1346        content=self.get_normalized_text( content, stage )
1347
1348    return content
1349
1350def dump_kickstart(self, name, normalize=False):
1351    return dump_kickstart_details(self, name, normalize=normalize)
1352
1353
1354def dump_kickstarts(self, basedir, doreturn = False):
1355    logging.info( "kickstart" )
1356    kickstarts = self.client.kickstart.listKickstarts(self.session)
1357
1358    for kickstart in kickstarts:
1359        name = kickstart.get('name')
1360        if self.is_current_stage( name ):
1361            logging.info( "  " + name )
1362            dir = basedir + self.get_common_name(name)
1363            content = self.dump_kickstart( name, normalize=True )
1364            if doreturn:
1365                return content
1366            else:
1367                # dump kickstart details and ks file content.
1368                # use  separate files
1369                self.dump( dir + '/' + self.get_common_name(name), content )
1370                self.dump( dir + '/' + self.get_common_name(name) + ".content", dump_kickstart_content(self, name, normalize=True) )
1371
1372
1373def dump_activationkey(self, name, normalize=False):
1374    # redirect stdout to string.
1375    # to be able to reuse the existing do_activationkey_details function
1376    old_stdout=sys.stdout
1377    output=StringIO()
1378    sys.stdout=output
1379    self.do_activationkey_details( name )
1380    sys.stdout=old_stdout
1381    content=output.getvalue().split("\n")
1382    output.close()
1383
1384    if normalize:
1385        stage = self.get_stage_from_name( name )
1386        content=self.get_normalized_text( content, stage )
1387
1388    return content
1389
1390
1391
1392def dump_activationkeys(self, basedir):
1393    logging.info( "activationkey" )
1394    activationkeys = self.do_activationkey_list('', True)
1395
1396    for name in activationkeys:
1397        if self.is_current_stage( name ):
1398            logging.info( "  " + name )
1399
1400            content = self.dump_activationkey( name, normalize=True)
1401
1402            dir = basedir + self.get_common_name(name)
1403            self.dump( dir + '/' + self.get_common_name(name), content )
1404
1405# vim:ts=4:expandtab:
Note: See TracBrowser for help on using the repository browser.