source: branches/numpy/anuga/utilities/system_tools.py @ 7024

Last change on this file since 7024 was 7024, checked in by rwilson, 15 years ago

Made 'tar a file' function more general.

File size: 18.9 KB
Line 
1"""Implementation of tools to do with system administration made as platform independent as possible.
2
3
4"""
5
6import sys
7import os
8import string
9import urllib
10import urllib2
11import getpass
12import tarfile
13import md5
14
15
16def log_to_file(filename, s, verbose=False):
17    """Log string to file name
18    """
19
20    fid = open(filename, 'a')
21    if verbose: print s
22    fid.write(s + '\n')
23    fid.close()
24
25
26def get_user_name():
27    """Get user name provide by operating system
28    """
29
30    if sys.platform == 'win32':
31        #user = os.getenv('USERPROFILE')
32        user = os.getenv('USERNAME')
33    else:
34        user = os.getenv('LOGNAME')
35
36
37    return user   
38
39def get_host_name():
40    """Get host name provide by operating system
41    """
42
43    if sys.platform == 'win32':
44        host = os.getenv('COMPUTERNAME')
45    else:
46        host = os.uname()[1]
47
48
49    return host   
50
51def get_revision_number():
52    """Get the version number of this repository copy.
53
54    Try getting data from stored_version_info.py first, otherwise
55    try using SubWCRev.exe (Windows) or svnversion (linux), otherwise
56    try reading file .svn/entries for version information, otherwise
57    throw an exception.
58
59    NOTE: This requires that the command svn is on the system PATH
60    (simply aliasing svn to the binary will not work)
61    """
62
63    def get_revision_from_svn_entries():
64        '''Get a subversion revision number from the .svn/entires file.'''
65
66        msg = '''
67No version info stored and command 'svn' is not recognised on the system PATH.
68
69If ANUGA has been installed from a distribution e.g. as obtained from SourceForge,
70the version info should be available in the automatically generated file
71'stored_version_info.py' in the anuga root directory.
72
73If run from a Subversion sandpit, ANUGA will try to obtain the version info by
74using the command 'svn info'.  In this case, make sure the command line client
75'svn' is accessible on the system path.  Simply aliasing 'svn' to the binary will
76not work.
77
78If you are using Windows, you have to install the file svn.exe which can be
79obtained from http://www.collab.net/downloads/subversion.
80
81Good luck!
82'''
83
84        try:
85            fd = open(os.path.join('.svn', 'entries'))
86        except:
87            raise Exception, msg
88
89        line = fd.readlines()[3]
90        fd.close()
91        try:
92            revision_number = int(line)
93        except:
94            msg = ".svn/entries, line 4 was '%s'?" % line.strip()
95            raise Exception, msg
96
97        return revision_number
98
99    def get_revision_from_svn_client():
100        '''Get a subversion revision number from an svn client.'''
101
102        if sys.platform[0:3] == 'win':
103            try:
104                fid = os.popen(r'C:\Program Files\TortoiseSVN\bin\SubWCRev.exe')
105            except:
106                return get_revision_from_svn_entries()
107            else:
108                version_info = fid.read()
109                if version_info == '':
110                    return get_revision_from_svn_entries()
111
112            # split revision number from data
113            for line in version_info.split('\n'):
114                if line.startswith('Updated to revision '):
115                    break
116
117            fields = line.split(' ')
118            msg = 'Keyword "Revision" was not found anywhere in text: %s' % version_info
119            assert fields[0].startswith('Updated'), msg
120
121            try:
122                revision_number = int(fields[3])
123            except:
124                msg = ("Revision number must be an integer. I got '%s' from "
125                       "'SubWCRev.exe'." % fields[3])
126                raise Exception, msg
127        else:                   # assume Linux
128            try:
129                fid = os.popen('svnversion -n . 2>/dev/null')
130            except:
131                return get_revision_from_svn_entries()
132            else:
133                version_info = fid.read()
134                if version_info == '':
135                    return get_revision_from_svn_entries()
136
137            # split revision number from data
138            if ':' in version_info:
139                (_, revision_number) =  version_info.split(':')
140            elif version_info.endswith('M'):
141                revision_number = version_info[:-1]
142            else:
143                revision_number = version_info
144
145            try:
146                revision_number = int(revision_number)
147            except:
148                msg = ("Revision number must be an integer. I got '%s' from "
149                       "'svn'." % version_info)
150                raise Exception, msg
151
152        return revision_number
153
154    # try to get revision information from stored_version_info.py
155    try:
156        from anuga.stored_version_info import version_info
157    except:
158        return get_revision_from_svn_client()
159
160    # split revision number from data
161    for line in version_info.split('\n'):
162        if line.startswith('Revision:'):
163            break
164
165    fields = line.split(':')
166    msg = 'Keyword "Revision" was not found anywhere in text: %s' % version_info
167    assert fields[0].startswith('Revision'), msg
168
169    try:
170        revision_number = int(fields[1])
171    except:
172        msg = ("Revision number must be an integer. I got '%s'.\n"
173               'Check that the command svn is on the system path.'
174               % fields[1])
175        raise Exception, msg
176
177    return revision_number
178
179
180def store_version_info(destination_path='.', verbose=False):
181    """Obtain current version from Subversion and store it.
182   
183    Title: store_version_info()
184
185    Author: Ole Nielsen (Ole.Nielsen@ga.gov.au)
186
187    CreationDate: January 2006
188
189    Description:
190        This function obtains current version from Subversion and stores it
191        is a Python file named 'stored_version_info.py' for use with
192        get_version_info()
193
194        If svn is not available on the system PATH, an Exception is thrown
195    """
196
197    # Note (Ole): This function should not be unit tested as it will only
198    # work when running out of the sandpit. End users downloading the
199    # ANUGA distribution would see a failure.
200    #
201    # FIXME: This function should really only be used by developers (
202    # (e.g. for creating new ANUGA releases), so maybe it should move
203    # to somewhere else.
204   
205    import config
206
207    try:
208        fid = os.popen('svn info')
209    except:
210        msg = 'Command "svn" is not recognised on the system PATH'
211        raise Exception(msg)
212    else:   
213        txt = fid.read()
214        fid.close()
215
216
217        # Determine absolute filename
218        if destination_path[-1] != os.sep:
219            destination_path += os.sep
220           
221        filename = destination_path + config.version_filename
222
223        fid = open(filename, 'w')
224
225        docstring = 'Stored version info.\n\n'
226        docstring += 'This file provides the version for distributions '
227        docstring += 'that are not accessing Subversion directly.\n'
228        docstring += 'The file is automatically generated and should not '
229        docstring += 'be modified manually.\n'
230        fid.write('"""%s"""\n\n' %docstring)
231       
232        fid.write('version_info = """\n%s"""' %txt)
233        fid.close()
234
235
236        if verbose is True:
237            print 'Version info stored to %s' %filename
238
239
240def safe_crc(string):
241    """64 bit safe crc computation.
242
243       See Guido's 64 bit fix at http://bugs.python.org/issue1202           
244    """
245
246    from zlib import crc32
247    import os
248
249    x = crc32(string)
250       
251    if os.name == 'posix' and os.uname()[4] in ['x86_64', 'ia64']:
252        crcval = x - ((x & 0x80000000) << 1)
253    else:
254        crcval = x
255       
256    return crcval
257
258
259def compute_checksum(filename, max_length=2**20):
260    """Compute the CRC32 checksum for specified file
261
262    Optional parameter max_length sets the maximum number
263    of bytes used to limit time used with large files.
264    Default = 2**20 (1MB)
265    """
266
267    fid = open(filename, 'rb') # Use binary for portability
268    crcval = safe_crc(fid.read(max_length))
269    fid.close()
270
271    return crcval
272
273def get_pathname_from_package(package):
274    """Get pathname of given package (provided as string)
275
276    This is useful for reading files residing in the same directory as
277    a particular module. Typically, this is required in unit tests depending
278    on external files.
279
280    The given module must start from a directory on the pythonpath
281    and be importable using the import statement.
282
283    Example
284    path = get_pathname_from_package('anuga.utilities')
285
286    """
287
288    exec('import %s as x' %package)
289
290    path = x.__path__[0]
291   
292    return path
293
294    # Alternative approach that has been used at times
295    #try:
296    #    # When unit test is run from current dir
297    #    p1 = read_polygon('mainland_only.csv')
298    #except:
299    #    # When unit test is run from ANUGA root dir
300    #    from os.path import join, split
301    #    dir, tail = split(__file__)
302    #    path = join(dir, 'mainland_only.csv')
303    #    p1 = read_polygon(path)
304       
305   
306##
307# @brief Split a string into 'clean' fields.
308# @param str The string to process.
309# @param delimiter The delimiter string to split 'line' with.
310# @return A list of 'cleaned' field strings.
311# @note Any fields that were initially zero length will be removed.
312# @note If a field contains '\n' it isn't zero length.
313def clean_line(str, delimiter):
314    """Split string on given delimiter, remove whitespace from each field."""
315
316    return [x.strip() for x in str.strip().split(delimiter) if x != '']
317
318
319################################################################################
320# The following two functions are used to get around a problem with numpy and
321# NetCDF files.  Previously, using Numeric, we could take a list of strings and
322# convert to a Numeric array resulting in this:
323#     Numeric.array(['abc', 'xy']) -> [['a', 'b', 'c'],
324#                                      ['x', 'y', ' ']]
325#
326# However, under numpy we get:
327#     numpy.array(['abc', 'xy']) -> ['abc',
328#                                    'xy']
329#
330# And writing *strings* to a NetCDF file is problematic.
331#
332# The solution is to use these two routines to convert a 1-D list of strings
333# to the 2-D list of chars form and back.  The 2-D form can be written to a
334# NetCDF file as before.
335#
336# The other option, of inverting a list of tag strings into a dictionary with
337# keys being the unique tag strings and the key value a list of indices of where
338# the tag string was in the original list was rejected because:
339#    1. It's a lot of work
340#    2. We'd have to rewite the I/O code a bit (extra variables instead of one)
341#    3. The code below is fast enough in an I/O scenario
342################################################################################
343
344##
345# @brief Convert 1-D list of strings to 2-D list of chars.
346# @param l 1-dimensional list of strings.
347# @return A 2-D list of 'characters' (1 char strings).
348# @note No checking that we supply a 1-D list.
349def string_to_char(l):
350    '''Convert 1-D list of strings to 2-D list of chars.'''
351
352    if not l:
353        return []
354
355    if l == ['']:
356        l = [' ']
357
358    maxlen = reduce(max, map(len, l))
359    ll = [x.ljust(maxlen) for x in l]
360    result = []
361    for s in ll:
362        result.append([x for x in s])
363    return result
364
365
366##
367# @brief Convert 2-D list of chars to 1-D list of strings.
368# @param ll 2-dimensional list of 'characters' (1 char strings).
369# @return A 1-dimensional list of strings.
370# @note Each string has had right-end spaces removed.
371def char_to_string(ll):
372    '''Convert 2-D list of chars to 1-D list of strings.'''
373
374    return map(string.rstrip, [''.join(x) for x in ll])
375
376################################################################################
377
378##
379# @brief Get list of variable names in a python expression string.
380# @param source A string containing a python expression.
381# @return A list of variable name strings.
382# @note Throws SyntaxError exception if not a valid expression.
383def get_vars_in_expression(source):
384    '''Get list of variable names in a python expression.'''
385
386    import compiler
387    from compiler.ast import Node
388
389    ##
390    # @brief Internal recursive function.
391    # @param node An AST parse Node.
392    # @param var_list Input list of variables.
393    # @return An updated list of variables.
394    def get_vars_body(node, var_list=[]):
395        if isinstance(node, Node):
396            if node.__class__.__name__ == 'Name':
397                for child in node.getChildren():
398                    if child not in var_list:
399                        var_list.append(child)
400            for child in node.getChildren():
401                if isinstance(child, Node):
402                    for child in node.getChildren():
403                        var_list = get_vars_body(child, var_list)
404                    break
405
406        return var_list
407
408    return get_vars_body(compiler.parse(source))
409
410
411##
412# @brief Get a file from the web.
413# @param file_url URL of the file to fetch.
414# @param file_name Path to file to create in the filesystem.
415# @param auth Auth tuple (httpproxy, proxyuser, proxypass).
416# @param blocksize Read file in this block size.
417# @return (True, auth) if successful, else (False, auth).
418# @note If 'auth' not supplied, will prompt user.
419# @note Will try using environment variable HTTP_PROXY for proxy server.
420# @note Will try using environment variable PROXY_USERNAME for proxy username.
421# @note Will try using environment variable PROXY_PASSWORD for proxy password.
422def get_web_file(file_url, file_name, auth=None, blocksize=1024*1024):
423    '''Get a file from the web (HTTP).
424
425    file_url:  The URL of the file to get
426    file_name: Local path to save loaded file in
427    auth:      A tuple (httpproxy, proxyuser, proxypass)
428    blocksize: Block size of file reads
429   
430    Will try simple load through urllib first.  Drop down to urllib2
431    if there is a proxy and it requires authentication.
432
433    Environment variable HTTP_PROXY can be used to supply proxy information.
434    PROXY_USERNAME is used to supply the authentication username.
435    PROXY_PASSWORD supplies the password, if you dare!
436    '''
437
438    # Simple fetch, if fails, check for proxy error
439    try:
440        urllib.urlretrieve(file_url, file_name)
441        return (True, auth)     # no proxy, no auth required
442    except IOError, e:
443        if e[1] == 407:     # proxy error
444            pass
445        elif e[1][0] == 113:  # no route to host
446            print 'No route to host for %s' % file_url
447            return (False, auth)    # return False
448        else:
449            print 'Unknown connection error to %s' % file_url
450            return (False, auth)
451
452    # We get here if there was a proxy error, get file through the proxy
453    # unpack auth info
454    try:
455        (httpproxy, proxyuser, proxypass) = auth
456    except:
457        (httpproxy, proxyuser, proxypass) = (None, None, None)
458
459    # fill in any gaps from the environment
460    if httpproxy is None:
461        httpproxy = os.getenv('HTTP_PROXY')
462    if proxyuser is None:
463        proxyuser = os.getenv('PROXY_USERNAME')
464    if proxypass is None:
465        proxypass = os.getenv('PROXY_PASSWORD')
466
467    # Get auth info from user if still not supplied
468    if httpproxy is None or proxyuser is None or proxypass is None:
469        print '-'*72
470        print ('You need to supply proxy authentication information.')
471        if httpproxy is None:
472            httpproxy = raw_input('                    proxy server: ')
473        else:
474            print '         HTTP proxy was supplied: %s' % httpproxy
475        if proxyuser is None:
476            proxyuser = raw_input('                  proxy username: ') 
477        else:
478            print 'HTTP proxy username was supplied: %s' % proxyuser
479        if proxypass is None:
480            proxypass = getpass.getpass('                  proxy password: ')
481        else:
482            print 'HTTP proxy password was supplied: %s' % '*'*len(proxyuser)
483        print '-'*72
484
485    # the proxy URL cannot start with 'http://', we add that later
486    httpproxy = httpproxy.lower()
487    if httpproxy.startswith('http://'):
488        httpproxy = httpproxy.replace('http://', '', 1)
489
490    # open remote file
491    proxy = urllib2.ProxyHandler({'http': 'http://' + proxyuser
492                                              + ':' + proxypass
493                                              + '@' + httpproxy})
494    authinfo = urllib2.HTTPBasicAuthHandler()
495    opener = urllib2.build_opener(proxy, authinfo, urllib2.HTTPHandler)
496    urllib2.install_opener(opener)
497    try:
498        webget = urllib2.urlopen(file_url)
499    except urllib2.HTTPError, e:
500        print 'Error received from proxy:\n%s' % str(e)
501        print 'Possibly the user/password is wrong.'
502        return (False, (httpproxy, proxyuser, proxypass))
503
504    # transfer file to local filesystem
505    fd = open(file_name, 'wb')
506    while True:
507        data = webget.read(blocksize)
508        if len(data) == 0:
509            break
510        fd.write(data)
511    fd.close
512    webget.close()
513
514    # return successful auth info
515    return (True, (httpproxy, proxyuser, proxypass))
516
517
518##
519# @brief Tar a file (or directory) into a tarfile.
520# @param files A list of files (or directories) to tar.
521# @param tarfile The created tarfile name.
522# @note 'files' may be a string (single file) or a list of strings.
523# @note We use gzip compression.
524def tar_file(files, tarname):
525    '''Compress a file or directory into a tar file.'''
526
527    if isinstance(files, basestring):
528        files = [files]
529
530    o = tarfile.open(tarname, 'w:gz')
531    for file in files:
532        o.add(file)
533    o.close()
534
535
536##
537# @brief Untar a file into an optional target directory.
538# @param tarname Name of the file to untar.
539# @param target_dir Directory to untar into.
540def untar_file(tarname, target_dir='.'):
541    '''Uncompress a tar file.'''
542
543    o = tarfile.open(tarname, 'r:gz')
544    members = o.getmembers()
545    for member in members:
546        o.extract(member, target_dir)
547    o.close()
548
549
550##
551# @brief Return a hex digest (MD5) of a given file.
552# @param filename Path to the file of interest.
553# @param blocksize Size of data blocks to read.
554# @return A hex digest string (16 bytes).
555# @note Uses MD5 digest.
556def get_file_hexdigest(filename, blocksize=1024*1024*10):
557    '''Get a hex digest of a file.'''
558   
559    m = md5.new()
560    fd = open(filename, 'r')
561           
562    while True:
563        data = fd.read(blocksize)
564        if len(data) == 0:
565            break
566        m.update(data)
567                                                               
568    fd.close()
569    return m.hexdigest()
570
571    fd = open(filename, 'r')
572
573
574##
575# @brief Create a file containing a hexdigest string of a data file.
576# @param data_file Path to the file to get the hexdigest from.
577# @param digest_file Path to hexdigest file to create.
578# @note Uses MD5 digest.
579def make_digest_file(data_file, digest_file):
580    '''Create a file containing the hex digest string of a data file.'''
581   
582    hexdigest = get_file_hexdigest(data_file)
583    fd = open(digest_file, 'w')
584    fd.write(hexdigest)
585    fd.close()
586
587
588##
589# @brief Function to return the length of a file.
590# @param in_file Path to file to get length of.
591# @return Number of lines in file.
592# @note Doesn't count '\n' characters.
593# @note Zero byte file, returns 0.
594# @note No \n in file at all, but >0 chars, returns 1.
595def file_length(in_file):
596    '''Function to return the length of a file.'''
597
598    fid = open(in_file)
599    data = fid.readlines()
600    fid.close()
601    return len(data)
602
603
Note: See TracBrowser for help on using the repository browser.