summaryrefslogtreecommitdiffstats
path: root/funcweb/funcweb/controllers.py
blob: d884720ae2c329b4ea3272344ad3c3b06da85304 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
import logging
log = logging.getLogger(__name__)

from turbogears import controllers, expose, flash, identity, redirect, error_handler,validate
from func.overlord.client import Overlord, Minions
from funcweb.widget_automation import WidgetListFactory,RemoteFormAutomation,RemoteFormFactory
from funcweb.widget_validation import WidgetSchemaFactory
from funcweb.async_tools import AsyncResultManager
from func.jobthing import purge_old_jobs,JOB_ID_RUNNING,JOB_ID_FINISHED,JOB_ID_PARTIAL

# it is assigned into method_display on every request 
global_form = None 

def validate_decorator_updater(validator_value=None):
    """
    When we pass the global_form directly to 
    turbogears.validate it is not updated on 
    every request we should pass a callable
    to trigger it ;)

    @param :validator_value : Does nothing in the code
    of validate it is passed but is irrelevant in our case
    because we compute the global_form in the method_display

    @return : the current form and schema to validate the
    current input in cherrypy's request 
    """
    global global_form
    return global_form

class Funcweb(object):
    #preventing the everytime polling and getting
    #func = Overlord("name") thing
    func_cache={          
                'fc_object':None,#the fc = Overlord() thing,
                'fc_async_obj':None,
                'glob':None,
                'minion_name':None,
                'module_name':None,
                'modules':None,
                'minions':None,
                'methods':None
            }
    async_manager = None
    first_run = True
    group_name = None # a cache variable for current group_name
    #will be reused for widget validation

    def get_current_minion_list(self,glob):
        """
        That method will not be reachable from web interface it just 
        a util method that gives back the current minion list back, 
        we use that minion glob form in lots of places so it is a hack
        to avoid writing stupid code again and again :)
        """
        
        if self.func_cache['glob'] == glob:
            minions = self.func_cache['minions']
        else:
            #we dont have it it is for first time so lets pull it
            minions=Minions(glob).get_all_hosts()
            self.func_cache['glob']=glob
            self.func_cache['minions']=minions
        
        return minions

    @expose(allow_json=True)
    @identity.require(identity.not_anonymous())
    def minions(self, glob='*',submit=None):
        """ Return a list of our minions that match a given glob """
        #make the cache thing

        minions = self.get_current_minion_list(glob) 
        if not submit:
            return dict(minions=minions,submit_adress="/funcweb/minions",tg_template="funcweb.templates.index")
        else:
            return dict(minions=minions,submit_adress="/funcweb/minions",tg_template="funcweb.templates.minions")


    index = minions # start with our minion view, for now

    @expose(template="funcweb.templates.modules")
    @identity.require(identity.not_anonymous())
    def minion(self, name="*", module=None, method=None):
        """ Display module or method details for a specific minion.

        If only the minion name is given, it will display a list of modules
        for that minion.  If a module is supplied, it will display a list of
        methods.
        """
        #if we have it in the cache
        if self.func_cache['minion_name'] == name:
            fc = self.func_cache['fc_object']
        else:
            fc = Overlord(name)
            self.func_cache['fc_object']=fc
            self.func_cache['minion_name']=name
            #reset the children :)
            self.func_cache['module_name']=None
            self.func_cache['modules']=None
            self.func_cache['methods']=None

            #should also reset the other fields or not ?

        
        if not module:
            if not self.func_cache['modules']:
                modules = fc.system.list_modules()
                display_modules = []

                for module in modules.itervalues():
                    for mod in module:
                        #if it is not empty
                        if getattr(fc,mod).get_method_args()[name]:
                            display_modules.append(mod)

                #put it into the cache to make that slow thing faster
                self.func_cache['modules']=display_modules
                
            else:
                #print "Im in the cache"
                #just list those who have get_method_args
                display_modules = self.func_cache['modules']
            
            modules = {}
            modules[name]=display_modules
            
            return dict(modules=modules)
        else: # a module is specified
            if not method: # return a list of methods for specified module
                #first check if we have it into the cache
                if self.func_cache['module_name'] == module and self.func_cache['methods']:
                    modules = self.func_cache['methods']
                    #print "Im in the cache"

                else:
                    self.func_cache['module_name']= module
                    #display the list only that is registered with register_method template !
                    registered_methods=getattr(fc,module).get_method_args()[name].keys()
                    modules = getattr(fc, module).list_methods()
                    for mods in modules.itervalues():
                        from copy import copy
                        cp_mods = copy(mods)
                        for m in cp_mods:
                            if not m in registered_methods:
                                mods.remove(m)

                    #store into cache if we get it again 
                    self.func_cache['methods'] = modules
                #display em
                return dict(modules=modules, module=module,
                            tg_template="funcweb.templates.methods")
            else:
                return "Wrong place :)"


    @expose(template="funcweb.templates.widgets")
    @identity.require(identity.not_anonymous())
    def method_display(self,minion=None,module=None,method=None):
        """
        That method generates the input widget for givent method.
        """
        
        global global_form
        if self.func_cache['minion_name'] == minion:
            fc = self.func_cache['fc_object']
        else:
            fc = Overlord(minion)
            self.func_cache['fc_object']=fc
            self.func_cache['minion_name']=minion
            #reset the children :)
            self.func_cache['module_name']=module
            self.func_cache['modules']=None
            self.func_cache['methods']=None

        #get the method args
        method_args = getattr(fc,module).get_method_args()
        
        if not method_args.values():
            #print "Not registered method here"
            return dict(minion_form = None,minion=minion,module=module,method=method)

        minion_arguments = method_args[minion][method]['args']
        #the description of the method we are going to display
        if method_args[minion][method].has_key('description'):
            description = method_args[minion][method]['description']
        else:
            description = None
        if minion_arguments:
            wlist_object = WidgetListFactory(minion_arguments,minion=minion,module=module,method=method)
            wlist_object = wlist_object.get_widgetlist_object()
            #create the validation parts for the remote form
            wf = WidgetSchemaFactory(minion_arguments)
            schema_man=wf.get_ready_schema()

            #create the final form
            minion_form = RemoteFormAutomation(wlist_object,schema_man)
            global_form = minion_form.for_widget
            #print global_form
            #i use that when something goes wrong to check the problem better to stay here ;)
            #self.minion_form =RemoteFormFactory(wlist_object,schema_man).get_remote_form()
            
            del wlist_object
            del minion_arguments

            return dict(minion_form =minion_form,minion=minion,module=module,method=method,description=description)
        else:
            return dict(minion_form = None,minion=minion,module=module,method=method,description = description)



    @expose(template="funcweb.templates.login")
    def login(self, forward_url=None, previous_url=None, *args, **kw):
        """
        The login form for not registered users
        """
        from cherrypy import request, response
        if not identity.current.anonymous \
            and identity.was_login_attempted() \
            and not identity.get_identity_errors():
            raise redirect(forward_url)

        forward_url=None
        previous_url= request.path

        if identity.was_login_attempted():
            msg=_("The credentials you supplied were not correct or "
                   "did not grant access to this resource.")
        elif identity.get_identity_errors():
            msg=_("You must provide your credentials before accessing "
                   "this resource.")
        else:
            msg=_("Please log in.")
            forward_url= request.headers.get("Referer", ".")

        response.status=403

        return dict(message=msg, previous_url=previous_url, logging_in=True,
                    original_parameters=request.params,
                    forward_url=forward_url)
        
    
    @expose() 
    @identity.require(identity.not_anonymous())
    def handle_minion_error(self,tg_errors=None):
        """
        The method checks the result from turbogears.validate
        decorator so if it has the tg_errors we know that the
        form validation is failed. That prevents the extra traffic
        to be sent to the minions!
        """
        if tg_errors:
            #print tg_errors
            return str(tg_errors)
        

    @expose(allow_json=True)
    @error_handler(handle_minion_error)
    @validate(form=validate_decorator_updater)
    @identity.require(identity.not_anonymous())
    def post_form(self,**kw):
        """
        Data processing part for methods that accept some inputs.
        Method recieves the method arguments for minion method then
        orders them into their original order and sends the xmlrpc
        request to the minion !
        """
        if kw.has_key('minion') and kw.has_key('module') and kw.has_key('method'):
            #assign them because we need the rest so dont control everytime
            #and dont make lookup everytime ...
            #the del statements above are important dont remove them :)
            minion = kw['minion']
            del kw['minion']
            module = kw['module']
            del kw['module']
            method = kw['method']
            del kw['method']

            if self.func_cache['minion_name'] == minion:
                fc = self.func_cache['fc_object']
            else:
                fc = Overlord(minion)
                self.func_cache['fc_object']=fc
                self.func_cache['minion_name']=minion
                #reset the children :)
                self.func_cache['module_name']=module
                self.func_cache['modules']=None
                self.func_cache['methods']=None


            #get again the method args to get their order :
            arguments=getattr(fc,module).get_method_args()
            #so we know the order just allocate and put them there 
            cmd_args=['']*(len(kw.keys()))
            
            for arg in kw.keys():
                #wow what a lookup :)
                index_of_arg = arguments[minion][method]['args'][arg]['order']
                cmd_args[index_of_arg]=kw[arg]
           
            #now execute the stuff
            #at the final execute it as a multiple if the glob suits for that
            #if not (actually there shouldnt be an option like that but who knows :))
            #it will run as a normal single command to clicked minion
            if self.func_cache['glob']:
                fc_async = Overlord(self.func_cache['glob'],async=True)
            
            result_id = getattr(getattr(fc_async,module),method)(*cmd_args)
            result = "".join(["The id for current job is :",str(result_id)," You will be notified when there is some change about that command !"])
            
            #that part gives a chance for short methods to finish their jobs and display them 
            #immediately so user will not wait for new notifications for that short thing
            import time 
            time.sleep(4)
            tmp_as_res = fc_async.job_status(result_id)
            if tmp_as_res[0] == JOB_ID_FINISHED:
                result = tmp_as_res[1]
                
                if not self.async_manager:
                    #cleanup tha database firstly 
                    purge_old_jobs()
                    self.async_manager = AsyncResultManager()
                self.async_manager.refresh_list()
            
            #TODO reformat that returning string to be more elegant to display :)
            return str(result)

        else:
            return "Missing arguments sorry can not proceess the form"
    
    @expose(template="funcweb.templates.result")
    @identity.require(identity.not_anonymous())
    def execute_link(self,minion=None,module=None,method=None):
        """
        Method is fot those minion methods that dont accept any 
        arguments so they provide only some information,executed
        by pressing only the link !
        """
        if self.func_cache['glob']:
            fc = Overlord(self.func_cache['glob'],async = True)
        else:
            if self.func_cache['minion_name'] == minion:
                fc = self.func_cache['fc_async_obj']
            else:
                fc = Overlord(minion,async = True)
                self.func_cache['fc_async_obj']=fc
                self.func_cache['minion_name']=minion
                #reset the children :)
                self.func_cache['module_name']=module
                self.func_cache['modules']=None
                self.func_cache['methods']=None

        #i assume that they are long enough so dont poll here
        result_id = getattr(getattr(fc,module),method)()
        result = "".join(["The id for current id is :",str(result_id)," You will be notified when there is some change about that command !"])
        return dict(result=str(result))

    @expose(format = "json")
    @identity.require(identity.not_anonymous())
    def check_async(self,check_change = False):
        """
        That method is polled by js code to see if there is some
        interesting change in current db
        """
        changed = False

        if not check_change :
            msg = "Method invoked with False parameter which makes it useless"
            return dict(changed = False,changes = [],remote_error=msg)
        
        if not self.async_manager:
            #cleanup tha database firstly 
            purge_old_jobs()
            self.async_manager = AsyncResultManager()
        changes = self.async_manager.check_for_changes()
        if changes:
            if not self.first_run:
                changed = True
            else:
                self.first_run = False
        
        return dict(changed = changed,changes = changes)

    
    @expose(template="funcweb.templates.result")
    @identity.require(identity.not_anonymous())
    def check_job_status(self,job_id):
        """
        Checking the job status for specific job_id
        that method will be useful to see the results from
        async_results table ...
        """
        if not job_id:
            return dict(result = "job id shouldn be empty!")

        if not self.func_cache['fc_async_obj']:
            if self.func_cache['glob']:
                fc_async = Overlord(self.func_cache['glob'],async=True)
                #store also into the cache
            else:
                fc_async = Overlord("*",async=True)
            
            self.func_cache['fc_async_obj'] = fc_async

        else:
            fc_async = self.func_cache['fc_async_obj']

        id_result = fc_async.job_status(job_id)

        #the final id_result
        return dict(result=id_result)

    @expose(template="funcweb.templates.async_table")
    @identity.require(identity.not_anonymous())
    def display_async_results(self):
        """
        Displaying the current db results that are in the memory
        """
        if not self.async_manager:
            #here should run the clean_old ids
            purge_old_jobs()
            self.async_manager = AsyncResultManager()
        else:
            #make a refresh of the memory copy
            self.async_manager.refresh_list()
        #get the actual db    
        func_db = self.async_manager.current_db()
        
        for job_id,code_status_pack in func_db.iteritems():
            parsed_job_id = job_id.split("-")
            func_db[job_id].extend(parsed_job_id)

        #print func_db
        return dict(func_db = func_db)

    @expose()
    def logout(self):
        """
        The logoout part 
        """
        identity.current.logout()
        raise redirect("/")
 
################################ Groups API methods here #############################    
    @expose(template="funcweb.templates.groups_main")
    @identity.require(identity.not_anonymous())
    def groups_main(self):
        """
        The main page of the groups
        """
        #a dummy object to let us to get the groups api
        #we dont supply a new group file it will handle the default
        minion_api = Minions("*")
        groups = minion_api.group_class.get_group_names()
        del minion_api
        #result to the template please :)
        return dict(groups = groups)

    @expose(template="funcweb.templates.list_group")
    @identity.require(identity.not_anonymous())
    def add_new_group(self,group_name,submit):
        """
        Adding a new group
        """
        minion_api = Minions("*")
        minion_api.group_class.add_group(group_name,save=True)
        groups = minion_api.group_class.get_group_names()
        del minion_api
        return dict(groups = groups)

    @expose(template="funcweb.templates.list_group")
    @identity.require(identity.not_anonymous())
    def remove_group(self,**kw):
        """
        Adding a new group
        """
        minion_api = Minions("*")
        minion_api.group_class.remove_group(kw['group_name'],save=True)
        groups = minion_api.group_class.get_group_names()
        del minion_api
        return dict(groups = groups)

    @expose(template="funcweb.templates.group_minion")
    @identity.require(identity.not_anonymous())
    def list_host_by_group(self,group_name):
        """
        Get the hosts for the specified group_name
        """
        from copy import copy
        copy_group_name = copy(group_name)
        if not group_name.startswith('@'):
            group_name = "".join(["@",group_name.strip()])
        
        minion_api = Minions("*")
        hosts = minion_api.group_class.get_hosts_by_group_glob(group_name)
        all_minions = minion_api.get_all_hosts()
        del minion_api

        #store the current group_name in cache variable 
        self.group_name = copy_group_name
        return dict(hosts = hosts,all_minions = all_minions,group_name = copy_group_name,submit_adress="/funcweb/filter_group_minions")

    @expose(template="funcweb.templates.group_small")
    @identity.require(identity.not_anonymous())
    def add_minions_togroup(self,**kw):
        """
        Add or remove multiple minions to given group
        """
        #print "The dict value is : ",kw
        minion_api = Minions("*")
        hosts = []
        if not kw.has_key('group_name') or not kw.has_key('action_name'):
            return dict(hosts =hosts,group_name = None)
        
        current_host_list = None

        #if we are adding some hosts 
        if kw['action_name'] == "add":
            if not kw.has_key('checkminion'):
                return dict(hosts =hosts,group_name = kw['group_name'])
            current_host_list = kw['checkminion']
        else:#it is a remove action
            if not kw.has_key('rmgroup'):
                return dict(hosts =hosts,group_name = kw['group_name'])
            current_host_list = kw['rmgroup']
        #sanity checks
        if type(current_host_list)!=list:
            hosts.extend(current_host_list.split(","))
        else:
            hosts.extend(current_host_list)
        
        if kw['action_name'] == "add":
            minion_api.group_class.add_host_list(kw['group_name'],hosts,save = True)
        else:#remove them
            minion_api.group_class.remove_host_list(kw['group_name'],hosts,save = True)

        from copy import copy
        #we need that check because @ is a sign for group search
        group_name = copy(kw['group_name'])
        if not group_name.startswith('@'):
            group_name = "".join(["@",group_name.strip()])
        
        hosts = minion_api.group_class.get_hosts_by_group_glob(group_name)
        return dict(hosts =hosts,group_name = kw['group_name'])
    
    @expose(allow_json=True)
    @identity.require(identity.not_anonymous())
    def filter_group_minions(self,glob='*',submit=None):
        """ Return a list of our minions that match a given glob """
        #make the cache thing

        minions = self.get_current_minion_list(glob) 
        if submit:
            return dict(all_minions=minions,submit_adress="/funcweb/filter_group_minions",tg_template="funcweb.templates.minion_small",group_name= self.group_name)
        else:
            return str("Wrong way :)")



############################# END of GROUPS API METHODS ############################
class Root(controllers.RootController):
    
    @expose()
    def index(self):
        raise redirect("/funcweb")
    
    index = index # start with our minion view, for now
    funcweb = Funcweb()