source: trunk/pxeconfig.in @ 126

Last change on this file since 126 was 126, checked in by bas, 16 years ago

pxeconfig.in:

  • added --dry-run option
  • reformat manage_links function
  • Property keywords set to Id
  • Property svn:executable set to *
  • Property svn:keywords set to Id
File size: 14.6 KB
RevLine 
[78]1#!@PYTHON@
[2]2#
[104]3# set ts=4, sw=4
4#
[2]5# Author: Bas van der Vlies <basv@sara.nl>
6# Date  : 16 February 2002
7#
8# Tester: Walter de Jong <walter@sara.nl>
9#
[29]10# SVN info
11#  $Id: pxeconfig.in 126 2008-04-14 07:44:08Z bas $
[2]12#
[14]13# Copyright (C) 2002
[6]14#
[14]15# This file is part of the pxeconfig utils
[6]16#
[14]17# This program is free software; you can redistribute it and/or modify it
18# under the terms of the GNU General Public License as published by the
19# Free Software Foundation; either version 2, or (at your option) any
20# later version.
[6]21#
[14]22# This program is distributed in the hope that it will be useful,
23# but WITHOUT ANY WARRANTY; without even the implied warranty of
24# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
25# GNU General Public License for more details.
[6]26#
[14]27# You should have received a copy of the GNU General Public License
28# along with this program; if not, write to the Free Software
29# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA
30#
[2]31"""
[19]32Usage: pxeconfig
[49]33
[123]34 -V,--version
35 [-r,--remove] -i,--interactive
36 [-r,--remove] -f,--file <filename> <hostname(s)>
37 [-r,--remove] -n,--net <C-net> -s,--start <number> -e,--end <number> -f,--file <filename>
[10]38
[123]39there are serveal ways to create a range of hostnames. they all start with:
40 * -b,--base-name <string>
41
42The different formats are:
43  1  [opt] -H,--host-range <number>-<number> -f,--file <filename>
44  2  [opt] -s,--start <number> -e,--end <number> -f,--file <filename>
45  3  [opt] -R,--rack <string>-<string>  -N,--node <string>-<string> -f,--file <filename>
46
47opt:
[118]48 -r,--remove
[123]49 -w,--equal-width
[126]50 --dry-run
[118]51
[123]52If leading zeros are used for <number>, then host names will be padded
53with zeros. --equal-width option will be enabled.
54
[19]55With this program you can configure which PXE configuration file a node
56will use when it boots. The program can be started interactivly or via
57command line options.
58
[126]59See following link for usage and examples:
60 - https://subtrac.sara.nl/oss/pxeconfig/wiki/PxeUsage
[2]61"""
62
63import string
64import sys
65import os
66import glob
[3]67import getopt
[47]68import socket
[74]69import ConfigParser
[123]70import re
[2]71
[126]72# Global Variables
[2]73#
[126]74DEBUG   = False
75VERBOSE = False
76DRY_RUN = False
[2]77
78# Constants
79#
[4]80PXE_CONF_DIR='/tftpboot/pxelinux.cfg'
[19]81NETWORK='network'
[104]82BASENAME='basename'
[19]83FILENAME='filename'
84START='start'
85END='end'
[53]86REMOVE='remove'
[123]87RACK='rack'
88NODE='node'
[55]89INTERACTIVE='interactive'
[117]90EQUALWIDTH='equalwidth'
[120]91VERSION='2.0.0'
[2]92
[123]93SHORTOPT_LIST='b:e:f:hin:s:rwvN:H:R:V'
[126]94LONGOPT_LIST=[ 'basename=', 'debug', 'dry-run', 'end=', 'equal-width',
95    'file=', 'help', 'host-range=', 'interactive', 'net=', 'node=',
96    'start=', 'rack=', 'remove', 'verbose', 'version', 'equal-width'
[117]97        ]
[19]98
[123]99# regex definition
100#
101# A regulare expression to parse string for rack/node
102id_re = re.compile(r"""
103        (?P<basename>[a-zA-Z-_]*)
104        (?P<number>[0-9]+)
105     """, re.VERBOSE)
106
[117]107def verbose(str):
108        if VERBOSE:
[120]109                print '%s' %str
[117]110
[119]111class PxeConfig(Exception):
112        def __init__(self, msg=''):
113                self.msg = msg
114                Exception.__init__(self, msg)
115
116        def __repr__(self):
117                return self.msg
118
119
[75]120def ReadConfig(file):
[74]121        """
122        Parse the config file
123        """
124        if not os.path.isfile(file):
125                print 'File %s does not exist' %file
126                sys.exit(1)
127
[75]128        config = ConfigParser.RawConfigParser()
[74]129        try:
130                config.read(file)
131        except ConfigParser.MissingSectionHeaderError,detail:
132                print detail
133                sys.exit(1)
134
[75]135        # Not yet uses
[74]136        #
[75]137        #projects = {}
138        #for section in config.sections():
139        #       projects[section] = {}
140        #       for option in  config.options(section):
141        #       projects[section][option] = config.get(section, option)
[74]142
[75]143        stanza = config.defaults()
144        return stanza
[74]145
[19]146def select_pxe_configfile():
[2]147  """
[9]148  Let user choose which pxeconfig file to use.
[2]149  """
150
[4]151  os.chdir(PXE_CONF_DIR)
[2]152
[9]153  # Try to determine to which file the default symlink points, and
154  # if it exists, remove it from the list.
[2]155  #
156  try:
157    default_file = os.readlink('default')
158  except OSError:
159    default_file = None
160    pass
161
162  files = glob.glob('default.*')
163  if not files:
164    print 'There are no pxe config files starting with: default.'
165    sys.exit(1)
166
167  if default_file:
168    files.remove(default_file)
[98]169
170  # sort the files
171  #
172  files.sort()
[2]173 
174  print 'Which pxe config file must we use: ?'
175  i = 1   
176  for file in files:
177    print "%d : %s" %(i,file)
178    i = i +1
179
180  while 1:
[16]181    index = raw_input('Select a number: ')
[2]182    try:
183      index = int(index)
184    except ValueError:
185      index = len(files) + 1
186
187    # Is the user smart enough to select
[9]188    # the right value??
[2]189    #
190    if 0 < index <= len(files): break
191
192  return files[index-1]
193
[53]194def manage_links(dict):
[126]195        """
196        Create the links in the PXE_CONF_DIR,
197        list : A list containing: network hex address, pxe config file,
[2]198           start number, end number
[126]199        """
200        str = 'manage_links'
201        verbose(str)
202       
203        os.chdir(PXE_CONF_DIR)
204        naddr = dict[NETWORK]
205        pxe_filename = dict[FILENAME]
206       
207        for i in range(dict[START], dict[END]+1):
208                haddr = '%s%02X' %(naddr, i)
209               
210                if dict[REMOVE] == True:
211                        if DEBUG or DRY_RUN:
212                                print 'removing %s/%s' %(PXE_CONF_DIR, haddr)
[125]213
[126]214                        if os.path.exists(haddr) and not DRY_RUN:
215                                os.unlink(haddr)
[122]216
[126]217                else:
218                        if DEBUG or DRY_RUN:
219                                print 'linking %s to %s' %(haddr, pxe_filename)
[15]220
[126]221                        if not DRY_RUN:
222                                if os.path.exists(haddr):
223                                        os.unlink(haddr)
224                                os.symlink(pxe_filename, haddr)
[15]225
[125]226def net_2_hex(net):
[119]227        """
228        This function checks if the give network is a Class C-network and will
229        convert the network address to a hex address if true.
230        """
[125]231        str = 'net_2_hex : %s' %(net)
[119]232        verbose(str)
233       
234        d = string.split(net, '.')
235        if len(d) != 3:
236                error = '%s is not a valid  C-class network address' %(net)
237                raise PxeConfig, error
[2]238
[119]239        # Check if we have valid network values
240        r = ''
241        for i in d:
242                i = check_number(i, True)
243                r = '%s%02X' %(r,i)
[2]244
[119]245        if DEBUG:
[123]246                print 'C-network in hex: ', r
[19]247
[119]248        return r
[19]249
[119]250def check_number(number_str, network):
[105]251        """
[119]252        number : a string. If string starts with a zero (0) then
253                 EQUALWIDTH wil be set.
254        network: if true then number must ben between 0 < number < 255
255                 else it must be a valid number.
[105]256        """
257        try:
[119]258                n = int(number_str)
[105]259        except ValueError, detail:
[119]260                error = "%s : is not a valid number" %number_str
261                raise PxeConfig, error
[2]262
[105]263        if not network:
264                return n
[19]265
[105]266        # Check if it is a correct network value
267        #
268        if 0 <= n <= 255:
269                return n
270        else:
[119]271                error = '%s is not a valid network number, must be between 0 and 255' %n
272                raise PxeConfig, error
[19]273
274def interactive(binfo):
[105]275        print __doc__
276       
[119]277
278        while 1:
279                network = raw_input('Give network address (xxx.xxx.xxx): ')
280
281                try:
[125]282                        naddr = net_2_hex(network)
[119]283                        break
284                except PxeConfig, detail:
285                        print '%s : not a valid C-class network number' %(network)
286                        continue
287
288        while 1:
289                start = raw_input('Starting number: ')
290
291                try:
292                        start = check_number(start, True)
293                        break
294                except PxeConfig, detail:
295                        print detail
296                        continue
297               
298        while 1:
299                end = raw_input('Ending number: ')
300
301                try:
302                        end = check_number(end, True)
303                        break
304                except PxeConfig, detail:
305                        print detail
306                        continue
[105]307       
308       
309        pxe_filename = select_pxe_configfile()
310       
311        binfo[NETWORK] = naddr
312        binfo[START] = start
313        binfo[END] = end
314        binfo[FILENAME] = pxe_filename
315       
316        if DEBUG:
317                print network, binfo
[2]318
[105]319        manage_links(binfo)
[2]320
[120]321def check_args(binfo, hostnames):
[105]322        """
323        Do you we have the right and propper values
324        """
[114]325        ### check_filename
[105]326        #
[120]327        str = 'check_args: '
328        verbose(str)   
[105]329        try:
330                if not os.path.isfile(os.path.join(PXE_CONF_DIR, binfo[FILENAME])):
[119]331                        error =  '%s: Filename does not exists' %binfo[FILENAME]
332                        raise Pxeconfig, detail
[105]333        except KeyError, detail:
[114]334                if binfo[REMOVE] :
335                        binfo[FILENAME] = 'Does not matter'
336                else:
337                        binfo[FILENAME] = select_pxe_configfile()
[2]338
[105]339        if hostnames:
[125]340                host_2_hex(hostnames, binfo)
[105]341                sys.exit(0)
342               
343        if binfo.has_key(BASENAME) and binfo.has_key(NETWORK):
[119]344                error =  "The option -n/--net and -b/--basename are mutually exclusive"
345                raise PxeConfig, error 
[2]346
[105]347        if binfo.has_key(BASENAME):
[123]348                if binfo[RACK] and binfo[NODE]:
349                        create_links = rack_2_net
350                else:
351                        set_padding(binfo)
[125]352                        create_links =  base_2_host
[2]353
[105]354        elif binfo.has_key(NETWORK):
[120]355                binfo[START] = check_number(binfo[START], True)
356                binfo[END] = check_number(binfo[END], True)
[105]357                create_links = manage_links
[119]358
[105]359        else:
[119]360                error = 'You have to specifiy -b,--basename or -n,--net'
361                raise PxeConfig, error
[2]362
[105]363        if DEBUG:
364                print binfo
[6]365
[105]366        create_links(binfo)
[19]367
[120]368def set_padding(binfo):
[119]369        """
[120]370        binfo          : boot info
371        network_number : must we check if  start,end values are
372                         valid network numbers
373        return:
374                - if equal_width is requested then the length will be set to end value
375                - if start value length > 1 and start with a zero (0), width is set to
376                  the end value
377                - if end value starts with a zero (0), width will be set to the end
378                  value
[119]379        """
[120]380        start_str = binfo[START]
381        end_str = binfo[END]
[105]382
[120]383        start = check_number(start_str, False)
384        end = check_number(end_str, False)
385
386        if binfo[EQUALWIDTH][0] == True:
387                binfo[EQUALWIDTH][1] = len(end_str)
388
389        elif len(start_str) > 1 and start_str[0] == '0':
390                binfo[EQUALWIDTH] = [ True, len(end_str) ]
391
392        elif end_str[0] == '0':
393                binfo[EQUALWIDTH] = [ True, len(end_str) ]
394
395        binfo[START] = start
396        binfo[END] = end
397
398
[123]399def parse_number_range(binfo, arg):
[119]400        """
401        Parse if arg is of format <digit-digit>, if it starts
402        with a zero (0) then set EQUALWIDTH
403        """
404        str = 'parse_hostrange %s' %(arg)
405        verbose(str)
406
407        l = arg.split('-')
408        if len(l) < 2:
409                error =  'hostrange syntax not valid: %s (number-number)' %(arg)
410                raise PxeConfig, error
411
[120]412        binfo[START] = l[0]
413        binfo[END] = l[1]
[119]414
[123]415def parse_string_range(binfo, arg, id):
416        """
417        Parse if arg is of format <(alpha)(digit)>-(alpha)(digit)>
418        if digit starts with a zero (0) then set EQUALWIDTH
419        """
420        str = 'parse_string_range: %s %s' %(arg, id)
421        verbose(str)
422
423        l = arg.split('-')
424        if len(l) < 2:
425                error =  '%s : range syntax not valid,eg <string>-<string>)' %(arg)
426                raise PxeConfig, error
[119]427       
[123]428        binfo[id] = dict()
429        binfo[id][EQUALWIDTH] = [False, 0]
430
431        i = 0
432        for item in l:
433                result = id_re.match(item)
434
435                if result:
436                        basename = result.group('basename')
437                        number = result.group('number')
438
439                        if DEBUG:
440                                print 'basename = %s, number = %s' %(basename, number)
441       
442                        if i == 0:
443                                binfo[id][BASENAME] = basename
444                                binfo[id][START] = number
445                                i += 1
446                        else:
447                                binfo[id][END] = number
448
449                else:
450                        error = '%s : string syntax is not valid, eg: <alpa>*<digit>+' %(item)
451                        raise PxeConfig, error
452
453        set_padding(binfo[id]) 
454       
[120]455def parse_args(argv, binfo):
[104]456        """
457        This function parses the command line options and returns the rest as
458        an list of hostnames:
459        argv     : a list of command line options.
460        binfo    : returning a dict with the netinfo. if used non-interactively
461        hostnames: the rest of the command lines options that are not-parseble.
462        """
463        try:
464                opts, args = getopt.gnu_getopt(argv[1:], SHORTOPT_LIST, LONGOPT_LIST)
465        except getopt.error, detail:
466                print __doc__
467                print detail
468                sys.exit(1)
469       
470        global DEBUG
[117]471        global VERBOSE
[126]472        global DRY_RUN
[3]473
[105]474        # if nothing is specified then print usage and exit
475        #
476        if not opts and not args:
477                print __doc__
478                sys.exit(1)
479
[123]480        # init vars
481        #
482        hostrange = node = rack = None
483
[104]484        # Check given options
485        #
486        for opt,value in opts:
487                       
[105]488                if opt in ['-b', '--basename']:
[104]489                        binfo[BASENAME] = value
490                       
491                elif opt in ['--debug']:
[126]492                        DEBUG = True
493
494                elif opt in ['--dry-run']:
495                        DRY_RUN = True
[104]496                       
497                elif opt in ['-e', '--end']:
[105]498                        binfo[END] = value
[104]499                       
500                elif opt in ['-f', '--file']:
501                        binfo[FILENAME] = value
502                       
503                elif opt in ['-h', '--help']:
504                        print __doc__
505                        sys.exit(0)
[105]506
507                elif opt in ['-i', '--interactive']:
508                        interactive(binfo)
509                        sys.exit(0)
510
[104]511                elif opt in ['-n', '--net']:
[125]512                        binfo[NETWORK] = net_2_hex(value)
[104]513                       
514                elif opt in ['-r', '--remove']:
515                        binfo[REMOVE] = 1
516                       
517                elif opt in ['-s', '--start']:
[105]518                        binfo[START] = value
[55]519
[117]520                elif opt in ['-w', '--equal-width']:
[120]521                        binfo[EQUALWIDTH] = [True, 0]
[117]522
523                elif opt in ['-v', '--verbose']:
[126]524                        VERBOSE = True
[117]525
[119]526                elif opt in ['-H', '--host-range']:
[123]527                        hostrange = value
528
529                elif opt in ['-N', '--node']:
530                        node = value
531
532                elif opt in ['-R', '--rack']:
533                        rack = value
[119]534                       
[104]535                elif opt in ['-V', '--version']:
536                        print VERSION
537                        sys.exit(0)
[4]538
[123]539        if node and rack:
540                        parse_string_range(binfo, node, NODE)
541                        parse_string_range(binfo, rack, RACK)
542        elif hostrange:
543                        parse_number_range(binfo, hostrange)
544               
545
[120]546        check_args(binfo, args)
[105]547
[125]548def host_2_hex(hosts, binfo):
[104]549        """
[117]550        Convert hostname(s) to a net address that can be handled by manage_links function
[104]551        """
[125]552        str = 'host_2_hex: %s' %hosts
553        verbose(str)
554
[47]555        for host in hosts:
556                try:
557                        addr = socket.gethostbyname(host)
558                except socket.error,detail:
[119]559                        error =  '%s not an valid hostname: %s' %(host,detail)
560                        raise PxeConfig, error
[47]561                       
562                net = string.splitfields(addr, '.')
563                cnet = string.joinfields(net[0:3], '.')
564
[125]565                binfo[NETWORK] = net_2_hex(cnet)
[47]566                binfo[START] = int(net[3])
567                binfo[END] =  int(net[3])
[53]568                manage_links(binfo)
[47]569
[125]570def base_2_host(binfo):
[104]571        """
572        Construct hostname(s) from the supplied basename and start and end numbers
573        """
[125]574        str = 'base_2_host'
575        verbose(str)
576
[119]577        start = binfo[START]
578        end = binfo[END]
[47]579
[119]580        if start > end:
581                error = '%d >= %d : start value is greater then end value' %(start, end)
582                raise PxeConfig, error
583
[104]584        hostnames = list()
[119]585        for i in xrange(start, end + 1):
[120]586                if binfo[EQUALWIDTH][0] == True:
587                        hostname = '%s%0*d' %(binfo[BASENAME],  binfo[EQUALWIDTH][1], i)
[117]588                else:
589                        hostname = '%s%d' %(binfo[BASENAME], i)
590
[104]591                if DEBUG:
[105]592                        print 'host = %s, Basename = %s, number = %d' %(hostname, binfo[BASENAME], i)
[104]593                hostnames.append(hostname)
[105]594
[125]595        host_2_hex(hostnames,binfo)
[123]596
597def rack_2_net(binfo):
598        """
599        """
[125]600        str = 'rack_2_net'
601        verbose(str)
[123]602        basename = binfo[BASENAME]
603        start = binfo[RACK][START]
604        end = binfo[RACK][END]
605
606        if start > end:
607                error = '%d >= %d : start value is greater then end value' %(start, end)
608                raise PxeConfig, error
609
610        for i in xrange(start, end + 1):
611                if binfo[RACK][EQUALWIDTH][0] == True:
612                        new_base = '%s%s%0*d' %(basename,  binfo[RACK][BASENAME], binfo[RACK][EQUALWIDTH][1], i)
613                else:
614                        new_base = '%s%s%d' %(basename, binfo[RACK][BASENAME], i)
615
616                # Make copy and file in the appropiate values for creating/removing links
617                #
618                new_bootinfo = binfo[NODE].copy()
619                new_bootinfo[BASENAME] = '%s%s' %(new_base, binfo[NODE][BASENAME])
620                new_bootinfo[FILENAME] = binfo[FILENAME]
621                new_bootinfo[REMOVE] = binfo[REMOVE]
622
623                if DEBUG:
624                        print 'rack ', new_bootinfo
625
[125]626                base_2_host(new_bootinfo)
[123]627
[2]628def main():
[104]629        # A dictionary holding the boot info
630        #
631        global DEBUG
[117]632        global PXE_CONF_DIR
[104]633       
634        bootinfo = {}
[123]635        bootinfo[NODE] = None
636        bootinfo[RACK] = None
[117]637        bootinfo[REMOVE] = False
[120]638        bootinfo[EQUALWIDTH] = [ False, 0 ]
[123]639
[104]640        configfile = '@pxeconfig_conf@'
641        settings = ReadConfig(configfile)
642       
643        try:
644                PXE_CONF_DIR = settings['pxe_config_dir']
[105]645                if not DEBUG:
646                        DEBUG = int(settings['debug'])
[117]647
[104]648        except KeyError:
649                pass
[105]650
[118]651        PXE_CONF_DIR = os.path.realpath(PXE_CONF_DIR)
[117]652        if not os.path.isdir(PXE_CONF_DIR):
[120]653                error =  'pxeconfig directory: %s does not exists' %(PXE_CONF_DIR)
654                raise PxeConfig, error
[117]655
[120]656        parse_args(sys.argv, bootinfo)
657       
658if __name__ == '__main__':
[119]659        try:
[120]660                main()
[119]661        except PxeConfig, detail:
662                print detail
663                sys.exit(1)
Note: See TracBrowser for help on using the repository browser.