source: branches/1.0/sara_cmt/bin/cmt @ 14178

Last change on this file since 14178 was 14178, checked in by sil, 12 years ago

Prepended terms for redistribution and changes in the sourcefiles, and added a SARA-specific copyright- and warranty notices.

  • Property svn:executable set to *
File size: 14.9 KB
Line 
1#!/usr/bin/env python
2
3#    This file is part of CMT, a Cluster Management Tool made at SARA.
4#    Copyright (C) 2012  Sil Westerveld
5#
6#    This program is free software; you can redistribute it and/or modify
7#    it under the terms of the GNU General Public License as published by
8#    the Free Software Foundation; either version 2 of the License, or
9#    (at your option) any later version.
10#
11#    This program is distributed in the hope that it will be useful,
12#    but WITHOUT ANY WARRANTY; without even the implied warranty of
13#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14#    GNU General Public License for more details.
15#
16#    You should have received a copy of the GNU General Public License
17#    along with this program; if not, write to the Free Software
18#    Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
19
20#####
21#
22# <Setup environment for Django's DB API>
23#
24from sara_cmt import settings
25from django.core.management import setup_environ
26setup_environ(settings)
27import os
28os.environ['DJANGO_SETTINGS_MODULE'] = settings.__name__
29#
30# </Setup environment for Django's DB API>
31#
32#####
33
34#####
35#
36# <Some imports for our Django-template related action>
37#
38from django.template.loader import render_to_string
39from django.template import TemplateDoesNotExist
40import re
41#
42# </Some imports for our Django-template related action>
43#
44#####
45
46from types import ListType, StringTypes
47
48import datetime
49
50import ConfigParser
51import sys
52
53from sara_cmt.django_cli import ModelExtension, ObjectManager, QueryManager, \
54    logger, parser
55
56import sara_cmt.cluster.models
57
58
59from django.db.models import get_model
60
61
62def search_model(value):
63    return get_model('cluster', value)
64
65
66#####
67#
68# <Decorators/etc/cmt/>
69#
70
71
72def crud_validate(func):
73    """
74        Validate the entity given as an argument to CRUD-functions
75    """
76
77    def crudFunc(option, opt_str, value, parser, *args, **kwargs):
78        model = search_model(value)
79        if model:
80            return func(option, opt_str, value, parser, *args, **kwargs)
81        else:
82            logger.error('Entity %s not known.' % (value.__repr__()))
83            sys.exit(1)
84    return crudFunc
85#
86# </Decorators>
87#
88#####
89
90
91
92#####
93#
94# <CMT-specific settings from file>
95#
96
97# Instantiate ConfigParser
98#TODO: think about a (better) way to make this dynamic:
99import site
100
101if site.sys.prefix in [ '/usr', '/' ]:
102    ETC_PREPEND = ''
103else:
104    ETC_PREPEND = site.sys.prefix
105
106configfile = '%s/etc/cmt/cmt.conf' % ETC_PREPEND
107config_parser = ConfigParser.ConfigParser()
108config_parser.optionxform = lambda x: x
109# ^^ Hack for case-insensitivity, reference:
110#    http://www.finalcog.com/python-config-parser-lower-case-names
111config_parser.read(configfile)
112
113# Setup the logger for logging
114loglevel = config_parser.get('defaults', 'LOGLEVEL')
115if loglevel == 'NOTSET' and settings.DEBUG == True:
116    logger.setLevel(config_parser.getint('loglevels', 'DEBUG'))
117else:
118    logger.setLevel(config_parser.getint('loglevels', loglevel))
119
120
121# Collect package information
122CMTSARA_VERSION = config_parser.get('info', 'version').strip("'")
123CMTSARA_DESCRIPTION = config_parser.get('info', 'description').strip("'")
124
125#
126# </CMT-specific settings from file>
127#
128#####
129
130
131
132#####
133#
134# <Managers for database related interactions>
135#
136object_mgr = ObjectManager()
137query_mgr = QueryManager()
138#
139# </Managers for database related interactions>
140#
141#####
142
143
144
145#####
146#
147# <CRUD methods>
148#
149
150
151@crud_validate
152def add(option, opt_str, value, parser, *args, **kwargs):
153    """
154        Add an object according to the given values.
155
156        When the INTERACTIVE-flag has been set and an object is not complete
157        yet, the user should be given the opportunity to add data to the
158        missing fields on an interactive manner.
159    """
160    my_args = collect_args(option, parser)    # get method-specific args-list
161    query_mgr.push_args(my_args, search_model(value), ['set'])
162    query = query_mgr.get_query()
163
164    new_obj = search_model(value)()
165    logger.debug('Initiated a new %s' % new_obj.__class__.__name__)
166
167    new_obj.setattrs_from_dict(query['set'])
168
169    # Complete if needed (only in interactive mode)
170    if parser.values.INTERACTIVE:
171        new_obj.interactive_completion()
172
173    # Save object
174    save_msg = 'Added a new %s'%value
175    if not parser.values.DRYRUN:
176        try:
177            new_obj.save()
178        except Exception, e:
179            logger.error('Could not add new %s: '%(value,e))
180    else:
181        logger.info('[DRYRUN] %s'%save_msg)
182
183
184@crud_validate
185def show(option, opt_str, value, parser, *args, **kwargs):
186    """
187        Show the objects which match the given values (queries).
188    """
189    # Split all given args to dict
190    my_args = collect_args(option, parser)
191    query_mgr.push_args(my_args, search_model(value), ['get'])
192    query = query_mgr.get_query()
193
194    objects = object_mgr.get_objects(query)
195
196    # !!! TODO: either print short, or print long lists !!!
197    # !!! TODO: use config files for fields (not) to print (maybe
198    for _object in objects:
199        ModelExtension.display(_object)
200
201
202@crud_validate
203def change(option, opt_str, value, parser, *args, **kwargs):
204    """
205        Search for an object which matches the given values, and change
206        it/them.
207    """
208    my_args = collect_args(option, parser)
209    query_mgr.push_args(my_args, search_model(value), ['get', 'set'])
210    query = query_mgr.get_query()
211
212    objects = object_mgr.get_objects(query)
213
214    if objects:
215        # Temporary try-except. See subtrac.sara.nl/osd/interne-services/ticket/164
216        try:
217            logger.info('Found %s entities matching query: %s'
218                % (len(objects), ', '.join([_object.label for _object in objects])))
219        except AttributeError:
220            logger.info('Found %s entities matching query: %s'
221                % (len(objects), ', '.join([_object.__unicode__() for _object in objects])))
222        confirmed = not parser.values.INTERACTIVE or \
223            raw_input('Are you sure? [Yn] ')
224        if confirmed in ['', 'y', 'Y', True]:
225            for _object in objects:
226                attr_set_msg = 'Attributes has been set: %s' % _object
227                if not parser.values.DRYRUN:
228                    _object.setattrs_from_dict(query['set'])
229                    logger.debug(attr_set_msg)
230                else:
231                    logger.debug('[DRYRUN] %s' % attr_set_msg)
232        else:
233            logger.info('Change has been cancelled')
234
235
236@crud_validate
237def remove(option, opt_str, value, parser, *args, **kwargs):
238    """
239        Remove the objects which match the given values (queries).
240    """
241    my_args = collect_args(option, parser)
242    query_mgr.push_args(my_args, search_model(value), ['get'])
243    query = query_mgr.get_query()
244
245    objects = object_mgr.get_objects(query)
246
247    if objects:
248        logger.info('Found %s objects matching query: %s'\
249            % (len(objects), ', '.join([_object.__str__()
250                for _object in objects])))
251        confirmation = not parser.values.INTERACTIVE or \
252            raw_input('Are you sure? [Yn] ')
253        print 'confirmation', confirmation
254        # Delete and log
255        if confirmation in ['', 'y', 'Y', True]:
256            logger.info('deleting...')
257            for _object in objects:
258                del_msg = 'Deleted %s' % _object
259                if not parser.values.DRYRUN:
260                    _object.delete()
261                    logger.info(del_msg)
262                else:
263                    logger.info('[DRYRUN] %s' % del_msg)
264
265    else:
266        logger.info('No existing objects found matching query')
267#
268# </CRUD methods>
269#
270#####
271
272
273def generate(option, opt_str, value, parser, *args, **kwargs):
274    from django.template import Context
275
276    # Save full path of templatefile to generate
277    template_filename = value
278
279    # Make a dict with filenames of the available templates
280    files = [f for f in os.listdir(settings.CMT_TEMPLATES_DIR) if f[-4:]=='.cmt']
281    files.sort()
282
283    fdict = {}
284    i = 1
285    for f in files:
286        fdict[i] = f
287        i+=1
288
289    if template_filename[-4:] != '.cmt':
290        template_filename += '.cmt'
291
292    # Loop until a valid template has been chosen by the user
293    while template_filename not in fdict.values():
294        logger.warning("File '%s' not known"%template_filename)
295
296        # Give a numbered overview of the available templates
297        for key,val in fdict.items():
298            print '%s : %s'%(str(key).rjust(2),val)
299        logger.debug('fdict: %s'%fdict.values())
300        template_filename = raw_input('\nChoose: ')
301
302        # If number given, lookup the filename in the dictionary
303        if template_filename.isdigit():
304            num = int(template_filename)
305            if num <= len(fdict):
306                template_filename = fdict[num]
307                logger.debug('filename: %s'%template_filename)
308            else:
309                continue
310        # Else check for the extension
311        elif template_filename[-4:] != '.cmt':
312            template_filename += '.cmt'
313
314        logger.debug('%s (%s)'%(template_filename,type(template_filename)))
315
316    template_fullpath = os.path.join(settings.CMT_TEMPLATES_DIR, template_filename)
317
318    try:
319        # Initialize a Context to render the template with
320        template_data = {}
321        template_data['version'] = CMTSARA_VERSION
322        template_data['svn_id'] = '$Id:$'
323        template_data['svn_url'] = '$URL:$'
324        template_data['input'] = template_fullpath
325        template_data['__template_outputfiles__'] = {} # reserved for data to write to files
326        context = Context(template_data)
327
328        template_data['stores'] = template_data['__template_outputfiles__'] # to stay bw compatible with ramon's code (temporary)
329       
330        rendered_string = render_to_string(template_filename, context_instance=context)
331        # While rendering the template there are variables added to the
332        # Context, so these can be used for post-processing.
333        logger.debug('<RESULT>\n%s\n</RESULT>'%rendered_string)
334
335    except IOError, e:
336        logger.error('Template does not exist: %s' % e)
337
338    for outputfile, content in context['__template_outputfiles__'].items():
339        write_msg = 'Writing outputfile: %s' % outputfile
340        created_msg = 'Outputfile(s) created: %s' % outputfile
341
342        if not parser.values.DRYRUN: # Write output file(s)
343            try:
344                logger.info(write_msg)
345                f = open(outputfile, 'w')
346                f.writelines(content)
347                f.close()
348                logger.info(created_msg)
349            except IOError, e:
350                logger.error('Failed creating outputfile: %s' % e)
351            except KeyError, e:
352                logger.error('No outputfiles defined in template')
353        else:
354            write_msg = '[DRYRUN] %s' % write_msg
355            created_msg = '[DRYRUN] %s' % created_msg
356            logger.info(write_msg)
357            logger.info(created_msg)
358
359    if not parser.values.DRYRUN:
360        try:
361            for script in context['epilogue']:
362                logger.info('Executing epilogue script')
363                os.system(script)
364                logger.info('Finished epilogue script')
365        except KeyError, e:
366            logger.debug('No epilogue script found')
367    return
368
369
370#####
371#
372# <Methods for processing of arguments>
373#
374
375
376def collect_args(option, parser):
377    """
378        Collects the arguments belonging to the given option, and removes them
379        from the arguments-datastructue. Returns the collected arguments as a
380        list.
381    """
382    collected = []
383
384    if parser.rargs:
385
386        def floatable(str):
387            try:
388                float(str)
389                return True
390            except ValueError:
391                return False
392
393        for arg in parser.rargs:
394            # stop on --foo like options
395            if arg[:2] == '--' and len(arg) > 2:
396                break
397            # stop on -a, but not on -3 or -3.0
398            if arg[:1] == '-' and len(arg) > 1 and not floatable(arg):
399                break
400            collected.append(arg)
401
402        parser.largs.extend(parser.rargs[:len(collected)])
403        del parser.rargs[:len(collected)]
404        setattr(parser.values, option.dest, collected)
405
406    logger.debug('Collected arguments for %s: %s' % (option, collected))
407    return collected
408
409
410def main():
411    parser.version = CMTSARA_VERSION
412    parser.description = CMTSARA_DESCRIPTION
413
414    parser.add_option('-n', '--dry-run',
415        action='store_true',
416        dest='DRYRUN',
417        default=config_parser.getboolean('defaults', 'DRYRUN'),
418        help='This flag has to be given before -[aclmr]')
419    parser.add_option('--script',
420        action='store_false',
421        dest='INTERACTIVE',
422        default=config_parser.getboolean('defaults', 'INTERACTIVE'))
423    parser.add_option('-a', '--add',
424        action='callback',
425        callback=add,
426        type='string',
427        metavar='ENTITY',
428        nargs=1,
429        help='Add an object of the given ENTITY. '\
430            'Extra arguments should be given like `set ASSIGNMENTS`, '\
431            'where ASSIGNMENTS are formatted like `ATTR=VAL`')
432    parser.add_option('-c', '--change',
433        action='callback',
434        callback=change,
435        type='string',
436        metavar='ENTITY',
437        nargs=1,
438        help='Change value(s) of object(s) of the given ENTITY. '\
439            'Arguments should be given like `get QUERY set ASSIGNMENTS`, '\
440            'where both QUERY and ASSIGNMENTS consist out of one or more '\
441            'terms formatted like `ATTR=VAL`. Only objects matching QUERY '\
442            'will be changed according to the given ASSIGNMENTS')
443    parser.add_option('-g', '--generate',
444        action='callback',
445        callback=generate,
446        type='string',
447        metavar='TEMPLATE',
448        nargs=1,
449        help='Render the given TEMPLATE, and store the result if specified')
450    parser.add_option('-l', '--list',
451        action='callback',
452        callback=show,
453        type='string',
454        metavar='ENTITY',
455        nargs=1,
456        help='List object(s) of the given ENTITY. Arguments should be given '\
457            'like `get QUERY`, where QUERY consists out of one or more terms '\
458            'formatted like `ATTR=VAL`')
459    parser.add_option('-r', '--remove',
460        action='callback',
461        callback=remove,
462        type='string',
463        metavar='ENTITY',
464        nargs=1,
465        help='Remove object(s) of the given ENTITY. '\
466            'Arguments should be given like `get QUERY`, where QUERY '\
467            'consists out of one or more terms formatted like `ATTR=VAL`. '\
468            'Only objects matching QUERY will be listed')
469
470    # TODO: implement the following option(s)
471    parser.add_option('-v', '--verbose',
472        action='store_true',
473        dest='VERBOSE',
474        default=config_parser.getboolean('defaults', 'VERBOSE'))
475
476    (options, args) = parser.parse_args()
477    logger.debug('(options, args) = (%s, %s)' % (options, args))
478
479    if len(sys.argv) == 1:
480        # No arguments are given
481        parser.print_help()
482        return 1
483
484    return 0
485#
486# </Methods for processing of arguments>
487#
488#####
489
490
491if __name__ == '__main__':
492    status = main()
493    sys.exit(status)
Note: See TracBrowser for help on using the repository browser.