refactored sfalogging to be less awkward and more reliable
[sfa.git] / sfa / client / sfaadmin.py
1 #!/usr/bin/python
2
3 # pylint: disable=c0111, c0103, w0402, w0622
4
5 from __future__ import print_function
6
7 import os
8 import sys
9 import copy
10 from pprint import PrettyPrinter
11 from optparse import OptionParser
12
13 from sfa.generic import Generic
14 from sfa.util.xrn import Xrn
15 from sfa.storage.record import Record
16
17 from sfa.trust.hierarchy import Hierarchy
18 from sfa.trust.gid import GID
19 from sfa.trust.certificate import convert_public_key
20
21 from sfa.client.common import (optparse_listvalue_callback,
22                                optparse_dictvalue_callback,
23                                terminal_render, filter_records)
24 from sfa.client.candidates import Candidates
25 from sfa.client.sfi import save_records_to_file
26
27 pprinter = PrettyPrinter(indent=4)
28
29 try:
30     help_basedir = Hierarchy().basedir
31 except Exception:
32     help_basedir = '*unable to locate Hierarchy().basedir'
33
34
35 def add_options(*args, **kwargs):
36     def _decorator(func):
37         func.__dict__.setdefault('add_options', []).insert(0, (args, kwargs))
38         return func
39     return _decorator
40
41
42 class Commands(object):
43
44     def _get_commands(self):
45         command_names = []
46         for attrib in dir(self):
47             if callable(getattr(self, attrib)) and not attrib.startswith('_'):
48                 command_names.append(attrib)
49         return command_names
50
51
52 class RegistryCommands(Commands):
53
54     def __init__(self, *args, **kwds):
55         self.api = Generic.the_flavour().make_api(interface='registry')
56
57     def version(self):
58         """Display the Registry version"""
59         version = self.api.manager.GetVersion(self.api, {})
60         pprinter.pprint(version)
61
62     @add_options('-x', '--xrn', dest='xrn', metavar='<xrn>',
63                  help='authority to list (hrn/urn - mandatory)')
64     @add_options('-t', '--type', dest='type', metavar='<type>',
65                  help='object type', default='all')
66     @add_options('-r', '--recursive', dest='recursive', metavar='<recursive>',
67                  help='list all child records',
68                  action='store_true', default=False)
69     @add_options('-v', '--verbose', dest='verbose',
70                  action='store_true', default=False)
71     def list(self, xrn, type=None, recursive=False, verbose=False):
72         """
73         List names registered at a given authority, possibly filtered by type
74         """
75         xrn = Xrn(xrn, type)
76         options_dict = {'recursive': recursive}
77         records = self.api.manager.List(
78             self.api, xrn.get_hrn(), options=options_dict)
79         list = filter_records(type, records)
80         # terminal_render expects an options object
81
82         class Options:                                  # pylint: disable=r0903
83             def __init__(self, verbose):
84                 self.verbose = verbose
85
86         options = Options(verbose)
87         terminal_render(list, options)
88
89     @add_options('-x', '--xrn', dest='xrn', metavar='<xrn>',
90                  help='object hrn/urn (mandatory)')
91     @add_options('-t', '--type', dest='type', metavar='<type>',
92                  help='object type', default=None)
93     @add_options('-o', '--outfile', dest='outfile', metavar='<outfile>',
94                  help='save record to file')
95     @add_options('-f', '--format', dest='format', metavar='<display>',
96                  type='choice', choices=('text', 'xml', 'simple'),
97                  help='display record in different formats')
98     def show(self, xrn, type=None, format=None, outfile=None):
99         """Display details for a registered object"""
100         records = self.api.manager.Resolve(self.api, xrn, type, details=True)
101         for record in records:
102             sfa_record = Record(dict=record)
103             sfa_record.dump(format)
104         if outfile:
105             save_records_to_file(outfile, records)
106
107     @staticmethod
108     def _record_dict(xrn, type, email, key,
109                      slices, researchers, pis,
110                      url, description, extras):
111         record_dict = {}
112         if xrn:
113             if type:
114                 xrn = Xrn(xrn, type)
115             else:
116                 xrn = Xrn(xrn)
117             record_dict['urn'] = xrn.get_urn()
118             record_dict['hrn'] = xrn.get_hrn()
119             record_dict['type'] = xrn.get_type()
120         if url:
121             record_dict['url'] = url
122         if description:
123             record_dict['description'] = description
124         if key:
125             try:
126                 pubkey = open(key, 'r').read()
127             except IOError:
128                 pubkey = key
129             record_dict['reg-keys'] = [pubkey]
130         if slices:
131             record_dict['slices'] = slices
132         if researchers:
133             record_dict['reg-researchers'] = researchers
134         if email:
135             record_dict['email'] = email
136         if pis:
137             record_dict['reg-pis'] = pis
138         if extras:
139             record_dict.update(extras)
140         return record_dict
141
142     @add_options('-x', '--xrn', dest='xrn', metavar='<xrn>',
143                  help='object hrn/urn', default=None)
144     @add_options('-t', '--type', dest='type', metavar='<type>',
145                  help='object type (mandatory)')
146     @add_options('-a', '--all', dest='all', metavar='<all>',
147                  action='store_true', default=False,
148                  help='check all users GID')
149     @add_options('-v', '--verbose', dest='verbose', metavar='<verbose>',
150                  action='store_true', default=False,
151                  help='verbose mode: display user\'s hrn ')
152     def check_gid(self, xrn=None, type=None, all=None, verbose=None):
153         """Check the correspondance between the GID and the PubKey"""
154
155         # db records
156         from sfa.storage.model import RegRecord
157         db_query = self.api.dbsession().query(RegRecord).filter_by(type=type)
158         if xrn and not all:
159             hrn = Xrn(xrn).get_hrn()
160             db_query = db_query.filter_by(hrn=hrn)
161         elif all and xrn:
162             print("Use either -a or -x <xrn>, not both !!!")
163             sys.exit(1)
164         elif not all and not xrn:
165             print("Use either -a or -x <xrn>, one of them is mandatory !!!")
166             sys.exit(1)
167
168         records = db_query.all()
169         if not records:
170             print("No Record found")
171             sys.exit(1)
172
173         OK = []
174         NOK = []
175         ERROR = []
176         NOKEY = []
177         for record in records:
178             # get the pubkey stored in SFA DB
179             if record.reg_keys:
180                 db_pubkey_str = record.reg_keys[0].key
181                 try:
182                     db_pubkey_obj = convert_public_key(db_pubkey_str)
183                 except Exception:
184                     ERROR.append(record.hrn)
185                     continue
186             else:
187                 NOKEY.append(record.hrn)
188                 continue
189
190             # get the pubkey from the gid
191             gid_str = record.gid
192             gid_obj = GID(string=gid_str)
193             gid_pubkey_obj = gid_obj.get_pubkey()
194
195             # Check if gid_pubkey_obj and db_pubkey_obj are the same
196             check = gid_pubkey_obj.is_same(db_pubkey_obj)
197             if check:
198                 OK.append(record.hrn)
199             else:
200                 NOK.append(record.hrn)
201
202         if not verbose:
203             print("Users NOT having a PubKey: %s\n\
204 Users having a non RSA PubKey: %s\n\
205 Users having a GID/PubKey correpondence OK: %s\n\
206 Users having a GID/PubKey correpondence Not OK: %s\n"
207                   % (len(NOKEY), len(ERROR), len(OK), len(NOK)))
208         else:
209             print("Users NOT having a PubKey: %s and are: \n%s\n\n\
210 Users having a non RSA PubKey: %s and are: \n%s\n\n\
211 Users having a GID/PubKey correpondence OK: %s and are: \n%s\n\n\
212 Users having a GID/PubKey correpondence NOT OK: %s and are: \n%s\n\n"
213                   % (len(NOKEY), NOKEY, len(ERROR), ERROR,
214                      len(OK), OK, len(NOK), NOK))
215
216
217     @add_options('-x', '--xrn', dest='xrn', metavar='<xrn>',
218                  help='object hrn/urn (mandatory)')
219     @add_options('-t', '--type', dest='type', metavar='<type>',
220                  help='object type', default=None)
221     @add_options('-e', '--email', dest='email', default="",
222                  help="email (mandatory for users)")
223     @add_options('-u', '--url', dest='url', metavar='<url>', default=None,
224                  help="URL, useful for slices")
225     @add_options('-d', '--description', dest='description',
226                  metavar='<description>',
227                  help='Description, useful for slices', default=None)
228     @add_options('-k', '--key', dest='key', metavar='<key>',
229                  help='public key string or file',
230                  default=None)
231     @add_options('-s', '--slices', dest='slices', metavar='<slices>',
232                  help='Set/replace slice xrns',
233                  default='', type="str", action='callback',
234                  callback=optparse_listvalue_callback)
235     @add_options('-r', '--researchers', dest='researchers',
236                  metavar='<researchers>', help='Set/replace slice researchers',
237                  default='', type="str", action='callback',
238                  callback=optparse_listvalue_callback)
239     @add_options('-p', '--pis', dest='pis', metavar='<PIs>',
240                  help='Set/replace Principal Investigators/Project Managers',
241                  default='', type="str", action='callback',
242                  callback=optparse_listvalue_callback)
243     @add_options('-X', '--extra', dest='extras',
244                  default={}, type='str', metavar="<EXTRA_ASSIGNS>",
245                  action="callback", callback=optparse_dictvalue_callback,
246                  nargs=1,
247                  help="set extra/testbed-dependent flags,"
248                       " e.g. --extra enabled=true")
249     def register(self, xrn, type=None, email='', key=None,
250                  slices='', pis='', researchers='',
251                  url=None, description=None, extras={}):
252         """Create a new Registry record"""
253         record_dict = self._record_dict(
254             xrn=xrn, type=type, email=email, key=key,
255             slices=slices, researchers=researchers, pis=pis,
256             url=url, description=description, extras=extras)
257         self.api.manager.Register(self.api, record_dict)
258
259     @add_options('-x', '--xrn', dest='xrn', metavar='<xrn>',
260                  help='object hrn/urn (mandatory)')
261     @add_options('-t', '--type', dest='type', metavar='<type>',
262                  help='object type', default=None)
263     @add_options('-u', '--url', dest='url', metavar='<url>',
264                  help='URL', default=None)
265     @add_options('-d', '--description', dest='description',
266                  metavar='<description>',
267                  help='Description', default=None)
268     @add_options('-k', '--key', dest='key', metavar='<key>',
269                  help='public key string or file',
270                  default=None)
271     @add_options('-s', '--slices', dest='slices', metavar='<slices>',
272                  help='Set/replace slice xrns',
273                  default='', type="str", action='callback',
274                  callback=optparse_listvalue_callback)
275     @add_options('-r', '--researchers', dest='researchers',
276                  metavar='<researchers>', help='Set/replace slice researchers',
277                  default='', type="str", action='callback',
278                  callback=optparse_listvalue_callback)
279     @add_options('-p', '--pis', dest='pis', metavar='<PIs>',
280                  help='Set/replace Principal Investigators/Project Managers',
281                  default='', type="str", action='callback',
282                  callback=optparse_listvalue_callback)
283     @add_options('-X', '--extra', dest='extras', default={}, type='str',
284                  metavar="<EXTRA_ASSIGNS>", nargs=1,
285                  action="callback", callback=optparse_dictvalue_callback,
286                  help="set extra/testbed-dependent flags,"
287                       " e.g. --extra enabled=true")
288     def update(self, xrn, type=None, email='', key=None,
289                slices='', pis='', researchers='',
290                url=None, description=None, extras={}):
291         """Update an existing Registry record"""
292         record_dict = self._record_dict(
293             xrn=xrn, type=type, email=email, key=key,
294             slices=slices, researchers=researchers, pis=pis,
295             url=url, description=description, extras=extras)
296         self.api.manager.Update(self.api, record_dict)
297
298     @add_options('-x', '--xrn', dest='xrn', metavar='<xrn>',
299                  help='object hrn/urn (mandatory)')
300     @add_options('-t', '--type', dest='type', metavar='<type>',
301                  help='object type', default=None)
302     def remove(self, xrn, type=None):
303         """Remove given object from the registry"""
304         xrn = Xrn(xrn, type)
305         self.api.manager.Remove(self.api, xrn)
306
307     @add_options('-x', '--xrn', dest='xrn', metavar='<xrn>',
308                  help='object hrn/urn (mandatory)')
309     @add_options('-t', '--type', dest='type', metavar='<type>',
310                  help='object type', default=None)
311     def credential(self, xrn, type=None):
312         """Invoke GetCredential"""
313         cred = self.api.manager.GetCredential(
314             self.api, xrn, type, self.api.hrn)
315         print(cred)
316
317
318     def import_registry(self):
319         """Run the importer"""
320         from sfa.importer import Importer
321         importer = Importer()
322         importer.run()
323
324
325     def sync_db(self):
326         """Initialize or upgrade the db"""
327         from sfa.storage.dbschema import DBSchema
328         dbschema = DBSchema()
329         dbschema.init_or_upgrade()
330
331
332     @add_options('-a', '--all', dest='all', metavar='<all>',
333                  action='store_true', default=False,
334                  help='Remove all registry records and all files in %s area'
335                  % help_basedir)
336     @add_options('-c', '--certs', dest='certs',
337                  metavar='<certs>', action='store_true', default=False,
338                  help='Remove all cached certs/gids found in %s'
339                  % help_basedir)
340     @add_options('-0', '--no-reinit', dest='reinit', metavar='<reinit>',
341                  action='store_false', default=True,
342                  help="Prevents new DB schema"
343                  " from being installed after cleanup")
344     def nuke(self, all=False, certs=False, reinit=True):
345         """
346         Cleanup local registry DB, plus various additional
347         filesystem cleanups optionally
348         """
349         from sfa.storage.dbschema import DBSchema
350         from sfa.util.sfalogging import init_logger, logger
351         init_logger('import')
352         logger.setLevelFromOptVerbose(self.api.config.SFA_API_LOGLEVEL)
353         logger.info("Purging SFA records from database")
354         dbschema = DBSchema()
355         dbschema.nuke()
356
357         # for convenience we re-create the schema here,
358         # so there's no need for an explicit
359         # service sfa restart
360         # however in some (upgrade) scenarios this might be wrong
361         if reinit:
362             logger.info("re-creating empty schema")
363             dbschema.init_or_upgrade()
364
365         # remove the server certificate and all gids found in
366         # /var/lib/sfa/authorities
367         if certs:
368             logger.info("Purging cached certificates")
369             for (dir, _, files) in os.walk('/var/lib/sfa/authorities'):
370                 for file in files:
371                     if file.endswith('.gid') or file == 'server.cert':
372                         path = dir + os.sep + file
373                         os.unlink(path)
374
375         # just remove all files that do not match 'server.key' or 'server.cert'
376         if all:
377             logger.info("Purging registry filesystem cache")
378             preserved_files = ['server.key', 'server.cert']
379             for dir, _, files in os.walk(Hierarchy().basedir):
380                 for file in files:
381                     if file in preserved_files:
382                         continue
383                     path = dir + os.sep + file
384                     os.unlink(path)
385
386
387 class CertCommands(Commands):
388
389     def __init__(self, *args, **kwds):
390         self.api = Generic.the_flavour().make_api(interface='registry')
391
392     def import_gid(self, xrn):
393         pass
394
395     @add_options('-x', '--xrn', dest='xrn', metavar='<xrn>',
396                  help='object hrn/urn (mandatory)')
397     @add_options('-t', '--type', dest='type', metavar='<type>',
398                  help='object type', default=None)
399     @add_options('-o', '--outfile', dest='outfile', metavar='<outfile>',
400                  help='output file', default=None)
401     def export(self, xrn, type=None, outfile=None):
402         """Fetch an object's GID from the Registry"""
403         from sfa.storage.model import RegRecord
404         hrn = Xrn(xrn).get_hrn()
405         request = self.api.dbsession().query(RegRecord).filter_by(hrn=hrn)
406         if type:
407             request = request.filter_by(type=type)
408         record = request.first()
409         if record:
410             gid = GID(string=record.gid)
411         else:
412             # check the authorities hierarchy
413             hierarchy = Hierarchy()
414             try:
415                 auth_info = hierarchy.get_auth_info(hrn)
416                 gid = auth_info.gid_object
417             except Exception:
418                 print("Record: %s not found" % hrn)
419                 sys.exit(1)
420         # save to file
421         if not outfile:
422             outfile = os.path.abspath('./%s.gid' % gid.get_hrn())
423         gid.save_to_file(outfile, save_parents=True)
424
425     @add_options('-g', '--gidfile', dest='gid', metavar='<gid>',
426                  help='path of gid file to display (mandatory)')
427     def display(self, gidfile):
428         """Print contents of a GID file"""
429         gid_path = os.path.abspath(gidfile)
430         if not gid_path or not os.path.isfile(gid_path):
431             print("No such gid file: %s" % gidfile)
432             sys.exit(1)
433         gid = GID(filename=gid_path)
434         gid.dump(dump_parents=True)
435
436
437 class AggregateCommands(Commands):
438
439     def __init__(self, *args, **kwds):
440         self.api = Generic.the_flavour().make_api(interface='aggregate')
441
442     def version(self):
443         """Display the Aggregate version"""
444         version = self.api.manager.GetVersion(self.api, {})
445         pprinter.pprint(version)
446
447     @add_options('-x', '--xrn', dest='xrn', metavar='<xrn>',
448                  help='object hrn/urn (mandatory)')
449     def status(self, xrn):
450         """
451         Retrieve the status of the slivers
452         belonging to the named slice (Status)
453         """
454         urns = [Xrn(xrn, 'slice').get_urn()]
455         status = self.api.manager.Status(self.api, urns, [], {})
456         pprinter.pprint(status)
457
458     @add_options('-r', '--rspec-version', dest='rspec_version',
459                  metavar='<rspec_version>', default='GENI',
460                  help='version/format of the resulting rspec response')
461     def resources(self, rspec_version='GENI'):
462         """Display the available resources at an aggregate"""
463         options = {'geni_rspec_version': rspec_version}
464         print(options)
465         resources = self.api.manager.ListResources(self.api, [], options)
466         print(resources)
467
468     @add_options('-x', '--xrn', dest='xrn', metavar='<xrn>',
469                  help='slice hrn/urn (mandatory)')
470     @add_options('-r', '--rspec', dest='rspec', metavar='<rspec>',
471                  help='rspec file (mandatory)')
472     def allocate(self, xrn, rspec):
473         """Allocate slivers"""
474         xrn = Xrn(xrn, 'slice')
475         slice_urn = xrn.get_urn()
476         rspec_string = open(rspec).read()
477         options = {}
478         manifest = self.api.manager.Allocate(
479             self.api, slice_urn, [], rspec_string, options)
480         print(manifest)
481
482     @add_options('-x', '--xrn', dest='xrn', metavar='<xrn>',
483                  help='slice hrn/urn (mandatory)')
484     def provision(self, xrn):
485         """Provision slivers"""
486         xrn = Xrn(xrn, 'slice')
487         slice_urn = xrn.get_urn()
488         options = {}
489         manifest = self.api.manager.provision(
490             self.api, [slice_urn], [], options)
491         print(manifest)
492
493     @add_options('-x', '--xrn', dest='xrn', metavar='<xrn>',
494                  help='slice hrn/urn (mandatory)')
495     def delete(self, xrn):
496         """Delete slivers"""
497         self.api.manager.Delete(self.api, [xrn], [], {})
498
499
500 class SliceManagerCommands(AggregateCommands):
501
502     def __init__(self, *args, **kwds):
503         self.api = Generic.the_flavour().make_api(interface='slicemgr')
504
505
506 class SfaAdmin:
507
508     CATEGORIES = {'certificate': CertCommands,
509                   'registry': RegistryCommands,
510                   'aggregate': AggregateCommands,
511                   'slicemgr': SliceManagerCommands}
512
513     # returns (name,class) or (None,None)
514     def find_category(self, input):
515         full_name = Candidates(SfaAdmin.CATEGORIES.keys()).only_match(input)
516         if not full_name:
517             return (None, None)
518         return (full_name, SfaAdmin.CATEGORIES[full_name])
519
520     def summary_usage(self, category=None):
521         print("Usage:", self.script_name + " category command [<options>]")
522         if category and category in SfaAdmin.CATEGORIES:
523             categories = [category]
524         else:
525             categories = SfaAdmin.CATEGORIES
526         for c in categories:
527             cls = SfaAdmin.CATEGORIES[c]
528             print("==================== category=%s" % c)
529             names = cls.__dict__.keys()
530             names.sort()
531             for name in names:
532                 method = cls.__dict__[name]
533                 if name.startswith('_'):
534                     continue
535                 margin = 15
536                 print("%-15s" % name, end=' ')
537                 doc = getattr(method, '__doc__', None)
538                 if not doc:
539                     print("<missing __doc__>")
540                     continue
541                 lines = [line.strip() for line in doc.split("\n")]
542                 line1 = lines.pop(0)
543                 print(line1)
544                 for extra_line in lines:
545                     print(margin * " ", extra_line)
546         sys.exit(2)
547
548     def main(self):
549         argv = copy.deepcopy(sys.argv)
550         self.script_name = argv.pop(0)
551         # ensure category is specified
552         if len(argv) < 1:
553             self.summary_usage()
554
555         # ensure category is valid
556         category_input = argv.pop(0)
557         (category_name, category_class) = self.find_category(category_input)
558         if not category_name or not category_class:
559             self.summary_usage(category_name)
560
561         usage = "%%prog %s command [options]" % (category_name)
562         parser = OptionParser(usage=usage)
563
564         # ensure command is valid
565         category_instance = category_class()
566         commands = category_instance._get_commands()
567         if len(argv) < 1:
568             # xxx what is this about ?
569             command_name = '__call__'
570         else:
571             command_input = argv.pop(0)
572             command_name = Candidates(commands).only_match(command_input)
573
574         if command_name and hasattr(category_instance, command_name):
575             command = getattr(category_instance, command_name)
576         else:
577             self.summary_usage(category_name)
578
579         # ensure options are valid
580         usage = "%%prog %s %s [options]" % (category_name, command_name)
581         parser = OptionParser(usage=usage)
582         for args, kwdargs in getattr(command, 'add_options', []):
583             parser.add_option(*args, **kwdargs)
584         (opts, cmd_args) = parser.parse_args(argv)
585         cmd_kwds = vars(opts)
586
587         # dont overrride meth
588         for k, v in cmd_kwds.items():
589             if v is None:
590                 del cmd_kwds[k]
591
592         # execute command
593         try:
594             # print "invoking %s *=%s **=%s"%(command.__name__, cmd_args,
595             # cmd_kwds)
596             command(*cmd_args, **cmd_kwds)
597             sys.exit(0)
598         except TypeError:
599             print("Possible wrong number of arguments supplied")
600             print(command.__doc__)
601             parser.print_help()
602             sys.exit(1)
603             # raise
604         except Exception:
605             print("Command failed, please check log for more info")
606             raise
607             sys.exit(1)