Merge branch 'master' into senslab2
[sfa.git] / sfa / client / sfaadmin.py
1 #!/usr/bin/python
2 import os
3 import sys
4 import copy
5 from pprint import pformat, PrettyPrinter
6 from optparse import OptionParser
7
8 from sfa.generic import Generic
9 from sfa.util.xrn import Xrn
10 from sfa.storage.record import Record 
11 from sfa.client.sfi import save_records_to_file
12 from sfa.trust.hierarchy import Hierarchy
13 from sfa.trust.gid import GID
14
15 from sfa.client.candidates import Candidates
16
17 pprinter = PrettyPrinter(indent=4)
18
19 try:
20     help_basedir=Hierarchy().basedir
21 except:
22     help_basedir='*unable to locate Hierarchy().basedir'
23
24 def optparse_listvalue_callback(option, opt, value, parser):
25     setattr(parser.values, option.dest, value.split(','))
26
27 def args(*args, **kwargs):
28     def _decorator(func):
29         func.__dict__.setdefault('options', []).insert(0, (args, kwargs))
30         return func
31     return _decorator
32
33 class Commands(object):
34     def _get_commands(self):
35         command_names = []
36         for attrib in dir(self):
37             if callable(getattr(self, attrib)) and not attrib.startswith('_'):
38                 command_names.append(attrib)
39         return command_names
40
41
42 class RegistryCommands(Commands):
43     def __init__(self, *args, **kwds):
44         self.api= Generic.the_flavour().make_api(interface='registry')
45  
46     def version(self):
47         """Display the Registry version""" 
48         version = self.api.manager.GetVersion(self.api, {})
49         pprinter.pprint(version)
50
51     @args('-x', '--xrn', dest='xrn', metavar='<xrn>', help='authority to list (hrn/urn - mandatory)') 
52     @args('-t', '--type', dest='type', metavar='<type>', help='object type', default=None) 
53     @args('-r', '--recursive', dest='recursive', metavar='<recursive>', help='list all child records', 
54           action='store_true', default=False) 
55     def list(self, xrn, type=None, recursive=False):
56         """List names registered at a given authority - possibly filtered by type"""
57         xrn = Xrn(xrn, type) 
58         options = {'recursive': recursive}    
59         records = self.api.manager.List(self.api, xrn.get_hrn(), options=options)
60         for record in records:
61             if not type or record['type'] == type:
62                 print "%s (%s)" % (record['hrn'], record['type'])
63
64
65     @args('-x', '--xrn', dest='xrn', metavar='<xrn>', help='object hrn/urn (mandatory)') 
66     @args('-t', '--type', dest='type', metavar='<type>', help='object type', default=None) 
67     @args('-o', '--outfile', dest='outfile', metavar='<outfile>', help='save record to file') 
68     @args('-f', '--format', dest='format', metavar='<display>', type='choice', 
69           choices=('text', 'xml', 'simple'), help='display record in different formats') 
70     def show(self, xrn, type=None, format=None, outfile=None):
71         """Display details for a registered object"""
72         records = self.api.manager.Resolve(self.api, xrn, type, True)
73         for record in records:
74             sfa_record = Record(dict=record)
75             sfa_record.dump(format) 
76         if outfile:
77             save_records_to_file(outfile, records)  
78
79
80     def _record_dict(self, xrn=None, type=None, 
81                      url=None, description=None, email='', 
82                      key=None, 
83                      slices=[], researchers=[], pis=[]):
84         record_dict = {}
85         if xrn:
86             if type:
87                 xrn = Xrn(xrn, type)
88             else:
89                 xrn = Xrn(xrn)
90             record_dict['urn'] = xrn.get_urn()
91             record_dict['hrn'] = xrn.get_hrn()
92             record_dict['type'] = xrn.get_type()
93         if url:
94             record_dict['url'] = url
95         if description:
96             record_dict['description'] = description
97         if key:
98             try:
99                 pubkey = open(key, 'r').read()
100             except IOError:
101                 pubkey = key
102             record_dict['keys'] = [pubkey]
103         if slices:
104             record_dict['slices'] = slices
105         if researchers:
106             record_dict['researcher'] = researchers
107         if email:
108             record_dict['email'] = email
109         if pis:
110             record_dict['pi'] = pis
111         return record_dict
112
113     @args('-x', '--xrn', dest='xrn', metavar='<xrn>', help='object hrn/urn (mandatory)') 
114     @args('-t', '--type', dest='type', metavar='<type>', help='object type', default=None) 
115     @args('-e', '--email', dest='email', default="",
116           help="email (mandatory for users)")
117     @args('-u', '--url', dest='url', metavar='<url>', default=None,
118           help="URL, useful for slices")
119     @args('-d', '--description', dest='description', metavar='<description>', 
120           help='Description, useful for slices', default=None)
121     @args('-k', '--key', dest='key', metavar='<key>', help='public key string or file', 
122           default=None)
123     @args('-s', '--slices', dest='slices', metavar='<slices>', help='slice xrns', 
124           default='', type="str", action='callback', callback=optparse_listvalue_callback)
125     @args('-r', '--researchers', dest='researchers', metavar='<researchers>', help='slice researchers', 
126           default='', type="str", action='callback', callback=optparse_listvalue_callback)
127     @args('-p', '--pis', dest='pis', metavar='<PIs>', 
128           help='Principal Investigators/Project Managers ', 
129           default='', type="str", action='callback', callback=optparse_listvalue_callback)
130     def register(self, xrn, type=None, url=None, description=None, key=None, slices='', 
131                  pis='', researchers='',email=''):
132         """Create a new Registry record"""
133         record_dict = self._record_dict(xrn=xrn, type=type, url=url, key=key, 
134                                         slices=slices, researchers=researchers, email=email, pis=pis)
135         self.api.manager.Register(self.api, record_dict)         
136
137
138     @args('-x', '--xrn', dest='xrn', metavar='<xrn>', help='object hrn/urn (mandatory)')
139     @args('-t', '--type', dest='type', metavar='<type>', help='object type', default=None)
140     @args('-u', '--url', dest='url', metavar='<url>', help='URL', default=None)
141     @args('-d', '--description', dest='description', metavar='<description>',
142           help='Description', default=None)
143     @args('-k', '--key', dest='key', metavar='<key>', help='public key string or file',
144           default=None)
145     @args('-s', '--slices', dest='slices', metavar='<slices>', help='slice xrns',
146           default='', type="str", action='callback', callback=optparse_listvalue_callback)
147     @args('-r', '--researchers', dest='researchers', metavar='<researchers>', help='slice researchers',
148           default='', type="str", action='callback', callback=optparse_listvalue_callback)
149     @args('-p', '--pis', dest='pis', metavar='<PIs>',
150           help='Principal Investigators/Project Managers ',
151           default='', type="str", action='callback', callback=optparse_listvalue_callback)
152     def update(self, xrn, type=None, url=None, description=None, key=None, slices='', 
153                pis='', researchers=''):
154         """Update an existing Registry record""" 
155         record_dict = self._record_dict(xrn=xrn, type=type, url=url, description=description, 
156                                         key=key, slices=slices, researchers=researchers, pis=pis)
157         self.api.manager.Update(self.api, record_dict)
158         
159     @args('-x', '--xrn', dest='xrn', metavar='<xrn>', help='object hrn/urn (mandatory)') 
160     @args('-t', '--type', dest='type', metavar='<type>', help='object type', default=None) 
161     def remove(self, xrn, type=None):
162         """Remove given object from the registry"""
163         xrn = Xrn(xrn, type)
164         self.api.manager.Remove(self.api, xrn)            
165
166
167     @args('-x', '--xrn', dest='xrn', metavar='<xrn>', help='object hrn/urn (mandatory)') 
168     @args('-t', '--type', dest='type', metavar='<type>', help='object type', default=None) 
169     def credential(self, xrn, type=None):
170         """Invoke GetCredential"""
171         cred = self.api.manager.GetCredential(self.api, xrn, type, self.api.hrn)
172         print cred
173    
174
175     def import_registry(self):
176         """Run the importer"""
177         from sfa.importer import Importer
178         importer = Importer()
179         importer.run()
180     
181     @args('-a', '--all', dest='all', metavar='<all>', action='store_true', default=False,
182           help='Remove all registry records and all files in %s area' % help_basedir)
183     @args('-c', '--certs', dest='certs', metavar='<certs>', action='store_true', default=False,
184           help='Remove all cached certs/gids found in %s' % help_basedir )
185     @args('-0', '--no-reinit', dest='reinit', metavar='<reinit>', action='store_false', default=True,
186           help='Prevents new DB schema from being installed after cleanup')
187     def nuke(self, all=False, certs=False, reinit=True):
188         """Cleanup local registry DB, plus various additional filesystem cleanups optionally"""
189         from sfa.storage.dbschema import DBSchema
190         from sfa.util.sfalogging import _SfaLogger
191         logger = _SfaLogger(logfile='/var/log/sfa_import.log', loggername='importlog')
192         logger.setLevelFromOptVerbose(self.api.config.SFA_API_LOGLEVEL)
193         logger.info("Purging SFA records from database")
194         dbschema=DBSchema()
195         dbschema.nuke()
196
197         # for convenience we re-create the schema here, so there's no need for an explicit
198         # service sfa restart
199         # however in some (upgrade) scenarios this might be wrong
200         if reinit:
201             logger.info("re-creating empty schema")
202             dbschema.init_or_upgrade()
203
204         # remove the server certificate and all gids found in /var/lib/sfa/authorities
205         if certs:
206             logger.info("Purging cached certificates")
207             for (dir, _, files) in os.walk('/var/lib/sfa/authorities'):
208                 for file in files:
209                     if file.endswith('.gid') or file == 'server.cert':
210                         path=dir+os.sep+file
211                         os.unlink(path)
212
213         # just remove all files that do not match 'server.key' or 'server.cert'
214         if all:
215             logger.info("Purging registry filesystem cache")
216             preserved_files = [ 'server.key', 'server.cert']
217             for (dir,_,files) in os.walk(Hierarchy().basedir):
218                 for file in files:
219                     if file in preserved_files: continue
220                     path=dir+os.sep+file
221                     os.unlink(path)
222         
223     
224 class CertCommands(Commands):
225     
226     def import_gid(self, xrn):
227         pass
228
229     @args('-x', '--xrn', dest='xrn', metavar='<xrn>', help='object hrn/urn (mandatory)')
230     @args('-t', '--type', dest='type', metavar='<type>', help='object type', default=None)
231     @args('-o', '--outfile', dest='outfile', metavar='<outfile>', help='output file', default=None)
232     def export(self, xrn, type=None, outfile=None):
233         """Fetch an object's GID from the Registry"""  
234         from sfa.storage.alchemy import dbsession
235         from sfa.storage.model import RegRecord
236         hrn = Xrn(xrn).get_hrn()
237         request=dbsession.query(RegRecord).filter_by(hrn=hrn)
238         if type: request = request.filter_by(type=type)
239         record=request.first()
240         if record:
241             gid = GID(string=record.gid)
242         else:
243             # check the authorities hierarchy
244             hierarchy = Hierarchy()
245             try:
246                 auth_info = hierarchy.get_auth_info(hrn)
247                 gid = auth_info.gid_object
248             except:
249                 print "Record: %s not found" % hrn
250                 sys.exit(1)
251         # save to file
252         if not outfile:
253             outfile = os.path.abspath('./%s.gid' % gid.get_hrn())
254         gid.save_to_file(outfile, save_parents=True)
255         
256     @args('-g', '--gidfile', dest='gid', metavar='<gid>', help='path of gid file to display (mandatory)') 
257     def display(self, gidfile):
258         """Print contents of a GID file"""
259         gid_path = os.path.abspath(gidfile)
260         if not gid_path or not os.path.isfile(gid_path):
261             print "No such gid file: %s" % gidfile
262             sys.exit(1)
263         gid = GID(filename=gid_path)
264         gid.dump(dump_parents=True)
265     
266
267 class AggregateCommands(Commands):
268
269     def __init__(self, *args, **kwds):
270         self.api= Generic.the_flavour().make_api(interface='aggregate')
271    
272     def version(self):
273         """Display the Aggregate version"""
274         version = self.api.manager.GetVersion(self.api, {})
275         pprinter.pprint(version)
276
277     def slices(self):
278         """List the running slices at this Aggregate"""
279         print self.api.manager.ListSlices(self.api, [], {})
280
281     @args('-x', '--xrn', dest='xrn', metavar='<xrn>', help='object hrn/urn (mandatory)') 
282     def status(self, xrn):
283         """Display the status of a slice or slivers"""
284         urn = Xrn(xrn, 'slice').get_urn()
285         status = self.api.manager.SliverStatus(self.api, urn, [], {})
286         pprinter.pprint(status)
287  
288     @args('-x', '--xrn', dest='xrn', metavar='<xrn>', help='object hrn/urn', default=None)
289     @args('-r', '--rspec-version', dest='rspec_version', metavar='<rspec_version>', 
290           default='GENI', help='version/format of the resulting rspec response')  
291     def resources(self, xrn=None, rspec_version='GENI'):
292         """Display the available resources at an aggregate 
293 or the resources allocated by a slice"""  
294         options = {'geni_rspec_version': rspec_version}
295         if xrn:
296             options['geni_slice_urn'] = Xrn(xrn, 'slice').get_urn()
297         print options
298         resources = self.api.manager.ListResources(self.api, [], options)
299         print resources
300         
301     @args('-x', '--xrn', dest='xrn', metavar='<xrn>', help='slice hrn/urn (mandatory)')
302     @args('-r', '--rspec', dest='rspec', metavar='<rspec>', help='rspec file (mandatory)')  
303     @args('-u', '--user', dest='user', metavar='<user>', help='hrn/urn of slice user (mandatory)')  
304     @args('-k', '--key', dest='key', metavar='<key>', help="path to user's public key file (mandatory)")  
305     def create(self, xrn, rspec, user, key):
306         """Allocate slivers"""
307         xrn = Xrn(xrn, 'slice')
308         slice_urn=xrn.get_urn()
309         rspec_string = open(rspec).read()
310         user_xrn = Xrn(user, 'user')
311         user_urn = user_xrn.get_urn()
312         user_key_string = open(key).read()
313         users = [{'urn': user_urn, 'keys': [user_key_string]}]
314         options={}
315         self.api.manager.CreateSliver(self, slice_urn, [], rspec_string, users, options) 
316
317     @args('-x', '--xrn', dest='xrn', metavar='<xrn>', help='slice hrn/urn (mandatory)')
318     def delete(self, xrn):
319         """Delete slivers""" 
320         self.api.manager.DeleteSliver(self.api, xrn, [], {})
321  
322     @args('-x', '--xrn', dest='xrn', metavar='<xrn>', help='slice hrn/urn (mandatory)')
323     def start(self, xrn):
324         """Start slivers"""
325         self.api.manager.start_slice(self.api, xrn, [])
326
327     @args('-x', '--xrn', dest='xrn', metavar='<xrn>', help='slice hrn/urn (mandatory)')
328     def stop(self, xrn):
329         """Stop slivers"""
330         self.api.manager.stop_slice(self.api, xrn, [])      
331
332     @args('-x', '--xrn', dest='xrn', metavar='<xrn>', help='slice hrn/urn (mandatory)')
333     def reset(self, xrn):
334         """Reset sliver"""
335         self.api.manager.reset_slice(self.api, xrn)
336
337
338 #    @args('-x', '--xrn', dest='xrn', metavar='<xrn>', help='object hrn/urn', default=None)
339 #    @args('-r', '--rspec', dest='rspec', metavar='<rspec>', help='request rspec', default=None)
340 #    def ticket(self, xrn, rspec):
341 #        pass
342
343
344
345 class SliceManagerCommands(AggregateCommands):
346     
347     def __init__(self, *args, **kwds):
348         self.api= Generic.the_flavour().make_api(interface='slicemgr')
349
350
351 class SfaAdmin:
352
353     CATEGORIES = {'certificate': CertCommands,
354                   'registry': RegistryCommands,
355                   'aggregate': AggregateCommands,
356                   'slicemgr': SliceManagerCommands}
357
358     # returns (name,class) or (None,None)
359     def find_category (self, input):
360         full_name=Candidates (SfaAdmin.CATEGORIES.keys()).only_match(input)
361         if not full_name: return (None,None)
362         return (full_name,SfaAdmin.CATEGORIES[full_name])
363
364     def summary_usage (self, category=None):
365         print "Usage:", self.script_name + " category command [<options>]"
366         if category and category in SfaAdmin.CATEGORIES: 
367             categories=[category]
368         else:
369             categories=SfaAdmin.CATEGORIES
370         for c in categories:
371             cls=SfaAdmin.CATEGORIES[c]
372             print "==================== category=%s"%c
373             names=cls.__dict__.keys()
374             names.sort()
375             for name in names:
376                 method=cls.__dict__[name]
377                 if name.startswith('_'): continue
378                 margin=15
379                 format="%%-%ds"%margin
380                 print "%-15s"%name,
381                 doc=getattr(method,'__doc__',None)
382                 if not doc: 
383                     print "<missing __doc__>"
384                     continue
385                 lines=[line.strip() for line in doc.split("\n")]
386                 line1=lines.pop(0)
387                 print line1
388                 for extra_line in lines: print margin*" ",extra_line
389         sys.exit(2)
390
391     def main(self):
392         argv = copy.deepcopy(sys.argv)
393         self.script_name = argv.pop(0)
394         # ensure category is specified    
395         if len(argv) < 1:
396             self.summary_usage()
397
398         # ensure category is valid
399         category_input = argv.pop(0)
400         (category_name, category_class) = self.find_category (category_input)
401         if not category_name or not category_class:
402             self.summary_usage(category_name)
403
404         usage = "%%prog %s command [options]" % (category_name)
405         parser = OptionParser(usage=usage)
406     
407         # ensure command is valid      
408         category_instance = category_class()
409         commands = category_instance._get_commands()
410         if len(argv) < 1:
411             # xxx what is this about ?
412             command_name = '__call__'
413         else:
414             command_input = argv.pop(0)
415             command_name = Candidates (commands).only_match (command_input)
416     
417         if command_name and hasattr(category_instance, command_name):
418             command = getattr(category_instance, command_name)
419         else:
420             self.summary_usage(category_name)
421
422         # ensure options are valid
423         options = getattr(command, 'options', [])
424         usage = "%%prog %s %s [options]" % (category_name, command_name)
425         parser = OptionParser(usage=usage)
426         for arg, kwd in options:
427             parser.add_option(*arg, **kwd)
428         (opts, cmd_args) = parser.parse_args(argv)
429         cmd_kwds = vars(opts)
430
431         # dont overrride meth
432         for k, v in cmd_kwds.items():
433             if v is None:
434                 del cmd_kwds[k]
435
436         # execute command
437         try:
438             #print "invoking %s *=%s **=%s"%(command.__name__,cmd_args, cmd_kwds)
439             command(*cmd_args, **cmd_kwds)
440             sys.exit(0)
441         except TypeError:
442             print "Possible wrong number of arguments supplied"
443             #import traceback
444             #traceback.print_exc()
445             print command.__doc__
446             parser.print_help()
447             #raise
448         except Exception:
449             print "Command failed, please check log for more info"
450             raise
451