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