source: trunk/pxeconfig.in @ 132

Last change on this file since 132 was 132, checked in by bas, 15 years ago

pxeconfig.in:

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