Revision fe35958e

b/docs/index.rst
107 107

  
108 108
First, you need to install the required dependencies which can be found here:
109 109

  
110
* `pyssf <https://code.grnet.gr/attachments/download/1182/pyssf-0.4.5.tar>`_
110
* `pyssf <https://pypi.python.org/pypi/pyssf>`_
111 111
* `kamaki <https://code.grnet.gr/projects/kamaki>`_  
112 112

  
113 113
Then you can install **snf-occi** API translation server by cloning our latest source code:
b/setup.py
6 6
    description='OCCI to Openstack/Cyclades API bridge',
7 7
    url='http://code.grnet.gr/projects/snf-occi',
8 8
    license='BSD',
9
    packages = ['snfOCCI'],
10
    entry_points = {
11
        'console_scripts' : ['snf-occi = snfOCCI.APIserver:main']
12
        }
13

  
14
    )
9
    packages = ['snfOCCI','snfOCCI.snf_voms','snfOCCI.httpd','snfOCCI.snfServer'],
10
    entry_points = ''' 
11
        [paste.app_factory]
12
        snf_occi_app = snfOCCI:main
13
        ''',   
14
    )
b/snfOCCI/APIserver.py
1 1
#!/usr/bin/env python
2 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

  
3 13
from snfOCCI.registry import snfRegistry
4 14
from snfOCCI.compute import ComputeBackend
15
from snfOCCI.config import SERVER_CONFIG, KAMAKI_CONFIG, VOMS_CONFIG
16
import snf_voms
5 17
from snfOCCI.network import NetworkBackend, IpNetworkBackend, IpNetworkInterfaceBackend, NetworkInterfaceBackend
6 18

  
7
from snfOCCI.config import SERVER_CONFIG, KAMAKI_CONFIG
8 19

  
9 20
from kamaki.clients.compute import ComputeClient
10 21
from kamaki.clients.cyclades import CycladesClient
22
from kamaki.clients import astakos
11 23
from kamaki.clients import ClientError
24
from kamaki.cli import config as kamaki_config
12 25

  
13 26
from occi.core_model import Mixin, Resource
14
from occi.backend import MixinBackend, KindBackend
27
from occi.backend import MixinBackend
15 28
from occi.extensions.infrastructure import COMPUTE, START, STOP, SUSPEND, RESTART, RESOURCE_TEMPLATE, OS_TEMPLATE, NETWORK, IPNETWORK, NETWORKINTERFACE,IPNETWORKINTERFACE 
16
from occi.wsgi import Application
29
from occi import wsgi
17 30
from occi.exceptions import HTTPError
18 31
from occi import core_model
19 32

  
20 33
from wsgiref.simple_server import make_server
21 34
from wsgiref.validate import validator
22
import uuid
35
from webob import Request
36
from pprint import pprint
37

  
23 38

  
24
class MyAPP(Application):
39
class MyAPP(wsgi.Application):
25 40
    '''
26 41
    An OCCI WSGI application.
27 42
    '''
28 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
        
29 77
    def refresh_images(self, snf, client):
30

  
31
        images = snf.list_images()
32
        for image in images:
33
            IMAGE_ATTRIBUTES = {'occi.core.id': str(image['id'])}
34
            IMAGE = Mixin("http://schemas.ogf.org/occi/infrastructure#", str(image['name']), [OS_TEMPLATE], attributes = IMAGE_ATTRIBUTES)
35
            self.register_backend(IMAGE, MixinBackend())
36

  
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
      
37 87
    def refresh_flavors(self, snf, client):
38 88
        
39 89
        flavors = snf.list_flavors()
40
        print "Retrieving details for each image id"
41 90
        for flavor in flavors:
42 91
            details = snf.get_flavor_details(flavor['id'])
43 92
            FLAVOR_ATTRIBUTES = {'occi.core.id': flavor['id'],
......
45 94
                                 'occi.compute.memory': str(details['ram']),
46 95
                                 'occi.storage.size': str(details['disk']),
47 96
                                 }
48
            FLAVOR = Mixin("http://schemas.ogf.org/occi/infrastructure#", str(flavor['name']), [RESOURCE_TEMPLATE], attributes = FLAVOR_ATTRIBUTES)
97
            FLAVOR = Mixin("http://schemas.ogf.org/occi/resource_tpl#", str(flavor['name']), [RESOURCE_TEMPLATE], attributes = FLAVOR_ATTRIBUTES)
49 98
            self.register_backend(FLAVOR, MixinBackend())
50 99
            
100
            
51 101
    def refresh_flavors_norecursive(self, snf, client):
52 102
        flavors = snf.list_flavors(True)
53 103
        print "Retrieving details for each image id"
54 104
        for flavor in flavors:
55
            # details = snf.get_flavor_details(flavor['id'])
56 105
            FLAVOR_ATTRIBUTES = {'occi.core.id': flavor['id'],
57 106
                                 'occi.compute.cores': str(flavor['vcpus']),
58 107
                                 'occi.compute.memory': str(flavor['ram']),
59 108
                                 'occi.storage.size': str(flavor['disk']),
60 109
                                 }
61 110
             
62
            FLAVOR = Mixin("http://schemas.ogf.org/occi/infrastructure#", str(flavor['name']), [RESOURCE_TEMPLATE], attributes = FLAVOR_ATTRIBUTES)
111
            FLAVOR = Mixin("http://schemas.ogf.org/occi/resource_tpl#", occify_terms(str(flavor['name'])), [RESOURCE_TEMPLATE], title='FLAVOR',attributes = FLAVOR_ATTRIBUTES)
63 112
            self.register_backend(FLAVOR, MixinBackend())
64 113
            
65
       
66 114
    def refresh_network_instances(self,client):
67 115
        networks =client.networks_get(command = 'detail')
68 116
        network_details = networks.json['networks']
......
90 138
                self.registry.add_resource(netID, snf_net, None)       
91 139
            
92 140
        
93
            
141
    
94 142
    def refresh_compute_instances(self, snf, client):
95 143
        '''Syncing registry with cyclades resources'''
96 144
        
......
102 150
        resources = self.registry.resources
103 151
        occi_keys = resources.keys()
104 152
        
105
        print resources.keys()
106
        
153
        print occi_keys
107 154
        for serverID in occi_keys:
108 155
            if '/compute/' in serverID and resources[serverID].attributes['occi.compute.hostname'] == "":
109 156
                self.registry.delete_resource(serverID, None)
......
113 160
            
114 161
        #Compute instances in synnefo not available in registry
115 162
        diff = [x for x in snf_keys if '/compute/'+x not in occi_keys]
163
        
116 164
        for key in diff:
117 165

  
118 166
            details = snf.get_server_details(int(key))
119 167
            flavor = snf.get_flavor_details(details['flavor']['id'])
120 168
            
121 169
            try:
122
                print "line 65:Finished getting image details for VM with ID" + str(details['flavor']['id'])
170
                print "line 65:Finished getting image details for VM "+key+" with ID" + str(details['flavor']['id'])
123 171
                image = snf.get_image_details(details['image']['id'])
124 172
                
125 173
                for i in self.registry.backends:
126
                    if i.term == str(image['name']):
174
                    if i.term ==  occify_terms(str(image['name'])):
127 175
                        rel_image = i
128
                    if i.term == str(flavor['name']):
176
                    if i.term ==  occify_terms(str(flavor['name'])):
129 177
                        rel_flavor = i
178

  
130 179
                        
131 180
                resource = Resource(key, COMPUTE, [rel_flavor, rel_image])
132 181
                resource.actions = [START]
......
138 187
                resource.attributes['occi.core.title'] = str(details['name'])
139 188
                networkIDs = details['addresses'].keys()
140 189
                if len(networkIDs)>0: 
141
                    #resource.attributes['occi.compute.hostname'] = SERVER_CONFIG['hostname'] % {'id':int(key)}
142 190
                    resource.attributes['occi.compute.hostname'] =  str(details['addresses'][networkIDs[0]][0]['addr'])
143
                    #resource.attributes['occi.networkinterface.address'] = str(details['addresses'][networkIDs[0]][0]['addr'])
144 191
                else:
145 192
                    resource.attributes['occi.compute.hostname'] = ""
146 193
                    
......
166 213
                                          'occi.networkinterface.allocation' : allocheme,
167 214
                                          'occi.networking.interface': str(item['id']),
168 215
                                          'occi.networkinterface.mac' : str(item['mac_address']),
169
                                          'occi.networkinterface.ip4' : ip4address,
216
                                          'occi.networkinterface.address' : ip4address,
170 217
                                          'occi.networkinterface.ip6' :  ip6address                      
171 218
                                      }
172 219
                    elif  len(details['addresses'][netKey])>0:
......
174 221
                                          'occi.networkinterface.allocation' : allocheme,
175 222
                                          'occi.networking.interface': '',
176 223
                                          'occi.networkinterface.mac' : '',
177
                                          'occi.networkinterface.ip4' : ip4address,
224
                                          'occi.networkinterface.address' : ip4address,
178 225
                                          'occi.networkinterface.ip6' :  ip6address                      
179 226
                                      }
180 227
    
......
183 230
                                          'occi.networkinterface.allocation' : '',
184 231
                                          'occi.networking.interface': '',
185 232
                                          'occi.networkinterface.mac' : '',
186
                                          'occi.networkinterface.ip4' :'',
233
                                          'occi.networkinterface.address' :'',
187 234
                                          'occi.networkinterface.ip6' : '' }
188 235
                                      
189 236
                    resource.links.append(NET_LINK)
......
192 239
                
193 240
            except ClientError as ce:
194 241
                if ce.status == 404:
195
                    print('Image not found, sorry!!!')
242
                    print('Image not found (probably older version')
196 243
                    continue
197 244
                else:
198 245
                    raise ce
......
203 250
            if '/network/' not in key:
204 251
                self.registry.delete_resource(key, None)
205 252

  
206
    
253

  
207 254
    def __call__(self, environ, response):
208 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:  
209 303
            compClient = ComputeClient(KAMAKI_CONFIG['compute_url'], environ['HTTP_AUTH_TOKEN'])
210 304
            cyclClient = CycladesClient(KAMAKI_CONFIG['compute_url'], environ['HTTP_AUTH_TOKEN'])
211 305

  
212 306
            #Up-to-date flavors and images
213
            print "@refresh_images"
307
           
214 308
            self.refresh_images(compClient,cyclClient)
215
            print "@refresh_flavors"
309
            
216 310
            self.refresh_flavors_norecursive(compClient,cyclClient)
217 311
            self.refresh_network_instances(cyclClient)
218
            print "@refresh_compute_instances"
219 312
            self.refresh_compute_instances(compClient,cyclClient)
220
           
313
            
221 314
            # token will be represented in self.extras
222 315
            return self._call_occi(environ, response, security = None, token = environ['HTTP_AUTH_TOKEN'], snf = compClient, client = cyclClient)
223 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)
224 339

  
225
def main():
340
    body = json.dumps(response)
341
    print body
342
    return [body]
226 343

  
227
    APP = MyAPP(registry = snfRegistry())
228
    COMPUTE_BACKEND = ComputeBackend()
229
    NETWORK_BACKEND = NetworkBackend() 
230
    NETWORKINTERFACE_BACKEND = NetworkInterfaceBackend()
231
    IPNETWORK_BACKEND = IpNetworkBackend()
232
    IPNETWORKINTERFACE_BACKEND = IpNetworkInterfaceBackend()
233
      
234
    APP.register_backend(COMPUTE, COMPUTE_BACKEND)
235
    APP.register_backend(START, COMPUTE_BACKEND)
236
    APP.register_backend(STOP, COMPUTE_BACKEND)
237
    APP.register_backend(RESTART, COMPUTE_BACKEND)
238
    APP.register_backend(SUSPEND, COMPUTE_BACKEND)
239
    APP.register_backend(RESOURCE_TEMPLATE, MixinBackend())
240
    APP.register_backend(OS_TEMPLATE, MixinBackend())
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']    
241 355
 
242
    # Network related backends
243
    APP.register_backend(NETWORK, NETWORK_BACKEND)
244
    APP.register_backend(IPNETWORK, IPNETWORK_BACKEND)
245
    APP.register_backend(NETWORKINTERFACE,NETWORKINTERFACE_BACKEND)
246
    APP.register_backend(IPNETWORKINTERFACE, IPNETWORKINTERFACE_BACKEND)
247
     
248
    VALIDATOR_APP = validator(APP)
249
  
250
    HTTPD = make_server('', SERVER_CONFIG['port'], VALIDATOR_APP)
251
    HTTPD.serve_forever()
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

  
252 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")
b/snfOCCI/__init__.py
1

  
2
"""
3
This it the entry point for paste deploy .
4

  
5
Paste config file needs to point to egg:<package name>:<entrypoint name>:
6

  
7
use = egg:snfOCCI#sample_app
8

  
9
sample_app entry point is defined in setup.py:
10

  
11
entry_points='''
12
[paste.app_factory]
13
sample_app = snf_voms:main
14
''',
15

  
16
which point to this function call (<module name>:function).
17
"""
18

  
19
# W0613:unused args
20
# pylint: disable=W0613
21

  
22
from snfOCCI import APIserver
23

  
24

  
25
#noinspection PyUnusedLocal
26
def main(global_config, **settings):
27
    """
28
This is the entry point for paste into the OCCI OS world.
29
"""
30
    return APIserver.MyAPP()
b/snfOCCI/config.py
1 1
SERVER_CONFIG = {
2 2
    'port': 8888,
3
    'hostname': '',
4
    'compute_arch': ''
3
    'hostname': '$vm_hostname$',
4
    'compute_arch': 'x86'
5 5
    }
6 6

  
7 7
KAMAKI_CONFIG = {
8
    'compute_url': ''
8
    'compute_url': 'https://cyclades.okeanos.grnet.gr/compute/v2.0/',
9
    'astakos_url': 'https://accounts.okeanos.grnet.gr/identity/v2.0/'
9 10
}
10 11
        
11
    
12
VOMS_CONFIG = {
13
    'enable_voms' : 'True',           
14
    'voms_policy' : '/etc/snf/voms.json',
15
    'vomsdir_path' : '/etc/grid-security/vomsdir/',
16
    'ca_path': '/etc/grid-security/certificates/',
17
    'cert_dir' : '/etc/ssl/certs/',
18
    'key_dir' : '/etc/ssl/private/'               
19
}
b/snfOCCI/httpd/snf_voms-paste.ini
1
# snf_voms authentication PasteDeploy configuration file
2

  
3
[composite:main]
4
use = egg:Paste#urlmap
5
/:snf_occiapp
6
/v2.0/tokens:authapp
7
/v2.0/tenants:tenantapp
8

  
9
[app:snf_occiapp]
10
use = egg:snf-occi#snf_occi_app
11

  
12
[app:authapp]
13
paste.app_factory = snfOCCI.APIserver:app_factory
14

  
15
[app:tenantapp]
16
paste.app_factory = snfOCCI.APIserver:tenant_app_factory
b/snfOCCI/httpd/snf_voms.py
1
import os
2

  
3
from paste import deploy
4

  
5
import logging 
6

  
7
LOG = logging.getLogger(__name__)
8

  
9
# NOTE(ldbragst): 'application' is required in this context by WSGI spec.
10
# The following is a reference to Python Paste Deploy documentation
11
# http://pythonpaste.org/deploy/
12
application = deploy.loadapp('config:/home/synnefo/snf_voms-paste.ini')
b/snfOCCI/httpd/snf_voms_auth-paste.ini
1
# snf_voms authentication PasteDeploy configuration file
2

  
3
[composite:main]
4
use = egg:Paste#urlmap
5
/v2.0/tokens:authapp
6
/v2.0/tenants:tenantapp
7

  
8
[app:authapp]
9
paste.app_factory = snfOCCI.APIserver:app_factory
10

  
11
[app:tenantapp]
12
paste.app_factory = snfOCCI.APIserver:tenant_app_factory
b/snfOCCI/httpd/snf_voms_auth.py
1
import os
2

  
3
from paste import deploy
4

  
5
import logging 
6

  
7
LOG = logging.getLogger(__name__)
8

  
9
# NOTE(ldbragst): 'application' is required in this context by WSGI spec.
10
# The following is a reference to Python Paste Deploy documentation
11
# http://pythonpaste.org/deploy/
12
application = deploy.loadapp('config:/home/synnefo/snf_voms_auth-paste.ini')
b/snfOCCI/registry.py
2 2
from kamaki.clients.cyclades import CycladesClient
3 3
from kamaki.cli.config  import Config
4 4

  
5
from snfOCCI.config import SERVER_CONFIG
6

  
5 7
from occi import registry
6 8
from occi.core_model import Mixin
7 9
from occi.backend import MixinBackend
......
15 17
        resource.identifier = key
16 18

  
17 19
        super(snfRegistry, self).add_resource(key, resource, extras)
20

  
21
    def set_hostname(self, hostname):
22
        hostname = "https://" + SERVER_CONFIG['hostname'] + ":" + str(SERVER_CONFIG['port']) 
23
        super(snfRegistry, self).set_hostname(hostname)
b/snfOCCI/snf_voms/__init__.py
1
# Copyright 2012 Spanish National Research Council
2
#
3
# Licensed under the Apache License, Version 2.0 (the "License"); you may
4
# not use this file except in compliance with the License. You may obtain
5
# a copy of the License at
6
#
7
#      http://www.apache.org/licenses/LICENSE-2.0
8
#
9
# Unless required by applicable law or agreed to in writing, software
10
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12
# License for the specific language governing permissions and limitations
13
# under the License.
14

  
15
import json
16
import M2Crypto
17
import ast
18

  
19
from logging import getLogger
20
import voms_helper
21
from kamaki.clients import ClientError
22

  
23
LOG =  getLogger(__name__)
24

  
25
# Environment variable used to pass the request context
26
CONTEXT_ENV = 'snf.context'
27

  
28

  
29
SSL_CLIENT_S_DN_ENV = "SSL_CLIENT_S_DN"
30
SSL_CLIENT_CERT_ENV = "SSL_CLIENT_CERT"
31
SSL_CLIENT_CERT_CHAIN_ENV_PREFIX = "SSL_CLIENT_CERT_CHAIN_"
32

  
33
"""Global variables that contain VOMS related paths
34
"""
35
VOMS_POLICY = "/etc/snf/voms.json"
36
VOMSDIR_PATH = "/etc/grid-security/vomsdir/"
37
CA_PATH = "/etc/grid-security/certificates/"
38
VOMSAPI_LIB = "/usr/lib/libvomsapi.so.1"
39
PARAMS_ENV = 'snf_voms.params'
40

  
41
class VomsAuthN():
42
    """Filter that checks for the SSL data in the reqest.
43

  
44
    Sets 'ssl' in the context as a dictionary containing this data.
45
    """
46
    
47
    def __init__(self, *args, **kwargs):
48
        
49
       
50
        # VOMS stuff
51
        try:
52
            self.voms_json = json.loads(
53
                open(VOMS_POLICY).read())
54
        except ValueError:
55
            raise ClientError(
56
                'Bad Formatted VOMS json',
57
                details='The VOMS json data was not corectly formatted in file %s' % VOMS_POLICY)
58
        except:
59
            raise ClientError(
60
                              'No loading of VOMS json file',
61
                details='The VOMS json file located in %s was not loaded' % VOMS_POLICY)
62
        
63
        self._no_verify = False
64

  
65
        #super(VomsAuthN, self).__init__(*args, **kwargs)
66

  
67
    @staticmethod
68
    def _get_cert_chain(ssl_info):
69
        """Return certificate and chain from the ssl info in M2Crypto format"""
70

  
71
        cert = M2Crypto.X509.load_cert_string(ssl_info.get("cert", ""))
72
        chain = M2Crypto.X509.X509_Stack()
73
        for c in ssl_info.get("chain", []):
74
            aux = M2Crypto.X509.load_cert_string(c)
75
            chain.push(aux)
76
        return cert, chain
77

  
78
    def _get_voms_info(self, ssl_info):
79
        """Extract voms info from ssl_info and return dict with it."""
80

  
81
        try:
82
            cert, chain = self._get_cert_chain(ssl_info)
83
        except M2Crypto.X509.X509Error:
84
            raise ClientError(
85
                              'SSL data not verified',
86
                              details=CONTEXT_ENV)
87
       
88
        with voms_helper.VOMS(VOMSDIR_PATH,
89
                              CA_PATH, VOMSAPI_LIB) as v:
90
            if self._no_verify:
91
                v.set_no_verify()
92
               
93
            voms_data = v.retrieve(cert, chain)
94
            
95
            if not voms_data:
96
                raise VomsError(v.error.value)
97

  
98
            d = {}
99
            for attr in ('user', 'userca', 'server', 'serverca',
100
                         'voname',  'uri', 'version', 'serial',
101
                         ('not_before', 'date1'), ('not_after', 'date2')):
102
                if isinstance(attr, basestring):
103
                    d[attr] = getattr(voms_data, attr)
104
                else:
105
                    d[attr[0]] = getattr(voms_data, attr[1])
106

  
107
            d["fqans"] = []
108
            for fqan in iter(voms_data.fqan):
109
                if fqan is None:
110
                    break
111
                d["fqans"].append(fqan)
112

  
113
        return d
114

  
115
    @staticmethod
116
    def _split_fqan(fqan):
117
        """
118
        gets a fqan and returns a tuple containing
119
        (vo/groups, role, capability)
120
        """
121
        l = fqan.split("/")
122
        capability = l.pop().split("=")[-1]
123
        role = l.pop().split("=")[-1]
124
        vogroup = "/".join(l)
125
        return (vogroup, role, capability)
126
    
127
    def _process_environ(self, environ):
128
        
129
        LOG.warning("Getting the environment parameters...")
130
        # the environment variable CONTENT_LENGTH may be empty or missing
131
        try:
132
            request_body_size = int(environ.get('CONTENT_LENGTH', 0))
133
        except (ValueError):
134
            request_body_size = 0
135
            raise ClientError(
136
                'Not auth method provided',
137
                details='The request body is empty, while it should contain the authentication method')
138
            
139
        request_body = environ['wsgi.input'].read(request_body_size)
140
        
141
        print request_body
142
        
143
        request_body = request_body.replace("true","\"true\"")
144
        request_body = request_body.replace('"','\'' )  
145
        
146
        params_parsed = ast.literal_eval(request_body)
147
        
148
        
149
        params = {}
150
        for k, v in params_parsed.iteritems():
151
            if k in ('self', 'context'):
152
                continue
153
            if k.startswith('_'):
154
                continue
155
            params[k] = v
156
            
157
        
158
        environ[PARAMS_ENV] = params
159
        print environ[PARAMS_ENV]
160

  
161
    def is_applicable(self, environ):
162
        """Check if the request is applicable for this handler or not"""
163
        print "Checking if the request is applicable for this handler or not..."
164
        self._process_environ(environ)
165
        params = environ.get(PARAMS_ENV, {})
166
        auth = params.get("auth", {})
167
        if "voms" in auth:
168
            if "true" in auth["voms"]:
169
                return True
170
            else:
171
                raise ClientError(
172
                'Error in json',
173
                details='Error in JSON, voms must be set to true')
174
            
175
        return False
176

  
177

  
178
    def authenticate(self,ssl_data):
179
        
180
        try:
181
            voms_info = self._get_voms_info(ssl_data)
182
        except VomsError as e:
183
            raise e
184
        user_dn = voms_info["user"]
185
        user_vo = voms_info["voname"]
186
        user_fqans = voms_info["fqans"] 
187
        
188
        return user_dn, user_vo, user_fqans 
189

  
190
          
191
    def process_request(self, environ):
192
        
193
        print "Inside process_Request at last!!!!"
194
        if not self.is_applicable(environ):
195
            return self.application
196

  
197
        ssl_dict = {
198
            "dn": environ.get(SSL_CLIENT_S_DN_ENV, None),
199
            "cert": environ.get(SSL_CLIENT_CERT_ENV, None),
200
            "chain": [],
201
        }
202
        for k, v in environ.iteritems():
203
            if k.startswith(SSL_CLIENT_CERT_CHAIN_ENV_PREFIX):
204
                ssl_dict["chain"].append(v)
205

  
206
        voms_info = self._get_voms_info(ssl_dict)
207

  
208
        params  = environ[PARAMS_ENV]
209
        
210
        tenant_from_req = params["auth"].get("tenantName", None)
211
        
212
        print voms_info, tenant_from_req
213
        user_dn = voms_info["user"]
214
        user_vo = voms_info["voname"]
215
        user_fqans = voms_info["fqans"] 
216
        environ['REMOTE_USER'] = user_dn
217
        
218
        return user_dn, user_vo, user_fqans
b/snfOCCI/snf_voms/voms_helper.py
1
# Copyright 2012 Spanish National Research Council
2
#
3
# Licensed under the Apache License, Version 2.0 (the "License"); you may
4
# not use this file except in compliance with the License. You may obtain
5
# a copy of the License at
6
#
7
#      http://www.apache.org/licenses/LICENSE-2.0
8
#
9
# Unless required by applicable law or agreed to in writing, software
10
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12
# License for the specific language governing permissions and limitations
13
# under the License.
14

  
15
import ctypes
16

  
17
#import M2Crypto
18

  
19

  
20
class _voms(ctypes.Structure):
21
    _fields_ = [
22
        ("siglen", ctypes.c_int32),
23
        ("signature", ctypes.c_char_p),
24
        ("user", ctypes.c_char_p),
25
        ("userca", ctypes.c_char_p),
26
        ("server", ctypes.c_char_p),
27
        ("serverca", ctypes.c_char_p),
28
        ("voname", ctypes.c_char_p),
29
        ("uri", ctypes.c_char_p),
30
        ("date1", ctypes.c_char_p),
31
        ("date2", ctypes.c_char_p),
32
        ("type", ctypes.c_int32),
33
        ("std", ctypes.c_void_p),
34
        ("custom", ctypes.c_char_p),
35
        ("datalen", ctypes.c_int32),
36
        ("version", ctypes.c_int32),
37
        ("fqan", ctypes.POINTER(ctypes.c_char_p)),
38
        ("serial", ctypes.c_char_p),
39
        ("ac", ctypes.c_void_p),
40
        ("holder", ctypes.c_void_p),
41
    ]
42

  
43

  
44
class _vomsdata(ctypes.Structure):
45
    _fields_ = [
46
        ("cdir", ctypes.c_char_p),
47
        ("vdir", ctypes.c_char_p),
48
        ("data", ctypes.POINTER(ctypes.POINTER(_voms))),
49
        ("workvo", ctypes.c_char_p),
50
        ("extra_data", ctypes.c_char_p),
51
        ("volen", ctypes.c_int32),
52
        ("extralen", ctypes.c_int32),
53
        ("real", ctypes.c_void_p),
54
    ]
55

  
56

  
57
class VOMS(object):
58
    """Context Manager for VOMS handling"""
59

  
60
    def __init__(self, vomsdir_path, ca_path, vomsapi_lib):
61
        self.VOMSApi = ctypes.CDLL(vomsapi_lib)
62
        self.VOMSApi.VOMS_Init.restype = ctypes.POINTER(_vomsdata)
63

  
64
        self.VOMSDIR = vomsdir_path
65
        self.CADIR = ca_path
66

  
67
        self.vd = None
68

  
69
    def __enter__(self):
70
        self.vd = self.VOMSApi.VOMS_Init(self.VOMSDIR, self.CADIR).contents
71
        return self
72

  
73
    def set_no_verify(self):
74
        """Skip verification of AC.
75

  
76
        This method skips the AC signature verification, this it should
77
        only be used for debugging and tests.
78
        """
79

  
80
        error = ctypes.c_int32(0)
81
        self.VOMSApi.VOMS_SetVerificationType(0x040,
82
                                              ctypes.byref(self.vd),
83
                                              ctypes.byref(error))
84

  
85
    def retrieve(self, cert, chain):
86
        """Retrieve VOMS credentials from a certificate and chain."""
87

  
88
        self.error = ctypes.c_int32(0)
89

  
90
        cert_ptr = ctypes.cast(long(cert._ptr()), ctypes.c_void_p)
91
        chain_ptr = ctypes.cast(long(chain._ptr()), ctypes.c_void_p)
92

  
93
        res = self.VOMSApi.VOMS_Retrieve(cert_ptr,
94
                                         chain_ptr,
95
                                         0,
96
                                         ctypes.byref(self.vd),
97
                                         ctypes.byref(self.error))
98
        if res == 0:
99
            return None
100
        else:
101
            return self.vd.data.contents.contents
102

  
103
    def __exit__(self, type, value, tb):
104
        self.VOMSApi.VOMS_Destroy(ctypes.byref(self.vd))

Also available in: Unified diff