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