source: trunk/pxeconfig.in @ 123

Last change on this file since 123 was 123, checked in by bas, 14 years ago

pxeconfig.in:

  • added --rack,-R and --node,-n option in combo with --basename,-b

Changelog:

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