Statistics
| Branch: | Revision:

root / snfOCCI / APIserver.py @ f25a4623

History | View | Annotate | Download (18.8 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

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

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

    
72

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

    
78
    def __init__(self):
79
        """
80
        Initialization of the WSGI OCCI application for synnefo
81
        """
82
        global ENABLE_VOMS, VOMS_DB
83
        ENABLE_VOMS = VOMS_CONFIG['enable_voms']
84
        super(MyAPP,self).__init__(registry=snfRegistry())
85
        self._register_backends()
86
        VALIDATOR_APP = validator(self)
87
         
88
        
89
    def _register_backends(self):
90
        COMPUTE_BACKEND = ComputeBackend()
91
        NETWORK_BACKEND = NetworkBackend() 
92
        NETWORKINTERFACE_BACKEND = NetworkInterfaceBackend()
93
        IPNETWORK_BACKEND = IpNetworkBackend()
94
        IPNETWORKINTERFACE_BACKEND = IpNetworkInterfaceBackend()
95
    
96
        self.register_backend(COMPUTE, COMPUTE_BACKEND)
97
        self.register_backend(START, COMPUTE_BACKEND)
98
        self.register_backend(STOP, COMPUTE_BACKEND)
99
        self.register_backend(RESTART, COMPUTE_BACKEND)
100
        self.register_backend(SUSPEND, COMPUTE_BACKEND)
101
        self.register_backend(RESOURCE_TEMPLATE, MixinBackend())
102
        self.register_backend(OS_TEMPLATE, MixinBackend())
103
       
104
        # Network related backends
105
        self.register_backend(NETWORK, NETWORK_BACKEND)
106
        self.register_backend(IPNETWORK, IPNETWORK_BACKEND)
107
        self.register_backend(NETWORKINTERFACE,NETWORKINTERFACE_BACKEND)
108
        self.register_backend(IPNETWORKINTERFACE, IPNETWORKINTERFACE_BACKEND)
109
     
110
        
111
    def refresh_images(self, snf, client):
112
        try:
113
            images = snf.list_images()
114
            for image in images:
115
                    IMAGE_ATTRIBUTES = {'occi.core.id': str(image['id'])}
116
                    IMAGE = Mixin("http://schemas.ogf.org/occi/os_tpl#", occify_terms(str(image['name'])), [OS_TEMPLATE],title='IMAGE' ,attributes = IMAGE_ATTRIBUTES)
117
                    self.register_backend(IMAGE, MixinBackend())
118
        except:
119
            raise HTTPError(404, "Unauthorized access")
120
      
121
    def refresh_flavors(self, snf, client):
122
        
123
        flavors = snf.list_flavors()
124
        for flavor in flavors:
125
            details = snf.get_flavor_details(flavor['id'])
126
            FLAVOR_ATTRIBUTES = {'occi.core.id': flavor['id'],
127
                                 'occi.compute.cores': str(details['vcpus']),
128
                                 'occi.compute.memory': str(details['ram']),
129
                                 'occi.storage.size': str(details['disk']),
130
                                 }
131
            FLAVOR = Mixin("http://schemas.ogf.org/occi/resource_tpl#", str(flavor['name']), [RESOURCE_TEMPLATE], attributes = FLAVOR_ATTRIBUTES)
132
            self.register_backend(FLAVOR, MixinBackend())
133
            
134
            
135
    def refresh_flavors_norecursive(self, snf, client):
136
        flavors = snf.list_flavors(True)
137
        print "Retrieving details for each image id"
138
        for flavor in flavors:
139
            FLAVOR_ATTRIBUTES = {'occi.core.id': flavor['id'],
140
                                 'occi.compute.cores': str(flavor['vcpus']),
141
                                 'occi.compute.memory': str(flavor['ram']),
142
                                 'occi.storage.size': str(flavor['disk']),
143
                                 }
144
             
145
            FLAVOR = Mixin("http://schemas.ogf.org/occi/resource_tpl#", occify_terms(str(flavor['name'])), [RESOURCE_TEMPLATE], title='FLAVOR',attributes = FLAVOR_ATTRIBUTES)
146
            self.register_backend(FLAVOR, MixinBackend())
147
            
148
    def refresh_network_instances(self,client):
149
        networks =client.networks_get(command = 'detail')
150
        network_details = networks.json['networks']
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'] = str(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
                        if version['version']==4:
239
                            ip4address = str(version['addr'])
240
                            allocheme = str(version['OS-EXT-IPS:type'])
241
                        elif version['version']==6:
242
                            ip6address = str(version['addr'])
243
                   
244
                    if 'attachments' in details.keys():
245
                        for item in details['attachments']:
246
                            NET_LINK.attributes ={'occi.core.id':link_id,
247
                                          'occi.networkinterface.allocation' : allocheme,
248
                                          'occi.networking.interface': str(item['id']),
249
                                          'occi.networkinterface.mac' : str(item['mac_address']),
250
                                          'occi.networkinterface.address' : ip4address,
251
                                          'occi.networkinterface.ip6' :  ip6address                      
252
                                      }
253
                    elif  len(details['addresses'][netKey])>0:
254
                        NET_LINK.attributes ={'occi.core.id':link_id,
255
                                          'occi.networkinterface.allocation' : allocheme,
256
                                          'occi.networking.interface': '',
257
                                          'occi.networkinterface.mac' : '',
258
                                          'occi.networkinterface.address' : ip4address,
259
                                          'occi.networkinterface.ip6' :  ip6address                      
260
                                      }
261
    
262
                    else:
263
                        NET_LINK.attributes ={'occi.core.id':link_id,
264
                                          'occi.networkinterface.allocation' : '',
265
                                          'occi.networking.interface': '',
266
                                          'occi.networkinterface.mac' : '',
267
                                          'occi.networkinterface.address' :'',
268
                                          'occi.networkinterface.ip6' : '' }
269
                                      
270
                    resource.links.append(NET_LINK)
271
                    self.registry.add_resource(link_id, NET_LINK, None)
272
                     
273
                
274
            except ClientError as ce:
275
                if ce.status == 404:
276
                    print('Image not found (probably older version')
277
                    continue
278
                else:
279
                    raise ce
280
                  
281
        #Compute instances in registry not available in synnefo
282
        diff = [x for x in occi_keys if x[9:] not in snf_keys]
283
        for key in diff:
284
            if '/network/' not in key:
285
                self.registry.delete_resource(key, None)
286

    
287

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

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

    
328
            else:
329
                
330
                #raise HTTPError(404, "Unauthorized access")
331
                status = '401 Not Authorized'
332
                headers = [('Content-Type', 'text/html'),('Www-Authenticate',auth_endpoint)]
333
                response(status,headers)
334
                return [str(response)]
335

    
336
        else:  
337
            compClient = ComputeClient(KAMAKI_CONFIG['compute_url'], environ['HTTP_AUTH_TOKEN'])
338
            cyclClient = CycladesClient(KAMAKI_CONFIG['compute_url'], environ['HTTP_AUTH_TOKEN'])
339

    
340
            #Up-to-date flavors and images
341
           
342
            self.refresh_images(compClient,cyclClient)
343
            
344
            self.refresh_flavors_norecursive(compClient,cyclClient)
345
            self.refresh_network_instances(cyclClient)
346
            self.refresh_compute_instances(compClient,cyclClient)
347
            
348
            # token will be represented in self.extras
349
            return self._call_occi(environ, response, security = None, token = environ['HTTP_AUTH_TOKEN'], snf = compClient, client = cyclClient)
350

    
351
def application(env, start_response):
352
    
353
    print "snf-occi will execute voms authentication"
354
    t =snf_voms.VomsAuthN()       
355
    (user_dn, user_vo, user_fqans) = t.process_request(env)
356
    print (user_dn, user_vo, user_fqans)
357
      
358
    env['HTTP_AUTH_TOKEN'] = get_user_token(user_dn)
359
   
360
    # Get user authentication details
361
    astakosClient = astakos.AstakosClient(KAMAKI_CONFIG['astakos_url'], env['HTTP_AUTH_TOKEN'])
362
    user_details = astakosClient.authenticate()
363
    
364
    response = {'access': {'token':{'issued_at':'','expires': user_details['access']['token']['expires'] , 'id':env['HTTP_AUTH_TOKEN']},
365
                           'serviceCatalog': [],
366
                           'user':{'username': user_dn,'roles_links':user_details['access']['user']['roles_links'],'id': user_details['access']['user']['id'], 'roles':[], 'name':user_dn },
367
                           'metadata': {'is_admin': 0, 'roles': user_details['access']['user']['roles']}}}        
368
           
369
   
370
    status = '200 OK'
371
    headers = [('Content-Type', 'application/json')]        
372
    start_response(status,headers)
373

    
374
    body = json.dumps(response)
375
    print body
376
    return [body]
377

    
378

    
379
def app_factory(global_config, **local_config):
380
    """This function wraps our simple WSGI app so it
381
    can be used with paste.deploy"""
382
    return application
383

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

    
406
    body = json.dumps(response)
407
    print body
408
    return [body]
409

    
410

    
411
def tenant_app_factory(global_config, **local_config):
412
    """This function wraps our simple WSGI app so it
413
    can be used with paste.deploy"""
414
    return tenant_application
415

    
416

    
417
    
418
def occify_terms(term_name):
419
    '''
420
    Occifies a term_name so that it is compliant with GFD 185.
421
    '''
422
    term = term_name.strip().replace(' ', '_').replace('.', '-').lower()
423
    term=term.replace('(','_').replace(')','_').replace('@','_').replace('+','-_')
424
    return term
425

    
426
def get_user_token(user_dn):
427
        config = kamaki_config.Config()
428
        return config.get_cloud("default", "token")