Statistics
| Branch: | Revision:

root / snfOCCI / APIserver.py @ b7ca7496

History | View | Annotate | Download (19.1 kB)

1
# Copyright 2012-2013 GRNET S.A. All rights reserved.
2
#
3
# Redistribution and use in source and binary forms, with or
4
# without modification, are permitted provided that the following
5
# conditions are met:
6
#
7
#   1. Redistributions of source code must retain the above
8
#     copyright notice, this list of conditions and the following
9
#     disclaimer.
10
#
11
#   2. Redistributions in binary form must reproduce the above
12
#     copyright notice, this list of conditions and the following
13
#     disclaimer in the documentation and/or other materials
14
#     provided with the distribution.
15
#
16
# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
17
# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
18
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
19
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
20
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
23
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
24
# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
25
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
26
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
27
# POSSIBILITY OF SUCH DAMAGE.
28
#
29
# The views and conclusions contained in the software and
30
# documentation are those of the authors and should not be
31
# interpreted as representing official policies, either expressed
32
# or implied, of GRNET S.A.
33

    
34

    
35
#!/usr/bin/env python
36

    
37
import sys
38
from optparse import OptionParser, OptionValueError
39
import string
40
import sqlite3
41
import eventlet
42
from eventlet import wsgi
43
import os
44
import json
45
import uuid
46

    
47
from snfOCCI.registry import snfRegistry
48
from snfOCCI.compute import ComputeBackend
49
from snfOCCI.config import SERVER_CONFIG, KAMAKI_CONFIG, VOMS_CONFIG
50
import snf_voms
51
from snfOCCI.network import NetworkBackend, IpNetworkBackend, IpNetworkInterfaceBackend, NetworkInterfaceBackend
52

    
53

    
54
from kamaki.clients.compute import ComputeClient
55
from kamaki.clients.cyclades import CycladesClient
56
from kamaki.clients import astakos
57
from kamaki.clients import ClientError
58
from kamaki.cli import config as kamaki_config
59
from kamaki.clients.cyclades import CycladesNetworkClient
60

    
61
from occi.core_model import Mixin, Resource
62
from occi.backend import MixinBackend
63
from occi.extensions.infrastructure import COMPUTE, START, STOP, SUSPEND, RESTART, RESOURCE_TEMPLATE, OS_TEMPLATE, NETWORK, IPNETWORK, NETWORKINTERFACE,IPNETWORKINTERFACE 
64
from occi import wsgi
65
from occi.exceptions import HTTPError
66
from occi import core_model
67

    
68
from wsgiref.simple_server import make_server
69
from wsgiref.validate import validator
70
from webob import Request
71
from pprint import pprint
72

    
73

    
74
class MyAPP(wsgi.Application):
75
    '''
76
    An OCCI WSGI application.
77
    '''
78

    
79
    def __init__(self):
80
        """
81
        Initialization of the WSGI OCCI application for synnefo
82
        """
83
        global ENABLE_VOMS, VOMS_DB
84
        ENABLE_VOMS = VOMS_CONFIG['enable_voms']
85
        super(MyAPP,self).__init__(registry=snfRegistry())
86
        self._register_backends()
87
        VALIDATOR_APP = validator(self)
88
         
89
        
90
    def _register_backends(self):
91
        COMPUTE_BACKEND = ComputeBackend()
92
        NETWORK_BACKEND = NetworkBackend() 
93
        NETWORKINTERFACE_BACKEND = NetworkInterfaceBackend()
94
        IPNETWORK_BACKEND = IpNetworkBackend()
95
        IPNETWORKINTERFACE_BACKEND = IpNetworkInterfaceBackend()
96
    
97
        self.register_backend(COMPUTE, COMPUTE_BACKEND)
98
        self.register_backend(START, COMPUTE_BACKEND)
99
        self.register_backend(STOP, COMPUTE_BACKEND)
100
        self.register_backend(RESTART, COMPUTE_BACKEND)
101
        self.register_backend(SUSPEND, COMPUTE_BACKEND)
102
        self.register_backend(RESOURCE_TEMPLATE, MixinBackend())
103
        self.register_backend(OS_TEMPLATE, MixinBackend())
104
       
105
        # Network related backends
106
        self.register_backend(NETWORK, NETWORK_BACKEND)
107
        self.register_backend(IPNETWORK, IPNETWORK_BACKEND)
108
        self.register_backend(NETWORKINTERFACE,NETWORKINTERFACE_BACKEND)
109
        self.register_backend(IPNETWORKINTERFACE, IPNETWORKINTERFACE_BACKEND)
110
     
111
        
112
    def refresh_images(self, snf, client):
113
        try:
114
            images = snf.list_images()
115
            for image in images:
116
                    IMAGE_ATTRIBUTES = {'occi.core.id': str(image['id'])}
117
                    IMAGE = Mixin("http://schemas.ogf.org/occi/os_tpl#", occify_terms(str(image['name'])), [OS_TEMPLATE],title='IMAGE' ,attributes = IMAGE_ATTRIBUTES)
118
                    self.register_backend(IMAGE, MixinBackend())
119
        except:
120
            raise HTTPError(404, "Unauthorized access")
121
      
122
    def refresh_flavors(self, snf, client):
123
        
124
        flavors = snf.list_flavors()
125
        for flavor in flavors:
126
            details = snf.get_flavor_details(flavor['id'])
127
            FLAVOR_ATTRIBUTES = {'occi.core.id': flavor['id'],
128
                                 'occi.compute.cores': str(details['vcpus']),
129
                                 'occi.compute.memory': str(details['ram']),
130
                                 'occi.storage.size': str(details['disk']),
131
                                 }
132
            FLAVOR = Mixin("http://schemas.ogf.org/occi/resource_tpl#", str(flavor['name']), [RESOURCE_TEMPLATE], attributes = FLAVOR_ATTRIBUTES)
133
            self.register_backend(FLAVOR, MixinBackend())
134
            
135
            
136
    def refresh_flavors_norecursive(self, snf, client):
137
        flavors = snf.list_flavors(True)
138
        print "Retrieving details for each image id"
139
        for flavor in flavors:
140
            FLAVOR_ATTRIBUTES = {'occi.core.id': flavor['id'],
141
                                 'occi.compute.cores': str(flavor['vcpus']),
142
                                 'occi.compute.memory': str(flavor['ram']),
143
                                 'occi.storage.size': str(flavor['disk']),
144
                                 }
145
             
146
            FLAVOR = Mixin("http://schemas.ogf.org/occi/resource_tpl#", occify_terms(str(flavor['name'])), [RESOURCE_TEMPLATE], title='FLAVOR',attributes = FLAVOR_ATTRIBUTES)
147
            self.register_backend(FLAVOR, MixinBackend())
148
            
149
    def refresh_network_instances(self,client):
150
        network_details = client.list_networks(detail='True')
151
        resources = self.registry.resources
152
        occi_keys = resources.keys()
153
         
154
        for network in network_details:
155
            if '/network/'+str(network['id']) not in occi_keys:
156
                netID = '/network/'+str(network['id'])   
157
                snf_net = core_model.Resource(netID,
158
                                           NETWORK,
159
                                           [IPNETWORK])
160
                
161
                snf_net.attributes['occi.core.id'] = str(network['id']) 
162
               
163
                #This info comes from the network details
164
                snf_net.attributes['occi.network.state'] = str(network['status'])
165
                snf_net.attributes['occi.network.gateway'] = ''
166
               
167
                if network['public'] == True:
168
                    snf_net.attributes['occi.network.type'] = "Public = True"
169
                else:
170
                    snf_net.attributes['occi.network.type'] = "Public = False"
171
                    
172
                self.registry.add_resource(netID, snf_net, None)       
173
            
174
        
175
    
176
    def refresh_compute_instances(self, snf, client):
177
        '''Syncing registry with cyclades resources'''
178
        
179
        servers = snf.list_servers()
180
        snf_keys = []
181
        for server in servers:
182
            snf_keys.append(str(server['id']))
183

    
184
        resources = self.registry.resources
185
        occi_keys = resources.keys()
186
        
187
        print occi_keys
188
        for serverID in occi_keys:
189
            if '/compute/' in serverID and resources[serverID].attributes['occi.compute.hostname'] == "":
190
                self.registry.delete_resource(serverID, None)
191
        
192
        occi_keys = resources.keys()
193
        
194
            
195
        #Compute instances in synnefo not available in registry
196
        diff = [x for x in snf_keys if '/compute/'+x not in occi_keys]
197
        
198
        for key in diff:
199

    
200
            details = snf.get_server_details(int(key))
201
            flavor = snf.get_flavor_details(details['flavor']['id'])
202
            
203
            try:
204
                print "line 65:Finished getting image details for VM "+key+" with ID" + str(details['flavor']['id'])
205
                image = snf.get_image_details(details['image']['id'])
206
                
207
                for i in self.registry.backends:
208
                    if i.term ==  occify_terms(str(image['name'])):
209
                        rel_image = i
210
                    if i.term ==  occify_terms(str(flavor['name'])):
211
                        rel_flavor = i
212

    
213
                        
214
                resource = Resource(key, COMPUTE, [rel_flavor, rel_image])
215
                resource.actions = [START]
216
                resource.attributes['occi.core.id'] = key
217
                resource.attributes['occi.compute.state'] = 'inactive'
218
                resource.attributes['occi.compute.architecture'] = SERVER_CONFIG['compute_arch']
219
                resource.attributes['occi.compute.cores'] = str(flavor['vcpus'])
220
                resource.attributes['occi.compute.memory'] = str(flavor['ram'])
221
                resource.attributes['occi.core.title'] = str(details['name'])
222
                networkIDs = details['addresses'].keys()
223
                if len(networkIDs)>0: 
224
                    resource.attributes['occi.compute.hostname'] =  str(details['addresses'][networkIDs[0]][0]['addr'])
225
                else:
226
                    resource.attributes['occi.compute.hostname'] = ""
227
                    
228
                self.registry.add_resource(key, resource, None)  
229
                
230
                for netKey in networkIDs:
231
                    link_id = str(uuid.uuid4())
232
                    NET_LINK = core_model.Link("http://schemas.ogf.org/occi/infrastructure#networkinterface" + link_id,
233
                                               NETWORKINTERFACE,
234
                                               [IPNETWORKINTERFACE], resource,
235
                                               self.registry.resources['/network/'+str(netKey)])
236
                    
237
                    for version in details['addresses'][netKey]:
238
                       
239
                        ip4address = ''
240
                        ip6address = ''
241

    
242
                        if version['version']==4:
243
                            ip4address = str(version['addr'])
244
                            allocheme = str(version['OS-EXT-IPS:type'])
245
                        elif version['version']==6:
246
                            ip6address = str(version['addr'])
247
                            allocheme = str(version['OS-EXT-IPS:type'])
248
                   
249
                    if 'attachments' in details.keys():
250
                        for item in details['attachments']:
251
                            NET_LINK.attributes ={'occi.core.id':link_id,
252
                                          'occi.networkinterface.allocation' : allocheme,
253
                                          'occi.networking.interface': str(item['id']),
254
                                          'occi.networkinterface.mac' : str(item['mac_address']),
255
                                          'occi.networkinterface.address' : ip4address,
256
                                          'occi.networkinterface.ip6' :  ip6address                      
257
                                      }
258
                    elif  len(details['addresses'][netKey])>0:
259
                        NET_LINK.attributes ={'occi.core.id':link_id,
260
                                          'occi.networkinterface.allocation' : allocheme,
261
                                          'occi.networking.interface': '',
262
                                          'occi.networkinterface.mac' : '',
263
                                          'occi.networkinterface.address' : ip4address,
264
                                          'occi.networkinterface.ip6' :  ip6address                      
265
                                      }
266
    
267
                    else:
268
                        NET_LINK.attributes ={'occi.core.id':link_id,
269
                                          'occi.networkinterface.allocation' : '',
270
                                          'occi.networking.interface': '',
271
                                          'occi.networkinterface.mac' : '',
272
                                          'occi.networkinterface.address' :'',
273
                                          'occi.networkinterface.ip6' : '' }
274
                                      
275
                    resource.links.append(NET_LINK)
276
                    self.registry.add_resource(link_id, NET_LINK, None)
277
                     
278
                
279
            except ClientError as ce:
280
                if ce.status == 404:
281
                    print('Image not found (probably older version')
282
                    continue
283
                else:
284
                    raise ce
285
                  
286
        #Compute instances in registry not available in synnefo
287
        diff = [x for x in occi_keys if x[9:] not in snf_keys]
288
        for key in diff:
289
            if '/network/' not in key:
290
                self.registry.delete_resource(key, None)
291

    
292

    
293
    def __call__(self, environ, response):
294
        
295
        # Enable VOMS Authorization
296
        print "snf-occi application has been called!"
297
        
298
        req = Request(environ) 
299
        auth_endpoint = 'snf-auth uri=\'https://'+SERVER_CONFIG['hostname']+':5000/main\''
300
        
301
        if not req.environ.has_key('HTTP_X_AUTH_TOKEN'):
302
              
303
                print "Error: An authentication token has not been provided!"
304
                status = '401 Not Authorized'
305
                headers = [('Content-Type', 'text/html'),('Www-Authenticate',auth_endpoint)]        
306
                response(status,headers)               
307
                return [str(response)]
308
   
309
   
310
        if ENABLE_VOMS:
311
                
312
            if req.environ.has_key('HTTP_X_AUTH_TOKEN'):
313
               
314
                environ['HTTP_AUTH_TOKEN']= req.environ['HTTP_X_AUTH_TOKEN']
315
                compClient = ComputeClient(KAMAKI_CONFIG['compute_url'], environ['HTTP_AUTH_TOKEN'])
316
                cyclClient = CycladesClient(KAMAKI_CONFIG['compute_url'], environ['HTTP_AUTH_TOKEN'])
317
                netClient = CycladesNetworkClient(KAMAKI_CONFIG['network_url'], environ['HTTP_AUTH_TOKEN'])
318

    
319
                try:
320
                    #Up-to-date flavors and images
321
                    self.refresh_images(compClient,cyclClient)           
322
                    self.refresh_flavors_norecursive(compClient,cyclClient)
323
                    self.refresh_network_instances(netClient)
324
                    self.refresh_compute_instances(compClient,cyclClient)
325
                    # token will be represented in self.extras
326
                    return self._call_occi(environ, response, security = None, token = environ['HTTP_AUTH_TOKEN'], snf = compClient, client = cyclClient)
327
                except HTTPError:
328
                    print "Exception from unauthorized access!"
329
                    status = '401 Not Authorized'
330
                    headers = [('Content-Type', 'text/html'),('Www-Authenticate',auth_endpoint)]
331
                    response(status,headers)
332
                    return [str(response)]
333

    
334
            else:
335
                
336
                #raise HTTPError(404, "Unauthorized access")
337
                status = '401 Not Authorized'
338
                headers = [('Content-Type', 'text/html'),('Www-Authenticate',auth_endpoint)]
339
                response(status,headers)
340
                return [str(response)]
341

    
342
        else:  
343
            compClient = ComputeClient(KAMAKI_CONFIG['compute_url'], environ['HTTP_AUTH_TOKEN'])
344
            cyclClient = CycladesClient(KAMAKI_CONFIG['compute_url'], environ['HTTP_AUTH_TOKEN'])
345

    
346
            #Up-to-date flavors and images
347
           
348
            self.refresh_images(compClient,cyclClient)
349
            
350
            self.refresh_flavors_norecursive(compClient,cyclClient)
351
            self.refresh_network_instances(cyclClient)
352
            self.refresh_compute_instances(compClient,cyclClient)
353
            
354
            # token will be represented in self.extras
355
            return self._call_occi(environ, response, security = None, token = environ['HTTP_AUTH_TOKEN'], snf = compClient, client = cyclClient)
356

    
357
def application(env, start_response):
358
    
359
    print "snf-occi will execute voms authentication"
360
    t =snf_voms.VomsAuthN()       
361
    (user_dn, user_vo, user_fqans) = t.process_request(env)
362
    print (user_dn, user_vo, user_fqans)
363
      
364
    env['HTTP_AUTH_TOKEN'] = get_user_token(user_dn)
365
   
366
    # Get user authentication details
367
    pool = False
368
    astakosClient = astakos.AstakosClient(env['HTTP_AUTH_TOKEN'], KAMAKI_CONFIG['astakos_url'] , use_pool = pool)
369

    
370
    user_details = astakosClient.authenticate()
371
    
372
    response = {'access': {'token':{'issued_at':'','expires': user_details['access']['token']['expires'] , 'id':env['HTTP_AUTH_TOKEN']},
373
                           'serviceCatalog': [],
374
                           'user':{'username': user_dn,'roles_links':user_details['access']['user']['roles_links'],'id': user_details['access']['user']['id'], 'roles':[], 'name':user_dn },
375
                           'metadata': {'is_admin': 0, 'roles': user_details['access']['user']['roles']}}}        
376
           
377
   
378
    status = '200 OK'
379
    headers = [('Content-Type', 'application/json')]        
380
    start_response(status,headers)
381

    
382
    body = json.dumps(response)
383
    print body
384
    return [body]
385

    
386

    
387
def app_factory(global_config, **local_config):
388
    """This function wraps our simple WSGI app so it
389
    can be used with paste.deploy"""
390
    return application
391

    
392
def tenant_application(env, start_response):
393
    
394
    print "snf-occi will return tenant information"
395
    if env.has_key('SSL_CLIENT_S_DN_ENV'):
396
        print env['SSL_CLIENT_S_DN_ENV'], env['SSL_CLIENT_CERT_ENV']    
397
 
398
    req = Request(env) 
399
    if req.environ.has_key('HTTP_X_AUTH_TOKEN'):
400
            env['HTTP_AUTH_TOKEN']= req.environ['HTTP_X_AUTH_TOKEN']
401
    else:
402
            raise HTTPError(404, "Unauthorized access") 
403
    # Get user authentication details
404
    print "@ refresh_user authentication details"
405
    astakosClient = astakos.AstakosClient(KAMAKI_CONFIG['astakos_url'], env['HTTP_AUTH_TOKEN'])
406
    user_details = astakosClient.authenticate()
407
   
408
    response = {'tenants_links': [], 'tenants':[{'description':'Instances of EGI Federated Clouds TF','enabled': True, 'id':user_details['access']['user']['id'],'name':'EGI_FCTF'}]}           
409
 
410
    status = '200 OK'
411
    headers = [('Content-Type', 'application/json')]        
412
    start_response(status,headers)
413

    
414
    body = json.dumps(response)
415
    print body
416
    return [body]
417

    
418

    
419
def tenant_app_factory(global_config, **local_config):
420
    """This function wraps our simple WSGI app so it
421
    can be used with paste.deploy"""
422
    return tenant_application
423

    
424

    
425
    
426
def occify_terms(term_name):
427
    '''
428
    Occifies a term_name so that it is compliant with GFD 185.
429
    '''
430
    term = term_name.strip().replace(' ', '_').replace('.', '-').lower()
431
    term=term.replace('(','_').replace(')','_').replace('@','_').replace('+','-_')
432
    return term
433

    
434
def get_user_token(user_dn):
435
        config = kamaki_config.Config()
436
        return config.get_cloud("default", "token")