more pep8-friendly
[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 _SfaLogger
351         logger = _SfaLogger(
352             logfile='/var/log/sfa_import.log', loggername='importlog')
353         logger.setLevelFromOptVerbose(self.api.config.SFA_API_LOGLEVEL)
354         logger.info("Purging SFA records from database")
355         dbschema = DBSchema()
356         dbschema.nuke()
357
358         # for convenience we re-create the schema here,
359         # so there's no need for an explicit
360         # service sfa restart
361         # however in some (upgrade) scenarios this might be wrong
362         if reinit:
363             logger.info("re-creating empty schema")
364             dbschema.init_or_upgrade()
365
366         # remove the server certificate and all gids found in
367         # /var/lib/sfa/authorities
368         if certs:
369             logger.info("Purging cached certificates")
370             for (dir, _, files) in os.walk('/var/lib/sfa/authorities'):
371                 for file in files:
372                     if file.endswith('.gid') or file == 'server.cert':
373                         path = dir + os.sep + file
374                         os.unlink(path)
375
376         # just remove all files that do not match 'server.key' or 'server.cert'
377         if all:
378             logger.info("Purging registry filesystem cache")
379             preserved_files = ['server.key', 'server.cert']
380             for dir, _, files in os.walk(Hierarchy().basedir):
381                 for file in files:
382                     if file in preserved_files:
383                         continue
384                     path = dir + os.sep + file
385                     os.unlink(path)
386
387
388 class CertCommands(Commands):
389
390     def __init__(self, *args, **kwds):
391         self.api = Generic.the_flavour().make_api(interface='registry')
392
393     def import_gid(self, xrn):
394         pass
395
396     @add_options('-x', '--xrn', dest='xrn', metavar='<xrn>',
397                  help='object hrn/urn (mandatory)')
398     @add_options('-t', '--type', dest='type', metavar='<type>',
399                  help='object type', default=None)
400     @add_options('-o', '--outfile', dest='outfile', metavar='<outfile>',
401                  help='output file', default=None)
402     def export(self, xrn, type=None, outfile=None):
403         """Fetch an object's GID from the Registry"""
404         from sfa.storage.model import RegRecord
405         hrn = Xrn(xrn).get_hrn()
406         request = self.api.dbsession().query(RegRecord).filter_by(hrn=hrn)
407         if type:
408             request = request.filter_by(type=type)
409         record = request.first()
410         if record:
411             gid = GID(string=record.gid)
412         else:
413             # check the authorities hierarchy
414             hierarchy = Hierarchy()
415             try:
416                 auth_info = hierarchy.get_auth_info(hrn)
417                 gid = auth_info.gid_object
418             except Exception:
419                 print("Record: %s not found" % hrn)
420                 sys.exit(1)
421         # save to file
422         if not outfile:
423             outfile = os.path.abspath('./%s.gid' % gid.get_hrn())
424         gid.save_to_file(outfile, save_parents=True)
425
426     @add_options('-g', '--gidfile', dest='gid', metavar='<gid>',
427                  help='path of gid file to display (mandatory)')
428     def display(self, gidfile):
429         """Print contents of a GID file"""
430         gid_path = os.path.abspath(gidfile)
431         if not gid_path or not os.path.isfile(gid_path):
432             print("No such gid file: %s" % gidfile)
433             sys.exit(1)
434         gid = GID(filename=gid_path)
435         gid.dump(dump_parents=True)
436
437
438 class AggregateCommands(Commands):
439
440     def __init__(self, *args, **kwds):
441         self.api = Generic.the_flavour().make_api(interface='aggregate')
442
443     def version(self):
444         """Display the Aggregate version"""
445         version = self.api.manager.GetVersion(self.api, {})
446         pprinter.pprint(version)
447
448     @add_options('-x', '--xrn', dest='xrn', metavar='<xrn>',
449                  help='object hrn/urn (mandatory)')
450     def status(self, xrn):
451         """
452         Retrieve the status of the slivers
453         belonging to the named slice (Status)
454         """
455         urns = [Xrn(xrn, 'slice').get_urn()]
456         status = self.api.manager.Status(self.api, urns, [], {})
457         pprinter.pprint(status)
458
459     @add_options('-r', '--rspec-version', dest='rspec_version',
460                  metavar='<rspec_version>', default='GENI',
461                  help='version/format of the resulting rspec response')
462     def resources(self, rspec_version='GENI'):
463         """Display the available resources at an aggregate"""
464         options = {'geni_rspec_version': rspec_version}
465         print(options)
466         resources = self.api.manager.ListResources(self.api, [], options)
467         print(resources)
468
469     @add_options('-x', '--xrn', dest='xrn', metavar='<xrn>',
470                  help='slice hrn/urn (mandatory)')
471     @add_options('-r', '--rspec', dest='rspec', metavar='<rspec>',
472                  help='rspec file (mandatory)')
473     def allocate(self, xrn, rspec):
474         """Allocate slivers"""
475         xrn = Xrn(xrn, 'slice')
476         slice_urn = xrn.get_urn()
477         rspec_string = open(rspec).read()
478         options = {}
479         manifest = self.api.manager.Allocate(
480             self.api, slice_urn, [], rspec_string, options)
481         print(manifest)
482
483     @add_options('-x', '--xrn', dest='xrn', metavar='<xrn>',
484                  help='slice hrn/urn (mandatory)')
485     def provision(self, xrn):
486         """Provision slivers"""
487         xrn = Xrn(xrn, 'slice')
488         slice_urn = xrn.get_urn()
489         options = {}
490         manifest = self.api.manager.provision(
491             self.api, [slice_urn], [], options)
492         print(manifest)
493
494     @add_options('-x', '--xrn', dest='xrn', metavar='<xrn>',
495                  help='slice hrn/urn (mandatory)')
496     def delete(self, xrn):
497         """Delete slivers"""
498         self.api.manager.Delete(self.api, [xrn], [], {})
499
500
501 class SliceManagerCommands(AggregateCommands):
502
503     def __init__(self, *args, **kwds):
504         self.api = Generic.the_flavour().make_api(interface='slicemgr')
505
506
507 class SfaAdmin:
508
509     CATEGORIES = {'certificate': CertCommands,
510                   'registry': RegistryCommands,
511                   'aggregate': AggregateCommands,
512                   'slicemgr': SliceManagerCommands}
513
514     # returns (name,class) or (None,None)
515     def find_category(self, input):
516         full_name = Candidates(SfaAdmin.CATEGORIES.keys()).only_match(input)
517         if not full_name:
518             return (None, None)
519         return (full_name, SfaAdmin.CATEGORIES[full_name])
520
521     def summary_usage(self, category=None):
522         print("Usage:", self.script_name + " category command [<options>]")
523         if category and category in SfaAdmin.CATEGORIES:
524             categories = [category]
525         else:
526             categories = SfaAdmin.CATEGORIES
527         for c in categories:
528             cls = SfaAdmin.CATEGORIES[c]
529             print("==================== category=%s" % c)
530             names = cls.__dict__.keys()
531             names.sort()
532             for name in names:
533                 method = cls.__dict__[name]
534                 if name.startswith('_'):
535                     continue
536                 margin = 15
537                 print("%-15s" % name, end=' ')
538                 doc = getattr(method, '__doc__', None)
539                 if not doc:
540                     print("<missing __doc__>")
541                     continue
542                 lines = [line.strip() for line in doc.split("\n")]
543                 line1 = lines.pop(0)
544                 print(line1)
545                 for extra_line in lines:
546                     print(margin * " ", extra_line)
547         sys.exit(2)
548
549     def main(self):
550         argv = copy.deepcopy(sys.argv)
551         self.script_name = argv.pop(0)
552         # ensure category is specified
553         if len(argv) < 1:
554             self.summary_usage()
555
556         # ensure category is valid
557         category_input = argv.pop(0)
558         (category_name, category_class) = self.find_category(category_input)
559         if not category_name or not category_class:
560             self.summary_usage(category_name)
561
562         usage = "%%prog %s command [options]" % (category_name)
563         parser = OptionParser(usage=usage)
564
565         # ensure command is valid
566         category_instance = category_class()
567         commands = category_instance._get_commands()
568         if len(argv) < 1:
569             # xxx what is this about ?
570             command_name = '__call__'
571         else:
572             command_input = argv.pop(0)
573             command_name = Candidates(commands).only_match(command_input)
574
575         if command_name and hasattr(category_instance, command_name):
576             command = getattr(category_instance, command_name)
577         else:
578             self.summary_usage(category_name)
579
580         # ensure options are valid
581         usage = "%%prog %s %s [options]" % (category_name, command_name)
582         parser = OptionParser(usage=usage)
583         for args, kwdargs in getattr(command, 'add_options', []):
584             parser.add_option(*args, **kwdargs)
585         (opts, cmd_args) = parser.parse_args(argv)
586         cmd_kwds = vars(opts)
587
588         # dont overrride meth
589         for k, v in cmd_kwds.items():
590             if v is None:
591                 del cmd_kwds[k]
592
593         # execute command
594         try:
595             # print "invoking %s *=%s **=%s"%(command.__name__, cmd_args,
596             # cmd_kwds)
597             command(*cmd_args, **cmd_kwds)
598             sys.exit(0)
599         except TypeError:
600             print("Possible wrong number of arguments supplied")
601             print(command.__doc__)
602             parser.print_help()
603             sys.exit(1)
604             # raise
605         except Exception:
606             print("Command failed, please check log for more info")
607             raise
608             sys.exit(1)