source: ccsd/trunk/crcnetd/_utils/ccsd_common.py @ 1036

Last change on this file since 1036 was 1036, checked in by mglb1, 7 years ago

Improve password generation so that passwords are more readable

  • Property svn:keywords set to Revision Id
File size: 37.5 KB
Line 
1# Copyright (C) 2006  The University of Waikato
2#
3# This file is part of crcnetd - CRCnet Configuration System Daemon
4#
5# This file contains common code used throughout the system and extensions
6# - Constant values
7# - Small helper functions
8# - Base classes
9#
10# Author:       Matt Brown <matt@crc.net.nz>
11# Version:      $Id$
12#
13# crcnetd is free software; you can redistribute it and/or modify it under the
14# terms of the GNU General Public License version 2 as published by the Free
15# Software Foundation.
16#
17# crcnetd is distributed in the hope that it will be useful, but WITHOUT ANY
18# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
19# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
20# details.
21#
22# You should have received a copy of the GNU General Public License along with
23# crcnetd; if not, write to the Free Software Foundation, Inc.,
24# 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
25import re
26import os
27import os.path
28import sys
29import socket
30import struct
31import optparse
32import fcntl
33from BaseHTTPServer import HTTPServer
34import xmlrpclib
35
36#############################################################################
37# Constants
38#############################################################################
39
40# Site details
41DEFAULT_ADMIN_EMAIL = "root@localhost"
42DEFAULT_SITE_NAME = "Default CRCnet Configuration System Installation"
43DEFAULT_SITE_ADDRESS = "https://localhost/"
44
45# How often to run generic maintenance commands (seconds)
46MAINT_INTERVAL = 60
47
48# How many minutes of activity cause a session to timeout
49DEFAULT_SESSION_TIMEOUT = 30
50
51# How many minutes should a cookie be issued for
52DEFAULT_COOKIE_TIMEOUT = 60*24*30
53
54# How many minutes should a session last for
55DEFAULT_SESSION_TIMEOUT = 30
56
57# Die after 10 fatal errors
58DEFAULT_FATAL_ERR_COUNT = 10
59
60# Default Server Port
61DEFAULT_SERVER_PORT = 5565
62# Default Client Port
63DEFAULT_CLIENT_PORT = 80
64
65# Possible command line options
66OPTION_LIST = "c:d"
67
68# Default File Locations
69DEFAULT_CONFFILE = "/etc/ccsd/%s.conf" % os.path.basename(sys.argv[0])
70DEFAULT_PIDFILE = "/var/run/%s.pid" % os.path.basename(sys.argv[0])
71DEFAULT_CONFIG_SVNROOT = "/var/lib/svn/ccsd/configs"
72DEFAULT_REQUEST_LOG = "/tmp/ccsd.log"
73DEFAULT_SERVICE_LIB_DIR = "/usr/lib/ccsd/services"
74DEFAULT_SERVICE_DATA_DIR = "/usr/share/ccsd/services"
75DEFAULT_TMPDIR = "/tmp"
76DEFAULT_PROFILE_DIR = "/tmp/ccsd-profile"
77
78# Types of session
79SESSION_NONE = "NO"
80SESSION_RO = "RO"
81SESSION_RW = "RW"
82
83# ID of the ever present session
84ADMIN_SESSION_ID = 0
85
86# RPC errors
87ERROR_BASE = 9000
88
89CCSD_ERROR = ERROR_BASE + 1
90
91CCSD_BADPARAM = ERROR_BASE + 10
92
93CCSD_DBERROR = ERROR_BASE + 20
94
95CCSD_AUTHFAIL = ERROR_BASE + 30
96
97CCSD_CALLFAILED = ERROR_BASE + 40
98
99TRUE = 1
100FALSE = 0
101
102RFC1918 = ["10.0.0.0/8", "192.168.0.0/16", "172.16.0.0/12"]
103   
104# Bytes (as floats so divisions and stuff work nicely)
105KILO = 1024.0
106MEGA = KILO * 1024.0
107GIGA = MEGA * 1024.0
108
109# Types of link
110CCSD_LINK_ADHOC = "AdHoc"
111CCSD_LINK_MANAGED = "Managed"
112CCSD_LINK_ETHERNET = "Ethernet"
113CCSD_LINK_QB20 = "QuickBridge20"
114CCSD_LINK_TRANGO = "Trango"
115
116# Group Names
117AUTH_NONE = "None"
118AUTH_AUTHENTICATED = "Authenticated"
119AUTH_ADMINISTRATOR = "Administrators"
120AUTH_USER = "Users"
121AUTH_ASSET_MANAGER = "Asset Managers"
122AUTH_CERT_ADMINS = "Certificate Administrators"
123
124# Care needs to be taken to keep these in sync with the database table
125ASSET_EVENT_IMPORTED = 1
126ASSET_EVENT_LOCATION_CHANGED = 2
127ASSET_EVENT_ATTACHED = 3
128ASSET_EVENT_CREATED = 4
129ASSET_EVENT_REMOVED = 5
130ASSET_EVENT_DETAILS_UPDATED = 6
131ASSET_EVENT_PROPERTY_UPDATED = 7
132ASSET_EVENT_SUBASSET_ADDED = 8
133ASSET_EVENT_SUBASSET_REMOVED = 9
134
135# Status Values
136STATUS_OK = "ok"
137STATUS_WARNING = "warning"
138STATUS_CRITICAL = "critical"
139STATUS_UNKNOWN = "unknown"
140
141# Server / Client Constants
142CCSD_NONE = -1
143CCSD_CLIENT = 0
144CCSD_SERVER = 1
145
146# From net/route.h
147RTF_UP = 0x0001             #U Route usable
148RTF_GATEWAY = 0x0002        #G Destination is a gateway.
149RTF_HOST = 0x0004           #H Host entry (net otherwise).
150RTF_REINSTATE = 0x0008      #A Reinstate route after timeout.
151RTF_DYNAMIC = 0x0010        #D Created dyn. (by redirect).
152RTF_MODIFIED = 0x0020       #D Modified dyn. (by redirect).
153RTF_MTU = 0x0040            #M Specific MTU for this route
154RTF_WINDOW = 0x0080         #W Per route window clamping.
155RTF_IRTT = 0x0100           #I Initial round trip time.
156RTF_REJECT = 0x0200         #R Reject route. 
157RTF_STATIC = 0x0400         #S Manually injected route.
158RTF_XRESOLVE = 0x0800       #E External resolver.
159RTF_NOFORWARD = 0x1000      #F Forwarding inhibited.
160RTF_THROW = 0x2000          #T Go to next class.
161RTF_NOPMTUDISC = 0x4000     #N Do not send packets with DF.
162
163ROUTE_FLAG_CHARS = {RTF_UP:"U", RTF_GATEWAY:"G", RTF_HOST:"H", \
164        RTF_REINSTATE:"A", RTF_DYNAMIC:"D", RTF_MODIFIED:"D", RTF_MTU:"M", \
165        RTF_WINDOW:"W", RTF_IRTT:"I", RTF_REJECT:"R", RTF_STATIC:"S", \
166        RTF_XRESOLVE:"X", RTF_NOFORWARD:"F", RTF_THROW:"T", RTF_NOPMTUDISC:"N"}
167
168# From if.h
169IFF_UP = 0x1                # interface is up
170IFF_BROADCAST = 0x2         # broadcast address valid
171IFF_LOOPBACK = 0x8          # is a loopback net       
172IFF_POINTOPOINT= 0x10       # interface is has p-p link
173IFF_RUNNING = 0x40          # resources allocated
174IFF_NOARP = 0x80            # no ARP protocol   
175IFF_PROMISC = 0x100         # receive all packets
176IFF_MULTICAST = 0x1000      # Supports multicast
177
178IFACE_FLAG_CHARS = {IFF_BROADCAST:"BROADCAST", IFF_LOOPBACK:"LOOPBACK", \
179        IFF_MULTICAST:"MULTICAST", IFF_NOARP:"NOARP", \
180        IFF_POINTOPOINT:"POINTOPOINT", IFF_PROMISC:"PROMISC", \
181        IFF_RUNNING:"RUNNING", IFF_UP:"UP"}
182       
183SIOCGIFCONF    = 0x8912
184SIOCGIFFLAGS   = 0x8913
185SIOCGIFADDR    = 0x8915
186SIOCGIFBRDADDR = 0x8919
187SIOCGIFMTU     = 0x8921
188SIOCGIFHWADDR  = 0x8927
189SIOCGIFINDEX   = 0x8933     
190SIOCGIFNETMASK = 0x891b
191
192#############################################################################
193# Base Classes
194#############################################################################
195class ccsd_error(Exception):
196    def __init__(self, value):
197        self.value = value
198    def __str__(self):
199        return repr(self.value)   
200   
201class ccs_class(object):
202    """Provides generic functionality that all ccs classes will use
203   
204    Derived classes must define global variables that are initialised
205    in the __init__ routine as specified below.
206
207    _session_id     The session ID that the class should use to access shared
208                    resources
209    _properties     A dictionary of properties that can be accessed via the
210                    class.
211    _errMsg         The last error that occured during processing
212    """
213   
214    SUCCESS = 0
215    FAILURE = -1
216   
217    # Internal variables
218    #
219    # Derived classes should set these
220    #
221    #_session_id = None
222    #_properties = {}
223    #_errMsg = ""
224
225    def __getitem__(self,x): 
226        """Returns an item from the classes properties.
227
228        This enables the class to act as a dictionary to reveal it's properties
229        in a read-only manner.
230        """
231        return self._properties[x]
232   
233    def keys(self):
234        return self._properties.keys()
235    def items(self):
236        return self._properties.items()
237    def values(self):
238        return self._properties.values()
239
240    def returnError(self, errMsg):
241        """Returns an error code and stores the error message
242       
243        Rollsback any implicit transactions started by this class.
244        """
245
246        # Record error message
247        self._errMsg = errMsg
248       
249        from ccsd_session import getSessionE
250        session = getSessionE(self._session_id)         
251       
252        # Rollback an implicit changeset
253        if session.changesetInitiator == self._csInit and self._csInit != "":
254            session.rollback()
255           
256        return self.FAILURE
257
258    def returnSuccess(self):
259        """Returns indicating success"""
260       
261        from ccsd_session import getSessionE
262        session = getSessionE(self._session_id) 
263       
264        # Commit implicit changeset
265        if session.changesetInitiator == self._csInit and self._csInit != "":
266            session.commit()
267
268        return self.SUCCESS     
269
270    def getErrorMessage(self):
271        """Returns the last error message generated and clears the buffer"""
272        t = self._errMsg
273        self._errMsg = ""
274        return t
275   
276    def _forceChangeset(self, message, initiator="ccs_class"):
277        """Starts an implicit changeset if there is no active changeset
278       
279        If you use this function it is *imperative* that you use the
280        returnError and returnSuccess functions to leave the function you
281        call it from to ensure any implicit changeset is finished properly.
282        """
283        from ccsd_session import getSessionE
284        session = getSessionE(self._session_id)
285       
286        if session.changeset==0:
287            session.begin(message, initiator=initiator)
288            self._csInit = initiator
289
290class CCSHTTPServer(HTTPServer):
291   
292    def server_bind(self):
293        """Overrides the standard bind to set the no_inherit flag"""
294        HTTPServer.server_bind(self)
295        fcntl.fcntl(self.socket, fcntl.F_SETFD, fcntl.FD_CLOEXEC)
296
297class CCSTransport(xmlrpclib.Transport):
298    """Adds support to the standard XMLRPC Transport class for client certs"""
299   
300    def __init__(self, key, cert):
301        self.key_file = key
302        self.cert_file = cert
303       
304    def make_connection(self, host):
305        # create a HTTPS connection object from a host descriptor
306        # host may be a string, or a (host, x509-dict) tuple
307        import httplib
308        host, extra_headers, x509 = self.get_host_info(host)
309        try:
310            HTTPS = httplib.HTTPS
311        except AttributeError:
312            raise NotImplementedError(
313                "your version of httplib doesn't support HTTPS"
314            )
315        else:
316            return HTTPS(host, None, key_file=self.key_file, \
317                    cert_file=self.cert_file)
318   
319#############################################################################
320# Helper Functions
321#############################################################################
322def enc(inobj):
323    """Convert a dictionary to the types needed by xmlrpclib"""
324   
325    if type(inobj) == type([]):
326        out = []
327        for row in inobj:
328            out.append(enc(row))
329    elif type(inobj) == type({}):
330        out = {}
331        for key,item in inobj.items():
332            nkey = str(key)
333            if type(item) == type({}):
334                out[nkey] = enc(item)
335            elif type(item) == type([]):
336                out[nkey] = enc(item)
337            elif type(item) == type(int(1)):
338                out[nkey] = item
339            elif type(item) == type(long(1)):
340                out[nkey] = item
341            elif type(item) == type(float(1)):
342                out[nkey] = item
343            elif type(item) == type(None):
344                out[nkey] = ""
345            else:
346                out[nkey] = str(item)
347    else:
348        out = inobj
349       
350    return out
351
352def buildInsertFromDict(tableName, props, newDetails, doNull=False, \
353        forceNull=[]):
354    """Builds an SQL insert string from the specified dictionary.
355
356    tableName is the name of the database table to insert into
357
358    props must list all possible fields in the database that can be inserted
359    newDetails is a dictionary containing new values for the table, indexed
360    by values in props. newDetails must contain all fields that are required
361    by the database or an error will occur when you try to execute the query.
362
363    If an entry in newDetails contains the value "DEFAULT" no quotes, then
364    the default value for the column will be used. If doNull is true any
365    property with a value of -1 is inserted as NULL in the update statement
366
367    The forceNull list may be used to specify a list of properties which must
368    be inserted as NULL into the table regardless of any value that may be
369    specified in the newDetails dictionary.
370   
371    A tuple containing two items is returned. The SQL insert string and a list
372    containing the values to be substituted into it by the database.
373    """
374   
375    # Base
376    sql = "INSERT INTO %s (" % tableName
377    f = 0
378   
379    # Go through the properties to be inserted
380    values = []
381    for prop in props:
382        if prop not in newDetails.keys() and prop not in forceNull:
383            continue
384        c=","
385        if f==0:
386            f=1
387            c=""
388        sql = "%s%s %s" % (sql, c, prop)
389        if prop not in forceNull:
390            values.append(newDetails[prop])
391        else:
392            values.append("NULL")
393   
394    # Error out if no values to insert
395    if f == 0:
396        return (None, None)
397
398    # Values
399    sql = "%s) VALUES (" % sql
400    values2 = []
401    f=0
402    for val in values:
403        c=","
404        if f==0:
405            f=1
406            c=""
407        try:
408            v = int(val)
409        except ValueError:
410            v = val
411        if v == "DEFAULT":
412            sql = "%s%s DEFAULT" % (sql, c)
413        elif (doNull and v==-1) or (len(forceNull)>0 and v=="NULL"):
414            sql = "%s%s NULL" % (sql, c)
415        else:
416            sql = "%s%s %%s" % (sql, c)
417            values2.append(val)
418
419    sql = "%s)" % sql
420    return (sql, values2)
421   
422def buildUpdateFromDict(tableName, props, newDetails, keyField, keyValue, \
423        doNull=False, forceNull=[]):
424    """Builds an SQL update string from the specified dictionary.
425
426    tableName is the name of the database table to update
427    keyField and keyValue specify the parameters required to limit the update
428
429    props must list all possible fields in the database that can be updated
430    newDetails is a dictionary containing new values for the table, indexed
431    by values in props. If doNull is true any property with a value of -1 is
432    inserted as NULL in the update statement
433   
434    The forceNull list may be used to specify a list of properties which must
435    be set to NULL.
436
437    A tuple containing two items is returned. The SQL update string and a list
438    containing the values to be substituted into it by the database.
439    """
440   
441    # Base
442    sql = "UPDATE %s SET " % tableName
443    f = 0
444   
445    # Go through the properties to be updated
446    values = []
447    for prop in props:
448        if prop not in newDetails.keys() or prop in forceNull:
449            continue
450        c=","
451        if f==0:
452            f=1
453            c=""
454        try:
455            v = int(newDetails[prop])
456        except ValueError:
457            v = newDetails[prop]
458        if doNull and v==-1:
459            sql = "%s%s %s=NULL" % (sql, c, prop)
460        else:
461            sql = "%s%s %s=%%s" % (sql, c, prop)
462            values.append(v)
463    # Go through the properties to be set to NULL
464    for prop in forceNull:
465        c=","
466        if f==0:
467            f=1
468            c=""
469        sql = "%s%s %s=NULL" % (sql, c, prop)
470       
471    # Error out if no values updated
472    if f == 0:
473        return (None, None)
474
475    # Condition
476    sql = "%s WHERE %s=%%s" % (sql, keyField)
477    values.append(keyValue)
478
479    return (sql, values)
480
481def filter_keys(indict):
482    """Returns the dictionary with all numeric keys removed"""
483    return dict(filter(lambda x:type(x[0])!=type(0), indict.items()))
484
485def createPassword(length):
486    """Creates a new password of the specified length
487
488    The password is created of characters from the set [A-Za-z0-9_] as chosen
489    by the rand.randint function, with some easily confused characters removed.
490    """
491    import random
492    out = ""
493    exclude = re.compile("[^\w]|[0Ol1_]")
494   
495    len = 0
496    while len < length:
497        # Return a random ascii char between 33 and 126
498        char = chr(random.randint(33, 126))
499        # Check it's not excluded
500        if exclude.match(char) != None:
501            continue
502        # Append to string
503        out = "%s%s" % (out, char)
504        len += 1
505   
506    return out
507
508def createSalt():
509    """Creates an MD5 salt for a password
510
511    See crypt(3) for details.
512    """
513    import random
514    out = ""
515    exclude = re.compile("[a-zA-Z0-9./]")
516   
517    len = 0
518    while len < 8:
519        # Return a random ascii char between 33 and 126
520        char = chr(random.randint(33, 126))
521        # Check it's allowed
522        if exclude.match(char) == None:
523            continue
524        # Append to string
525        out = "%s%s" % (out, char)
526        len += 1
527   
528    return "$1$%s$" % out
529
530def ensureDirExists(dir):
531    """Ensures that the specified directory exists.
532
533    Obviously this will create parent directories too if need be.
534
535    Returns the number of directories that were created.
536    """
537
538    # If the path exists all is good
539    if os.path.exists(dir):
540        return 0
541   
542    # Otherwise check if the parent path exists
543    head, tail = os.path.split(dir)
544    n = ensureDirExists(head)
545   
546    # Now make the directory
547    os.mkdir(dir, 0750)
548    return 1 + n
549
550def ensureFileExists(file):
551    """Ensures that the specified file exists.
552
553    Obviously this will create parent directories too if need be.
554
555    Returns True if the file was successfully created.
556    """
557   
558    # If the file exists all is good
559    if os.path.exists(file):
560        return True
561   
562    try:
563        remountrw("ensureFileExists")
564       
565        # Ensure the parent path exists
566        ensureDirExists(os.path.dirname(file))
567       
568        # Touch the file
569        fp = open(file, "w")
570        fp.close()
571        remountro("ensureFileExists")
572    except:
573        remountro("ensureFileExists")
574        return False
575   
576    return True
577
578def removeDir(rDir):
579    """Recursively removes a directory and its children"""
580   
581    try:
582        for root, dirs, files in os.walk(rDir, topdown=False):
583            for name in files:
584                os.remove(os.path.join(root, name))
585            for name in dirs:
586                os.rmdir(os.path.join(root, name))
587        os.rmdir(rDir)
588    except OSError:
589        # Ignore dir not exists errors
590        pass
591
592def getIP(name):
593    """Resolves the hostname to an IP address"""
594
595    return socket.gethostbyname(name)
596
597def bitsToNetmask(length):
598    """Retuns an integer netmask representing the given number of bits"""
599    return (2 ** length) - 1L << (32 - length)
600def netmaskToBits(bits):
601    """Returns an integer represnting the bitlength of the given netmask"""
602    if bits==0: return 0
603    pos = 0
604    while ((2**pos) & bits) == 0:
605        pos+=1
606    return 32-pos
607def cidrToNetwork(cidr_network):
608    """Returns an integer network address from a CIDR string of the network"""
609    validateCIDR(cidr_network)
610    parts = str(cidr_network).split('/')
611    return ipnum(parts[0]) & bitsToNetmask(int(parts[1]))
612
613def cidrToNetworkS(cidr_network):
614    """Returns a string network address from a CIDR string of the network"""
615    return formatIP(cidrToNetwork(cidr_network))
616
617def cidrToNetmask(cidr_network):
618    """Returns an integer netmask from a CIDR string of the network"""
619    validateCIDR(cidr_network)
620    bits = int(str(cidr_network).split('/')[1])
621    return bitsToNetmask(bits)
622
623def cidrToIP(cidr_network):
624    validateCIDR(cidr_network)
625    return str(cidr_network).split('/')[0]
626
627def cidrToNetmaskS(cidr_network):
628    """Returns a string netmask from a CIDR string of the network"""
629    return formatIP(cidrToNetmask(cidr_network))
630
631def cidrToBroadcast(cidr_network):
632    """Returns the broadcast address from a CIDR string of the network"""
633    validateCIDR(cidr_network)
634    parts = str(cidr_network).split('/')
635    netmask = bitsToNetmask(int(parts[1]))
636    return ipbroadcast(ipnum(parts[0]), netmask)
637
638def cidrToBroadcastS(cidr_network):
639    """Returns a string broadcast address from a CIDR string of the network"""
640    return formatIP(cidrToBroadcast(cidr_network))
641
642def cidrToLength(cidr_network):
643    """Returns the mask length portion of a CIDR address"""
644    validateCIDR(cidr_network)
645    return int(str(cidr_network).split('/')[1])
646
647def ipnetwork(ip, netmask):
648    """Calculate an integer network address from a given ip and netmask"""
649    return ip & netmask
650
651def ipbroadcast(ip, netmask):
652    """Claculate an integer broadcast address from a given ip and netmask"""
653    return (ip & netmask) | (pow(2L,32)-1-netmask)
654
655def inNetwork(ip, network, netmask):
656    """Returns true if the specified IP is part of the specified network"""
657    return (ip & netmask)==network
658
659def ipnum(ip_netmask):
660    """Convert a IP string into an integer"""
661    if (ip_netmask == 0):
662        return 0
663    else:
664        return reduce(lambda a,b:a*256+b,
665                      map(int, str(ip_netmask).split(".")),0L)
666
667def ipcmp(a, b):
668    """Compare two IP addresses"""
669    return cmp(ipnum(a), ipnum(b))
670           
671def formatIP(ip):
672    """Returns a string formatted IP address from a numeric ip"""
673   
674    # Make sure our input is valid
675    try:
676        ip = int(ip)
677    except ValueError:
678        return ""
679   
680    result = ""
681   
682    # Move "left" along the integer prepending the octets to the string
683    step = 256
684    for i in range(1,5):
685       result = ".%s%s" % (str(ip%step), result)
686       ip /= step
687   
688    # First char will be an extra ., strip it and return
689    return result[1:]
690
691def formatIPB(ip):
692    """Returns a binary string formatted IP address from a numeric ip"""
693   
694    # Make sure our input is valid
695    try:
696        ip = int(ip)
697    except ValueError:
698        return ""
699   
700    result = ""
701   
702    # Move "left" along the integer prepending the octets to the string
703    step = 256
704    for i in range(1,5):
705        bits = ip%step
706        result = ".%s%s" % (binary(bits), result)
707        ip /= step
708   
709    # First char will be an extra ., strip it and return
710    return result[1:]
711
712def binary(bits):
713    """Returns a binary string for the specified octet"""
714    # Make sure our input is valid
715    try:
716        ip = int(ip)
717    except ValueError:
718        return ""
719    str = ""
720    pos = 0
721    while pos < 8:
722        if (bits & (2**pos)) != 0:
723            str = "1%s" % str
724        else:
725            str = "0%s" % str
726        pos+=1
727    return str
728
729def validateCIDR(cidr_network):
730    """Checks that the specified network in cidr format is valid"""
731    parts = str(cidr_network).split('/')
732    if len(parts) != 2:
733        raise ccsd_error("Invalid CIDR format! %s" % cidr_network)
734    try:
735        if int(parts[1]) < 0 or int(parts[1]) > 32:
736            raise ccsd_error("Invalid bitmask in CIDR expression! %s" % \
737                    cidr_network)
738    except:
739        raise ccsd_error("Invalid bitmask in CIDR expression! %s" % \
740                cidr_network)
741    octets = parts[0].split(".")
742    if len(octets) != 4:
743        raise ccsd_error("Invalid IP format! %s" % cidr_network)
744    for octet in octets:
745        try:
746            if int(octet) < 0 or int(octet)>255:
747                raise ccsd_error("Invalid octet value in CIDR expression!" \
748                        "%s" % cidr_network)
749        except:
750            raise ccsd_error("Invalid octet value in CIDR expression! %s" % \
751                    cidr_network)
752   
753    return
754
755def formatTime(secs, display_seconds=False):
756    """Take a period of time in seconds and format it as a readable string"""
757
758    days = hours = mins = 0
759   
760    if secs > (60*60*24):
761        days = int(secs/(60*60*24))
762        secs -= (days*(60*60*24))
763
764    if secs > (60*60):
765        hours = int(secs/(60*60))
766        secs -= (hours*(60*60))
767
768    if secs > 60:
769        mins = int(secs/60)
770        secs -= (mins*60)
771
772    str = ""
773    if days > 0:
774        str += pluralise(days, "day")
775        str += " "
776    if hours > 0:
777        str += pluralise(hours, "hour")
778        str += " "
779    if mins > 0:
780        str += pluralise(mins, "min")
781        str += " "
782    if secs > 0 and display_seconds:
783        str += pluralise(secs, "sec")
784
785    return str
786
787def roundTime(secs):
788    """Takes a period of time in seconds and rounds it to the nearest unit"""
789   
790    # Years
791    if secs > (60*60*24*365):
792        years = int(secs/(60*60*24*365))
793        return pluralise(years, "year")
794
795    # Months
796    if secs > (60*60*24*30):
797        months = int(secs/(60*60*24*30))
798        return pluralise(months, "month")
799
800    # Weeks
801    if secs > (60*60*24*7):
802        weeks = int(secs/(60*60*24*7))
803        return pluralise(weeks, "week")
804
805    # Days
806    if secs > (60*60*24):
807        days = int(secs/(60*60*24))
808        return pluralise(days, "day")
809
810    # Hours
811    if secs > (60*60):
812        hours = int(secs/(60*60))
813        return pluralise(hours, "hour")
814
815    # Minutes
816    if secs > (60):
817        minutes = int(secs/(60))
818        return pluralise(minutes, "minute")
819   
820    # Seconds
821    return pluralise(int(secs), "sec")
822
823def pluralise(number, root):
824
825    if number > 1:
826        return "%s %ss" % (number, root)
827    else:
828        return "%s %s" % (number, root) 
829
830def formatBytes(bytes):
831    """Displays the number of bytes in the largest unit whose value is > 1"""
832   
833    if bytes > GIGA:
834        bytes /= GIGA
835        return "%2.2fGB" % bytes
836
837    if bytes > MEGA:
838        bytes /= MEGA
839        return "%2.1fMB" % bytes
840   
841    if bytes > KILO:
842        bytes /= KILO
843        return "%2.1fKB" % bytes
844
845    return "%sB" % bytes
846
847def remountrw(name=None):
848    """Ensures that the root drive is mounted read-write"""
849
850    if name is None:
851        name = sys.argv[0]
852   
853    os.system("/usr/bin/remountrw %s" % name)
854
855def remountro(name=None):
856    """Ensures that the root drive is mounted read-only"""
857
858    if name is None:
859        name = sys.argv[0]
860   
861    os.system("/usr/bin/remountro %s" % name)
862   
863def bisect(a, x, lo=0, hi=None, cmpfunc=cmp):
864    """Return the index where to insert item x in list a, assuming a is sorted.
865
866    The return value i is such that all e in a[:i] have e <= x, and all e in
867    a[i:] have e > x.  So if x already appears in the list, i points just
868    beyond the rightmost x already there.
869
870    Optional args lo (default 0) and hi (default len(a)) bound the
871    slice of a to be searched.
872    """
873
874    if hi is None:
875        hi = len(a)
876    while lo < hi:
877        mid = (lo+hi)//2
878        if cmpfunc(x,a[mid])<0: hi = mid
879        else: lo = mid+1
880    return lo
881
882def do_ioctl(ifname, req):
883    ifreq = (ifname + '\0'*32)[:32]
884    try:
885        sfd = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
886        result = fcntl.ioctl(sfd.fileno(), req, ifreq)
887        sfd.close()
888    except IOError:
889        return None
890    return result
891
892def isAdministrativeInterface(ifname):
893    """Returns true if the specified interface is classied as Administrative"""
894   
895    if ifname.startswith("lo") or ifname.startswith("dummy") or \
896            ifname.endswith("-base") or ifname.startswith("vbase") or \
897            ifname.startswith("wifi"):
898        return True
899
900    return False
901
902def getInterfaces(returnAll=False, returnOne=None):
903    """Reads the interface informatin from /proc"""
904    interfaces = []
905   
906    f=open("/proc/net/dev", "r")
907    for l in f.readlines()[2:]:
908        # Parse the line
909        name,l = l.strip().split(":")
910        parts = [name] + l.split()
911        # Store the information
912        iface = {}
913        iface["name"] = parts[0]
914        # If we have a specific interface specified return only that
915        if returnOne is not None and iface["name"] != returnOne:
916            continue
917        # Skip 'Administrative' interfaces, the user doesn't care about them
918        if isAdministrativeInterface(iface["name"]) and not (returnAll or \
919                returnOne):
920            continue
921        # Get interface flags
922        t = do_ioctl(parts[0], SIOCGIFFLAGS)
923        if t:
924            iface["flags"], = struct.unpack("H", t[16:18])
925        else:
926            iface["flags"] = 0
927        iface["up"] = iface["flags"] & IFF_UP or False
928        # Skip down interfaces
929        if not iface["up"] and not (returnAll or returnOne):
930            continue
931        rcv = {}
932        rcv["bytes"] = parts[1]
933        rcv["packets"] = parts[2]
934        rcv["errs"] = parts[3]
935        rcv["drop"] = parts[4]
936        rcv["fifo"] = parts[5]
937        rcv["frame"] = parts[6]
938        rcv["compressed"] = parts[7]
939        rcv["multicast"] = parts[8]
940        iface["rcv_stats"] = rcv
941        tx = {}
942        tx["bytes"] = parts[9]
943        tx["packets"] = parts[10]
944        tx["errs"] = parts[11]
945        tx["drop"] = parts[12]
946        tx["fifo"] = parts[13]
947        tx["frame"] = parts[14]
948        tx["compressed"] = parts[15]
949        tx["multicast"] = parts[16]
950        iface["tx_stats"] = tx
951        t = do_ioctl(parts[0], SIOCGIFADDR)
952        if t:
953            iface["address"] = socket.inet_ntoa(t[20:24])
954        else:
955            iface["address"] = ""
956        t = do_ioctl(parts[0], SIOCGIFNETMASK)
957        if t:
958            iface["address"] += "/%s" % \
959                    netmaskToBits(ipnum(socket.inet_ntoa(t[20:24])))
960        t = do_ioctl(parts[0], SIOCGIFHWADDR)
961        if t:
962            iface["mac"] = ""
963            for b in t[18:24]:
964                iface["mac"] += "%02x:" % ord(b)
965            iface["mac"] = iface["mac"][:len(iface["mac"])-1]
966        else:
967            iface["mac"] = ""
968        t = do_ioctl(parts[0], SIOCGIFINDEX)
969        if t:
970            iface["ifindex"] = int(struct.unpack("I",t[16:20])[0])
971        else:
972            iface["ifindex"] = -1
973        t = do_ioctl(parts[0], SIOCGIFMTU)
974        if t:
975            iface["mtu"] = int(struct.unpack("I",t[16:20])[0])
976        else:
977            iface["mtu"] = -1
978        flag_str = []
979        for flag, char in IFACE_FLAG_CHARS.items():
980            if (iface["flags"] & flag)==flag:
981                flag_str.append(char)
982        iface["flag_str"] = flag_str
983        interfaces.append(iface)
984    f.close()
985   
986    return interfaces
987
988def getInterfaceNames(returnAll=False):
989    """Return a list of internet names"""
990    interfaces = []
991   
992    f=open("/proc/net/dev", "r")
993    for l in f.readlines()[2:]:
994        # Parse the line
995        name,l = l.strip().split(":")
996        # Skip 'Administrative' interfaces, the user doesn't care about them
997        if isAdministrativeInterface(name) and not returnAll:
998            continue
999        interfaces.append(name)
1000    f.close()
1001
1002    return interfaces
1003
1004def getRoutes():
1005    """Reads the routing table from /proc"""
1006    routes = []
1007   
1008    f=open("/proc/net/route","r")
1009    for l in f.readlines()[1:]:
1010        # Parse the line
1011        iface,network,gateway,flags,x,x,metric,mask,x,x,x = l.split()
1012        # Parse the flags
1013        flags = int(flags, 16)
1014        flag_str = ""
1015        for flag, char in ROUTE_FLAG_CHARS.items():
1016            if (flags & flag)==flag:
1017                flag_str += char
1018        # Store the values
1019        route = {}
1020        route["network"] = formatIP(socket.ntohl(long(network, 16)))
1021        route["gateway"] = formatIP(socket.ntohl(long(gateway, 16)))
1022        route["netmask"] = formatIP(socket.ntohl(long(mask, 16)))
1023        route["iface"] = iface
1024        route["metric"] = metric
1025        route["flags"] = flag_str
1026        routes.append(route)
1027    f.close()
1028   
1029    # Kernel gives us a list sorted by netmask length, sort by prefix
1030    # as well
1031    routes.sort(route_cmp)
1032   
1033    return routes
1034
1035def route_cmp(a, b):
1036    """Comparison function to sort the route table by netmask then prefix"""
1037
1038    # Compare netmask first
1039    rv = cmp(ipnum(a["netmask"]), ipnum(b["netmask"]))
1040    if rv != 0:
1041        return rv*-1
1042
1043    # Equal netmasks compare prefixes
1044    return cmp(ipnum(a["network"]), ipnum(b["network"]))*-1
1045
1046def getIfaceForIP(ipaddr):
1047    """Returns the name of the interface this IP address is connected to
1048
1049    Assumes no assymetric routing.
1050    """
1051   
1052    ip = ipnum(ipaddr)
1053    iface = None
1054   
1055    # Find the interface we would route to that client on
1056    routes = getRoutes()
1057    for r in routes:
1058        if not inNetwork(ip, ipnum(r["network"]), ipnum(r["netmask"])):
1059            continue
1060        # Found the network
1061        iface = r["iface"]
1062        break
1063   
1064    return iface
1065
1066def getIfaceIPForIP(ipaddr):
1067    """Returns the IP address of the iface connected to the specified IP
1068
1069    We assume that we don't have assymetric any routing
1070    """
1071
1072    # Find the interface we would route to that client on
1073    iface = getIfaceForIP(ipaddr)
1074   
1075    # Return the IP address for that interface
1076    ifaces = getInterfaces()
1077    for i in ifaces:
1078        if i["name"] != iface:
1079            continue
1080        return cidrToIP(i["address"])
1081
1082    # Uh-oh
1083    return ""
1084
1085def getNeighbourIP(ifname):
1086    """Returns the IP address of the interfaces neighbour
1087
1088    Assumes that if this interface has the lowest IP in the network, the
1089    neighbour will have the highest, and vice versa. If this interface has
1090    neither the lowest or the highest IP address, then the lowest IP is
1091    returned.
1092    """
1093   
1094    iface = getInterfaces(returnOne=ifname)
1095    if len(iface)!=1:
1096        return ""
1097    iface = iface[0]
1098
1099    ip = ipnum(cidrToIP(iface["address"]))
1100    net = cidrToNetwork(iface["address"])
1101    mask = cidrToNetmask(iface["address"])
1102    bcast = ipbroadcast(net, mask)
1103
1104    # If this interface has the lowest IP, neighbour is the highest
1105    if ip == (net+1):
1106        return formatIP(bcast-1)
1107
1108    # Otherwise return the lowest
1109    return formatIP(net+1)
1110   
1111def ensureFileContains(search, stanza, file):
1112    """Checks whether the specified file contains the specified stanza
1113
1114    Searchs for this stanza using the regexp specified in search. If this
1115    regexp does not match any part of the file then the stanza is appended
1116    to the end.
1117    """
1118
1119    # Open file and read all lines
1120    fp = open(file, "r")
1121    if not fp:
1122        return False
1123    content = "".join(fp.readlines())
1124    fp.close()
1125
1126    if re.search(search, content) is not None:
1127        # Stanza found
1128        return True
1129   
1130    # Not found, insert
1131    return appendFile(stanza, file)
1132   
1133def filterFile(search, replace, file):
1134    """Filters all lines of the file
1135   
1136    Replacing all instances of search with the value of replace.
1137    """
1138   
1139    # Open file and read all lines
1140    fp = open(file, "r")
1141    if not fp:
1142        return False
1143    lines = fp.readlines()
1144    fp.close()
1145   
1146    # Perform the substitution
1147    nlines = [re.sub(search, replace, line) for line in lines]
1148
1149    # Check if the replace value is found, return false if not
1150    tstr = "\n".join(nlines)
1151    if tstr.find(replace) == -1:
1152        return False
1153
1154    # Write out the file again
1155    fp = open(file, "w")
1156    if not fp:
1157        return False
1158    fp.writelines(nlines)
1159    fp.close()
1160   
1161    return True
1162
1163def appendFile(string, file):
1164    """Appends the string to the file"""
1165    fp = open(file, "a")
1166    if not fp:
1167        return False
1168    fp.write(string)
1169    fp.close()
1170   
1171    return True
1172
1173def getSoekrisMac(serial, iface=0):
1174    """Returns (in hexadecimal format) the MAC address for a soekris interface
1175
1176    The mac address is derived from the soekris serial number via the format
1177    described in the following mailing post:
1178    http://lists.soekris.com/pipermail/soekris-tech/2005-August/023941.html
1179
1180    For posterity:
1181    00 00 24 CX XX XX, where the XXXXX are the serial number from label on
1182    the bottom multiplied by 4, then add 0, 1, 2 for each eth controller.
1183    """
1184   
1185    if serial < 0 or serial > 0xFFFFF:
1186        raise ccsd_error("Invalid soekris Serial number!")
1187    if iface < 0 or iface > 3:
1188        raise ccsd_error("Invalid interface number!")
1189   
1190    # Calculation is easy
1191    mac = int(serial)*4 + iface
1192   
1193    # Now convert to hexadecimal
1194    hexmac = "%05x" % mac
1195    return "00:00:24:c%s:%s:%s" % (hexmac[0:1], hexmac[1:3], hexmac[3:])
1196
1197def getSoekrisSerial(mac):
1198    """Returns the serial number for a soekris given the MAC of eth0
1199
1200    See the getSoekrisMac function for details of how this works.
1201
1202    The incoming mac must be formatted as:
1203    XX:XX:XX:XX:XX:XX
1204    """
1205   
1206    if len(mac) != 17 or not \
1207            (mac.startswith("00:00:24:c") or mac.startswith("00:00:24:C")):
1208        # Invalid MAC address
1209        raise ccsd_error("Invalid soekris MAC address")
1210   
1211    serial = int(mac.replace(":", "")[-5:], 16)/4
1212    return serial
1213
1214def getGatewayIP():
1215    """Parses the routing table to retrieve the default GW IP address"""
1216
1217    routes = getRoutes()
1218    if len(routes) <= 0:
1219        return ""
1220
1221    # Try the last one first
1222    t = routes[len(routes)-1]
1223    if t["network"]=="0.0.0.0" and t["netmask"]=="0.0.0.0":
1224        return t["gateway"]
1225
1226    # Loop through
1227    for route in routes:
1228        if route["network"]=="0.0.0.0" and route["netmask"]=="0.0.0.0":
1229            return t["gateway"]
1230
1231    # No default gateway
1232    return ""
1233
1234
1235def getGatewayIface():
1236    """Parses the routing table to retrieve the default GW interface"""
1237
1238    routes = getRoutes()
1239    if len(routes) <= 0:
1240        return ""
1241
1242    # Try the last one first
1243    t = routes[len(routes)-1]
1244    if t["network"]=="0.0.0.0" and t["netmask"]=="0.0.0.0":
1245        return t["iface"]
1246
1247    # Loop through
1248    for route in routes:
1249        if route["network"]=="0.0.0.0" and route["netmask"]=="0.0.0.0":
1250            return t["iface"]
1251
1252    # No default gateway
1253    return ""
1254
1255def isHostUp(ip):
1256    """Using fping to test whether a host is alive"""
1257    if ip == "" or ip is None:
1258        return 0
1259    command = "fping -am  %s" % ip
1260    result = os.popen("%s 2>/dev/null" % command).readlines()
1261    return len(result)
1262
1263def getNeighbourMAC(neigh_ip):
1264    """Returns the MAC address for a directly connected neighbour"""
1265
1266    # Ping the neighbour first to ensure we have an ARP entry
1267    os.system("fping -c 1 -r 1 -a %s 2>/dev/null" % neigh_ip)
1268
1269    # Read the arp cache from /proc
1270    fp = open("/proc/net/arp", "r")
1271    lines = fp.readlines()
1272    fp.close()
1273   
1274    for line in lines[1:]:
1275        parts = line.strip().split()
1276        if len(parts) != 6:
1277            continue
1278        if parts[0] == neigh_ip:
1279            return parts[3]
1280
1281    # Not found
1282    return None
1283
1284def processExists(pname):
1285    """Returns true if the named process is currently running"""
1286    fh = os.popen("/bin/ps | /bin/grep %s | /bin/grep -v grep" % pname)
1287    output = fh.readlines()
1288    rv = fh.close()
1289    if len(output) > 0:
1290        return True
1291    return False
1292
1293def isValidMAC(mac):
1294    """Checks that the specified MAC address is valid"""
1295   
1296    if re.match("^([0-9a-f]{1,2}:){5}[0-9a-f]{1,2}$", mac.lower()) is None:
1297        return False
1298   
1299    return True
Note: See TracBrowser for help on using the repository browser.