Back to the main page

Add DHCP reservation, python tool

Intro

This can be independent tool, or part of some install server project (like Cobbler).
Basically, a user is asked for some input and DHCP reservation file is created.

Design

Implementation

The module to create cgf file is lib/dhcp_cfg.py, hence lib directory is place for modules. There is the empty file lib/__init__.py so Python knows to treat this directory as having modules (packages).

[user@cobbler-server ~] python add_to_dhcp-sca.py -h

usage: add_to_dhcp-sca.py [-h] -s SYSTEM -m MAC [-b BOOT_FILE]

Add SCA Cobbler client into DHCP (SCA subnet list is hardcoded)

optional arguments:
  -h, --help            show this help message and exit
  -s SYSTEM, --system SYSTEM
                        System's FQDN
  -m MAC, --mac MAC     System's MAC ( : as must delimiter)
  -b BOOT_FILE, --boot_file BOOT_FILE
                        Some custom boot file, /tftpboot/ will be prefixed

Brought to you by LabOps
Failure because of some deliberate input mistakes.

[user@cobbler-server ~] python add_to_dhcp-sca.py -s ca-alisa.domain.ca -m 00:21:f6:c4:bf:52

ERROR: ca-alisa.domain.ca is not in DNS

[user@cobbler-server ~] python add_to_dhcp-sca.py -s ca-zdudic1 -m ZZ:21:f6:c4:bf:ZZ

ERROR: ZZ:21:f6:c4:bf:ZZ is not valid MAC
This is run on Cobbler server, it fails because provided boot file is not in /tftpboot directory.

[user@cobbler-server ~]python add_to_dhcp-sca.py -s ca-zdudic1 -m 00:21:f6:c4:bf:22 -b pxelinux.zz

ERROR: Can't find /tftpboot/pxelinux.zz
The system is not on supported subnet.

[user@cobbler-server ~]python add_to_dhcp-sca.py -s ca-zdudic1 -m 00:21:f6:c4:bf:22 -b pxelinux.0

WARNING: 10.211.9.201 is on SCA subnet not support on this Cobbler, exiting!
Finally success.

[user@cobbler-server ~]python add_to_dhcp-sca.py -s ca-install-dev -m 00:21:f6:c4:bf:22 -b pxelinux.0

Creating file /tmp/ca-install-dev
This is the end of script's work so far, not completed, since cfg file has to be pushed to DHCP server, hence this is to be continued.

Logging

Logs are in the directory /var/log/add_to_dhcp-sca.py/ named mm-dd-yyyy_hhmmss, success log reads:

2019-02-22 15:49:05,107:DEBUG:
2019-02-22 15:49:05,107:DEBUG: START AT : Fri, 22 Feb 2019 23:49:05
2019-02-22 15:49:05,107:DEBUG: Run by: zdudic
2019-02-22 15:49:05,107:DEBUG: Subnet list is from the file sca-subnets.txt
2019-02-22 15:49:05,111:DEBUG: OK: /tftpboot/pxelinux.0 exists
2019-02-22 15:49:05,116:DEBUG: OK: ca-install-dev's IP address is 10.132.28.239
2019-02-22 15:49:05,131:DEBUG: OK: 10.132.28.239 is on supported SCA subnet 10.132.28.0/24
2019-02-22 15:49:05,131:DEBUG: Default router (assumption: network IP + 1): 10.132.28.1
2019-02-22 15:49:05,131:DEBUG: Broadcast IP: 10.132.28.255
2019-02-22 15:49:05,139:DEBUG: System's object/ID: ca_install_dev
2019-02-22 15:49:05,139:DEBUG: Creating DHCP cfg with: ca-install-dev 00:21:f6:c4:bf:22 10.132.28.1 pxelinux.0
2019-02-22 15:49:05,140:DEBUG: Checking the cfg file:
Reading file /tmp/ca-install-dev
host ca-install-dev {
        hardware ethernet 00:21:f6:c4:bf:22 ;
        fixed-address 10.132.28.239 ;
        option routers 10.132.28.1 ;
        next-server 10.132.26.74 ;
        filename "pxelinux.0" ;
         }

2019-02-22 15:49:05,140:DEBUG: FINISH AT : Fri, 22 Feb 2019 23:49:05

Files

List of supported subnets

This is ASCII file, list of subnets, like:

20.162.48.0/24

20.162.49.0/24

...shortened ...
20.169.131.0/24

20.169.132.0/24

20.169.133.0/24

Module lib/dhcp_cfg.py


" Module to create DHCP config file for Cobbler system "
# The module name is .py
# When import, use only : ex.
# import  
# Use sys.path.append("/path/to/module") if needed
# ------------------------------------------------------
import os
import socket
 
class COBBLER_SYSTEM(object):
    """
    Blueprint for DHCP Cobbler system's config file
    Attributes:
       1. hostname (string)
       2. mac (string)
       3. ip (string)
       4. router (string)
       5. bootfile (string): default is None object
    """
    
    # default next-server is cobbler server where script is run, or module imported
    # make sure there is no host entry in /etc/hosts
    global nextserver_ip
    nextserver=socket.gethostname()   # returns hostname
    nextserver_ip=socket.gethostbyname(nextserver)   # returns IP
    
    def __init__(self, hostname, mac, ip, router, bootfile=None):
       """
       Return DHCP_COBBLER_SYSTEM object with initial attributes,
       """
       self.hostname = hostname
       self.mac = mac
       self.ip = ip
       self.router = router
       self.bootfile = bootfile
       global CFGFILE 
       CFGFILE = "/tmp/" + self.hostname 

    # ---------------------------------------------------------------
    # this is of you want to manipulate filename attribute of an object
    # ---------------------------------------------------------------
    @property
    def bootfile(self):
       """ Get 'bootfile' property/value """
       return self._bootfile

    @bootfile.setter
    def bootfile(self, value):
       """ Set value to 'bootfile' """
       self._bootfile = value

    @bootfile.deleter
    def bootfile(self):
       """ Delete 'bootfile' """
       del self._bootfile
    # ---------------------------------------------------------------

    def create_cfg_file(self):
        """ Creates config file """
        print ("Creating file " + CFGFILE )
        try: 
            if self.bootfile != None:
                f = open(CFGFILE, "w+")
                f.write("host " + self.hostname + " {\n")
                f.write("\thardware ethernet " + self.mac + " ;\n")
                f.write("\tfixed-address " + self.ip + " ;\n")
                f.write("\toption routers " + self.router + " ;\n")
                f.write("\tnext-server " + nextserver_ip + " ;\n")
                f.write("\tfilename " + "\"" + str(self.bootfile) + "\"" + " ;\n")
                f.write("\t }\n")
                f.close()
            else:
                f = open(CFGFILE, "w+")
                f.write("host " + self.hostname + " {\n")
                f.write("\thardware ethernet " + self.mac + " ;\n")
                f.write("\tfixed-address " + self.ip + " ;\n")
                f.write("\toption routers " + self.router + " ;\n")
                f.write("\tnext-server " + nextserver_ip + " ;\n")
                f.write("\t }\n")
                f.close()
        except IOError as err:
            print("IOError: {0}".format(err))

    def remove_cfg_file(self):
        """ Removes config file """
        try:
            os.remove(CFGFILE)
            print ("File " + CFGFILE + " removed!" )
        except IOError as err:
            print("IOError: {0}".format(err))


    def read_cfg_file(self):
        """ Prints config file """
        print ("Reading file " + CFGFILE ) 
        try:
            f = open(CFGFILE, "r")
            print f.read()
            f.close()
        except IOError as err:
            print("IOError: {0}".format(err))

if __name__ == '__main__': 

   # some test, any when you run this module directly !
   print COBBLER_SYSTEM.__doc__
   ca_zdudic = COBBLER_SYSTEM("ca-zdudic1.us.oracle.com", "10:2:30:4:5:61", "10.211.22.33", "10.211.0.1", "/path/boot/me/file")
   #ca_zdudic = COBBLER_SYSTEM("ca-zdudic2.us.oracle.com", "1:21:3:41:5:6", "10.211.22.33", "10.211.0.1")
   print "hostname is " + ca_zdudic.hostname
   print "mac is " + ca_zdudic.mac
   print "boot file is " + str(ca_zdudic.bootfile)
   ca_zdudic.create_cfg_file()
   ca_zdudic.read_cfg_file()
   ca_zdudic.remove_cfg_file()

Main script add_to_dhcp.py


#!/usr/bin/env python

# Adding Cobbler client into SCA DHCP
# -----------------------------------
import os
import sys
import getpass
import socket
import netaddr 
import argparse
from time import gmtime, strftime
import datetime
import logging, logging.handlers
sys.path.append('../')  # so we can import from lib directory
import lib.dhcp_cfg as dhcp_cfg # to create COBBLER_SYSTEM object

# ------ LOGGING
PROGRAM = os.path.basename(sys.argv[0])   # name of this script
LOG_PATH = ("/var/log/" + PROGRAM)        # put together "/var/log/"
if not os.path.exists(LOG_PATH):          # create LOG_PATH if doesn't exists
    os.makedirs(LOG_PATH)
    os.chown(LOG_PATH,-1,485400023)       # root(-1 means no change):userg_sa
    os.chmod(LOG_PATH,0775)               # drwxrwxr-x
LOG_FILE = (LOG_PATH + "/" + datetime.datetime.now().strftime("%m-%d-%Y_%Hh%Mm%Ss"))
# create logger (interface that script uses for logging)
logger = logging.getLogger(__name__)
logger.setLevel(logging.DEBUG)
# create file handler (define log destination)
handler = logging.handlers.TimedRotatingFileHandler(LOG_FILE, when='MIDNIGHT', backupCount=50, utc=False)
# create formatter (define layout of logs)
formatter = logging.Formatter('%(asctime)s:%(levelname)s: %(message)s')
handler.setFormatter(formatter)
# add handler to logger
logger.addHandler(handler)
# -----------------------

logger.debug("")
logger.debug("START AT : " + strftime("%a, %d %b %Y %H:%M:%S", gmtime()))

i_am=getpass.getuser()
logger.debug("Run by: " + i_am)

SUBNETFILE = "sca-subnets.txt"  # subnet file is hardcoded for a location
logger.debug("Subnet list is from the file " + SUBNETFILE)

def verify_mac(hwaddr):
    """
    To be used as type for MAC argument
    """
    if not netaddr.valid_mac(hwaddr):
        logger.debug("ERROR: %s is not valid MAC" % hwaddr)
        sys.exit("ERROR: %s is not valid MAC" % hwaddr)
    return hwaddr

# -- argument work
parser = argparse.ArgumentParser(
                                description="Add SCA Cobbler client into DHCP (SCA subnet list is hardcoded)",
                                epilog='Brought to you by LabOps')
parser.add_argument("-s", "--system", help="System's FQDN", required=True)
parser.add_argument("-m", "--mac", help="System's MAC ( : as must delimiter)", type=verify_mac, required=True)
parser.add_argument("-b", "--boot_file", help="Some custom boot file, /tftpboot/ will be prefixed")
args = parser.parse_args()
system=args.system   
mac=args.mac
if args.boot_file:
    if os.path.isfile("/tftpboot/" + args.boot_file):
        logger.debug("OK: /tftpboot/" + args.boot_file + " exists")
    else:
        logger.debug("ERROR: Can't find /tftpboot/" + args.boot_file )
        sys.exit("ERROR: Can't find /tftpboot/" + args.boot_file )


def find_ip():
    """
    Determine IP for given system
    """
    try:
        global ipaddress 	# define global to be used outside of this function
        ipaddress = socket.gethostbyname(system)
        logger.debug("OK: " + system + "'s IP address is " + ipaddress)
        #return ipaddress
    except:
        logger.debug("ERROR: " + system + " is not in DNS")
        sys.exit("ERROR: " + system + " is not in DNS")

def subnet_check(ipaddress): 
    """
    Check if IP is in subnet,
    and find router and broadcast address.
    Arguments: 
             1. IP, determined by find_ip()
             2. file (list of subnets), it's hard coded here!
    """
    global default_router
    f = open(SUBNETFILE, 'r')
    ip_ok = False
    try:
        for subnet in f:
            if netaddr.IPAddress(ipaddress) in netaddr.IPNetwork(subnet):
                logger.debug("OK: " + ipaddress + " is on supported SCA subnet " + str(netaddr.IPNetwork(subnet)))
                default_router = str(netaddr.IPNetwork(subnet).network + 1) # assumption: default router is network IP + 1
                broadcast_ip = str(netaddr.IPNetwork(subnet).broadcast)
                logger.debug("Default router (assumption: network IP + 1): " + default_router)
                logger.debug("Broadcast IP: " + broadcast_ip)
                ip_ok = True
        f.close()
    except:
        logger.debug("ERROR: Can't analyze " + ipaddress + " and determine subnet and default router")
        sys.exit("ERROR: Can't analyze " + ipaddress + " and determine subnet and default router")

    if not ip_ok:
        logger.debug("WARNING: " + ipaddress + " is on SCA subnet not support on this Cobbler, exiting!")
        sys.exit("WARNING: " + ipaddress + " is on SCA subnet not support on this Cobbler, exiting!")

def create_dhcp_cfg_file():
    """
    Note: dash (-) & dot (.) can't be used in object name,
          hence we create first systemobject
    """
    systemobject = system.replace("-", "_").replace(".", "_")    
    logger.debug("System's object/ID: " + systemobject) 
    try:
        if args.boot_file:
            systemobject =  dhcp_cfg.COBBLER_SYSTEM(system, mac, ipaddress, default_router, args.boot_file)
            logger.debug("Creating DHCP cfg with: " + systemobject.hostname + 
                         " " + systemobject.mac + " " + systemobject.router + 
                         " " + str(systemobject.bootfile) )
            systemobject.create_cfg_file()
            orig = sys.stdout  # initial stdout
            sys.stdout = open(LOG_FILE, "a")  # want to read cfg file into log 
            logger.debug("Checking the cfg file:")
            systemobject.read_cfg_file()
            sys.stdout.close()
            sys.stdout = orig  # restore initial stdout
        else:
            systemobject =  dhcp_cfg.COBBLER_SYSTEM(system, mac, ipaddress, default_router)
            logger.debug("Creating DHCP cfg with: " + systemobject.hostname + 
                         " " + systemobject.mac + " " + systemobject.router )
            systemobject.create_cfg_file()
            orig = sys.stdout  # initial stdout
            sys.stdout = open(LOG_FILE, "a")  # want to read cfg file into log 
            logger.debug("Checking the cfg file:")
            systemobject.read_cfg_file()
            sys.stdout.close()
            sys.stdout = orig  # restore initial stdout
    except:
        logger.debug("ERROR: Can't create DHCP cfg for " + system)
        sys.exit("ERROR: Can't create DHCP cfg for " + system)

# ---- MAIN -----
if __name__ == '__main__':
    find_ip()
    subnet_check(ipaddress)
    create_dhcp_cfg_file()
 
    logger.debug("FINISH AT : " + strftime("%a, %d %b %Y %H:%M:%S", gmtime()))
Back to the main page