source: trunk/sara_cmt/bin/cmt @ 14194

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

Merged branch 1.0 (until tag 1.0.0) back to trunk

  • 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        template_data['epilogue'] = []
327        context = Context(template_data)
328
329        template_data['stores'] = template_data['__template_outputfiles__'] # to stay bw compatible with ramon's code (temporary)
330       
331        rendered_string = render_to_string(template_filename, context_instance=context)
332        # While rendering the template there are variables added to the
333        # Context, so these can be used for post-processing.
334        logger.debug('<RESULT>\n%s\n</RESULT>'%rendered_string)
335
336    except IOError, e:
337        logger.error('Template does not exist: %s' % e)
338
339    for outputfile, content in context['__template_outputfiles__'].items():
340        write_msg = 'Writing outputfile: %s' % outputfile
341        created_msg = 'Outputfile(s) created: %s' % outputfile
342
343        if not parser.values.DRYRUN: # Write output file(s)
344            try:
345                logger.info(write_msg)
346                f = open(outputfile, 'w')
347                f.writelines(content)
348                f.close()
349                logger.info(created_msg)
350            except IOError, e:
351                logger.error('Failed creating outputfile: %s' % e)
352            except KeyError, e:
353                logger.error('No outputfiles defined in template')
354        else:
355            write_msg = '[DRYRUN] %s' % write_msg
356            created_msg = '[DRYRUN] %s' % created_msg
357            logger.info(write_msg)
358            logger.info(created_msg)
359
360    if not parser.values.DRYRUN:
361        try:
362            for script in context['epilogue']:
363                logger.info('Executing epilogue script')
364                os.system(script)
365                logger.info('Finished epilogue script')
366        except KeyError, e:
367            logger.debug('No epilogue script found')
368    return
369
370
371#####
372#
373# <Methods for processing of arguments>
374#
375
376
377def collect_args(option, parser):
378    """
379        Collects the arguments belonging to the given option, and removes them
380        from the arguments-datastructue. Returns the collected arguments as a
381        list.
382    """
383    collected = []
384
385    if parser.rargs:
386
387        def floatable(str):
388            try:
389                float(str)
390                return True
391            except ValueError:
392                return False
393
394        for arg in parser.rargs:
395            # stop on --foo like options
396            if arg[:2] == '--' and len(arg) > 2:
397                break
398            # stop on -a, but not on -3 or -3.0
399            if arg[:1] == '-' and len(arg) > 1 and not floatable(arg):
400                break
401            collected.append(arg)
402
403        parser.largs.extend(parser.rargs[:len(collected)])
404        del parser.rargs[:len(collected)]
405        setattr(parser.values, option.dest, collected)
406
407    logger.debug('Collected arguments for %s: %s' % (option, collected))
408    return collected
409
410
411def main():
412    parser.version = CMTSARA_VERSION
413    parser.description = CMTSARA_DESCRIPTION
414
415    parser.add_option('-n', '--dry-run',
416        action='store_true',
417        dest='DRYRUN',
418        default=config_parser.getboolean('defaults', 'DRYRUN'),
419        help='This flag has to be given before -[aclmr]')
420    parser.add_option('--script',
421        action='store_false',
422        dest='INTERACTIVE',
423        default=config_parser.getboolean('defaults', 'INTERACTIVE'))
424    parser.add_option('-a', '--add',
425        action='callback',
426        callback=add,
427        type='string',
428        metavar='ENTITY',
429        nargs=1,
430        help='Add an object of the given ENTITY. '\
431            'Extra arguments should be given like `set ASSIGNMENTS`, '\
432            'where ASSIGNMENTS are formatted like `ATTR=VAL`')
433    parser.add_option('-c', '--change',
434        action='callback',
435        callback=change,
436        type='string',
437        metavar='ENTITY',
438        nargs=1,
439        help='Change value(s) of object(s) of the given ENTITY. '\
440            'Arguments should be given like `get QUERY set ASSIGNMENTS`, '\
441            'where both QUERY and ASSIGNMENTS consist out of one or more '\
442            'terms formatted like `ATTR=VAL`. Only objects matching QUERY '\
443            'will be changed according to the given ASSIGNMENTS')
444    parser.add_option('-g', '--generate',
445        action='callback',
446        callback=generate,
447        type='string',
448        metavar='TEMPLATE',
449        nargs=1,
450        help='Render the given TEMPLATE, and store the result if specified')
451    parser.add_option('-l', '--list',
452        action='callback',
453        callback=show,
454        type='string',
455        metavar='ENTITY',
456        nargs=1,
457        help='List object(s) of the given ENTITY. Arguments should be given '\
458            'like `get QUERY`, where QUERY consists out of one or more terms '\
459            'formatted like `ATTR=VAL`')
460    parser.add_option('-r', '--remove',
461        action='callback',
462        callback=remove,
463        type='string',
464        metavar='ENTITY',
465        nargs=1,
466        help='Remove object(s) of the given ENTITY. '\
467            'Arguments should be given like `get QUERY`, where QUERY '\
468            'consists out of one or more terms formatted like `ATTR=VAL`. '\
469            'Only objects matching QUERY will be listed')
470
471    # TODO: implement the following option(s)
472    parser.add_option('-v', '--verbose',
473        action='store_true',
474        dest='VERBOSE',
475        default=config_parser.getboolean('defaults', 'VERBOSE'))
476
477    (options, args) = parser.parse_args()
478    logger.debug('(options, args) = (%s, %s)' % (options, args))
479
480    if len(sys.argv) == 1:
481        # No arguments are given
482        parser.print_help()
483        return 1
484
485    return 0
486#
487# </Methods for processing of arguments>
488#
489#####
490
491
492if __name__ == '__main__':
493    status = main()
494    sys.exit(status)
Note: See TracBrowser for help on using the repository browser.