simple_ssl_context() is now a helper exposed in module sfa.util.ssl
[sfa.git] / sfa / client / manifolduploader.py
1 #!/usr/bin/env python3
2 #
3 # inspired from tophat/bin/uploadcredential.py
4 #
5 # the purpose here is to let people upload their delegated credentials
6 # to a manifold/myslice infrastructure, without the need for having to
7 # install a separate tool; so duplicating this code is suboptimal in
8 # terms of code sharing but acceptable for hopefully easier use
9 #
10 # As of Nov. 2013, the signature for the forward API call has changed
11 # and now requires authentication to be passed as an annotation
12 # We take this chance to make things much simpler here by dropping
13 # support for multiple API versions/flavours
14 #
15 # As of April 2013, manifold is moving from old-fashioned API known as
16 # v1, that offers an AddCredential API call, towards a new API v2 that
17 # manages credentials with the same set of Get/Update calls as other
18 # objects
19 #
20
21 # mostly this is intended to be used through 'sfi myslice'
22 # so the defaults below are of no real importance
23 # this for now points at demo.myslice.info, but sounds like a
24 # better default for the long run
25 import getpass
26
27 import xmlrpc.client
28
29 from sfa.util.ssl import simple_ssl_context
30
31 DEFAULT_URL = "http://myslice.onelab.eu:7080"
32 DEFAULT_PLATFORM = 'ple'
33
34
35 class ManifoldUploader:
36     """A utility class for uploading delegated credentials to a manifold/MySlice infrastructure"""
37
38     # platform is a name internal to the manifold deployment,
39     # that maps to a testbed, like e.g. 'ple'
40     def __init__(self, logger, url=None, platform=None, username=None, password=None, ):
41         self._url = url
42         self._platform = platform
43         self._username = username
44         self._password = password
45         self.logger = logger
46         self._proxy = None
47
48     def username(self):
49         if not self._username:
50             self._username = input("Enter your manifold username: ")
51         return self._username
52
53     def password(self):
54         if not self._password:
55             username = self.username()
56             self._password = getpass.getpass(
57                 "Enter password for manifold user %s: " % username)
58         return self._password
59
60     def platform(self):
61         if not self._platform:
62             self._platform = input(
63                 "Enter your manifold platform [%s]: " % DEFAULT_PLATFORM)
64             if self._platform.strip() == "":
65                 self._platform = DEFAULT_PLATFORM
66         return self._platform
67
68     def url(self):
69         if not self._url:
70             self._url = input(
71                 "Enter the URL for your manifold API [%s]: " % DEFAULT_URL)
72             if self._url.strip() == "":
73                 self._url = DEFAULT_URL
74         return self._url
75
76     def prompt_all(self):
77         self.username()
78         self.password()
79         self.platform()
80         self.url()
81
82     # looks like the current implementation of manifold server
83     # won't be happy with several calls issued in the same session
84     # so we do not cache this one
85     def proxy(self):
86         url = self.url()
87         self.logger.debug("Connecting manifold url %s" % url)
88         return xmlrpc.client.ServerProxy(url, allow_none=True,
89                                          context=simple_ssl_context())
90
91     # does the job for one credential
92     # expects the credential (string) and an optional message (e.g. hrn) for reporting
93     # return True upon success and False otherwise
94     def upload(self, delegated_credential, message=None):
95         platform = self.platform()
96         username = self.username()
97         password = self.password()
98         auth = {'AuthMethod': 'password',
99                 'Username': username, 'AuthString': password}
100         if not message:
101             message = ""
102
103         try:
104             manifold = self.proxy()
105             # the code for a V2 interface
106             query = {'action':     'update',
107                      'object':     'local:account',
108                      'filters':    [['platform', '=', platform]],
109                      'params':     {'credential': delegated_credential, },
110                      }
111             annotation = {'authentication': auth, }
112             # in principle the xmlrpc call should not raise an exception
113             # but fill in error code and messages instead
114             # however this is only theoretical so let's be on the safe side
115             try:
116                 self.logger.debug(
117                     "Using new v2 method forward+annotation@%s %s" % (platform, message))
118                 retcod2 = manifold.forward(query, annotation)
119             except Exception as e:
120                 # xxx we need a constant constant for UNKNOWN, how about using
121                 # 1
122                 MANIFOLD_UNKNOWN = 1
123                 retcod2 = {'code': MANIFOLD_UNKNOWN, 'description': "%s" % e}
124             if retcod2['code'] == 0:
125                 info = ""
126                 if message:
127                     info += message + " "
128                 info += 'v2 upload OK'
129                 self.logger.info(info)
130                 return True
131             # everything has failed, let's report
132             self.logger.error("Could not upload %s" %
133                               (message if message else "credential"))
134             self.logger.info("  V2 Update returned code %s and error >>%s<<" % (
135                 retcod2['code'], retcod2['description']))
136             self.logger.debug("****** full retcod2")
137             for k, v in list(retcod2.items()):
138                 self.logger.debug("**** %s: %s" % (k, v))
139             return False
140         except Exception as e:
141             if message:
142                 self.logger.error("Could not upload %s %s" % (message, e))
143             else:
144                 self.logger.error("Could not upload credential %s" % e)
145             if self.logger.debugEnabled():
146                 import traceback
147                 traceback.print_exc()
148             return False
149
150 # this is mainly for unit testing this class but can come in handy as well
151
152
153 def main():
154     from argparse import ArgumentParser
155     parser = ArgumentParser(description="manifoldupoader simple tester.")
156     parser.add_argument('credential_files', metavar='FILE', type=str, nargs='+',
157                         help="the filenames to upload")
158     parser.add_argument('-u', '--url', dest='url', action='store', default=None,
159                         help='the URL of the manifold API')
160     parser.add_argument('-p', '--platform', dest='platform',
161                         action='store', default=None,
162                         help='the manifold platform name')
163     parser.add_argument('-U', '--user', dest='username',
164                         action='store', default=None,
165                         help='the manifold username')
166     parser.add_argument('-P', '--password', dest='password',
167                         action='store', default=None,
168                         help='the manifold password')
169     parser.add_argument('-v', '--verbose', dest='verbose',
170                         action='count', default=0,
171                         help='more and more verbose')
172     args = parser.parse_args()
173
174     from sfa.util.sfalogging import init_logger, logger as sfi_logger
175     init_logger('console')
176     sfi_logger.enable_console()
177     sfi_logger.setLevelFromOptVerbose(args.verbose)
178     uploader = ManifoldUploader(url=args.url, platform=args.platform,
179                                 username=args.username, password=args.password,
180                                 logger=sfi_logger)
181
182     for filename in args.credential_files:
183         with open(filename) as f:
184             result = uploader.upload(f.read(), filename)
185             sfi_logger.info('... result=%s' % result)
186
187 if __name__ == '__main__':
188     main()