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