allow raw output to be written to file; support raw output formats json and pickle
[sfa.git] / sfa / client / sfi.py
1 #
2 # sfi.py - basic SFA command-line client
3 # the actual binary in sfa/clientbin essentially runs main()
4 # this module is used in sfascan
5 #
6
7 import sys
8 sys.path.append('.')
9
10 import os, os.path
11 import socket
12 import datetime
13 import codecs
14 import pickle
15 import json
16 from lxml import etree
17 from StringIO import StringIO
18 from optparse import OptionParser
19 from pprint import PrettyPrinter
20
21 from sfa.trust.certificate import Keypair, Certificate
22 from sfa.trust.gid import GID
23 from sfa.trust.credential import Credential
24 from sfa.trust.sfaticket import SfaTicket
25
26 from sfa.util.sfalogging import sfi_logger
27 from sfa.util.xrn import get_leaf, get_authority, hrn_to_urn
28 from sfa.util.config import Config
29 from sfa.util.version import version_core
30 from sfa.util.cache import Cache
31
32 from sfa.storage.record import SfaRecord, UserRecord, SliceRecord, NodeRecord, AuthorityRecord
33
34 from sfa.rspecs.rspec import RSpec
35 from sfa.rspecs.rspec_converter import RSpecConverter
36 from sfa.rspecs.version_manager import VersionManager
37
38 from sfa.client.sfaclientlib import SfaClientBootstrap
39 from sfa.client.sfaserverproxy import SfaServerProxy, ServerException
40 from sfa.client.client_helper import pg_users_arg, sfa_users_arg
41 from sfa.client.return_value import ReturnValue
42
43 CM_PORT=12346
44
45 # utility methods here
46 # display methods
47 def display_rspec(rspec, format='rspec'):
48     if format in ['dns']:
49         tree = etree.parse(StringIO(rspec))
50         root = tree.getroot()
51         result = root.xpath("./network/site/node/hostname/text()")
52     elif format in ['ip']:
53         # The IP address is not yet part of the new RSpec
54         # so this doesn't do anything yet.
55         tree = etree.parse(StringIO(rspec))
56         root = tree.getroot()
57         result = root.xpath("./network/site/node/ipv4/text()")
58     else:
59         result = rspec
60
61     print result
62     return
63
64 def display_list(results):
65     for result in results:
66         print result
67
68 def display_records(recordList, dump=False):
69     ''' Print all fields in the record'''
70     for record in recordList:
71         display_record(record, dump)
72
73 def display_record(record, dump=False):
74     if dump:
75         record.dump()
76     else:
77         info = record.getdict()
78         print "%s (%s)" % (info['hrn'], info['type'])
79     return
80
81
82 def filter_records(type, records):
83     filtered_records = []
84     for record in records:
85         if (record['type'] == type) or (type == "all"):
86             filtered_records.append(record)
87     return filtered_records
88
89
90 # save methods
91 def save_raw_to_file(var, filename, format="text", banner=None):
92     if filename == "-":
93         # if filename is "-", send it to stdout
94         f = sys.stdout
95     else:
96         f = open(filename, "w")
97     if banner:
98         f.write(banner+"\n")
99     if format == "text":
100         f.write(str(var))
101     elif format == "pickled":
102         f.write(pickle.dumps(var))
103     elif format == "json":
104         if hasattr(json, "dumps"):
105             f.write(json.dumps(var))   # python 2.6
106         else:
107             f.write(json.write(var))   # python 2.5
108     else:
109         # this should never happen
110         print "unknown output format", format
111     if banner:
112         f.write('\n'+banner+"\n")
113
114 def save_rspec_to_file(rspec, filename):
115     if not filename.endswith(".rspec"):
116         filename = filename + ".rspec"
117     f = open(filename, 'w')
118     f.write(rspec)
119     f.close()
120     return
121
122 def save_records_to_file(filename, recordList, format="xml"):
123     if format == "xml":
124         index = 0
125         for record in recordList:
126             if index > 0:
127                 save_record_to_file(filename + "." + str(index), record)
128             else:
129                 save_record_to_file(filename, record)
130             index = index + 1
131     elif format == "xmllist":
132         f = open(filename, "w")
133         f.write("<recordlist>\n")
134         for record in recordList:
135             record = SfaRecord(dict=record)
136             f.write('<record hrn="' + record.get_name() + '" type="' + record.get_type() + '" />\n')
137         f.write("</recordlist>\n")
138         f.close()
139     elif format == "hrnlist":
140         f = open(filename, "w")
141         for record in recordList:
142             record = SfaRecord(dict=record)
143             f.write(record.get_name() + "\n")
144         f.close()
145     else:
146         # this should never happen
147         print "unknown output format", format
148
149 def save_record_to_file(filename, record):
150     if record['type'] in ['user']:
151         record = UserRecord(dict=record)
152     elif record['type'] in ['slice']:
153         record = SliceRecord(dict=record)
154     elif record['type'] in ['node']:
155         record = NodeRecord(dict=record)
156     elif record['type'] in ['authority', 'ma', 'sa']:
157         record = AuthorityRecord(dict=record)
158     else:
159         record = SfaRecord(dict=record)
160     str = record.save_to_string()
161     f=codecs.open(filename, encoding='utf-8',mode="w")
162     f.write(str)
163     f.close()
164     return
165
166
167 # load methods
168 def load_record_from_file(filename):
169     f=codecs.open(filename, encoding="utf-8", mode="r")
170     str = f.read()
171     f.close()
172     record = SfaRecord(string=str)
173     return record
174
175
176 import uuid
177 def unique_call_id(): return uuid.uuid4().urn
178
179 class Sfi:
180     
181     # dirty hack to make this class usable from the outside
182     required_options=['verbose',  'debug',  'registry',  'sm',  'auth',  'user', 'user_private_key']
183
184     @staticmethod
185     def default_sfi_dir ():
186         if os.path.isfile("./sfi_config"): 
187             return os.getcwd()
188         else:
189             return os.path.expanduser("~/.sfi/")
190
191     # dummy to meet Sfi's expectations for its 'options' field
192     # i.e. s/t we can do setattr on
193     class DummyOptions:
194         pass
195
196     def __init__ (self,options=None):
197         if options is None: options=Sfi.DummyOptions()
198         for opt in Sfi.required_options:
199             if not hasattr(options,opt): setattr(options,opt,None)
200         if not hasattr(options,'sfi_dir'): options.sfi_dir=Sfi.default_sfi_dir()
201         self.options = options
202         self.user = None
203         self.authority = None
204         self.logger = sfi_logger
205         self.logger.enable_console()
206         self.available_names = [ tuple[0] for tuple in Sfi.available ]
207         self.available_dict = dict (Sfi.available)
208    
209     # tuples command-name expected-args in the order in which they should appear in the help
210     available = [ 
211         ("version", ""),  
212         ("list", "authority"),
213         ("show", "name"),
214         ("add", "record"),
215         ("update", "record"),
216         ("remove", "name"),
217         ("slices", ""),
218         ("resources", "[slice_hrn]"),
219         ("create", "slice_hrn rspec"),
220         ("delete", "slice_hrn"),
221         ("status", "slice_hrn"),
222         ("start", "slice_hrn"),
223         ("stop", "slice_hrn"),
224         ("reset", "slice_hrn"),
225         ("renew", "slice_hrn time"),
226         ("shutdown", "slice_hrn"),
227         ("get_ticket", "slice_hrn rspec"),
228         ("redeem_ticket", "ticket"),
229         ("delegate", "name"),
230         ("create_gid", "[name]"),
231         ("get_trusted_certs", "cred"),
232         ]
233
234     def print_command_help (self, options):
235         verbose=getattr(options,'verbose')
236         format3="%18s %-15s %s"
237         line=80*'-'
238         if not verbose:
239             print format3%("command","cmd_args","description")
240             print line
241         else:
242             print line
243             self.create_parser().print_help()
244         for command in self.available_names:
245             args=self.available_dict[command]
246             method=getattr(self,command,None)
247             doc=""
248             if method: doc=getattr(method,'__doc__',"")
249             if not doc: doc="*** no doc found ***"
250             doc=doc.strip(" \t\n")
251             doc=doc.replace("\n","\n"+35*' ')
252             if verbose:
253                 print line
254             print format3%(command,args,doc)
255             if verbose:
256                 self.create_command_parser(command).print_help()
257
258     def create_command_parser(self, command):
259         if command not in self.available_dict:
260             msg="Invalid command\n"
261             msg+="Commands: "
262             msg += ','.join(self.available_names)            
263             self.logger.critical(msg)
264             sys.exit(2)
265
266         parser = OptionParser(usage="sfi [sfi_options] %s [cmd_options] %s" \
267                                      % (command, self.available_dict[command]))
268
269         # user specifies remote aggregate/sm/component                          
270         if command in ("resources", "slices", "create", "delete", "start", "stop", 
271                        "restart", "shutdown",  "get_ticket", "renew", "status"):
272             parser.add_option("-d", "--delegate", dest="delegate", default=None, 
273                              action="store_true",
274                              help="Include a credential delegated to the user's root"+\
275                                   "authority in set of credentials for this call")
276
277         # registy filter option
278         if command in ("list", "show", "remove"):
279             parser.add_option("-t", "--type", dest="type", type="choice",
280                             help="type filter ([all]|user|slice|authority|node|aggregate)",
281                             choices=("all", "user", "slice", "authority", "node", "aggregate"),
282                             default="all")
283         if command in ("resources"):
284             # rspec version
285             parser.add_option("-r", "--rspec-version", dest="rspec_version", default="SFA 1",
286                               help="schema type and version of resulting RSpec")
287             # disable/enable cached rspecs
288             parser.add_option("-c", "--current", dest="current", default=False,
289                               action="store_true",  
290                               help="Request the current rspec bypassing the cache. Cached rspecs are returned by default")
291             # display formats
292             parser.add_option("-f", "--format", dest="format", type="choice",
293                              help="display format ([xml]|dns|ip)", default="xml",
294                              choices=("xml", "dns", "ip"))
295             #panos: a new option to define the type of information about resources a user is interested in
296             parser.add_option("-i", "--info", dest="info",
297                                 help="optional component information", default=None)
298
299
300         # 'create' does return the new rspec, makes sense to save that too
301         if command in ("resources", "show", "list", "create_gid", 'create'):
302            parser.add_option("-o", "--output", dest="file",
303                             help="output XML to file", metavar="FILE", default=None)
304
305         if command in ("show", "list"):
306            parser.add_option("-f", "--format", dest="format", type="choice",
307                              help="display format ([text]|xml)", default="text",
308                              choices=("text", "xml"))
309
310            parser.add_option("-F", "--fileformat", dest="fileformat", type="choice",
311                              help="output file format ([xml]|xmllist|hrnlist)", default="xml",
312                              choices=("xml", "xmllist", "hrnlist"))
313
314         if command in ("delegate"):
315            parser.add_option("-u", "--user",
316                             action="store_true", dest="delegate_user", default=False,
317                             help="delegate user credential")
318            parser.add_option("-s", "--slice", dest="delegate_slice",
319                             help="delegate slice credential", metavar="HRN", default=None)
320         
321         if command in ("version"):
322             parser.add_option("-R","--registry-version",
323                               action="store_true", dest="version_registry", default=False,
324                               help="probe registry version instead of sliceapi")
325             parser.add_option("-l","--local",
326                               action="store_true", dest="version_local", default=False,
327                               help="display version of the local client")
328
329         return parser
330
331         
332     def create_parser(self):
333
334         # Generate command line parser
335         parser = OptionParser(usage="sfi [sfi_options] command [cmd_options] [cmd_args]",
336                              description="Commands: %s"%(" ".join(self.available_names)))
337         parser.add_option("-r", "--registry", dest="registry",
338                          help="root registry", metavar="URL", default=None)
339         parser.add_option("-s", "--sliceapi", dest="sm", default=None, metavar="URL",
340                          help="slice API - in general a SM URL, but can be used to talk to an aggregate")
341         parser.add_option("-R", "--raw", dest="raw", default=None,
342                           help="Save raw, unparsed server response to a file")
343         parser.add_option("", "--rawformat", dest="rawformat", type="choice",
344                           help="raw file format ([text]|pickled|json)", default="text",
345                           choices=("text","pickled","json"))
346         parser.add_option("", "--rawbanner", dest="rawbanner", default=None,
347                           help="text string to write before and after raw output")
348         parser.add_option("-d", "--dir", dest="sfi_dir",
349                          help="config & working directory - default is %default",
350                          metavar="PATH", default=Sfi.default_sfi_dir())
351         parser.add_option("-u", "--user", dest="user",
352                          help="user name", metavar="HRN", default=None)
353         parser.add_option("-a", "--auth", dest="auth",
354                          help="authority name", metavar="HRN", default=None)
355         parser.add_option("-v", "--verbose", action="count", dest="verbose", default=0,
356                          help="verbose mode - cumulative")
357         parser.add_option("-D", "--debug",
358                           action="store_true", dest="debug", default=False,
359                           help="Debug (xml-rpc) protocol messages")
360         # would it make sense to use ~/.ssh/id_rsa as a default here ?
361         parser.add_option("-k", "--private-key",
362                          action="store", dest="user_private_key", default=None,
363                          help="point to the private key file to use if not yet installed in sfi_dir")
364         parser.add_option("-t", "--timeout", dest="timeout", default=None,
365                          help="Amout of time to wait before timing out the request")
366         parser.add_option("-?", "--commands", 
367                          action="store_true", dest="command_help", default=False,
368                          help="one page summary on commands & exit")
369         parser.disable_interspersed_args()
370
371         return parser
372         
373
374     def print_help (self):
375         self.sfi_parser.print_help()
376         self.command_parser.print_help()
377
378     #
379     # Main: parse arguments and dispatch to command
380     #
381     def dispatch(self, command, command_options, command_args):
382         return getattr(self, command)(command_options, command_args)
383
384     def main(self):
385         self.sfi_parser = self.create_parser()
386         (options, args) = self.sfi_parser.parse_args()
387         if options.command_help: 
388             self.print_command_help(options)
389             sys.exit(1)
390         self.options = options
391
392         self.logger.setLevelFromOptVerbose(self.options.verbose)
393
394         if len(args) <= 0:
395             self.logger.critical("No command given. Use -h for help.")
396             self.print_command_help(options)
397             return -1
398     
399         command = args[0]
400         self.command_parser = self.create_command_parser(command)
401         (command_options, command_args) = self.command_parser.parse_args(args[1:])
402         self.command_options = command_options
403
404         self.read_config () 
405         self.bootstrap ()
406         self.logger.info("Command=%s" % command)
407
408         try:
409             self.dispatch(command, command_options, command_args)
410         except KeyError:
411             self.logger.critical ("Unknown command %s"%command)
412             raise
413             sys.exit(1)
414     
415         return
416     
417     ####################
418     def read_config(self):
419         config_file = os.path.join(self.options.sfi_dir,"sfi_config")
420         try:
421            config = Config (config_file)
422         except:
423            self.logger.critical("Failed to read configuration file %s"%config_file)
424            self.logger.info("Make sure to remove the export clauses and to add quotes")
425            if self.options.verbose==0:
426                self.logger.info("Re-run with -v for more details")
427            else:
428                self.logger.log_exc("Could not read config file %s"%config_file)
429            sys.exit(1)
430      
431         errors = 0
432         # Set SliceMgr URL
433         if (self.options.sm is not None):
434            self.sm_url = self.options.sm
435         elif hasattr(config, "SFI_SM"):
436            self.sm_url = config.SFI_SM
437         else:
438            self.logger.error("You need to set e.g. SFI_SM='http://your.slicemanager.url:12347/' in %s" % config_file)
439            errors += 1 
440
441         # Set Registry URL
442         if (self.options.registry is not None):
443            self.reg_url = self.options.registry
444         elif hasattr(config, "SFI_REGISTRY"):
445            self.reg_url = config.SFI_REGISTRY
446         else:
447            self.logger.errors("You need to set e.g. SFI_REGISTRY='http://your.registry.url:12345/' in %s" % config_file)
448            errors += 1 
449
450         # Set user HRN
451         if (self.options.user is not None):
452            self.user = self.options.user
453         elif hasattr(config, "SFI_USER"):
454            self.user = config.SFI_USER
455         else:
456            self.logger.errors("You need to set e.g. SFI_USER='plc.princeton.username' in %s" % config_file)
457            errors += 1 
458
459         # Set authority HRN
460         if (self.options.auth is not None):
461            self.authority = self.options.auth
462         elif hasattr(config, "SFI_AUTH"):
463            self.authority = config.SFI_AUTH
464         else:
465            self.logger.error("You need to set e.g. SFI_AUTH='plc.princeton' in %s" % config_file)
466            errors += 1 
467
468         if errors:
469            sys.exit(1)
470
471     #
472     # Get various credential and spec files
473     #
474     # Establishes limiting conventions
475     #   - conflates MAs and SAs
476     #   - assumes last token in slice name is unique
477     #
478     # Bootstraps credentials
479     #   - bootstrap user credential from self-signed certificate
480     #   - bootstrap authority credential from user credential
481     #   - bootstrap slice credential from user credential
482     #
483     
484     # init self-signed cert, user credentials and gid
485     def bootstrap (self):
486         bootstrap = SfaClientBootstrap (self.user, self.reg_url, self.options.sfi_dir)
487         # if -k is provided, use this to initialize private key
488         if self.options.user_private_key:
489             bootstrap.init_private_key_if_missing (self.options.user_private_key)
490         else:
491             # trigger legacy compat code if needed 
492             # the name has changed from just <leaf>.pkey to <hrn>.pkey
493             if not os.path.isfile(bootstrap.private_key_filename()):
494                 self.logger.info ("private key not found, trying legacy name")
495                 try:
496                     legacy_private_key = os.path.join (self.options.sfi_dir, "%s.pkey"%get_leaf(self.user))
497                     self.logger.debug("legacy_private_key=%s"%legacy_private_key)
498                     bootstrap.init_private_key_if_missing (legacy_private_key)
499                     self.logger.info("Copied private key from legacy location %s"%legacy_private_key)
500                 except:
501                     self.logger.log_exc("Can't find private key ")
502                     sys.exit(1)
503             
504         # make it bootstrap
505         bootstrap.bootstrap_my_gid()
506         # extract what's needed
507         self.private_key = bootstrap.private_key()
508         self.my_credential_string = bootstrap.my_credential_string ()
509         self.my_gid = bootstrap.my_gid ()
510         self.bootstrap = bootstrap
511
512
513     def my_authority_credential_string(self):
514         if not self.authority:
515             self.logger.critical("no authority specified. Use -a or set SF_AUTH")
516             sys.exit(-1)
517         return self.bootstrap.authority_credential_string (self.authority)
518
519     def slice_credential_string(self, name):
520         return self.bootstrap.slice_credential_string (name)
521
522     # xxx should be supported by sfaclientbootstrap as well
523     def delegate_cred(self, object_cred, hrn, type='authority'):
524         # the gid and hrn of the object we are delegating
525         if isinstance(object_cred, str):
526             object_cred = Credential(string=object_cred) 
527         object_gid = object_cred.get_gid_object()
528         object_hrn = object_gid.get_hrn()
529     
530         if not object_cred.get_privileges().get_all_delegate():
531             self.logger.error("Object credential %s does not have delegate bit set"%object_hrn)
532             return
533
534         # the delegating user's gid
535         caller_gidfile = self.my_gid()
536   
537         # the gid of the user who will be delegated to
538         delegee_gid = self.bootstrap.gid(hrn,type)
539         delegee_hrn = delegee_gid.get_hrn()
540         dcred = object_cred.delegate(delegee_gid, self.private_key, caller_gidfile)
541         return dcred.save_to_string(save_parents=True)
542      
543     #
544     # Management of the servers
545     # 
546
547     def registry (self):
548         # cache the result
549         if not hasattr (self, 'registry_proxy'):
550             self.logger.info("Contacting Registry at: %s"%self.reg_url)
551             self.registry_proxy = SfaServerProxy(self.reg_url, self.private_key, self.my_gid, 
552                                                  timeout=self.options.timeout, verbose=self.options.debug)  
553         return self.registry_proxy
554
555     def sliceapi (self):
556         # cache the result
557         if not hasattr (self, 'sliceapi_proxy'):
558             # if the command exposes the --component option, figure it's hostname and connect at CM_PORT
559             if hasattr(self.command_options,'component') and self.command_options.component:
560                 # resolve the hrn at the registry
561                 node_hrn = self.command_options.component
562                 records = self.registry().Resolve(node_hrn, self.my_credential_string)
563                 records = filter_records('node', records)
564                 if not records:
565                     self.logger.warning("No such component:%r"% opts.component)
566                 record = records[0]
567                 cm_url = "http://%s:%d/"%(record['hostname'],CM_PORT)
568                 self.sliceapi_proxy=SfaServerProxy(cm_url, self.private_key, self.my_gid)
569             else:
570                 # otherwise use what was provided as --sliceapi, or SFI_SM in the config
571                 if not self.sm_url.startswith('http://') or self.sm_url.startswith('https://'):
572                     self.sm_url = 'http://' + self.sm_url
573                 self.logger.info("Contacting Slice Manager at: %s"%self.sm_url)
574                 self.sliceapi_proxy = SfaServerProxy(self.sm_url, self.private_key, self.my_gid, 
575                                                      timeout=self.options.timeout, verbose=self.options.debug)  
576         return self.sliceapi_proxy
577
578     def get_cached_server_version(self, server):
579         # check local cache first
580         cache = None
581         version = None 
582         cache_file = os.path.join(self.options.sfi_dir,'sfi_cache.dat')
583         cache_key = server.url + "-version"
584         try:
585             cache = Cache(cache_file)
586         except IOError:
587             cache = Cache()
588             self.logger.info("Local cache not found at: %s" % cache_file)
589
590         if cache:
591             version = cache.get(cache_key)
592
593         if not version: 
594             result = server.GetVersion()
595             version= ReturnValue.get_value(result)
596             # cache version for 20 minutes
597             cache.add(cache_key, version, ttl= 60*20)
598             self.logger.info("Updating cache file %s" % cache_file)
599             cache.save_to_file(cache_file)
600
601         return version   
602         
603     ### resurrect this temporarily so we can support V1 aggregates for a while
604     def server_supports_options_arg(self, server):
605         """
606         Returns true if server support the optional call_id arg, false otherwise. 
607         """
608         server_version = self.get_cached_server_version(server)
609         result = False
610         # xxx need to rewrite this 
611         if int(server_version.get('geni_api')) >= 2:
612             result = True
613         return result
614
615     def server_supports_call_id_arg(self, server):
616         server_version = self.get_cached_server_version(server)
617         result = False      
618         if 'sfa' in server_version and 'code_tag' in server_version:
619             code_tag = server_version['code_tag']
620             code_tag_parts = code_tag.split("-")
621             version_parts = code_tag_parts[0].split(".")
622             major, minor = version_parts[0], version_parts[1]
623             rev = code_tag_parts[1]
624             if int(major) == 1 and minor == 0 and build >= 22:
625                 result = True
626         return result                 
627
628     ### ois = options if supported
629     # to be used in something like serverproxy.Method (arg1, arg2, *self.ois(api_options))
630     def ois (self, server, option_dict):
631         if self.server_supports_options_arg (server): 
632             return [option_dict]
633         elif self.server_supports_call_id_arg (server):
634             return [ unique_call_id () ]
635         else: 
636             return []
637
638     ### cis = call_id if supported - like ois
639     def cis (self, server):
640         if self.server_supports_call_id_arg (server):
641             return [ unique_call_id ]
642         else:
643             return []
644
645     ######################################## miscell utilities
646     def get_rspec_file(self, rspec):
647        if (os.path.isabs(rspec)):
648           file = rspec
649        else:
650           file = os.path.join(self.options.sfi_dir, rspec)
651        if (os.path.isfile(file)):
652           return file
653        else:
654           self.logger.critical("No such rspec file %s"%rspec)
655           sys.exit(1)
656     
657     def get_record_file(self, record):
658        if (os.path.isabs(record)):
659           file = record
660        else:
661           file = os.path.join(self.options.sfi_dir, record)
662        if (os.path.isfile(file)):
663           return file
664        else:
665           self.logger.critical("No such registry record file %s"%record)
666           sys.exit(1)
667
668
669     #==========================================================================
670     # Following functions implement the commands
671     #
672     # Registry-related commands
673     #==========================================================================
674
675     def version(self, options, args):
676         """
677         display an SFA server version (GetVersion)
678 or version information about sfi itself
679         """
680         if options.version_local:
681             version=version_core()
682         else:
683             if options.version_registry:
684                 server=self.registry()
685             else:
686                 server = self.sliceapi()
687             result = server.GetVersion()
688             version = ReturnValue.get_value(result)
689         if self.options.raw:
690             save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
691         else:
692             pprinter = PrettyPrinter(indent=4)
693             pprinter.pprint(version)
694
695     def list(self, options, args):
696         """
697         list entries in named authority registry (List)
698         """
699         if len(args)!= 1:
700             self.print_help()
701             sys.exit(1)
702         hrn = args[0]
703         try:
704             list = self.registry().List(hrn, self.my_credential_string)
705         except IndexError:
706             raise Exception, "Not enough parameters for the 'list' command"
707
708         # filter on person, slice, site, node, etc.
709         # THis really should be in the self.filter_records funct def comment...
710         list = filter_records(options.type, list)
711         for record in list:
712             print "%s (%s)" % (record['hrn'], record['type'])
713         if options.file:
714             save_records_to_file(options.file, list, options.fileformat)
715         return
716     
717     def show(self, options, args):
718         """
719         show details about named registry record (Resolve)
720         """
721         if len(args)!= 1:
722             self.print_help()
723             sys.exit(1)
724         hrn = args[0]
725         records = self.registry().Resolve(hrn, self.my_credential_string)
726         records = filter_records(options.type, records)
727         if not records:
728             self.logger.error("No record of type %s"% options.type)
729         for record in records:
730             if record['type'] in ['user']:
731                 record = UserRecord(dict=record)
732             elif record['type'] in ['slice']:
733                 record = SliceRecord(dict=record)
734             elif record['type'] in ['node']:
735                 record = NodeRecord(dict=record)
736             elif record['type'].startswith('authority'):
737                 record = AuthorityRecord(dict=record)
738             else:
739                 record = SfaRecord(dict=record)
740             if (options.format == "text"): 
741                 record.dump()  
742             else:
743                 print record.save_to_string() 
744         if options.file:
745             save_records_to_file(options.file, records, options.fileformat)
746         return
747     
748     def add(self, options, args):
749         "add record into registry from xml file (Register)"
750         auth_cred = self.my_authority_credential_string()
751         if len(args)!=1:
752             self.print_help()
753             sys.exit(1)
754         record_filepath = args[0]
755         rec_file = self.get_record_file(record_filepath)
756         record = load_record_from_file(rec_file).as_dict()
757         return self.registry().Register(record, auth_cred)
758     
759     def update(self, options, args):
760         "update record into registry from xml file (Update)"
761         if len(args)!=1:
762             self.print_help()
763             sys.exit(1)
764         rec_file = self.get_record_file(args[0])
765         record = load_record_from_file(rec_file)
766         if record['type'] == "user":
767             if record.get_name() == self.user:
768                 cred = self.my_credential_string
769             else:
770                 cred = self.my_authority_credential_string()
771         elif record['type'] in ["slice"]:
772             try:
773                 cred = self.slice_credential_string(record.get_name())
774             except ServerException, e:
775                # XXX smbaker -- once we have better error return codes, update this
776                # to do something better than a string compare
777                if "Permission error" in e.args[0]:
778                    cred = self.my_authority_credential_string()
779                else:
780                    raise
781         elif record.get_type() in ["authority"]:
782             cred = self.my_authority_credential_string()
783         elif record.get_type() == 'node':
784             cred = self.my_authority_credential_string()
785         else:
786             raise "unknown record type" + record.get_type()
787         record = record.as_dict()
788         return self.registry().Update(record, cred)
789   
790     def remove(self, options, args):
791         "remove registry record by name (Remove)"
792         auth_cred = self.my_authority_credential_string()
793         if len(args)!=1:
794             self.print_help()
795             sys.exit(1)
796         hrn = args[0]
797         type = options.type 
798         if type in ['all']:
799             type = '*'
800         return self.registry().Remove(hrn, auth_cred, type)
801     
802     # ==================================================================
803     # Slice-related commands
804     # ==================================================================
805
806     def slices(self, options, args):
807         "list instantiated slices (ListSlices) - returns urn's"
808         server = self.sliceapi()
809         # creds
810         creds = [self.my_credential_string]
811         if options.delegate:
812             delegated_cred = self.delegate_cred(self.my_credential_string, get_authority(self.authority))
813             creds.append(delegated_cred)  
814         # options and call_id when supported
815         api_options = {}
816         api_options['call_id']=unique_call_id()
817         result = server.ListSlices(creds, *self.ois(server,api_options))
818         value = ReturnValue.get_value(result)
819         if self.options.raw:
820             save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
821         else:
822             display_list(value)
823         return
824
825     # show rspec for named slice
826     def resources(self, options, args):
827         """
828         with no arg, discover available resources, (ListResources)
829 or with an slice hrn, shows currently provisioned resources
830         """
831         server = self.sliceapi()
832
833         # set creds
834         creds = []
835         if args:
836             creds.append(self.slice_credential_string(args[0]))
837         else:
838             creds.append(self.my_credential_string)
839         if options.delegate:
840             creds.append(self.delegate_cred(cred, get_authority(self.authority)))
841
842         # no need to check if server accepts the options argument since the options has
843         # been a required argument since v1 API
844         api_options = {}
845         # always send call_id to v2 servers
846         api_options ['call_id'] = unique_call_id()
847         # ask for cached value if available
848         api_options ['cached'] = True
849         if args:
850             hrn = args[0]
851             api_options['geni_slice_urn'] = hrn_to_urn(hrn, 'slice')
852         if options.info:
853             api_options['info'] = options.info
854         if options.current:
855             if options.current == True:
856                 api_options['cached'] = False
857             else:
858                 api_options['cached'] = True
859         if options.rspec_version:
860             version_manager = VersionManager()
861             server_version = self.get_cached_server_version(server)
862             if 'sfa' in server_version:
863                 # just request the version the client wants
864                 api_options['geni_rspec_version'] = version_manager.get_version(options.rspec_version).to_dict()
865             else:
866                 api_options['geni_rspec_version'] = {'type': 'geni', 'version': '3.0'}
867         else:
868             api_options['geni_rspec_version'] = {'type': 'geni', 'version': '3.0'}
869         result = server.ListResources (creds, api_options)
870         value = ReturnValue.get_value(result)
871         if self.options.raw:
872             save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
873         if options.file is not None:
874             save_rspec_to_file(value, options.file)
875         if (self.options.raw is None) and (options.file is None):
876             display_rspec(value, options.format)
877
878         return
879
880     def create(self, options, args):
881         """
882         create or update named slice with given rspec
883         """
884         server = self.sliceapi()
885
886         # xxx do we need to check usage (len(args)) ?
887         # slice urn
888         slice_hrn = args[0]
889         slice_urn = hrn_to_urn(slice_hrn, 'slice')
890
891         # credentials
892         creds = [self.slice_credential_string(slice_hrn)]
893         delegated_cred = None
894         server_version = self.get_cached_server_version(server)
895         if server_version.get('interface') == 'slicemgr':
896             # delegate our cred to the slice manager
897             # do not delegate cred to slicemgr...not working at the moment
898             pass
899             #if server_version.get('hrn'):
900             #    delegated_cred = self.delegate_cred(slice_cred, server_version['hrn'])
901             #elif server_version.get('urn'):
902             #    delegated_cred = self.delegate_cred(slice_cred, urn_to_hrn(server_version['urn']))
903
904         # rspec
905         rspec_file = self.get_rspec_file(args[1])
906         rspec = open(rspec_file).read()
907
908         # users
909         # need to pass along user keys to the aggregate.
910         # users = [
911         #  { urn: urn:publicid:IDN+emulab.net+user+alice
912         #    keys: [<ssh key A>, <ssh key B>]
913         #  }]
914         users = []
915         slice_records = self.registry().Resolve(slice_urn, [self.my_credential_string])
916         if slice_records and 'researcher' in slice_records[0] and slice_records[0]['researcher']!=[]:
917             slice_record = slice_records[0]
918             user_hrns = slice_record['researcher']
919             user_urns = [hrn_to_urn(hrn, 'user') for hrn in user_hrns]
920             user_records = self.registry().Resolve(user_urns, [self.my_credential_string])
921
922             if 'sfa' not in server_version:
923                 users = pg_users_arg(user_records)
924                 rspec = RSpec(rspec)
925                 rspec.filter({'component_manager_id': server_version['urn']})
926                 rspec = RSpecConverter.to_pg_rspec(rspec.toxml(), content_type='request')
927             else:
928                 users = sfa_users_arg(user_records, slice_record)
929
930         # do not append users, keys, or slice tags. Anything
931         # not contained in this request will be removed from the slice
932
933         # CreateSliver has supported the options argument for a while now so it should
934         # be safe to assume this server support it
935         api_options = {}
936         api_options ['append'] = False
937         api_options ['call_id'] = unique_call_id()
938
939         result = server.CreateSliver(slice_urn, creds, rspec, users, *self.ois(server, api_options))
940         value = ReturnValue.get_value(result)
941         if self.options.raw:
942             save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
943         if options.file is not None:
944             save_rspec_to_file (value, options.file)
945         if (self.options.raw is None) and (options.file is None):
946             print value
947
948         return value
949
950     def delete(self, options, args):
951         """
952         delete named slice (DeleteSliver)
953         """
954         server = self.sliceapi()
955
956         # slice urn
957         slice_hrn = args[0]
958         slice_urn = hrn_to_urn(slice_hrn, 'slice') 
959
960         # creds
961         slice_cred = self.slice_credential_string(slice_hrn)
962         creds = [slice_cred]
963         if options.delegate:
964             delegated_cred = self.delegate_cred(slice_cred, get_authority(self.authority))
965             creds.append(delegated_cred)
966         
967         # options and call_id when supported
968         api_options = {}
969         api_options ['call_id'] = unique_call_id()
970         result = server.DeleteSliver(slice_urn, creds, *self.ois(server, api_options ) )
971         value = ReturnValue.get_value(result)
972         if self.options.raw:
973             save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
974         else:
975             print value
976         return value 
977   
978     def status(self, options, args):
979         """
980         retrieve slice status (SliverStatus)
981         """
982         server = self.sliceapi()
983
984         # slice urn
985         slice_hrn = args[0]
986         slice_urn = hrn_to_urn(slice_hrn, 'slice') 
987
988         # creds 
989         slice_cred = self.slice_credential_string(slice_hrn)
990         creds = [slice_cred]
991         if options.delegate:
992             delegated_cred = self.delegate_cred(slice_cred, get_authority(self.authority))
993             creds.append(delegated_cred)
994
995         # options and call_id when supported
996         api_options = {}
997         api_options['call_id']=unique_call_id()
998         result = server.SliverStatus(slice_urn, creds, *self.ois(server,api_options))
999         value = ReturnValue.get_value(result)
1000         if self.options.raw:
1001             save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1002         else:
1003             print value
1004
1005     def start(self, options, args):
1006         """
1007         start named slice (Start)
1008         """
1009         server = self.sliceapi()
1010
1011         # the slice urn
1012         slice_hrn = args[0]
1013         slice_urn = hrn_to_urn(slice_hrn, 'slice') 
1014         
1015         # cred
1016         slice_cred = self.slice_credential_string(args[0])
1017         creds = [slice_cred]
1018         if options.delegate:
1019             delegated_cred = self.delegate_cred(slice_cred, get_authority(self.authority))
1020             creds.append(delegated_cred)
1021         # xxx Thierry - does this not need an api_options as well ?
1022         result = server.Start(slice_urn, creds)
1023         value = ReturnValue.get_value(result)
1024         if self.options.raw:
1025             save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1026         else:
1027             print value
1028         return value
1029     
1030     def stop(self, options, args):
1031         """
1032         stop named slice (Stop)
1033         """
1034         server = self.sliceapi()
1035         # slice urn
1036         slice_hrn = args[0]
1037         slice_urn = hrn_to_urn(slice_hrn, 'slice') 
1038         # cred
1039         slice_cred = self.slice_credential_string(args[0])
1040         creds = [slice_cred]
1041         if options.delegate:
1042             delegated_cred = self.delegate_cred(slice_cred, get_authority(self.authority))
1043             creds.append(delegated_cred)
1044         result =  server.Stop(slice_urn, creds)
1045         value = ReturnValue.get_value(result)
1046         if self.options.raw:
1047             save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1048         else:
1049             print value
1050         return value
1051     
1052     # reset named slice
1053     def reset(self, options, args):
1054         """
1055         reset named slice (reset_slice)
1056         """
1057         server = self.sliceapi()
1058         # slice urn
1059         slice_hrn = args[0]
1060         slice_urn = hrn_to_urn(slice_hrn, 'slice') 
1061         # cred
1062         slice_cred = self.slice_credential_string(args[0])
1063         creds = [slice_cred]
1064         if options.delegate:
1065             delegated_cred = self.delegate_cred(slice_cred, get_authority(self.authority))
1066             creds.append(delegated_cred)
1067         result = server.reset_slice(creds, slice_urn)
1068         value = ReturnValue.get_value(result)
1069         if self.options.raw:
1070             save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1071         else:
1072             print value
1073         return value
1074
1075     def renew(self, options, args):
1076         """
1077         renew slice (RenewSliver)
1078         """
1079         server = self.sliceapi()
1080         # slice urn    
1081         slice_hrn = args[0]
1082         slice_urn = hrn_to_urn(slice_hrn, 'slice') 
1083         # creds
1084         slice_cred = self.slice_credential_string(args[0])
1085         creds = [slice_cred]
1086         if options.delegate:
1087             delegated_cred = self.delegate_cred(slice_cred, get_authority(self.authority))
1088             creds.append(delegated_cred)
1089         # time
1090         time = args[1]
1091         # options and call_id when supported
1092         api_options = {}
1093         api_options['call_id']=unique_call_id()
1094         result =  server.RenewSliver(slice_urn, creds, time, *self.ois(server,api_options))
1095         value = ReturnValue.get_value(result)
1096         if self.options.raw:
1097             save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1098         else:
1099             print value
1100         return value
1101
1102
1103     def shutdown(self, options, args):
1104         """
1105         shutdown named slice (Shutdown)
1106         """
1107         server = self.sliceapi()
1108         # slice urn
1109         slice_hrn = args[0]
1110         slice_urn = hrn_to_urn(slice_hrn, 'slice') 
1111         # creds
1112         slice_cred = self.slice_credential_string(slice_hrn)
1113         creds = [slice_cred]
1114         if options.delegate:
1115             delegated_cred = self.delegate_cred(slice_cred, get_authority(self.authority))
1116             creds.append(delegated_cred)
1117         result = server.Shutdown(slice_urn, creds)
1118         value = ReturnValue.get_value(result)
1119         if self.options.raw:
1120             save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1121         else:
1122             print value
1123         return value         
1124     
1125
1126     def get_ticket(self, options, args):
1127         """
1128         get a ticket for the specified slice
1129         """
1130         server = self.sliceapi()
1131         # slice urn
1132         slice_hrn, rspec_path = args[0], args[1]
1133         slice_urn = hrn_to_urn(slice_hrn, 'slice')
1134         # creds
1135         slice_cred = self.slice_credential_string(slice_hrn)
1136         creds = [slice_cred]
1137         if options.delegate:
1138             delegated_cred = self.delegate_cred(slice_cred, get_authority(self.authority))
1139             creds.append(delegated_cred)
1140         # rspec
1141         rspec_file = self.get_rspec_file(rspec_path) 
1142         rspec = open(rspec_file).read()
1143         # options and call_id when supported
1144         api_options = {}
1145         api_options['call_id']=unique_call_id()
1146         # get ticket at the server
1147         ticket_string = server.GetTicket(slice_urn, creds, rspec, *self.ois(server,api_options))
1148         # save
1149         file = os.path.join(self.options.sfi_dir, get_leaf(slice_hrn) + ".ticket")
1150         self.logger.info("writing ticket to %s"%file)
1151         ticket = SfaTicket(string=ticket_string)
1152         ticket.save_to_file(filename=file, save_parents=True)
1153
1154     def redeem_ticket(self, options, args):
1155         """
1156         Connects to nodes in a slice and redeems a ticket
1157 (slice hrn is retrieved from the ticket)
1158         """
1159         ticket_file = args[0]
1160         
1161         # get slice hrn from the ticket
1162         # use this to get the right slice credential 
1163         ticket = SfaTicket(filename=ticket_file)
1164         ticket.decode()
1165         ticket_string = ticket.save_to_string(save_parents=True)
1166
1167         slice_hrn = ticket.gidObject.get_hrn()
1168         slice_urn = hrn_to_urn(slice_hrn, 'slice') 
1169         #slice_hrn = ticket.attributes['slivers'][0]['hrn']
1170         slice_cred = self.slice_credential_string(slice_hrn)
1171         
1172         # get a list of node hostnames from the RSpec 
1173         tree = etree.parse(StringIO(ticket.rspec))
1174         root = tree.getroot()
1175         hostnames = root.xpath("./network/site/node/hostname/text()")
1176         
1177         # create an xmlrpc connection to the component manager at each of these
1178         # components and gall redeem_ticket
1179         connections = {}
1180         for hostname in hostnames:
1181             try:
1182                 self.logger.info("Calling redeem_ticket at %(hostname)s " % locals())
1183                 cm_url="http://%s:%s/"%(hostname,CM_PORT)
1184                 server = SfaServerProxy(cm_url, self.private_key, self.my_gid)
1185                 server = self.server_proxy(hostname, CM_PORT, self.private_key, 
1186                                            timeout=self.options.timeout, verbose=self.options.debug)
1187                 server.RedeemTicket(ticket_string, slice_cred)
1188                 self.logger.info("Success")
1189             except socket.gaierror:
1190                 self.logger.error("redeem_ticket failed on %s: Component Manager not accepting requests"%hostname)
1191             except Exception, e:
1192                 self.logger.log_exc(e.message)
1193         return
1194
1195     def create_gid(self, options, args):
1196         """
1197         Create a GID (CreateGid)
1198         """
1199         if len(args) < 1:
1200             self.print_help()
1201             sys.exit(1)
1202         target_hrn = args[0]
1203         gid = self.registry().CreateGid(self.my_credential_string, target_hrn, self.bootstrap.my_gid_string())
1204         if options.file:
1205             filename = options.file
1206         else:
1207             filename = os.sep.join([self.options.sfi_dir, '%s.gid' % target_hrn])
1208         self.logger.info("writing %s gid to %s" % (target_hrn, filename))
1209         GID(string=gid).save_to_file(filename)
1210          
1211
1212     def delegate(self, options, args):
1213         """
1214         (locally) create delegate credential for use by given hrn
1215         """
1216         delegee_hrn = args[0]
1217         if options.delegate_user:
1218             cred = self.delegate_cred(self.my_credential_string, delegee_hrn, 'user')
1219         elif options.delegate_slice:
1220             slice_cred = self.slice_credential_string(options.delegate_slice)
1221             cred = self.delegate_cred(slice_cred, delegee_hrn, 'slice')
1222         else:
1223             self.logger.warning("Must specify either --user or --slice <hrn>")
1224             return
1225         delegated_cred = Credential(string=cred)
1226         object_hrn = delegated_cred.get_gid_object().get_hrn()
1227         if options.delegate_user:
1228             dest_fn = os.path.join(self.options.sfi_dir, get_leaf(delegee_hrn) + "_"
1229                                   + get_leaf(object_hrn) + ".cred")
1230         elif options.delegate_slice:
1231             dest_fn = os.path.join(self.options.sfi_dir, get_leaf(delegee_hrn) + "_slice_"
1232                                   + get_leaf(object_hrn) + ".cred")
1233
1234         delegated_cred.save_to_file(dest_fn, save_parents=True)
1235
1236         self.logger.info("delegated credential for %s to %s and wrote to %s"%(object_hrn, delegee_hrn,dest_fn))
1237     
1238     def get_trusted_certs(self, options, args):
1239         """
1240         return uhe trusted certs at this interface (get_trusted_certs)
1241         """ 
1242         trusted_certs = self.registry().get_trusted_certs()
1243         for trusted_cert in trusted_certs:
1244             gid = GID(string=trusted_cert)
1245             gid.dump()
1246             cert = Certificate(string=trusted_cert)
1247             self.logger.debug('Sfi.get_trusted_certs -> %r'%cert.get_subject())
1248         return 
1249