features for voms authentication
[snf-occi] / snfOCCI / APIserver.py
1 #!/usr/bin/env python
2
3 import sys
4 from optparse import OptionParser, OptionValueError
5 import string
6 import sqlite3
7 import eventlet
8 from eventlet import wsgi
9 import os
10 import json
11 import uuid
12
13 from snfOCCI.registry import snfRegistry
14 from snfOCCI.compute import ComputeBackend
15 from snfOCCI.config import SERVER_CONFIG, KAMAKI_CONFIG, VOMS_CONFIG
16 import snf_voms
17 from snfOCCI.network import NetworkBackend, IpNetworkBackend, IpNetworkInterfaceBackend, NetworkInterfaceBackend
18
19
20 from kamaki.clients.compute import ComputeClient
21 from kamaki.clients.cyclades import CycladesClient
22 from kamaki.clients import astakos
23 from kamaki.clients import ClientError
24 from kamaki.cli import config as kamaki_config
25
26 from occi.core_model import Mixin, Resource
27 from occi.backend import MixinBackend
28 from occi.extensions.infrastructure import COMPUTE, START, STOP, SUSPEND, RESTART, RESOURCE_TEMPLATE, OS_TEMPLATE, NETWORK, IPNETWORK, NETWORKINTERFACE,IPNETWORKINTERFACE 
29 from occi import wsgi
30 from occi.exceptions import HTTPError
31 from occi import core_model
32
33 from wsgiref.simple_server import make_server
34 from wsgiref.validate import validator
35 from webob import Request
36 from pprint import pprint
37
38
39 class MyAPP(wsgi.Application):
40     '''
41     An OCCI WSGI application.
42     '''
43
44     def __init__(self):
45         """
46         Initialization of the WSGI OCCI application for synnefo
47         """
48         global ENABLE_VOMS, VOMS_DB
49         ENABLE_VOMS = VOMS_CONFIG['enable_voms']
50         super(MyAPP,self).__init__(registry=snfRegistry())
51         self._register_backends()
52         VALIDATOR_APP = validator(self)
53          
54         
55     def _register_backends(self):
56         COMPUTE_BACKEND = ComputeBackend()
57         NETWORK_BACKEND = NetworkBackend() 
58         NETWORKINTERFACE_BACKEND = NetworkInterfaceBackend()
59         IPNETWORK_BACKEND = IpNetworkBackend()
60         IPNETWORKINTERFACE_BACKEND = IpNetworkInterfaceBackend()
61     
62         self.register_backend(COMPUTE, COMPUTE_BACKEND)
63         self.register_backend(START, COMPUTE_BACKEND)
64         self.register_backend(STOP, COMPUTE_BACKEND)
65         self.register_backend(RESTART, COMPUTE_BACKEND)
66         self.register_backend(SUSPEND, COMPUTE_BACKEND)
67         self.register_backend(RESOURCE_TEMPLATE, MixinBackend())
68         self.register_backend(OS_TEMPLATE, MixinBackend())
69        
70         # Network related backends
71         self.register_backend(NETWORK, NETWORK_BACKEND)
72         self.register_backend(IPNETWORK, IPNETWORK_BACKEND)
73         self.register_backend(NETWORKINTERFACE,NETWORKINTERFACE_BACKEND)
74         self.register_backend(IPNETWORKINTERFACE, IPNETWORKINTERFACE_BACKEND)
75      
76         
77     def refresh_images(self, snf, client):
78         try:
79             images = snf.list_images()
80             for image in images:
81                     IMAGE_ATTRIBUTES = {'occi.core.id': str(image['id'])}
82                     IMAGE = Mixin("http://schemas.ogf.org/occi/os_tpl#", occify_terms(str(image['name'])), [OS_TEMPLATE],title='IMAGE' ,attributes = IMAGE_ATTRIBUTES)
83                     self.register_backend(IMAGE, MixinBackend())
84         except:
85             raise HTTPError(404, "Unauthorized access")
86       
87     def refresh_flavors(self, snf, client):
88         
89         flavors = snf.list_flavors()
90         for flavor in flavors:
91             details = snf.get_flavor_details(flavor['id'])
92             FLAVOR_ATTRIBUTES = {'occi.core.id': flavor['id'],
93                                  'occi.compute.cores': str(details['vcpus']),
94                                  'occi.compute.memory': str(details['ram']),
95                                  'occi.storage.size': str(details['disk']),
96                                  }
97             FLAVOR = Mixin("http://schemas.ogf.org/occi/resource_tpl#", str(flavor['name']), [RESOURCE_TEMPLATE], attributes = FLAVOR_ATTRIBUTES)
98             self.register_backend(FLAVOR, MixinBackend())
99             
100             
101     def refresh_flavors_norecursive(self, snf, client):
102         flavors = snf.list_flavors(True)
103         print "Retrieving details for each image id"
104         for flavor in flavors:
105             FLAVOR_ATTRIBUTES = {'occi.core.id': flavor['id'],
106                                  'occi.compute.cores': str(flavor['vcpus']),
107                                  'occi.compute.memory': str(flavor['ram']),
108                                  'occi.storage.size': str(flavor['disk']),
109                                  }
110              
111             FLAVOR = Mixin("http://schemas.ogf.org/occi/resource_tpl#", occify_terms(str(flavor['name'])), [RESOURCE_TEMPLATE], title='FLAVOR',attributes = FLAVOR_ATTRIBUTES)
112             self.register_backend(FLAVOR, MixinBackend())
113             
114     def refresh_network_instances(self,client):
115         networks =client.networks_get(command = 'detail')
116         network_details = networks.json['networks']
117         resources = self.registry.resources
118         occi_keys = resources.keys()
119          
120         for network in network_details:
121             if '/network/'+str(network['id']) not in occi_keys:
122                 netID = '/network/'+str(network['id'])   
123                 snf_net = core_model.Resource(netID,
124                                            NETWORK,
125                                            [IPNETWORK])
126                 
127                 snf_net.attributes['occi.core.id'] = str(network['id']) 
128                
129                 #This info comes from the network details
130                 snf_net.attributes['occi.network.state'] = str(network['status'])
131                 snf_net.attributes['occi.network.gateway'] = str(network['gateway'])
132                
133                 if network['public'] == True:
134                     snf_net.attributes['occi.network.type'] = "Public = True"
135                 else:
136                     snf_net.attributes['occi.network.type'] = "Public = False"
137                     
138                 self.registry.add_resource(netID, snf_net, None)       
139             
140         
141     
142     def refresh_compute_instances(self, snf, client):
143         '''Syncing registry with cyclades resources'''
144         
145         servers = snf.list_servers()
146         snf_keys = []
147         for server in servers:
148             snf_keys.append(str(server['id']))
149
150         resources = self.registry.resources
151         occi_keys = resources.keys()
152         
153         print occi_keys
154         for serverID in occi_keys:
155             if '/compute/' in serverID and resources[serverID].attributes['occi.compute.hostname'] == "":
156                 self.registry.delete_resource(serverID, None)
157         
158         occi_keys = resources.keys()
159         
160             
161         #Compute instances in synnefo not available in registry
162         diff = [x for x in snf_keys if '/compute/'+x not in occi_keys]
163         
164         for key in diff:
165
166             details = snf.get_server_details(int(key))
167             flavor = snf.get_flavor_details(details['flavor']['id'])
168             
169             try:
170                 print "line 65:Finished getting image details for VM "+key+" with ID" + str(details['flavor']['id'])
171                 image = snf.get_image_details(details['image']['id'])
172                 
173                 for i in self.registry.backends:
174                     if i.term ==  occify_terms(str(image['name'])):
175                         rel_image = i
176                     if i.term ==  occify_terms(str(flavor['name'])):
177                         rel_flavor = i
178
179                         
180                 resource = Resource(key, COMPUTE, [rel_flavor, rel_image])
181                 resource.actions = [START]
182                 resource.attributes['occi.core.id'] = key
183                 resource.attributes['occi.compute.state'] = 'inactive'
184                 resource.attributes['occi.compute.architecture'] = SERVER_CONFIG['compute_arch']
185                 resource.attributes['occi.compute.cores'] = str(flavor['vcpus'])
186                 resource.attributes['occi.compute.memory'] = str(flavor['ram'])
187                 resource.attributes['occi.core.title'] = str(details['name'])
188                 networkIDs = details['addresses'].keys()
189                 if len(networkIDs)>0: 
190                     resource.attributes['occi.compute.hostname'] =  str(details['addresses'][networkIDs[0]][0]['addr'])
191                 else:
192                     resource.attributes['occi.compute.hostname'] = ""
193                     
194                 self.registry.add_resource(key, resource, None)  
195                 
196                 for netKey in networkIDs:
197                     link_id = str(uuid.uuid4())
198                     NET_LINK = core_model.Link("http://schemas.ogf.org/occi/infrastructure#networkinterface" + link_id,
199                                                NETWORKINTERFACE,
200                                                [IPNETWORKINTERFACE], resource,
201                                                self.registry.resources['/network/'+str(netKey)])
202                     
203                     for version in details['addresses'][netKey]:
204                         if version['version']==4:
205                             ip4address = str(version['addr'])
206                             allocheme = str(version['OS-EXT-IPS:type'])
207                         elif version['version']==6:
208                             ip6address = str(version['addr'])
209                    
210                     if 'attachments' in details.keys():
211                         for item in details['attachments']:
212                             NET_LINK.attributes ={'occi.core.id':link_id,
213                                           'occi.networkinterface.allocation' : allocheme,
214                                           'occi.networking.interface': str(item['id']),
215                                           'occi.networkinterface.mac' : str(item['mac_address']),
216                                           'occi.networkinterface.address' : ip4address,
217                                           'occi.networkinterface.ip6' :  ip6address                      
218                                       }
219                     elif  len(details['addresses'][netKey])>0:
220                         NET_LINK.attributes ={'occi.core.id':link_id,
221                                           'occi.networkinterface.allocation' : allocheme,
222                                           'occi.networking.interface': '',
223                                           'occi.networkinterface.mac' : '',
224                                           'occi.networkinterface.address' : ip4address,
225                                           'occi.networkinterface.ip6' :  ip6address                      
226                                       }
227     
228                     else:
229                         NET_LINK.attributes ={'occi.core.id':link_id,
230                                           'occi.networkinterface.allocation' : '',
231                                           'occi.networking.interface': '',
232                                           'occi.networkinterface.mac' : '',
233                                           'occi.networkinterface.address' :'',
234                                           'occi.networkinterface.ip6' : '' }
235                                       
236                     resource.links.append(NET_LINK)
237                     self.registry.add_resource(link_id, NET_LINK, None)
238                      
239                 
240             except ClientError as ce:
241                 if ce.status == 404:
242                     print('Image not found (probably older version')
243                     continue
244                 else:
245                     raise ce
246                   
247         #Compute instances in registry not available in synnefo
248         diff = [x for x in occi_keys if x[9:] not in snf_keys]
249         for key in diff:
250             if '/network/' not in key:
251                 self.registry.delete_resource(key, None)
252
253
254     def __call__(self, environ, response):
255         
256         # Enable VOMS Authorization
257         print "snf-occi application has been called!"
258         
259         req = Request(environ) 
260         auth_endpoint = 'snf-auth uri=\'https://'+SERVER_CONFIG['hostname']+':5000/main\''
261         
262         if not req.environ.has_key('HTTP_X_AUTH_TOKEN'):
263               
264                 print "Error: An authentication token has not been provided!"
265                 status = '401 Not Authorized'
266                 headers = [('Content-Type', 'text/html'),('Www-Authenticate',auth_endpoint)]        
267                 response(status,headers)               
268                 return [str(response)]
269    
270    
271         if ENABLE_VOMS:
272                 
273             if req.environ.has_key('HTTP_X_AUTH_TOKEN'):
274                
275                 environ['HTTP_AUTH_TOKEN']= req.environ['HTTP_X_AUTH_TOKEN']
276                 compClient = ComputeClient(KAMAKI_CONFIG['compute_url'], environ['HTTP_AUTH_TOKEN'])
277                 cyclClient = CycladesClient(KAMAKI_CONFIG['compute_url'], environ['HTTP_AUTH_TOKEN'])
278
279                 try:
280                     #Up-to-date flavors and images
281                     self.refresh_images(compClient,cyclClient)           
282                     self.refresh_flavors_norecursive(compClient,cyclClient)
283                     self.refresh_network_instances(cyclClient)
284                     self.refresh_compute_instances(compClient,cyclClient)
285                     # token will be represented in self.extras
286                     return self._call_occi(environ, response, security = None, token = environ['HTTP_AUTH_TOKEN'], snf = compClient, client = cyclClient)
287                 except HTTPError:
288                     print "Exception from unauthorized access!"
289                     status = '401 Not Authorized'
290                     headers = [('Content-Type', 'text/html'),('Www-Authenticate',auth_endpoint)]
291                     response(status,headers)
292                     return [str(response)]
293
294             else:
295                 
296                 #raise HTTPError(404, "Unauthorized access")
297                 status = '401 Not Authorized'
298                 headers = [('Content-Type', 'text/html'),('Www-Authenticate',auth_endpoint)]
299                 response(status,headers)
300                 return [str(response)]
301
302         else:  
303             compClient = ComputeClient(KAMAKI_CONFIG['compute_url'], environ['HTTP_AUTH_TOKEN'])
304             cyclClient = CycladesClient(KAMAKI_CONFIG['compute_url'], environ['HTTP_AUTH_TOKEN'])
305
306             #Up-to-date flavors and images
307            
308             self.refresh_images(compClient,cyclClient)
309             
310             self.refresh_flavors_norecursive(compClient,cyclClient)
311             self.refresh_network_instances(cyclClient)
312             self.refresh_compute_instances(compClient,cyclClient)
313             
314             # token will be represented in self.extras
315             return self._call_occi(environ, response, security = None, token = environ['HTTP_AUTH_TOKEN'], snf = compClient, client = cyclClient)
316
317 def application(env, start_response):
318     
319     print "snf-occi will execute voms authentication"
320     t =snf_voms.VomsAuthN()       
321     (user_dn, user_vo, user_fqans) = t.process_request(env)
322     print (user_dn, user_vo, user_fqans)
323       
324     env['HTTP_AUTH_TOKEN'] = get_user_token(user_dn)
325    
326     # Get user authentication details
327     astakosClient = astakos.AstakosClient(KAMAKI_CONFIG['astakos_url'], env['HTTP_AUTH_TOKEN'])
328     user_details = astakosClient.authenticate()
329     
330     response = {'access': {'token':{'issued_at':'','expires': user_details['access']['token']['expires'] , 'id':env['HTTP_AUTH_TOKEN']},
331                            'serviceCatalog': [],
332                            'user':{'username': user_dn,'roles_links':user_details['access']['user']['roles_links'],'id': user_details['access']['user']['id'], 'roles':[], 'name':user_dn },
333                            'metadata': {'is_admin': 0, 'roles': user_details['access']['user']['roles']}}}        
334            
335    
336     status = '200 OK'
337     headers = [('Content-Type', 'application/json')]        
338     start_response(status,headers)
339
340     body = json.dumps(response)
341     print body
342     return [body]
343
344
345 def app_factory(global_config, **local_config):
346     """This function wraps our simple WSGI app so it
347     can be used with paste.deploy"""
348     return application
349
350 def tenant_application(env, start_response):
351     
352     print "snf-occi will return tenant information"
353     if env.has_key('SSL_CLIENT_S_DN_ENV'):
354         print env['SSL_CLIENT_S_DN_ENV'], env['SSL_CLIENT_CERT_ENV']    
355  
356     req = Request(env) 
357     if req.environ.has_key('HTTP_X_AUTH_TOKEN'):
358             env['HTTP_AUTH_TOKEN']= req.environ['HTTP_X_AUTH_TOKEN']
359     else:
360             raise HTTPError(404, "Unauthorized access") 
361     # Get user authentication details
362     print "@ refresh_user authentication details"
363     astakosClient = astakos.AstakosClient(KAMAKI_CONFIG['astakos_url'], env['HTTP_AUTH_TOKEN'])
364     user_details = astakosClient.authenticate()
365    
366     response = {'tenants_links': [], 'tenants':[{'description':'Instances of EGI Federated Clouds TF','enabled': True, 'id':user_details['access']['user']['id'],'name':'EGI_FCTF'}]}           
367  
368     status = '200 OK'
369     headers = [('Content-Type', 'application/json')]        
370     start_response(status,headers)
371
372     body = json.dumps(response)
373     print body
374     return [body]
375
376
377 def tenant_app_factory(global_config, **local_config):
378     """This function wraps our simple WSGI app so it
379     can be used with paste.deploy"""
380     return tenant_application
381
382
383     
384 def occify_terms(term_name):
385     '''
386     Occifies a term_name so that it is compliant with GFD 185.
387     '''
388     term = term_name.strip().replace(' ', '_').replace('.', '-').lower()
389     term=term.replace('(','_').replace(')','_').replace('@','_').replace('+','-_')
390     return term
391
392 def get_user_token(user_dn):
393         config = kamaki_config.Config()
394         return config.get_cloud("default", "token")